Skip to content

fix(sdk): respect max_buffer_size when merging batches - #3933

Open
haubur wants to merge 1 commit into
apache:masterfrom
haubur:fix/sdk-producer-max-buffer
Open

fix(sdk): respect max_buffer_size when merging batches#3933
haubur wants to merge 1 commit into
apache:masterfrom
haubur:fix/sdk-producer-max-buffer

Conversation

@haubur

@haubur haubur commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR address?

Closes #3932

Rationale

Respect the max_buffer_size such that the BackpressureMode can kick in.

What changed?

Merge both inner IggyMessages and the OwnedSemaphorePermits (bytes_permit), freeing only when written, not after merge.

Local Execution

  • Passed
  • Pre-commit hooks ran

AI Usage

None

@github-actions

Copy link
Copy Markdown

Thanks for the PR. It is labeled S-waiting-on-review and queued for review.

Slash commands (own line, regular comment) move it around the queue:

  • /ready - back to S-waiting-on-review after addressing feedback
  • /author - flip to S-waiting-on-author while you finish changes
  • /request-review @user-or-team - request a reviewer

See CONTRIBUTING.md for details.

@github-actions github-actions Bot added the S-waiting-on-review PR is waiting on a reviewer label Aug 20, 2026
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.31%. Comparing base (c0c7493) to head (635eed5).

Additional details and impacted files
@@              Coverage Diff              @@
##             master    #3933       +/-   ##
=============================================
- Coverage     83.87%   70.31%   -13.56%     
  Complexity     1358     1358               
=============================================
  Files          1212     1212               
  Lines        166843   142867    -23976     
  Branches     134306   110454    -23852     
=============================================
- Hits         139937   100459    -39478     
- Misses        23266    38588    +15322     
- Partials       3640     3820      +180     
Components Coverage Δ
Rust Core 67.07% <100.00%> (-17.58%) ⬇️
Java SDK 66.67% <ø> (ø)
C# SDK 75.06% <ø> (-1.49%) ⬇️
Python SDK 90.13% <ø> (ø)
PHP SDK 84.48% <ø> (ø)
Node SDK 95.84% <ø> (ø)
Go SDK 68.32% <ø> (ø)
Files with missing lines Coverage Δ
core/sdk/src/clients/producer_sharding.rs 95.85% <100.00%> (+1.18%) ⬆️

... and 354 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

/// charged against the `max_buffer_size` budget until the merged batch has been written.
fn merge(&mut self, other: Self) {
self.inner.messages.extend(other.inner.messages);
self.bytes_permit.merge(other.bytes_permit);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OwnedSemaphorePermit::merge sums u32 permit counts, and after this change the sum is bounded only by max_buffer_size — which nothing validates.

IggyByteSize::from(u64) accepts any value and BackgroundConfig has no validator, so a max_buffer_size above 4 GiB is constructible. With batch_size(0) + batch_length(0) and a long linger, one shard buffer accumulates adjacent same-destination batches until the merged sum passes u32::MAX. Note this needs only normal-sized batches — each individual charge is lossless, e.g. 4096 x 1 MiB against a 6 GiB budget; it is the sum that wraps.

Reproduced on tokio 1.53.1:

  • release: wraps silently and permanently destroys permits (6 GiB budget leaves 1.7 GiB available after one merge + drop), after which the default BackpressureMode::Block blocks send() forever with no error and no log.
  • debug: panics attempt to add with overflow inside the shard task, where it is swallowed — the JoinHandle at line 135 is never awaited, so no test in this file can observe it.

Worth separating from the pre-existing issues nearby: this is a new failure mode, since nothing summed permits before this change. On master the same configuration merely under-enforces the budget and the producer keeps running.

Suggested fix is one line at construction — reject max_buffer_size > u32::MAX in BackgroundConfig / ProducerDispatcher::new. That also closes the related truncation at producer_dispatcher.rs:135, where IggyByteSize::as_bytes_u32() charges size mod 2^32 (exactly 0 at whole multiples of 4 GiB, so such a batch bypasses the budget entirely).

}

/// Takes over `other`'s messages together with its byte permit, so the buffered bytes stay
/// charged against the `max_buffer_size` budget until the merged batch has been written.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This doc states the new contract correctly, but the public documentation of max_buffer_size no longer matches it.

core/sdk/src/clients/producer_config.rs:108-109 still says:

Upper bound for the bytes held in memory across all shards.

After this change permits are held through core.send_internal until the write returns, so the budget covers buffered plus in-flight bytes, and it now couples to max_in_flight. That is the user-visible semantic change this PR makes, and it is currently undocumented.

The practical consequence is that Block-mode producers can block where they previously did not, most visibly on the linger-only shape (batch_length(0) + batch_size(0)) that this repo's own integration tests use at core/integration/tests/sdk/producer/background.rs:135-136,177-178,216-217, which pins close to the whole budget for a full write RTT. Default config pins under 3% (32 MiB budget against a 1 MiB batch_size), so there is no out-of-the-box cliff.

Suggest amending the max_buffer_size doc to say the budget spans buffered and in-flight bytes until the write completes, and calling the behavior change out in the release notes.

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

Labels

S-waiting-on-review PR is waiting on a reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(Rust SDK): on merge of ShardMessages the buffer budget is freed before messages are send

2 participants