Skip to content

Fix buffer size include in flight messages in backpressure calculation logic - #836

Merged
Roman Kolesnev (rkolesnev) merged 2 commits into
masterfrom
fix/buffer-size-include-in-flight
Oct 21, 2024
Merged

Roman Kolesnev (rkolesnev) merged 2 commits into
masterfrom
fix/buffer-size-include-in-flight

Conversation

@rkolesnev

Copy link
Copy Markdown
Contributor

When determination is done if consumer should be paused - the messages in processing queues / worker thread pool queue and threads should be taken into account along with messages in the shards.
Note that pausing can happen sooner now in certain situations - as inflight / queued in thread pool messages were not counted for buffer size previously but now they are counted in.

Checklist

  • Documentation (if applicable)
  • Changelog

@rkolesnev
Roman Kolesnev (rkolesnev) requested a review from a team as a code owner October 18, 2024 17:15
@confluent-cla-assistant

Copy link
Copy Markdown

🎉 All Contributor License Agreements have been signed. Ready to merge.
Please push an empty commit if you would like to re-run the checks to verify CLA status for all contributors.

@airlock-confluentinc
airlock-confluentinc Bot force-pushed the fix/buffer-size-include-in-flight branch from e27d71e to a1414b7 Compare October 18, 2024 17:18
@rkolesnev
Roman Kolesnev (rkolesnev) merged commit b24b5cd into master Oct 21, 2024
@rkolesnev
Roman Kolesnev (rkolesnev) deleted the fix/buffer-size-include-in-flight branch October 21, 2024 09:47
Antony Stubbs (astubbs) added a commit to astubbs/parallel-consumer that referenced this pull request Jul 31, 2026
…isfiable shards-drained condition

Characterization-first: arrival-syncing the original awaitingSelection==0 await
made it time out deterministically on a quiet box (RED, 10.0s at the exact
predicted line), confirming the diagnosis - the static messageBufferSize load
factor caps take-out at 150 of 200 records, so the condition only ever passed
vacuously before partition assignment.

Rewritten as brokerPollPausedWhenBlockedInFlightFillsBuffer: awaits the real
steady state (150 out / 50 awaiting, from named constants - pinning the
take-cap as a regression guard), then asserts the pause, which non-vacuously
verifies upstream confluentinc#836's in-flight-counts-toward-backpressure behaviour for
the first time. 30s bounds (conditions stronger, only wall-clock allowance
grew). Solutions doc records the new 'vacuous await condition' flake class;
changelog + README regenerated; plan units ticked; ledger entry DIAGNOSED+FIXED.

Validation: RED reproducer deterministic; rewritten class green 3/3; full
integration suite 110/110 (forkCount=4) + unit suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Antony Stubbs (astubbs) added a commit to astubbs/parallel-consumer that referenced this pull request Aug 4, 2026
BrokerPollerBackpressureTest.brokerPollPausedWithEmptyShardsButHighInFlight
failed on the highcpu lane (run 30603617471) while the gating hosted run on
the same commit was green. The ledger said "diagnose, don't dismiss". It was
not contention: the test asserted a state the code cannot reach, and every
green run had been passing without testing anything.

The first await waited for getNumberOfWorkQueuedInShardsAwaitingSelection()
== 0 with all ten workers latch-blocked. messageBufferSize(150) with
maxConcurrency(10) makes PCModule.initDynamicLoadFactor pin a STATIC load
factor of 15, so the control loop takes at most 10x15 = 150 records out of
the shards. The test produces 200, nothing ever completes while the workers
are blocked, and the count floors at 50. Zero is unreachable - at any
timeout.

It passed anyway because the await starts before partition assignment
(~1-2s), when the shard map is empty and the condition is vacuously true.
The same-sha hosted run proves it rather than merely suggesting it: the
whole 2-test class finished in 5.5s including two deliberate 1s sleeps, and
draining 200 records through a 150-record cap cannot happen at all, let
alone in that budget. Under load the setup ran long, the await opened after
the records had landed, and the failure became deterministic.

The resemblance to the tight-absolute-timeout contention pattern already
recorded in docs/solutions/test-flakiness/ was therefore a trap: same
symptom, different mechanism, and "loosen the bound" would have restored
the green without restoring the coverage. Not a main-code bug either - the
50-record floor is the static buffer cap working as designed, and the
sibling test passed in the same failing run.

The diagnosis was proven before anything was rewritten. Keeping the original
await but preceding it with an arrival-sync makes it time out on an idle
box, deterministically, first try - load had only been inverting a race.
That reproducer was scaffolding and is not committed.

Rewritten as brokerPollPausedWhenBlockedInFlightFillsBuffer, awaiting the
real steady state: 150 out for processing AND 50 awaiting selection,
asserted together so no partial state satisfies it, both derived from named
constants. That pins the take-cap as a regression guard, and it gives
upstream confluentinc#836's behaviour - blocked in-flight records counting toward
backpressure - its first non-vacuous coverage here, since the 50 records
left in the shards are far below the pause threshold and only the 150
counted in-flight ones can trip the pause. The scenario the old name
described is unreachable by construction: the take-cap and the pause
threshold are the same number.

Await bounds went from 10s to 30s. The conditions got strictly stronger;
only the wall-clock allowance grew, which costs nothing when green and
absorbs a busy box.

The lesson generalises, so it is recorded as its own flake class in
docs/solutions/test-flakiness/vacuous-await-condition-brokerpoller-backpressure-2026-07-31.md:
an await for "X == 0", where X starts at 0 and only later becomes non-zero,
can pass before the system has reached the state under test. The working
plan document was collapsed into that write-up rather than kept beside it -
it was created and completed inside this PR, so its forward-looking half was
inert and its diagnosis narrative duplicated the solutions doc line for
line. The inflight ledger entry shrank to a pointer, per its own scope rule.

The test is inherited from upstream (confluentinc#682 configurable buffer size, confluentinc#836
in-flight backpressure), so the latent race shipped with it and is still
there.

Also folds in the duplication this kept surfacing. BrokerPollerBackpressure-
Test, RetriesTest and DrainCloseTest each opened with the same 18 lines -
consumer/pcOpts/pc fields, then setupTopic() -> createNewConsumer(NEW_GROUP)
-> options builder -> new ParallelEoSStreamProcessor -> subscribe - varying
only in the options the test cares about.
BrokerIntegrationTest.startPcOnNewTopic(options) now does it once, taking
the builder pre-seeded with the consumer, so each setUp keeps just its
distinctive line. The javadoc is written to be found: usage snippet first,
then pointers to the sibling helpers (produceMessages, getTopic, getKcu,
setupTopic, ensureTopic), so the next test author reuses the toolkit instead
of re-deriving it and getting the setup subtly wrong.

It returns the processor rather than assigning inherited fields on purpose:
five other subclasses already declare their own consumer/pc fields, which
base-class fields of the same name would silently shadow - the exact class
of trap the refactor exists to remove. Additive to the base, no lifecycle
change, so the other 18 integration tests are untouched. PMD CPD: clones
33 -> 29, duplicated lines 1001 -> 874.

Verified: RED reproducer deterministic on an idle box; rewritten test green
across repeated runs; the three refactored classes green; full integration
suite 110/110 (forkCount=4) plus the unit suites, re-run after merging master
so the result includes #100's broker-poll / rebalance-commit change.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants