Add ConfirmationPublisher for async publisher confirmations#1824
Add ConfirmationPublisher for async publisher confirmations#1824lukebakken wants to merge 3 commits into
Conversation
|
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. |
6a31a46 to
714370e
Compare
|
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:
|
714370e to
44c3aca
Compare
c64f7ba to
5520ef4
Compare
|
@lukebakken I have sent you an external contributor invite for this repo. |
5520ef4 to
dd2fb5e
Compare
ConfirmationChannel for async publisher confirmations
|
@michaelklishin @acogoluegnes I took this comment to heart and re-implemented this feature as a |
041b932 to
b0761bf
Compare
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
b0761bf to
49a0ff5
Compare
c002375 to
8112046
Compare
|
Alrighty gang, all set I think. Small tutorial here - rabbitmq/rabbitmq-tutorials#707 |
8112046 to
9a31acc
Compare
|
@lukebakken since this is a feature by most definitions, we now would have to go through a special approval process on our end :( |
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
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.
9a31acc to
42079f4
Compare
lukebakken
left a comment
There was a problem hiding this comment.
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.
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.
ConfirmationChannel for async publisher confirmationsThe 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.
|
This looks really handy! |
Traditional publisher confirms in the Java client require manual tracking of sequence numbers and manual correlation of
basic.returnmessages. 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 standaloneAutoCloseablethat provides automatic publisher confirmation tracking with aCompletableFuture-based API, optional bounded-outstanding backpressure, and a generic context parameter for message correlation. It owns a dedicated channel created from the suppliedConnectionand requires no modifications toChannel,ChannelN, or any other existing class.API
The future completes with the caller-supplied context on
basic.ack, or exceptionally with aPublishExceptiononbasic.nackorbasic.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 (defaultForkJoinPool.commonPool()), never on the connection's I/O thread.A
basic.returnframe carries no delivery tag, so anx-seq-noheader is injected only whenmandatory = 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
RecoveryListenerfails all outstanding futures at the start of recovery, beforeconfirm.selectresets the delivery-tag sequence, so a reused tag cannot mis-correlate against a stale entry.PublishException
PublishExceptioncarries the publish sequence number, the user context, and, for abasic.return, the fullReturn(reply code and text, exchange, routing key, properties, body). UseisReturn()to distinguish a return from a nack;getReturned()is null for a nack.