Skip to content

Add ConfirmationPublisher for async publisher confirmations#1824

Open
lukebakken wants to merge 3 commits into
rabbitmq:mainfrom
lukebakken:lukebakken/publisher-confirm-tracking
Open

Add ConfirmationPublisher for async publisher confirmations#1824
lukebakken wants to merge 3 commits into
rabbitmq:mainfrom
lukebakken:lukebakken/publisher-confirm-tracking

Conversation

@lukebakken

@lukebakken lukebakken commented Dec 8, 2025

Copy link
Copy Markdown
Collaborator

Traditional publisher confirms in the Java client require manual tracking of sequence numbers and manual correlation of basic.return messages. This makes per-message error handling complex and provides no built-in async pattern, backpressure mechanism, or message correlation support.

This change introduces ConfirmationPublisher, a standalone AutoCloseable that provides automatic publisher confirmation tracking with a CompletableFuture-based API, optional bounded-outstanding backpressure, and a generic context parameter for message correlation. It owns a dedicated channel created from the supplied Connection and requires no modifications to Channel, ChannelN, or any other existing class.

API

// Unlimited outstanding confirmations
ConfirmationPublisher publisher = ConfirmationPublisher.create(connection);

// At most 100 unconfirmed messages; basicPublishAsync blocks at the limit
ConfirmationPublisher publisher = ConfirmationPublisher.create(connection, 100);

// Custom executor for future completion
ConfirmationPublisher publisher = ConfirmationPublisher.create(connection, 100, executor);

CompletableFuture<T> future =
    publisher.basicPublishAsync(exchange, routingKey, props, body, context);

The future completes with the caller-supplied context on basic.ack, or exceptionally with a PublishException on basic.nack or basic.return.

Design

The publisher owns a private channel, so no external publish can desynchronize the delivery-tag space. It keys its outstanding map by the channel's authoritative getNextPublishSeqNo(), read atomically with the publish under a lock, rather than maintaining a shadow counter. Futures are completed on a configurable executor (default ForkJoinPool.commonPool()), never on the connection's I/O thread.

A basic.return frame carries no delivery tag, so an x-seq-no header is injected only when mandatory = true (returns are impossible otherwise) to correlate the return back to a publish. Non-mandatory publishes avoid the header entirely. Note that when a mandatory message is routable, the broker echoes this header on delivery, so its consumers observe it; applications that must not expose it should publish such messages through their own channel.

Recovery and failure handling

  • When the connection supports automatic recovery, the publisher keeps working across recoveries. A RecoveryListener fails all outstanding futures at the start of recovery, before confirm.select resets the delivery-tag sequence, so a reused tag cannot mis-correlate against a stale entry.
  • A connection-level publish failure is left to the natural shutdown and recovery path. A publish failure that leaves the channel genuinely stuck open with an advanced sequence number aborts the private channel, since correlation could no longer be trusted.
  • Publishing while a channel is transiently not in confirm mode (mid-recovery) fails the future rather than sending an untracked message.
  • Completions fall back to the calling thread if the configured executor rejects them, so a saturated or shut-down user executor never leaves a future hanging.

PublishException

PublishException carries the publish sequence number, the user context, and, for a basic.return, the full Return (reply code and text, exchange, routing key, properties, body). Use isReturn() to distinguish a return from a nack; getReturned() is null for a nack.

@lukebakken

Copy link
Copy Markdown
Collaborator Author

Hey @acogoluegnes!

Here's a proposal for implementing "automatic" publisher confirmation tracking in a similar manner to the .NET client. As you're the expert here, please suggest better names, a better implementation, etc, as I'm just barely familiar enough with Java and this project to implement this feature with the help of a genie. I did, of course, review the code.

If you like the way this is going, I thought I'd also add rate-throttling in a similar manner to the .NET client, i.e. as the outstanding confirmation window closes, increase delay between publishes to (hopefully) allow the broker to catch up.

@lukebakken
lukebakken force-pushed the lukebakken/publisher-confirm-tracking branch 2 times, most recently from 6a31a46 to 714370e Compare December 8, 2025 23:53
Comment thread src/main/java/com/rabbitmq/client/impl/ChannelN.java Outdated
Comment thread src/main/java/com/rabbitmq/client/impl/ChannelN.java Outdated
Comment thread src/main/java/com/rabbitmq/client/impl/ChannelN.java Outdated
Comment thread src/main/java/com/rabbitmq/client/impl/ChannelN.java Outdated
Comment thread src/main/java/com/rabbitmq/client/ChannelOptions.java Outdated
@acogoluegnes

Copy link
Copy Markdown
Contributor

Thanks for this contributon @lukebakken. I think it is on the right track. I added some comments in the code. Just a few more remarks:

  • the new methods in the public interfaces should have a default implementation if we back port the PR to 5.x. This is for backward compatibility. Throwing an exception is good enough.
  • synchronized blocks and Object-based synchronization is considered old-style Java concurrency. We could see if there are more modern utilities in the Java concurrency toolkit to handle what we need in ChannelN. I'm not super opinionated on this topic though and this is an implementation details we can polish later.
  • the bulk of the PR is in ChannelN, it is not rocket science but add some non-trivial logic to a somewhat already complex class. I was wondering if it could be possible to externalize all the logic in a PublishConfirmState class used from ChannelN. This state class would register regular return and confirm listeners on the channel. This could even be an interface with a no-op implementation when the feature is not activated, minimalizing the impact on ChannelN. We can discuss this design when all the features are added.

Comment thread src/main/java/com/rabbitmq/client/ChannelOptions.java Outdated
@lukebakken
lukebakken force-pushed the lukebakken/publisher-confirm-tracking branch from 714370e to 44c3aca Compare December 9, 2025 16:36
@lukebakken lukebakken changed the title Add automatic publisher confirmation tracking with async API Add automatic publisher confirmation tracking with throttling Dec 9, 2025
@lukebakken
lukebakken force-pushed the lukebakken/publisher-confirm-tracking branch 6 times, most recently from c64f7ba to 5520ef4 Compare December 10, 2025 21:15
@michaelklishin

Copy link
Copy Markdown
Contributor

@lukebakken I have sent you an external contributor invite for this repo.

@lukebakken
lukebakken force-pushed the lukebakken/publisher-confirm-tracking branch from 5520ef4 to dd2fb5e Compare December 11, 2025 04:02
@lukebakken lukebakken changed the title Add automatic publisher confirmation tracking with throttling Add ConfirmationChannel for async publisher confirmations Dec 11, 2025
@lukebakken

Copy link
Copy Markdown
Collaborator Author

@michaelklishin @acogoluegnes I took this comment to heart and re-implemented this feature as a ConfirmationChannel class that wraps ChannelN and does its own confirmation tracking. That way, no modifications to the existing Channel interface or ChannelN class are necessary.

@lukebakken
lukebakken force-pushed the lukebakken/publisher-confirm-tracking branch 2 times, most recently from 041b932 to b0761bf Compare December 11, 2025 04:15
lukebakken added a commit to rabbitmq/rabbitmq-tutorials that referenced this pull request Dec 11, 2025
The new `ConfirmationChannel` API introduced in
rabbitmq/rabbitmq-java-client#1824 provides asynchronous publisher
confirmation tracking with a `CompletableFuture`-based API, rate
limiting, and message correlation support.

This change adds `PublisherConfirmsAsync.java` to demonstrate the
`ConfirmationChannel` API. The tutorial shows how to create a
`ConfirmationChannel` wrapper with rate limiting, publish messages
asynchronously with correlation context, and wait for all confirmations
using `CompletableFuture.allOf()`.

Depends on rabbitmq/rabbitmq-java-client#1824
@lukebakken
lukebakken marked this pull request as ready for review December 11, 2025 17:45
@lukebakken
lukebakken force-pushed the lukebakken/publisher-confirm-tracking branch from b0761bf to 49a0ff5 Compare December 11, 2025 17:52
@lukebakken
lukebakken force-pushed the lukebakken/publisher-confirm-tracking branch 2 times, most recently from c002375 to 8112046 Compare December 11, 2025 17:59
@lukebakken

Copy link
Copy Markdown
Collaborator Author

Alrighty gang, all set I think. Small tutorial here - rabbitmq/rabbitmq-tutorials#707

@lukebakken
lukebakken force-pushed the lukebakken/publisher-confirm-tracking branch from 8112046 to 9a31acc Compare December 11, 2025 20:35
@michaelklishin

Copy link
Copy Markdown
Contributor

@lukebakken since this is a feature by most definitions, we now would have to go through a special approval process on our end :(

lukebakken added a commit to rabbitmq/rabbitmq-tutorials that referenced this pull request Dec 18, 2025
The new `ConfirmationChannel` API introduced in
rabbitmq/rabbitmq-java-client#1824 provides asynchronous publisher
confirmation tracking with a `CompletableFuture`-based API, rate
limiting, and message correlation support.

This change adds `PublisherConfirmsAsync.java` to demonstrate the
`ConfirmationChannel` API. The tutorial shows how to create a
`ConfirmationChannel` wrapper with rate limiting, publish messages
asynchronously with correlation context, and wait for all confirmations
using `CompletableFuture.allOf()`.

Depends on rabbitmq/rabbitmq-java-client#1824
@lukebakken lukebakken self-assigned this Jul 12, 2026
Traditional publisher confirms in the Java client require manual
tracking of sequence numbers and correlation of Basic.Return messages.
This makes per-message error handling complex and provides no built-in
async pattern, backpressure mechanism, or message correlation support.

This change introduces `ConfirmationChannel`, a wrapper that provides
automatic publisher confirmation tracking with a
`CompletableFuture`-based API, optional throttling, and generic context
parameter for message correlation. The implementation uses
listener-based integration with existing `Channel` instances, requiring
no modifications to `ChannelN`.

New API components:
- `ConfirmationChannel` interface - Extends `Channel` and adds
  `basicPublishAsync()` methods that return `CompletableFuture<T>`
- `ConfirmationChannelN` implementation - Wraps any `Channel` instance
  and tracks confirmations via return/confirm/shutdown listeners
- `PublishException` - Exception thrown when message is nack'd or
  returned, with sequence number, routing details, and user context

The wrapper maintains independent sequence numbers using `AtomicLong`
and stores confirmation state in a `ConcurrentHashMap`. Each entry holds
the future, rate limiter permit, and user-provided context. Messages
include an `x-seq-no` header for correlating Basic.Return responses.

Rate limiting is optional via `RateLimiter` parameter. The
`ThrottlingRateLimiter` implementation uses progressive delays
(0-1000ms) based on capacity usage, applying backpressure when available
permits fall below a threshold (default 50%).

The `basicPublish()` and `waitForConfirms()` methods throw
`UnsupportedOperationException` on `ConfirmationChannel` to prevent
mixing synchronous and asynchronous patterns. All other `Channel`
methods delegate to the wrapped instance.

Tests include 9 unit tests for `ThrottlingRateLimiter` and 24
integration tests for publisher confirmation tracking with context
parameter verification, rate limiting scenarios, and error handling.
@lukebakken
lukebakken force-pushed the lukebakken/publisher-confirm-tracking branch from 9a31acc to 42079f4 Compare July 12, 2026 16:06

@lukebakken lukebakken left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review findings, tracked as inline comments so each can be resolved as it is addressed. Ranked roughly by severity in each comment's first line (F1 = most severe, F10 = least).

Note that F1, F2, F3, and part of F10 share one root cause: the wrapper keeps its own AtomicLong sequence counter parallel to the broker's delivery-tag space instead of using the channel's authoritative getNextPublishSeqNo() (or integrating tracking into ChannelN itself, where the counter and unconfirmedSet already live). Fixing that design point addresses all four together.

Comment thread src/main/java/com/rabbitmq/client/impl/ConfirmationChannelN.java Outdated
Comment thread src/main/java/com/rabbitmq/client/impl/ConfirmationChannelN.java Outdated
Comment thread src/main/java/com/rabbitmq/client/impl/ConfirmationChannelN.java Outdated
Comment thread src/main/java/com/rabbitmq/client/impl/ConfirmationChannelN.java Outdated
Comment thread src/main/java/com/rabbitmq/client/impl/ConfirmationChannelN.java Outdated
Comment thread src/main/java/com/rabbitmq/client/impl/ConfirmationChannelN.java Outdated
Comment thread src/main/java/com/rabbitmq/client/ThrottlingRateLimiter.java Outdated
Comment thread src/main/java/com/rabbitmq/client/ConfirmationChannel.java Outdated
Comment thread src/main/java/com/rabbitmq/client/ThrottlingRateLimiter.java Outdated
Comment thread src/main/java/com/rabbitmq/client/impl/ConfirmationChannelN.java Outdated
@lukebakken
lukebakken marked this pull request as draft July 12, 2026 16:41
Replace the ConfirmationChannel design, which extended Channel and kept a
shadow AtomicLong sequence counter parallel to the broker's delivery-tag
space, with a standalone ConfirmationPublisher that owns a private channel
and keys its outstanding map by the channel's own getNextPublishSeqNo()
read under a lock. This removes the counter-desync bug class at its root
without modifying any Channel or ChannelN class.

Correctness properties:

- A connection-level publish failure (IOException) is left to the natural
  shutdown and recovery path rather than aborting the channel, so a single
  transient failure no longer unregisters an autorecovering channel from
  recovery. Only a channel left genuinely stuck open with an advanced
  counter is aborted.
- Publishing while an autorecovering channel is mid-recovery (sequence
  number 0, confirm mode not yet re-established) fails the future instead
  of sending an untracked message that would never be confirmed.
- A RecoveryListener fails all outstanding futures at the start of recovery,
  before confirm.select resets the delivery-tag sequence, so a reused tag
  cannot mis-correlate against a stale entry.
- Completions fall back to the calling thread if the configured executor
  rejects them, so a saturated or shut-down user executor never leaves a
  future hanging.

PublishException now carries a nullable Return for the basic.return case
instead of duplicating the exchange, routing key, reply code and reply text
fields.
@lukebakken lukebakken changed the title Add ConfirmationChannel for async publisher confirmations Add ConfirmationPublisher for async publisher confirmations Jul 12, 2026
lukebakken added a commit to rabbitmq/rabbitmq-tutorials that referenced this pull request Jul 12, 2026
The new `ConfirmationChannel` API introduced in
rabbitmq/rabbitmq-java-client#1824 provides asynchronous publisher
confirmation tracking with a `CompletableFuture`-based API, rate
limiting, and message correlation support.

This change adds `PublisherConfirmsAsync.java` to demonstrate the
`ConfirmationChannel` API. The tutorial shows how to create a
`ConfirmationChannel` wrapper with rate limiting, publish messages
asynchronously with correlation context, and wait for all confirmations
using `CompletableFuture.allOf()`.

Depends on rabbitmq/rabbitmq-java-client#1824
A ConfirmationPublisher created with a user-supplied executor that rejects
every task must still complete its futures, inline on the connection thread,
rather than leaving callers blocked forever.
@lukebakken
lukebakken marked this pull request as ready for review July 13, 2026 14:39
@lukebakken
lukebakken requested a review from Gsantomaggio July 13, 2026 14:39
@sdewhitt

Copy link
Copy Markdown

This looks really handy!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants