feat: add max_batch_age to drop stale batches instead of retrying - #117
Merged
Conversation
zmstone
force-pushed
the
260716-max-batch-age
branch
from
July 17, 2026 06:32
3373a83 to
18a41d9
Compare
Add a new producer option `max_batch_age` (default `infinity`, i.e. disabled). When set to a number of milliseconds, a batch is dropped instead of being sent to Kafka if ALL of its messages are older than `max_batch_age` (measured from when they were appended to the buffer). This lets callers bound the delivery latency of buffered messages across long broker outages rather than sending arbitrarily stale data. The check is applied in two places: - `maybe_send_to_kafka/1` peeks the front of the queue and drops expired batches before the next send. Because queue items are appended in timestamp order, once the front is not expired nothing behind it is either, so this is an O(number-expired) front drain. It runs regardless of connection or inflight-window state, so stale data is shed (and memory reclaimed) even while disconnected. These items are still in the pending-ack backlog, so their callbacks are dropped via `wolff_pendack:drop_backlog/2`. - `resend_sent_reqs/2` drops in-flight batches (in `sent_reqs`) that expired while a connection was down, instead of retrying them on reconnect. These are inflight acks, released via `wolff_pendack:take_inflight/2`. Each dropped message has its ack callback evaluated with the reason `message_expired` and bumps the `dropped` counter plus a new `dropped_expired` counter. `infinity` (the default) preserves the current behavior exactly.
zmstone
force-pushed
the
260716-max-batch-age
branch
from
July 18, 2026 18:40
18a41d9 to
0ff524a
Compare
zmstone
marked this pull request as ready for review
July 18, 2026 19:31
ieQu1
approved these changes
Jul 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add a new producer option
max_batch_age(defaultinfinity, disabled).Motivation
When a connection to Kafka drops (request timeout / broker restart), buffered and in-flight batches are (re)sent on recovery no matter how old they are. For long outages this delivers very stale data, and there is no way to bound the delivery latency of buffered messages.
What this does
When
max_batch_ageis set to a number of milliseconds, a batch is dropped instead of sent if ALL of its messages are older thanmax_batch_age(measured from when they were appended to the buffer — the queue-item timestamp, an absolute wall-clock value that survives a disk-replayqreload).Whole-batch granularity is deliberate: a Kafka produce request is a single encoded batch, so partial drops would require re-encoding. A batch is dropped only when every message in it has expired.
The check runs in two places:
maybe_send_to_kafka/1—peeks the front of the queue and drops expired batches before the next send. Queue items are appended in timestamp order, so once the front is not expired nothing behind it is either → an O(#expired) front drain. It runs regardless of connection or inflight-window state, so stale data is shed (and memory reclaimed) even while disconnected. These items are still in the pending-ack backlog → dropped viawolff_pendack:drop_backlog/2. Structurally this ishandle_overflow/3’s twin, but triggered by age at send time rather than by byte-overflow.resend_sent_reqs/2— drops in-flight batches (insent_reqs) that expired while a connection was down, instead of retrying them on reconnect. These are the oldest / most-stale batches and are not inreplayq, so the drain above cannot see them. They are inflight acks → released viawolff_pendack:take_inflight/2.Each dropped message has its ack callback evaluated with the reason
message_expired(a new sentinel alongsidebuffer_overflow_discarded/partition_lost), so async callers are notified rather than waiting forever, and bumps the existingdroppedcounter plus a newdropped_expiredtelemetry counter.infinity(the default) preserves current behavior exactly, with nonow_ts/0call on the hot path.Notes
message_expiredreaches the ack callback; downstream callers that pattern-match ack results should add a clause for it.Tests
is_batch_expired_test_/0— pure unit tests of the predicate (infinity, all-expired, one-fresh, boundary).drop_expired_batch_on_reconnect_test_/0— in-flight batch ages out during a connection drop and is dropped (not retried) on reconnect.drop_expired_queued_batch_test_/0— a queued (never-dispatched) batch ages out and is dropped from the front on the next send.Draft: compiles clean (incl.
warnings_as_errors); the eunit/integration suite has not been run in this environment yet and will be validated in CI / once a broker is free.