Skip to content

feat(ReloadingProducer): Add delivery futures - #591

Merged
lvthanh03 merged 8 commits into
mainfrom
markus/reloading-producer-delivery-futures
Nov 5, 2026
Merged

lvthanh03 merged 8 commits into
mainfrom
markus/reloading-producer-delivery-futures

Conversation

@untitaker

@untitaker untitaker commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Makes ReloadingKafkaProducer implement AsyncProducer<KafkaPayload>, the same interface as AsyncKafkaProducer. The returned future resolves once the broker acknowledged the message, or with the reason it was not delivered.

Taskbroker waits for delivery before it marks retries and deadletters as done in its store. produce_record only tells you the message was enqueued, and a reload can purge it from the old client, so Taskbroker can't move to the reloading producer without this.

How it works:

  • The host's context is wrapped in an internal context whose delivery opaque carries an optional oneshot sender. The host's context still gets every delivery call, and all other callbacks are forwarded.
  • Messages buffered during a swap keep their sender, so they resolve once replayed on the new client. If the replay fails, the future gets that error.
  • Messages purged with the old client resolve with rdkafka's purge error.
  • KafkaProducer now accepts contexts with any DeliveryOpaque. Existing callers are unaffected because BaseRecord's opaque defaults to ().

ReloadingKafkaProducer now implements both Producer and AsyncProducer, so code that imports both traits has to call produce with the trait path.

ref STREAM-1964

Also fixes KafkaProducer::flush_for to wait until in_flight_count is zero. From rdkafka 0.38, flush returns once the broker has the messages, while their delivery reports may not have been served yet, so a reload could swap out a client with reports still pending. This made test_old_queue_is_empty_when_the_swap_happens flaky on 0.38. With this, delivery futures for messages on the old client resolve before the swap.

Add ReloadingKafkaProducer::produce_record_with_delivery, which returns a
future that resolves once the broker acknowledged the message. Messages
buffered during a swap carry their sender through the replay, and messages
purged with the old client resolve with an error.

Taskbroker needs this to only mark retries and deadletters as done once they
are actually in Kafka.

ref STREAM-1964
@untitaker
untitaker requested review from a team as code owners October 28, 2026 12:31
@linear-code

linear-code Bot commented Oct 28, 2026

Copy link
Copy Markdown

STREAM-1964

Gives ReloadingKafkaProducer the same interface as AsyncKafkaProducer, so
hosts can switch over without changing how they produce.
Make produce_record_with_delivery private so AsyncProducer is the one way
to wait for delivery. Always box the delivery opaque, which removes the
unsafe IntoOpaque impl. Report the actual error when replaying a buffered
message fails, instead of a generic one.
…ucer-delivery-futures

# Conflicts:
#	rust-arroyo/src/backends/kafka/reloading_producer.rs
From rdkafka 0.38, flush returns once the broker has the messages, while
the polling thread may not have served their delivery reports yet. A
reload could then swap out a client with reports still pending, which made
test_old_queue_is_empty_when_the_swap_happens flaky on 0.38. Also wait for
in_flight_count to reach zero, which is what 0.37's flush did.
@untitaker
untitaker marked this pull request as ready for review November 3, 2026 10:14

@evanh evanh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this looks good, one question though.

}
// Delivered, but the polling thread has not served every
// report yet. Give it a moment.
Ok(()) => std::thread::sleep(remaining.min(Duration::from_millis(1))),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we want to time out if the records have been delivered but we are waiting for the polling thread? Should we be more gracious with the timeout in that case?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yeah that's a bug, fixed it

Once flush has succeeded, the messages are with the broker. Running out of
time while the polling thread serves their reports is not a failed drain:
the reports are still served by the polling thread or when the client is
dropped. Returning an error there made drain() report a timeout and claim
queued messages would be dropped.
@lvthanh03
lvthanh03 merged commit 232d37b into main Nov 5, 2026
29 of 30 checks passed
@lvthanh03
lvthanh03 deleted the markus/reloading-producer-delivery-futures branch November 5, 2026 16:46
mwarkentin pushed a commit that referenced this pull request Nov 5, 2026
…am-2030-fix-flaky-reloading-producer-test

Brings in #591 (delivery futures) and #600 (QueueFull when the reload
buffer is full). The only conflict was drain()'s signature: it keeps
this branch's bool return and takes main's Client<C>.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QUYJLCL2crYejXVqdCdBL9
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.

3 participants