Repository navigation
fix(core): draining PC stops polling - 10kHz busy-spin + zombie partition hold (#857-family silent stall) - #80
Conversation
…same-repo only) Optional, non-gating fast feedback that runs unit, integration and performance as parallel matrix jobs on a self-hosted many-core runner (label `highcpu`), guarded to same-repo PRs so fork code never executes on it. The GitHub-hosted gate in maven.yml stays the required check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
…rkflow # Conflicts: # .github/actionlint.yaml
…-only The high-CPU self-hosted matrix now runs unit + integration + performance + mutation as parallel jobs (a strict superset of the mac runner, which is offline indefinitely). The mac workflow is switched to workflow_dispatch-only so it stops queuing dead jobs on every PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
failingHttpCall drove an HTTP call at a dotless bogus hostname ("xxxxxxxxx") and asserted the failure
was a DNS *resolution* error ("failed resolve"). That only holds where the name fails to resolve - on any
network with a local resolver + search domain (e.g. a Pi-hole LAN) the name resolves and the failure mode
becomes "connection refused", so the test fails there while passing on GitHub's public DNS.
Point the bad request at a closed local port (127.0.0.1:1) instead: an immediate, deterministic
"connection refused" on every environment, no DNS lookup at all (and faster than one). Keep the real
failed==true check; assert the deterministic "connection refused" message. Found bringing up the
self-hosted high-CPU runner.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
…with failingHttpCall) The failingHttpCall fix (127.0.0.1:1 -> connection refused) also flipped this sibling test through the shared getBadRequest() helper, so its assertion is updated to match. Caught by ce-review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
Load was ~18/32 with 34GB free - the box is I/O-bound (cold cache), not CPU-bound - so use all the cores. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
…ateCommittedOffsetIT Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
…epo guard (ce-review P2) The if: guard lives in the PR-head file, so the real RCE backstop is the repo Actions setting "require approval for all outside collaborators". Documented in the workflow header + referenced SELF_HOSTED_RUNNER.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
Unit forkCount 1C->2C, Integration 8->16 (probe the oversubscription ceiling). Mutation PIT threads 2->16 (CPU-bound, stays in the matrix). Performance split into its own job that needs the matrix, so the throughput benchmark measures on a quiet box (solo 1m54 vs 7m18 when it shares the box). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
…formance Doubling was flat (unit 2C=1C) to worse (integration 16 slower than 8), so back to the knee. Performance returns to the concurrent matrix - it is a correctness gate here; noisy timing is fine and isolation wrecked total wall-clock (it queued behind a 17-min mutation). Mutation keeps threads=16. Accurate benchmarking -> separate on-demand run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
Unit/Integration now also enable JUnit method-parallelism (-Dparallel-tests=true) on top of forking, forks reduced so forks x threads ~= cores - tests whether thread-parallelism is still flaky after recent fixes (revert to fork-only if red). ci-mutation-test.sh: on a PR (GITHUB_BASE_REF) mutate ONLY changed core classes, skip if none - the full internal.* sweep has never completed. Full sweep still runs for push/nightly (no base ref). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
…eads finding fork×threads (parallel-tests=true) was safe (threading no longer races - unit green; integration red was the known flaky PartitionStateCommittedOffsetIT) but no faster than fork-only, so back to 1C/8. PIT: grep no-match under set -euo pipefail made the changed-class skip exit non-zero (0m19 red); wrap grep with || true so it exits 0. Recorded the thread-parallelism-vs-forking finding in inflight next to the shelved Step-2 question. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
…bserve a green board TEMPORARY - not a fix. Disables the known-flaky PartitionStateCommittedOffsetIT.committedOffsetRemoved (awaitility timeout, in inflight, fails on GitHub-hosted too) purely to see a fully-green highcpu run. Revert and harden the test properly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
Now that ci-mutation-test.sh scopes PIT to changed classes on PRs (GITHUB_BASE_REF), it is fast enough for the 2-core hosted runner - so mutation runs on GitHub-hosted as well, not just the self-hosted box. Non-gating (continue-on-error). fetch-depth: 0 so the changed-class diff resolves. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
…already passed Each GitHub-hosted test job, before running its slow 2-core suite, polls whether the self-hosted highcpu equivalent for the same commit already PASSED (ci-concede-check.sh). If so it skips and goes green; the hosted job stays the required gate but finishes faster when grumpy wins. Never depends on the self-hosted runner: offline/queued/slow/failed -> run the tests normally. Bounded wait, continue-on-error so a check bug can never fail the gate. Caveat documented: conceding trusts the self-hosted env (env-specific failures could be missed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
… the green-board experiment) Reverts the temporary experiment disable. The test is still the known flaky awaitility timeout (tracked in inflight) - harden it (adaptive await / underlying timing) rather than leave it disabled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
…fluentinc#857 hunt) DEBUG-only, behaviour-preserving observability for the silent-stall family: - ShardManager.getWorkIfAvailable: under-served detector - when less work is returned than requested while work is still tracked, log the breakdown [awaitingSelection, inFlight, retryQueue.size, readyToRetry] so 'asked for work / got none / but it's in the queues (or missing)' can be told apart from normal in-flight back-pressure. - WorkManager.isSufficientlyLoaded: log the throttle-decision inputs (awaitingSelection + outForProcessing vs target*loadingFactor). This gates broker-poller pause/resume, so numberRecordsOutForProcessing counter drift (confluentinc#857) shows up here as 'loaded' with no real work. - KafkaClientUtils.buildPc: unique myId per test PC so pc-control-PCn / pc-broker-poll-PCn thread names and the pcId MDC make concurrent instances attributable in logs (the myId hook was never wired; all instances collided). Safe: RebalanceEoSDeadlockTest matches thread names with .contains(). - logback-test.xml: labelled (commented) logger set for the stall hunt. These diagnostics fired during the forkCount=16 reproductions documented in docs/solutions/test-flakiness/pc-silent-stall-under-contention-2026-07-29.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbtaCN1Re14pJ99VFW4R18
…ain-path spin + zombie partition hold (confluentinc#857 family) Research report assessing PR #75's red Integration check: not the grumpy runner, and NOT a benign flaky timeout - a PC under contention polls zero records even with a 120s bound (a stall, not slowness). No test was masked. Root-caused a drain-path defect: drain() fires ConsumerManager.signalStop() whose shutdownRequested flag short-circuits poll() so consumer.poll() is never called - defeating the intended 'paused 2s long poll = drain-loop sleep' (handlePoll() comment). Measured 92,880 poll-loop iterations in 9.0s (~10kHz busy-spin). With consumer.poll() never invoked, a draining PC cannot join rebalances while background heartbeats keep it a live member - a zombie holding its partitions (up to max.poll.interval.ms) while burning a core. Maps directly onto confluentinc#857's 'paused consumption after rebalance' (partition- hold hypothesis credit: Antony). Report includes a drain fail-safe design review (honour the 2s poll; hard deadline + explicit LeaveGroup; eager leave trade-offs; spin watchdog) and the reproduction recipe. inflight.md entry reframed accordingly: DO NOT MASK, route to confluentinc#857 (PR #29). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbtaCN1Re14pJ99VFW4R18
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
✅ Duplicate Code ReportTwo engines run in parallel for cross-validation. Each has its own thresholds tuned to its baseline - the real safety net is the per-engine "max increase vs base" check. ✅ PMD CPD
No new clones introduced by this PR. ✅ jscpd (language-agnostic)
|
✅ SpotBugs ReportNo bugs found (new bugs only — baseline from base branch excluded). |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## ci/grumpy-runner-workflow #80 +/- ##
===============================================================
- Coverage 78.01% 68.36% -9.65%
+ Complexity 1072 851 -221
===============================================================
Files 81 76 -5
Lines 4039 3806 -233
Branches 372 370 -2
===============================================================
- Hits 3151 2602 -549
- Misses 712 1007 +295
- Partials 176 197 +21
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…anch experiment Checked both in-flight stall PRs against the zombie-drain root cause: - PR #29 (confluentinc#857): drain() still calls signalStop() first and the while(!shutdownRequested) short-circuit is intact - the ~10kHz spin and zombie partition hold survive unchanged. It fixes a SIBLING mechanism (onPartitionsAssigned resets pausedForThrottling - RUNNING-state throttle-pause) and even adds trace logging inside the loop that spins. Its 10-20% residual chaos-stall rate is a testable prediction: the chaos monkey's stop/start cycling is a drain-window factory. - PR #31 (confluentinc#909): main-code diff is only ProcessingShard.java - no overlap. All three efforts chase the same 'alive but not progressing' symptom via different, non-conflicting mechanisms, so the report now recommends an uber-branch experiment: merge #29 + #31 + the minimal drain fix (defer signalStop() to CLOSING), then measure #29's chaos run and this report's forkCount=16 recipe - if the residual stall rate drops to ~0, the zombie-drainer mechanism explains the residue. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbtaCN1Re14pJ99VFW4R18
The check name is "<workflow> / <job>", and both halves carried "High-CPU"
plus verbose filler ("Build & Test - ...", "self-hosted, optional"), pushing
past the PR merge-box truncation width.
- workflow name: "PR High-CPU Fast Feedback" -> "highcpu"
- job name: "Build & Test - <suite> (high-CPU self-hosted, optional)"
-> "<suite> (optional)" (keeps the non-gating signal)
Result: "highcpu / Unit (optional)", "highcpu / Integration (optional)", etc.
- longest ~35 chars, well under the cutoff.
Kept the concede matcher in sync (it resolves the highcpu run/jobs by name):
bin/ci-concede-check.sh WORKFLOW default -> "highcpu", JOB_PREFIX -> "${SUITE}"
(the job is now named "<suite> (optional)", so a suite-name prefix still
uniquely matches). Added a keep-in-sync comment on the job. Updated the
SELF_HOSTED_RUNNER.md reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ytagqk6daL4uTtbebQNNv
… never invoked while draining BrokerPollSystemDrainTest parks in-flight work on a latch, starts close(DRAIN) on another thread, and counts consumer.poll() invocations on the consumer spy across three long-poll periods of the drain window. CHARACTERISATION (asserts the CURRENT broken behaviour): the count is ZERO. drain() fires ConsumerManager.signalStop() before entering DRAINING, and poll()'s while(!shutdownRequested) guard then short-circuits without ever calling consumer.poll() - so the loop's intended sleep (the paused 2s long poll, per handlePoll()'s own comment) never happens (~10kHz spin measured), and the consumer cannot participate in rebalances while background heartbeats keep it a live member zombie-holding its partitions. The follow-up commit collapses the duplicated lifecycle flag and flips this test to assert the desired behaviour (poll continues at long-poll cadence). Also adds the implementation plan: docs/plans/2026-07-29-002-fix-drain-zombie-spin-and-uber-experiment-plan.md Full unit suite green (one unrelated pre-existing ReactorPCTest flake, passed on rerun). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbtaCN1Re14pJ99VFW4R18
…down flag into poll-system lifecycle ConsumerManager.shutdownRequested was a private shadow of BrokerPollSystem.runState, settable out of phase - and it was: drain() set it on entry to DRAINING, so poll()'s while(!shutdownRequested) guard short-circuited and consumer.poll() was never invoked again during the drain window. Measured consequences: the poll loop busy-spun at ~10k iterations/s (the paused 2s long poll that handlePoll()'s own comment designates as the loop's sleep never ran), and the consumer became a rebalance-unresponsive zombie group member - background heartbeats kept it alive holding its full partition assignment (up to max.poll.interval.ms) while consuming nothing, starving same-group siblings. Maps onto upstream issue confluentinc#857's 'paused consumption after rebalance'. The fix collapses the duplicated state rather than re-timing it: - ConsumerManager: shutdownRequested + signalStop() deleted. The poll guard, SASL retryBackOff abort, and commitSync retry abort now consult an injected closeInProgressSignal (defaults to 'never' for standalone use). close() no longer raises a flag - by then the poll system is already CLOSING/CLOSED so the signal reports true to in-flight retry loops. Side-benefit: commits/retries during DRAIN are no longer wrongly aborted. - BrokerPollSystem: wires closeInProgressSignal = (runState == CLOSING or CLOSED) in its constructor - runState is the single source of truth, so the desync class is structurally impossible. Both signalStop() call sites removed; a draining consumer keeps polling (paused, long-poll cadence) and can promptly ack revokes/rejoin. runState is now volatile (it was already read cross-thread; the signal adds committer-thread readers). Test flipped RED->GREEN: BrokerPollSystemDrainTest, committed first as a characterisation of the defect (0 polls during drain), now asserts the desired property (>=1 poll per window at long-poll cadence, bounded above - no spin) and that close still completes cleanly. Full unit suite green (one unrelated pre-existing ReactorPCTest flake, passed on rerun, identical on the pre-fix baseline run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbtaCN1Re14pJ99VFW4R18
The build's README generator embeds CHANGELOG.adoc; this syncs the generated README.adoc with the changelog entry added in the drain-fix commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbtaCN1Re14pJ99VFW4R18
…ring handover during drain Integration-level guard for the drain fix, calibrated against the planted defect on the experiment arms: - GREEN on this branch (fix present): B, joining mid-drain, consumed 200 records while A was still draining; A closed promptly on release; ledger: A=1100, B=200, duplicates=100 - exactly the parked in-flight tail (maxConcurrency), NOT a wholesale reprocess: the handover honoured A's commits. - RED on the no-fix arm (defect present): the group stayed frozen while A drained (B starved) until the worker-pool force-interrupt bailed A out - the mid-drain liveness window could never be established. Encodes the drain semantics explicitly (see report design review): a draining member CORRECTLY keeps its partitions to finish + commit (that is what bounds duplicates); what it must also do is stay rebalance-responsive so a mid-drain join never freezes the group. Report updated in the same spirit: design-review option 3 (eager LeaveGroup at drain start) REJECTED - it would guarantee duplicate reprocessing of in-flight work; explicit LeaveGroup belongs only in the deadline-expiry fail-safe (option 2). PR #29/#31 non-fix verification notes sharpened. Also the W1 skateboard scenario of the Chaos Pain Suite design (separate branch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbtaCN1Re14pJ99VFW4R18
…nst #80 This branch was cut before the rebrand and before the rest of the confluentinc#857 family landed, so it still targeted `master-confluent` and sat 61 commits behind. Three conflicts, none of them mechanical: - `ConsumerManager` - the branch added `claimConsumerOwnership()` and `signalStop()` side by side. #80 deleted the whole `shutdownRequested` flag, collapsing it into the poll system's `runState` (see `BrokerPollSystemDrainTest`), and rewrote the drain path that was `signalStop()`'s only caller. Kept the ownership claim, dropped `signalStop()` as dead - reinstating it would restore the second source of truth #80 removed. - `ManagedPCInstance` - both sides added it. Master's copy is a strict superset (MDC cleanup on pooled threads, `extraConsumerProps`, try/finally), so master's wins outright. - `pom.xml` - took master's test dependency versions and the byte-buddy pin, and dropped the branch's duplicate `archunit-junit5` declaration; master already declares it at 1.4.2 for all modules. Also brought the branch's headers under the fork's provenance policy, which postdates it: fork-original files (`ThreadConfinedConsumer`, `ArchitectureTest`, `ShardManagerStaleContainerTest`, `ManagedPCInstanceLifecycleTest`) now carry the fork header rather than a Confluent one, and the five modified upstream-derived files carry the modifications line. `MultiInstanceRebalanceTest` also had its Confluent year bumped to 2026 on the branch; restored to 2020-2022 per policy. Compiles clean across main, test and test-integration sources.
…ach the gate about anchors Two things the reference sweep surfaced in Java sources. The quarantine script tests built their fixture registries with "Owner: PR #999", "PR #80" and "PR #123". The last two are real fork PRs, so the fixtures read as genuine references to anyone grepping, and #999 is close enough to the live range to be mistaken for one. They are now #999999 and #999998 - unmistakably fake, and above the threshold where a bare number is ambiguous. Two distinct values, because the owner-mismatch tests compare a @Quarantined value against a registry value and need them to differ. 27 tests pass. The gate also flagged references that were already qualified by an html anchor - TransactionMarkersTest has <a href=".../issues/329">Github issue #329</a>, where the href names the repo and the number in the link text is unambiguous to any reader. stripQualified removed bare URLs but not the anchor element, so the visible "#329" looked bare. It now strips the whole element, with a test confirming a bare number elsewhere on the same line is still caught. Together these take the tree-wide backlog from 390 to 374, and the Java share from 53 to 36. The follow-up note now carries the finished classification of all 36 Java references - file, line, and which repo each means, resolved against both - so that pass does not repeat this research. Also records that @tag("#355") is a tag-string rename rather than a prose prefix, and that nothing selects on that tag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QqHpNSXC39ANv9kG1ZvUzn
…r repo Mirrors all 78 open issues from confluentinc/parallel-consumer into this fork (#44, #117-#195, label upstream-mirror), each carrying a code-backed diagnosis, and each backlinked from its upstream original while that is still possible - archival kills writes, not reads, so the backlinks were the half with a deadline. Seven are closed against a released version. Everything else here follows from that. Mirroring made bare issue numbers ambiguous. The fork numbers from 1 and confluentinc reaches confluentinc#922, so the ranges overlap completely: of the 51 numbers cited across the files this touches, 48 exist in BOTH repos meaning different things. #29 is our rebalance fix and confluentinc#29 is an async-sending request; #114 is a docs PR and confluentinc#114 is a GPG key issue. So a reference now names its repo below #1000, and a CI gate enforces it on added lines. The gate went through three designs, and the discarded two look plausible enough to be worth recording. Comparing against "the fork is at #N" raced - CI read 196 while #197 already existed. Checking whether a number resolves here fails worse: `#200` resolves, to a fork issue about ManagedTruth, while the author meant confluentinc#200, shared-nothing architecture. A wrong reference that resolves is worse than a broken one, because nothing looks amiss. The rule is textual, so it makes no API calls and cannot race. The qualifier names the owner rather than the role - confluentinc#857, not "upstream confluentinc#857". "Upstream" describes a relationship and is not stable: this repo is upstream to anyone who forks it. "Fork" is out for the same reason. Also swept every reference in the files touched here, fixed the source comments behind the generated TODO index rather than the index, and stopped the quarantine fixtures borrowing real PR numbers - #80 and #123 are live fork PRs, so the fixtures read as genuine references. The map shrinks to match: upstream-map.yaml tracks upstream PRs only, because issues now live in the mirror, and the manifest-driven backlink tooling is retired - it commented one issue per map entry, and the map no longer holds issues. Two corrections the work surfaced: the sweep was reporting our own backlink comments as upstream activity, hiding the two real replies among all 78; and confluentinc#548 was recorded open when it merged in 2023 and is already in this fork. Remaining tree-wide references are deliberately out of scope, tracked in docs/inflight/next-qualify-remaining-refs.md with the Java set already classified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QqHpNSXC39ANv9kG1ZvUzn
…r repo Mirrors all 78 open issues from confluentinc/parallel-consumer into this fork (#44, #117-#195, label upstream-mirror), each carrying a code-backed diagnosis, and each backlinked from its upstream original while that is still possible - archival kills writes, not reads, so the backlinks were the half with a deadline. Seven are closed against a released version. Everything else here follows from that. Mirroring made bare issue numbers ambiguous. The fork numbers from 1 and confluentinc reaches confluentinc#922, so the ranges overlap completely: of the 51 numbers cited across the files this touches, 48 exist in BOTH repos meaning different things. #29 is our rebalance fix and confluentinc#29 is an async-sending request; #114 is a docs PR and confluentinc#114 is a GPG key issue. So a reference now names its repo below #1000, and a CI gate enforces it on added lines. The gate went through three designs, and the discarded two look plausible enough to be worth recording. Comparing against "the fork is at #N" raced - CI read 196 while #197 already existed. Checking whether a number resolves here fails worse: `#200` resolves, to a fork issue about ManagedTruth, while the author meant confluentinc#200, shared-nothing architecture. A wrong reference that resolves is worse than a broken one, because nothing looks amiss. The rule is textual, so it makes no API calls and cannot race. The qualifier names the owner rather than the role - confluentinc#857, not "upstream confluentinc#857". "Upstream" describes a relationship and is not stable: this repo is upstream to anyone who forks it. "Fork" is out for the same reason. Also swept every reference in the files touched here, fixed the source comments behind the generated TODO index rather than the index, and stopped the quarantine fixtures borrowing real PR numbers - #80 and #123 are live fork PRs, so the fixtures read as genuine references. The map shrinks to match: upstream-map.yaml tracks upstream PRs only, because issues now live in the mirror, and the manifest-driven backlink tooling is retired - it commented one issue per map entry, and the map no longer holds issues. Two corrections the work surfaced: the sweep was reporting our own backlink comments as upstream activity, hiding the two real replies among all 78; and confluentinc#548 was recorded open when it merged in 2023 and is already in this fork. Remaining tree-wide references are deliberately out of scope, tracked in docs/inflight/next-qualify-remaining-refs.md with the Java set already classified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QqHpNSXC39ANv9kG1ZvUzn
…r repo Mirrors all 78 open issues from confluentinc/parallel-consumer into this fork (#44, #117-#195, label upstream-mirror), each carrying a code-backed diagnosis, and each backlinked from its upstream original while that is still possible - archival kills writes, not reads, so the backlinks were the half with a deadline. Seven are closed against a released version. Everything else here follows from that. Mirroring made bare issue numbers ambiguous. The fork numbers from 1 and confluentinc reaches confluentinc#922, so the ranges overlap completely: of the 51 numbers cited across the files this touches, 48 exist in BOTH repos meaning different things. #29 is our rebalance fix and confluentinc#29 is an async-sending request; #114 is a docs PR and confluentinc#114 is a GPG key issue. So a reference now names its repo below #1000, and a CI gate enforces it on added lines. The gate went through three designs, and the discarded two look plausible enough to be worth recording. Comparing against "the fork is at #N" raced - CI read 196 while #197 already existed. Checking whether a number resolves here fails worse: `#200` resolves, to a fork issue about ManagedTruth, while the author meant confluentinc#200, shared-nothing architecture. A wrong reference that resolves is worse than a broken one, because nothing looks amiss. The rule is textual, so it makes no API calls and cannot race. The qualifier names the owner rather than the role - confluentinc#857, not "upstream confluentinc#857". "Upstream" describes a relationship and is not stable: this repo is upstream to anyone who forks it. "Fork" is out for the same reason. Also swept every reference in the files touched here, fixed the source comments behind the generated TODO index rather than the index, and stopped the quarantine fixtures borrowing real PR numbers - #80 and #123 are live fork PRs, so the fixtures read as genuine references. The map shrinks to match: upstream-map.yaml tracks upstream PRs only, because issues now live in the mirror, and the manifest-driven backlink tooling is retired - it commented one issue per map entry, and the map no longer holds issues. Two corrections the work surfaced: the sweep was reporting our own backlink comments as upstream activity, hiding the two real replies among all 78; and confluentinc#548 was recorded open when it merged in 2023 and is already in this fork. Remaining tree-wide references are deliberately out of scope, tracked in docs/inflight/next-qualify-remaining-refs.md with the Java set already classified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QqHpNSXC39ANv9kG1ZvUzn
…r repo Mirrors all 78 open issues from confluentinc/parallel-consumer into this fork (#44, #117-#195, label upstream-mirror), each carrying a code-backed diagnosis, and each backlinked from its upstream original while that is still possible - archival kills writes, not reads, so the backlinks were the half with a deadline. Seven are closed against a released version. Everything else here follows from that. Mirroring made bare issue numbers ambiguous. The fork numbers from 1 and confluentinc reaches confluentinc#922, so the ranges overlap completely: of the 51 numbers cited across the files this touches, 48 exist in BOTH repos meaning different things. #29 is our rebalance fix and confluentinc#29 is an async-sending request; #114 is a docs PR and confluentinc#114 is a GPG key issue. So a reference now names its repo below #1000, and a CI gate enforces it on added lines. The gate went through three designs, and the discarded two look plausible enough to be worth recording. Comparing against "the fork is at #N" raced - CI read 196 while #197 already existed. Checking whether a number resolves here fails worse: `#200` resolves, to a fork issue about ManagedTruth, while the author meant confluentinc#200, shared-nothing architecture. A wrong reference that resolves is worse than a broken one, because nothing looks amiss. The rule is textual, so it makes no API calls and cannot race. The qualifier names the owner rather than the role - confluentinc#857, not "upstream confluentinc#857". "Upstream" describes a relationship and is not stable: this repo is upstream to anyone who forks it. "Fork" is out for the same reason. Also swept every reference in the files touched here, fixed the source comments behind the generated TODO index rather than the index, and stopped the quarantine fixtures borrowing real PR numbers - #80 and #123 are live fork PRs, so the fixtures read as genuine references. The map shrinks to match: upstream-map.yaml tracks upstream PRs only, because issues now live in the mirror, and the manifest-driven backlink tooling is retired - it commented one issue per map entry, and the map no longer holds issues. Two corrections the work surfaced: the sweep was reporting our own backlink comments as upstream activity, hiding the two real replies among all 78; and confluentinc#548 was recorded open when it merged in 2023 and is already in this fork. Remaining tree-wide references are deliberately out of scope, tracked in docs/inflight/next-qualify-remaining-refs.md with the Java set already classified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QqHpNSXC39ANv9kG1ZvUzn
Reviewing the new solution doc surfaced a hole in the checklist that same PR had just added: it said to check open PRs, which is a collision check. The PR that already solved something in your file is by definition merged, so searching only the open list finds nothing and feels like diligence - worse than not looking, because it produces false confidence. Same for issues: the useful ones are usually closed. Searching merged PRs BY TOUCHED FILE - not by keywords in their titles - finds, for this investigation: - #110, the sibling flake on this same producerTransactionLock, fixed four days earlier and the source of the control-arm method - #86, which introduced the ambient probe whose "probe clean" verdict this work had to qualify - #98, the backpressure test that only passed by racing its own setup - the sibling rule this doc deliberately does not merge with None would surface from a symptom search or from the open list. All three are now cited in the solution doc, with #80, #115 and #68 from the same family, and the confluentinc#803 / confluentinc#809 / confluentinc#833 cluster upstream. Adds to the doc what the prior-art search actually covered, including what it did NOT (the highcpu and quarantine lanes), so the next reader knows where the gaps are instead of assuming there are none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e upstream map Every remaining line-number citation in the tree had already drifted onto something else, which is the failure mode this PR exists to remove: the file and the section are both intact, so the pointer still reads as valid. The chaos-suite plan's pom pointer now lands on `<skipTests>`, an unrelated build property. It meant the `-Dexcluded.groups=` override comment, so it names that instead. The drain/zombie-spin plan cited `BrokerPollSystem` and `ConsumerManager` throughout its research section. None of those numbers resolves today, because the two names the plan turns on - `signalStop` and `shutdownRequested` - were deleted by the very fix the plan proposed, landed in bd71724 (#80). The bullets keep their method names as anchors and gain a pointer to `bd7172418^` for the pre-fix code, plus the `closeInProgressSignal` sites that succeeded the flag. The wakeup-guard pointer did not resolve even before that fix, so it says which guard was meant rather than guessing which line was. The silent-stall write-up verified its finding against PR #29's branch, which has since merged master and so no longer contains either name - both cited lines there are now blank. The finding stands as a dated record; the repair says where to look and states that it does not re-verify. The untracked-flakes note pins its number to "the commit CI ran", which is never named, and on master that number now lands on a different `waitAtMost` block - close enough to the description to be believed by anyone who checks. The number stays, because it is what the failure report said rather than a pointer the note chose, and the assertion the note already quotes becomes the anchor. Widening the sweep past `docs/` found one more that every earlier pass had missed for that reason alone: the upstream map's `PartitionState` pointer, whose field and constructor citations had each drifted onto a neighbouring line. Captured tool output is not a citation and is untouched - the PIT rows, the surefire rows and the stack-trace frame - as are `#L` permalinks, which are pinned to a SHA and do not drift. No claim, finding or conclusion is changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fpk9yEDEhPnbad2WKvZUmP
…283) Documents cited source locations as `file:line`. Line numbers drift: a commit inserting lines above the target invalidates the citation while the file and the section are both still intact, and nothing checks it, so the citation goes on reading as valid. Every such citation now uses an anchor that cannot drift - the file path plus a distinctive greppable string. This is a repair, not a tidy-up, because most of the citations had already broken. A pom citation landed on `<skipTests>`. Two landed on blank lines. One in `ParallelConsumerOptions` pointed at a field it described as deprecated, which is not deprecated - the deprecated one is several fields further down. Two in `PartitionState` landed on plausible neighbours (`private final PCMetrics pcMetrics;`), which is the dangerous failure: a reader who checks finds something credible and wrong. Several drifted for the same reason, and it is worth stating because it will recur. The citations named `signalStop` and `shutdownRequested` - the fields the documents proposed removing - and those fields were deleted by #80, the fix those documents argued for. A document that succeeds invalidates its own citations. References to `docs/inflight.md` were dangling outright: it was deleted when the ledger became a directory. Live instructions that told a reader to go update it now name the successor note; historical mentions keep their wording and gain `git show 0de96fc^:docs/inflight.md`, so the record still resolves. Where nothing succeeded an item - the deleted flaky-gate entry, whose own text said to remove it once its fix merged, and it did - the repair says so rather than naming a plausible neighbour. The search had to be widened five times, and each widening found citations the narrower one could not see. `ClassName:NN` forms carry no file extension, so an extension-anchored pattern misses them. Bare `:NN` continuations carry no filename. Approximate prose - "~line 1348", "~lines 1340-1425" - drifts exactly like an exact form while surviving any search for a colon followed by digits. Bare `L218:` lists carry neither extension nor colon-prefix. And a `docs/` pathspec cannot see `src/docs/`. One sweep reported clean because `git grep -E` was given `\b`, which POSIX ERE does not support: it matched nothing and exited non-zero, which is indistinguishable from a clean tree. That last failure is the general one. A verification result is worth only what its detector could have caught, and every under-report here came from an instrument that could not have fired - a link check that only matched markdown link syntax when the references were bare backticked paths, a `file:line` sweep that could not match a deleted-file reference carrying no line number. Every anchor was verified by running the grep it implies and confirming exactly one match. That step earned itself twice: review found an anchor naming a local variable reused in two methods, and a later round rejected `commitAndWait`, `doStep` and `syncBeginTransaction` for the same reason. A unique-looking identifier is not a unique thing; the anchors now use declaration-level forms. AGENTS.md gains the rule, with a distinction it was missing. Dated documents under docs/plans/ and docs/solutions/ are historical records and must not be rewritten to say what they did not say. That convention is right and was being read too broadly, as though it also forbade fixing a broken pointer. Rewriting a document's claims to match today's code falsifies what was known at the time; repairing a reference so it still finds what the document pointed at is required, because a citation that no longer resolves makes the record less faithful, not more. The repair procedure itself moved to docs/citations.md - it fires only when editing a dated record, and the root file is for what every agent needs whatever it is doing. Four rejected alternatives are worth recording. Converting everything matching the pattern would have been wrong. Several `file:line` forms are deliberately preserved - PIT survivor listings, a verbatim stack trace, surefire failure rows - because they are captured tool output, not citations, and rewriting them would falsify a record of what a tool printed. The test is not whether something looks like `file:line`, but whether it is a pointer or a transcript. GitHub permalinks pinned to a full SHA are kept for the same reason: they do not drift. Repairing the citations in docs/inflight/release-0.6.0.0.md was also wrong, and review caught it. Two entries there describe finished work, so repairing their anchors made two closed jobs point more precisely at themselves and read as outstanding. docs/inflight/ is a live ledger, not a historical record: its rules say a closed entry is deleted, not made accurate. Pointing a repair at the nearest plausible successor was rejected after review found two cases. A flake fixed by #110 had been pointed at an adjacent open item, and a report about one race had been pointed at a different, still-open one. Worse, a live entry exists under the same test name for a different defect entirely. Where the correct successor does not exist, the pointer names the history and says what it does not establish. Making this a standing check was rejected last. A permanent gate would have to name the pattern it forbids, and the enforcement this needs is a review habit, not a job that runs forever on a problem that has been fixed once. Nothing any document claims, concludes or reports was changed. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… expires Chaos Pain Suite went red on this branch with an ambient-probe kill in ChaosChurnStormIT.churnStormMeetsSlosAndBalancesLedger. The sighting joins the confluentinc#857 family's existing ones. The reason it is captured on a fork-identity branch rather than left in CI is that the replay seed cannot be recovered once the log and artifact expire, and the seed is what makes the run reproducible at all. Everything else in the entry is retrievable; that number is not. The signature is ZOMBIE_MEMBER/REBALANCE_BLOCKED - a member not answering the rebalance - with rebalance dwell at 15426ms against a 15s bound and 23 partitions frozen around 100s. That is a detector firing on its own terms rather than a short window clipping a threshold, which is worth stating because the project's standing caution runs the other way: a clean probe is only evidence when its detectors could have fired. The two sibling chaos tests in the same run are recorded as a control arm rather than a footnote. Both passed, on their own seeds, on the same runner and broker that hour - so the failure is not an ambient property of the machine, and the revoke-under-work scenarios did not draw this interleaving. The entry does not claim which defect this is. The zombie-member defect is recorded as landed via #80 and the family's original deadlock in #29 is still open; deciding between them is what the replay is for, and asserting it here would put a guess into the ledger. Also recorded: the autopsy was not in the CI log. GitHub truncated the job's stream partway through, so neither gh run view --log nor --log-failed contained the block and the annotations carried only an exit code. It came from the uploaded test-report artifact, inside the failsafe XML for the failing class. The next investigator would lose the same time, and the console will look like the verdict does not exist rather than like it was cut off. The durable home for that is the ambient-probe section of docs/testing.md, which currently says every failure log includes the block; that file was owned by another branch, so the note sits here and names where it belongs. Nothing is remediated here: no test quarantined, disabled, retried or weakened, and no lane re-run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fpk9yEDEhPnbad2WKvZUmP
… record the mode The family ledger held six sightings and recorded the commit mode for none of them. Mode is the discriminator: it decides which defect can explain a sighting, and its absence let a transactional-mode failure be logged as "live confirmation the deadlock is still present" for a cycle that cannot occur in that mode. Recovered each sighting's mode from test source and verified it: RebalanceEoSDeadlockTest is PERIODIC_TRANSACTIONAL_PRODUCER; both ChaosRevokeUnderWork variants inherit PERIODIC_CONSUMER_SYNC from AbstractRevokeUnderWorkScenario; ChaosChurnStormIT is PERIODIC_CONSUMER_ASYNCHRONOUS. Split one file per item, per docs/inflight/AGENTS.md: - bug-857-transactional-revoke-wait.md - the unbounded `while (isTransactionCommittingInProgress()) Thread.sleep(100)` on the revoke path, transactional mode only, on master and predating #29. Carries the corrected first sighting and #44 (confluentinc#803), the only issue upstream ever labelled a verified bug, re-triaged onto this block off #29. - test-857-revoke-under-work-sightings.md - PERIODIC_CONSUMER_SYNC, the only sightings whose mode permits #29's AB-BA cycle to close. - test-857-churn-storm-async-stalls.md - PERIODIC_CONSUMER_ASYNCHRONOUS, where the cycle cannot close, the transactional wait cannot run, and #100 and #80 are landed. Three sightings no known defect explains; either a fourth defect or the harness's own teardown races. Nobody had said this, because modes were never recorded. - test-857-parallel-integration-proof.md - the Step-2 experiment, a work item with a trigger rather than an observation. bug-857-family.md keeps the decomposition and gains the mode table. None of the five captured seeds has ever been replayed. That, not a release date and not #29 merging, is what should retire these files - so the deletion trigger is recorded per sighting as "seed replayed and explained". All seeds, job links and replay invocations verified preserved across the split. Repaired the citations the move stranded, address-only, leaving every claim alone: two solutions docs, the W4 plan, two live inflight notes, and the actor ideation doc, whose `bug-857-family.md:33,110` line-number citations the split invalidated - repaired beside the original rather than over it, since it is a dated record.
… correct cluster 3's record
RENAME: pausedForThrottling -> subscriptionsPausedForBackPressure
"paused" means three different things in this codebase and the wrong reading is easy
to reach - I reached it myself while reviewing this branch, and concluded the flag
was a redundant mirror of ConsumerManager.getPausedPartitionSize() when it is not.
The javadoc now separates them explicitly:
1. this flag - WE called consumer.pause(assignment) to apply back pressure
because the pipeline is full. Set only by doPause(), cleared
only by resumeIfPaused(), both on the poll thread.
2. State.PAUSED - the USER asked PC to stop. Stops the poll loop and work
registration; does NOT pause any partition. Unrelated.
3. consumer.paused() - Kafka's own record, and the source of truth. This flag
mirrors it.
It also records why being a mirror is the hazard, which is the actual defect in this
cluster: Kafka clears pause state for every partition on an EAGER rebalance (the
assignment map is replaced - SubscriptionState.assignFromSubscribed) but KEEPS it for
partitions retained across a COOPERATIVE one. resumeIfPaused() is gated on the flag,
so when the two disagree nothing resumes and consumption stops silently - which is
confluentinc#857's own symptom.
Also updates the one caller, which used Lombok's generated isPausedForThrottling().
Worth noting for the next rename: grepping the field name does not find callers of a
Lombok getter, because the generated name capitalises differently.
CORRECTION to the cluster decomposition plan
An earlier draft called cluster 3's threading problem "pre-existing". It is not. On
master every mutation of numberRecordsOutForProcessing runs on the CONTROL thread, so
a plain int is correct there. The cluster adds a fifth site,
adjustOutForProcessingOnRevoke, called from onPartitionsRevoked - the POLL thread,
inside the rebalance callback. It makes a single-threaded counter cross-thread, on
the very counter whose drift is the silent-stall signature. Corrected in place rather
than silently, because "pre-existing" is the judgement that would have let it land.
The plan now also states what the drift measurement is FOR: not how to fix the two
defects, but whether the cluster should exist. Its target symptom predates
#100, #80 and confluentinc#882. If the drift is gone on current master,
all 42 lines go; if it remains, it gets rewritten with the control thread owning the
counter.
…ract nobody wrote Name the defect class behind four separate incidents (three in the confluentinc#857 family): a component kept its own copy of a fact another component is the authority for, and the copy drifted. Instances: the deleted shutdown-request flag (#80), pausedForThrottling (deleted, replaced by asking Kafka), metaCache (repaired), and commitCommand as the boundary case where the unwritten obligation was a monitor rather than a copy. The write-up deliberately records the counter-pressure: each mirror was the cheap local fix for a threading constraint, which is the argument for fixing the constraint (#142, confluentinc#200). Mechanisms are cited from the owning docs, not restated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QiZYbruWmTFGkfepmZn2er
…t by what kind of work it is `feature` carried no impact, on the reasoning that proposed work has an opportunity rather than a consequence. That made it the bin for anything not yet broken: 21 of 72 notes were `feature` with no impact, including CI work, process notes, refactors and test debt. The tag stopped sorting anything. Two rules fix it, both from review: **A feature is something a USER of the library would notice.** CI automation, process, refactors and test work are not features however new they are - they are `task`, which forces an impact, which forces the question of what it costs to not do them. **A feature that exists to prevent a consequence carries that consequence.** The point of the tag is that work falls out in priority order, not that it is filed under the correct part of speech. A commit-failure seam whose motivation is PC shutting down is `feature` + `crash`, so it sorts with crashes rather than being buried among cosmetic features. Producer fencing likewise carries `reliability`. So `feature` may now carry an impact, and should whenever one applies. New impacts, all on the existing "what does it cost you not to know" axis: `crash` (the process dies or exhausts a resource until it does), `reliability` (survives less than it should, no single defect named yet), `refactor` (the code is harder to change than it needs to be), `ci`, `process`, and `test-debt`. `security` is extended from bugs to tasks, for the CVE backlog. 23 notes retagged. Two filenames were lying and are renamed: `parked-docs-site` is in planning draft, so it is open and its state tag is removed; `parked-stale-arrival-guard-...` was already tagged `blocked`, not parked. Citations to the old name in two dated plans and a solutions doc are repointed - a path repair, not a rewrite of what those records claim. Two judgements recorded because they went against the first reading: - `branch-stall-experiments` stays `stranded-work` rather than becoming `stall`. Its own first line is "one result worth keeping, then delete", and the composition experiment behind #80 has merged. It is stranded branches, not an open stall. - `parked-stale-arrival-guard-...` becomes `task`/`reliability`, not `bug`. Checked against master: `addWorkContainer` tests the RESIDENT for staleness, which is #31's merged half; the ARRIVAL check is still absent. But the note itself records that stale arrivals are harmless today - `couldBeTakenAsWork` refuses them and `getWorkIfAvailable` evicts them inline - so `bug` would overstate a live defect. It is work that prevents churn rather than cleans it up. The vocabulary itself and its gate live one PR up, and change with it.
…t by what kind of work it is `feature` carried no impact, on the reasoning that proposed work has an opportunity rather than a consequence. That made it the bin for anything not yet broken: 21 of 72 notes were `feature` with no impact, including CI work, process notes, refactors and test debt. The tag stopped sorting anything. Two rules fix it, both from review: **A feature is something a USER of the library would notice.** CI automation, process, refactors and test work are not features however new they are - they are `task`, which forces an impact, which forces the question of what it costs to not do them. **A feature that exists to prevent a consequence carries that consequence.** The point of the tag is that work falls out in priority order, not that it is filed under the correct part of speech. A commit-failure seam whose motivation is PC shutting down is `feature` + `crash`, so it sorts with crashes rather than being buried among cosmetic features. Producer fencing likewise carries `reliability`. So `feature` may now carry an impact, and should whenever one applies. New impacts, all on the existing "what does it cost you not to know" axis: `crash` (the process dies or exhausts a resource until it does), `reliability` (survives less than it should, no single defect named yet), `refactor` (the code is harder to change than it needs to be), `ci`, `process`, and `test-debt`. `security` is extended from bugs to tasks, for the CVE backlog. 23 notes retagged. Two filenames were lying and are renamed: `parked-docs-site` is in planning draft, so it is open and its state tag is removed; `parked-stale-arrival-guard-...` was already tagged `blocked`, not parked. Citations to the old name in two dated plans and a solutions doc are repointed - a path repair, not a rewrite of what those records claim. Two judgements recorded because they went against the first reading: - `branch-stall-experiments` stays `stranded-work` rather than becoming `stall`. Its own first line is "one result worth keeping, then delete", and the composition experiment behind #80 has merged. It is stranded branches, not an open stall. - `parked-stale-arrival-guard-...` becomes `task`/`reliability`, not `bug`. Checked against master: `addWorkContainer` tests the RESIDENT for staleness, which is #31's merged half; the ARRIVAL check is still absent. But the note itself records that stale arrivals are harmless today - `couldBeTakenAsWork` refuses them and `getWorkIfAvailable` evicts them inline - so `bug` would overstate a live defect. It is work that prevents churn rather than cleans it up. The vocabulary itself and its gate live one PR up, and change with it.
…assert, and even out the discriminator Two findings from the round-2 review, both about a claim reading wider than the evidence behind it. **The ledger's scope.** "Asserts ordering under churn" reads as "no two owners ever concurrently touch a key across a revoke", and it does not mean that. Overlap is compared only WITHIN a window, so a cross-epoch overlap - an old-epoch delivery still executing on a stopped-but-not-drained owner while the new owner processes the same key - lands in two windows and cannot raise LEDGER_KEY_CONCURRENCY by construction. That is the window working: a new epoch legitimately opens a new window, which is what stops at-least-once redelivery after a revoke from reading as a violation. It needs saying out loud because that gap is the shape of a product bug this repo has already found once - the drain-path zombie fixed in #80 - so a reader could reasonably assume the new ledger covers it. It does not, and is not trying to; detecting cross-epoch overlap needs an instrument that outlives an epoch. **The discriminator was asymmetric and I had it backwards.** The RED side of the uncontended-replay test is positive evidence - the schedule crossed the bound with no contention - and one instance carries it. The GREEN side is an ABSENCE, and this family is intermittent: the ledger records the same seed swinging 5 / 7 / 10 violations across otherwise-identical uncontended runs. So an intermittent occurrence that simply does not fire on one replay is indistinguishable from contention, which makes a single green the WEAKEST cell in the table while it was being read as decisive. The green side now asks for two or three, and the occurrence recorded there is marked as having one.
…Kafka rather than mirroring it (#376) `BrokerPollSystem` kept a `volatile boolean pausedForThrottling` in step with its own `consumer.pause()` / `resume()` calls. It is a copy of a fact Kafka owns, and the contract for keeping it in step was written nowhere. This deletes the copy and asks Kafka. ## Why the mirror cannot be kept correct, so this is a deletion and not a fix Kafka does opposite things under the two rebalance protocols - verified against kafka-clients 3.9.2 source, not inferred from behaviour: - **Eager**: `ConsumerCoordinator.onJoinPrepare` calls `assignFromSubscribed(emptySet())`, replacing the assignment map. Every `TopicPartitionState` is destroyed, so every partition comes back unpaused. - **Cooperative**: `assignFromSubscribed` reuses the existing per-partition state object for retained partitions, and that object holds the `paused` boolean, so retained partitions stay paused. When the revoked set is empty it is not called at all. A mirror therefore has to model both protocols and stay right as Kafka changes. Deriving the answer makes the protocol irrelevant and the logic self-correcting: after a cooperative rebalance the retained partitions are still paused, so `resumeIfPaused` resumes them; after an eager one there is nothing to do. **No reset-on-assignment hook is needed, and adding one is the specific mistake this shape prevents.** A reset is right for eager and leaves cooperative permanently paused - consumption stopping with no error. ## Two accessors, because two threads `consumer.paused()` is a consumer call and `KafkaConsumer` is not thread-safe, so the poll thread gets `subscriptionsArePausedForBackPressure()` and the control thread gets `isSubscriptionsPausedForBackPressure()`, which reads `ConsumerManager`'s cache - refreshed once per poll, and acceptable ONLY for the heuristic that wakes a poller it believes is paused. `isPausedForThrottling()`, the Lombok getter on the deleted field, had one caller (`AbstractParallelEoSStreamProcessor.maybeWakeupPoller`), re-pointed at the control-thread accessor. ## The hot path reads Kafka once per pass, not three to five times `paused()` is a real call, and `managePauseOfSubscription` plus the three methods below it were each asking again. The set is now read once and threaded down as a PARAMETER rather than held in a field - a local cannot outlive the pass, so there is no invalidation to get wrong and nothing that can go stale between iterations, which is exactly the failure mode of the mirror it replaces. The resume path makes strictly fewer calls than before, since it used to fetch the set a second time to act on it. ## What this is NOT **It is not a fix for a live master defect**, and it closes no issue. Master has the mirror but never resets it on assignment, so the cooperative failure is not reachable here - it was reachable on #29, where the reset was added, and that is where the symptom was found: a fix for confluentinc#857 reintroducing confluentinc#857's own symptom. `BrokerPollSystemCooperativeRebalancePauseTest` therefore passes on master too. It is a REGRESSION GUARD against reintroducing the reset, not a red control for this diff. Stated because a test that passes on both arms proves nothing about the change it ships with, and reading it as evidence would be wrong. ## The defect class, and what else carries it Ships `docs/solutions/architecture-patterns/a-mirror-of-state-another-component-owns-is-a-contract-nobody-wrote.md`, which names the class - state carrying a synchronisation obligation nobody wrote down - and inventories every instance in this codebase: the consumer-manager shutdown flag (deleted, #80), this one (deleted), `metaCache` (repaired with `volatile` on the confluentinc#857 branch, and STILL A LIVE MIRROR), and `commitCommand`, whose monitor silently became the lock guarding commit execution - the AB-BA deadlock at the centre of confluentinc#857. The honest counter-pressure is recorded with it: a mirror is usually a threading constraint wearing a field's clothes, and the seam that makes asking the owner hard is the poll/control split over a consumer that is not thread-safe - the shared-nothing refactor, still open as #142 / confluentinc#200. Extracted from #29 so it can be reviewed on its own merits while that PR stays blocked on its dependencies. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Checked every issue and PR the transactional registers name, against GitHub rather than against memory. Four claims had gone false, and one of them is the kind that stops a reader looking. **#177's mirror is closed while confluentinc#833 is open.** The map listed it as open; it was closed as completed on 2026-09-01 by the fork owner, with no reason on the issue and no linking commit, and the upstream report it mirrors is still open and still unanswered. The last comment on the mirror, five days earlier, argues the opposite - #267 renames the type in the title but does not fix the report - and ends "Left open". Nothing written down says what changed. That matters beyond bookkeeping: AGENTS.md makes the fork mirror the owner of an upstream issue's live status, so the mirror is currently asserting a status confluentinc#833 contradicts. Both `branch-transactional-battle-test.md` and `next-tell-809-833-they-are-likely-fixed.md` now say so, the latter with its own section, because that note's whole purpose is "tell the reporters" and closing the mirror is not telling anyone. Task 3 in it is half unreachable now - commenting on a closed mirror reaches nobody - and task 4 has grown to include reopening it or recording why it closed. No reason is invented here; only what is observable is written. **#29 targets `master`, not `master-confluent`, and is out of draft. #31 has merged.** The blockers register still said both target the pinned pre-rebrand mirror and needed retargeting. The retarget happened; what survives is the half that was never mechanical - #29's deadlock fix predates the internals #80 reshaped, so it needs reconciling rather than replaying. **Both of #262's parents have merged**, so the transactional stack section is rewritten from a land order into what it is now: history, plus one debt. **The debt is real and worth naming.** #257's merged commit message describes redelivery - "handed records back for a second delivery" - and never says *stall*. What #262 established is more severe: only a success marks a partition dirty, so every batch failing meant no commit was ever attempted and the source offset froze at 3 of 201. `git log -1 --format=%B b36ad94 | grep -i stall` returns nothing. Release notes are generated from the commit log, so 0.6.0.0 publishes the weaker claim unless the release section is amended or a follow-up commit says so. Also confirmed unchanged, so the map is not merely un-contradicted but checked: #44, #175, #189, #232 and #241 are all still open; confluentinc#809 and confluentinc#833 are both still open upstream; #100 is merged. `docs/inflight/handoff-262-master-merge.md` was already deleted in b99377b and is confirmed absent from the tree; this commit does not touch it.
… it actually answered Help text says what each command does and when to reach for it. What it cannot carry is what the answers look like on a real question - and, more to the point, why the working-tree version of each answer is wrong. That is what this document is for, and every example in it is a lookup that happened rather than one composed to read well. THE FIRST EXAMPLE IS THIS PR'S OWN CI FAILURE. `PartitionStateCommittedOffsetIT.committedOffsetRemoved[1]` went red on a branch that changes no Java at all. One `prior-art --headings committedOffsetRemoved` returned three hits: - a dated plan on master: it re-flaked after #80 un-quarantined it - a SOLVED write-up on master: a previous instance, an offset-reset race in the test harness - docs/inflight/test-load-tightness-flakes.md, NOT on master, on sixteen refs, carrying entries dated 2026-09-01 that track this exact test and parameter and say it is still re-flaking A working-tree search finds the first two and concludes the flake is fixed. The ledger saying otherwise is on sixteen branches and none of them is master. The tool caught its own thesis on its own pull request, which is a better argument for it than anything written in its header. The other examples cover the same ground for the commands that answer a different question: `note drift` before editing a shared note, where 198 of 274 carrying refs are merely behind and reporting them would bury the two dozen that differ; `stranded` and the three filters that run before it reports; and `note find` for the path `note drift` wants. Also states the exit-code contract in plain terms, because it is load-bearing rather than decorative: both P0s found while building this were failures rendering as confident empty results. Routed from AGENTS.md's topic table, since a document nobody opens is a document that does not exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UTX8obQMsjs9kq2rkpU5cZ
…ng PC waits on the coordinator that is waiting on it
THE FRAME, FROM RUN 33824474506 (3 failures in 60)
RUNNABLE parked-at=sun.nio.ch.EPoll.wait
via NetworkClient.poll <- ConsumerNetworkClient.poll
<- ConsumerNetworkClient.awaitPendingRequests(ConsumerNetworkClient.java:355)
<- AbstractCoordinator.close(AbstractCoordinator.java:1140)
<- ConsumerCoordinator.close(ConsumerCoordinator.java:987)
<- ClassicKafkaConsumer.close(ClassicKafkaConsumer.java:1140)
The poll thread is inside consumer.close(), waiting on the group coordinator. Three candidates are
refuted by this one frame: the RetryQueue write lock (already refuted by experiment),
ConsumerManager.close's Thread.sleep wait for pendingRequests, and a commitSync retry. None of them
appear; the thread is waiting on the BROKER.
THE CYCLE, WHICH IS WHY IT NEVER RECOVERS
closeDontDrainFirst transitions straight to CLOSING. handlePoll is guarded on RUNNING || DRAINING, so
the instance stops calling consumer.poll() while still an open group member. doClose then hands
Kafka's close a 30-second budget, and Kafka's close waits for pending coordinator requests - which
the coordinator cannot answer, because the group is in PreparingRebalance waiting for JoinGroup from
the very members now sitting in that wait. Each closing member waits for a coordinator that is
waiting for it. The 23-25s poll-pass age is that budget being burned; waitForClose has a shorter one,
which is why ten instances logged a TimeoutException while still CLOSING.
IT IS THE #80 DEFECT, ONE LIFECYCLE STATE ALONG
#80 fixed "draining PC stops polling - zombie partition hold" for DRAINING, and its guard's javadoc
already states the general reason: rebalance participation happens inside consumer.poll(), so a
member that stops polling but keeps its membership cannot answer. CLOSING never got that fix, and
close() bypasses DRAINING entirely - so the most common close takes the unprotected path.
The invariant this makes explicit: A PC INSTANCE THAT HAS NOT YET LEFT THE GROUP MUST KEEP POLLING
IT. DRAINING honours it; CLOSING does not.
Found by re-running the prior-art sweep against the NEW mechanism rather than the old one. The first
sweep searched the retry-queue/rebalance-callback shape and could not have found this; searching
closeDontDrainFirst and waitForClose surfaced #80, its write-up and its guard test immediately. The
sweep is per-mechanism, not per-investigation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WErnxQd9Ew57F9SsqzdPU5
… has actually left the group
THE DEFECT
A member that has stopped polling but has NOT yet left the group cannot answer a rebalance - rejoin
and revoke-ack both happen inside consumer.poll(). PC stopped polling the moment it entered CLOSING,
while its consumer was still subscribed, and then handed that consumer to close().
The cost is a distributed livelock, not a slow shutdown. With a JoinGroup already in flight,
consumer.close() reaches AbstractCoordinator.close -> ConsumerNetworkClient.awaitPendingRequests and
waits for that request; the coordinator will not answer a JoinGroup until every member has joined -
including the members already stuck in that same wait. Each closing member waits for a coordinator
that is waiting for it, for the whole DEFAULT_TIMEOUT budget, as a silent group member. Peers see the
group frozen in PreparingRebalance and stop making progress entirely.
Captured stack from a stalled instance:
RUNNABLE EPoll.wait <- ... <- ConsumerNetworkClient.awaitPendingRequests(355)
<- AbstractCoordinator.close(1140) <- ConsumerCoordinator.close(987)
<- ClassicKafkaConsumer.close(1140)
IT IS #80's DEFECT ONE LIFECYCLE STATE ALONG
#80 fixed exactly this for DRAINING - "draining PC stops polling - busy-spin + zombie partition
hold" - and CLOSING never got the fix. That mattered more than it sounds, because
closeDontDrainFirst() is what a bare close() calls and it reaches CLOSING WITHOUT passing through
DRAINING at all: the commonest close took the unprotected path. BrokerPollSystemDrainTest's javadoc
had already written down the general reason; only the DRAINING half was implemented.
TWO GUARDS, BOTH LOAD-BEARING, AND THE SECOND WAS NEARLY MISSED
BrokerPollSystem.handlePoll() skipped CLOSING, and ConsumerManager.poll()'s retry loop ALSO
short-circuits on closeInProgressSignal. Fixing only the first still yields zero polls; the new test
caught that. The commit path one method away already had the right shape -
"allow to try to commit at least once during close / shutdown regardless of the signal",
while (tryCount == 0 || !closeInProgressSignal...) - and the poll path simply never got the same
allowance. It now does, so a closing instance gets exactly ONE poll attempt, never a retry loop:
bounding retries during shutdown is what that signal is actually for.
WHAT CHANGED FOR A USER
Closing one PC instance no longer freezes its peers' consumer group for the close timeout. This is
not test-only: a rolling restart of a PC fleet is the ordinary case that hits it.
EVIDENCE, AND WHAT IS NOT YET CLAIMED
Measured on the capacity profile at 2 failures in 30 and 3 in 60 before this change, always the same
signature, with ten of twelve instances parked in the stack above. BrokerPollSystemCloseTest asserts
the poll happens, and is proven red with EITHER guard reverted and green with both. Full core unit
suite green.
The end-to-end rate is NOT yet re-measured against this fix - that run is what turns this from a
fix that should work into one that does, and the note carries the pre-fix baseline to beat.
Test asserts on poll(1ms) rather than a poll COUNT deliberately: pollBrokerForRecords gives every
state that is not RUNNING/DRAINING a 1ms timeout and CLOSING is the only such state reaching
handlePoll, so the argument identifies a closing poll exactly, where a count would race the loop's
ordinary long polls and could pass with the defect present.
No @GuardedBy annotation: this adds no shared state, it removes a state exclusion.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WErnxQd9Ew57F9SsqzdPU5
…etire the two notes this PR overrode A ce-doc-review pass over this branch's three commits falsified six factual claims they made and found two inflight notes the PR had silently overridden. Each is a claim that read as verified and was not, which is the class this repo's own rules call a false green: none of them would have gone red anywhere. 1. The `@Disabled` grep claim was wrong in two places - the new test-hardening entry and release-0.6.0.0.md's gate section both said `grep -rn "@disabled" --include="*.java" .` returns nothing but historical prose. Running it returns a live `@DisabledOnOs(OS.WINDOWS)` on AbstractQuarantineScriptTest and two `@Disabled` string literals built into TransactionalClaimCoverageTest's assertion messages, on top of the prose. Both sentences now describe what the command actually returns and why the gate still holds - no live bare `@Disabled` on a test class or method - and name the command to re-run rather than a count that will drift. 2. The new ChaosRevokeUnderWorkTransactionalIT sighting said the PR "touches no Java" in the same sentence that named the Java file it deletes. Replaced with ground the deletion does not undermine: nothing under `src/main` and nothing in the `chaostests` package changed, and `.github/workflows/maven.yml` gives each chaos shard a hardcoded `scenarios:` list (verified - Suite 2/4 is ChaosRevokeUnderWorkTransactionalIT,ChaosRevokeUnderWorkKeyOrderIT, passed as CHAOS_SCENARIOS), so removing a sanity-package class cannot reshuffle a shard. The opening parenthetical's artefact counts become the shape plus `git diff --name-status <base>..<head>`, and the bare `AbstractRevokeUnderWorkScenario.java:211` becomes the seed banner's own greppable literal, matching how its sibling citation two lines up already cites. 3. The blockers recheck said "every occurrence is already conditional" of module-maturity.yaml's production-use wording. Half true: the `support_posture` lines are conditional, the `maturity: production-use` field values are bare. The record now says exactly that, so the second recheck starts from the state rather than from the first pass's summary of it. No yaml value changed - whether an unqualified maturity value is a claim the open confluentinc#857 family falsifies is a release call for the maintainer. 4. release-0.6.0.0.md's opening block said #80 emptied the quarantine registry so release.yml's gate now passes, while the section this PR rewrote records MultiInstanceRebalanceTest.largeNumberOfInstances as quarantined and blocking. docs/quarantined-tests.md confirms the entry is live and unowned, so rule 5 still bites; the opening block now says the gate does NOT pass and points at the registry as the enforced copy. 5. Retired docs/inflight/test-progressbar-width-needs-a-machine-assertion.md. It argued for splitting the test into a machine assertion plus a tagged demo rather than deleting it, and this PR deleted it without naming that argument. Per AGENTS.md - record the reasoning you are overriding, where you override it - the note's alternative and why deletion won (the test asserted nothing, so the split describes a test still to be written rather than one being preserved; ProgressBarUtils.getNewMessagesBar keeps its other in-tree callers; and reinstating a tagged demo stays a maintainer's call this deletion does not block) are migrated into the test-hardening entry before the note is removed. 6. Retired docs/inflight/test-disabled-tests-before-v6.md. Its stated delete-when condition is met - #263 landed and no test carries a live bare `@Disabled` - and its four-name list is the same stale one this PR already replaced. Its one framing not owned elsewhere, that the disabled tests were inherited debt rather than a rule broken here, is migrated to release-0.6.0.0.md's gate section, which points at the 2026-08-08 audit for the per-test provenance behind it. No inbound links to fix - `grep -rn` over the tree returned none for either note. 7. docs/refactoring.md described ProgressBarTest.width in the present tense as a deliberate manual check. Past tense now, citing the deletion entry. 8. Reconciled "three of four races refound" (the new testing-evidence entry) with "Lincheck refound four real races unaided" (release-0.6.0.0.md, untouched by the earlier commits). docs/plans/2026-08-25-001-test-lincheck-poc-plan.md's verdict table supports the first: three of four found by the stress strategy plus one nobody had named, with the fourth half-found once by model checking and not reproducible. The release note now says that, and says to use that wording in the announcement. Left alone deliberately, because they are decisions rather than errors: the lincheck entry's merge_gate field versus the CI leg, and the jcstress anomaly disposition. Gates: bin/check-all.sh (16 passed, 0 failed), plus check-issue-refs.sh, check-docs-data.sh and check-file-refs.sh individually. Docs only - no code, no test behaviour, no changelog entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019SVm2cT6ZgUyPMim9CukYK
One flaky CI check (
PartitionStateCommittedOffsetIT.committedOffsetRemoved[latest]) turned out to conceal two independent bugs - one real production defect in the close path, one long-lived race in the test harness. This PR fixes both, with each fix proven RED→GREEN against its captured defect, and lands the diagnostics that made the hunt possible so the next investigation costs hours instead of days.Bug 1 (product): draining consumer busy-spins and zombie-holds its partitions
ConsumerManager.shutdownRequestedwas a private shadow ofBrokerPollSystem.runState, set at the wrong lifecycle phase:drain()raised it on entry to DRAINING, sopoll()'s guard short-circuited andconsumer.poll()was never invoked again during the drain window. Measured consequences:handlePoll()'s own comment designates as the loop's sleep never ran;poll(), so the draining consumer became a rebalance-unresponsive zombie: background heartbeats kept it a live group member holding its full partition assignment (up tomax.poll.interval.ms) while consuming nothing - freezing same-group siblings. Relates to upstream Paused consumption across multiple consumers confluentinc/parallel-consumer#857's "paused consumption after rebalance".Fix: collapse the duplicated lifecycle state rather than re-time it - the flag and
signalStop()are deleted;ConsumerManagerderives "close in progress" from the poll system'srunState(now volatile) via an injected signal. One source of truth; the desync class is structurally impossible. Drain semantics preserved: a draining member correctly keeps its partitions to finish in-flight work and commit (that is what bounds duplicates) - what it must also do, and now does, is stay rebalance-responsive.Guards:
BrokerPollSystemDrainTest(unit; committed first as a characterisation of the defect, then flipped RED→GREEN by the fix) andDrainingMemberRebalanceIT(integration: PC-B joining mid-drain makes progress while PC-A drains; clean close; correctness ledger - no records lost, duplicates exactly the parked in-flight tail, proving the handover honoured A's commits).Bug 2 (test harness): the
auto.offset.reset=latestnudge raceThe flake itself was not a product stall: a consumer with LATEST reset and no committed offset resolves its start position when the reset executes.
runPcUntilOffsetproduced its single nudge record before the await - under contention, bootstrap takes 1-3s, the reset resolves after the nudge, and the consumer is positioned past every record that will ever exist: the await is unwinnable at any timeout. Only the[latest]param can lose this race - and every observed failure ever was[latest]. Captured via the Kafka client's own (normally silenced) internals:Resetting offset ... to position FetchPosition{offset=201}on a topic holding exactly 201 records.Fix, DRY'd per the ensureTopic drift lesson: shared
BrokerIntegrationTest#awaitWithTopicNudge- produces a nudge inside each await attempt (the patternrunPcCheckStartIsalready used inline) and self-diagnoses on timeout (topic end vs group committed vs nudges sent). Both helpers delegate to it; no timeout enlarged, no assertion weakened.LatestResetTailNudgeITbottles the race deterministically (RED with the old pattern in ~23s every run; GREEN with the primitive).Validation
committedOffsetRemoved0/20 (historical failure rate ~33% → P(luck) ≈ 0.03%); 15/20 fully clean; zero stall-class failures. Residuals catalogued indocs/inflight.mdas the load-tightness flake family (fast-fail assertion flakes, pass isolated).Also included
ShardManagerunder-served detector,WorkManagerthrottle-decision log, per-PCmyIdlog attribution, correlation markers on tail-nudge awaits, a standing (commented) kafka-client bootstrap DEBUG harness, andSubscriptionStateat INFO (offset-reset lines are rare and decisive).docs/solutions/test-flakiness/pc-silent-stall-under-contention-2026-07-29.md(the investigation + drain design review) andlatest-reset-nudge-race-committedoffsetremoved-2026-07-30.md(the solved mechanism + diagnosability lessons).docs/inflight.mdupdated throughout.docs/plans/.ChaosChurnStormIT.churnStormMeetsSlosAndBalancesLedger,PartitionStateCommittedOffsetIT.committedOffsetRemoved): their fixes are this PR, so the@Quarantinedannotations anddocs/QUARANTINED_TESTS.mdentries are deleted here - the registry is now empty. Triggered by the Quarantine Lane's unexpected-pass detection on this branch.highcpuwith a trivially-passing job and make the required gate skip real tests), somaven.ymlis now byte-identical to master andbin/ci-concede-check.shis deleted; collapsed three doubled copyright headers (amaven-license-pluginartifact) inBrokerIntegrationTest,KafkaClientUtils,RetriesTest; and pruned satisfieddocs/inflight.mdentries.Checklist
CHANGELOG.adoc, under== Unreleased) - drain-zombie fix entrydocs/solutions/write-ups,docs/inflight.md,docs/QUARANTINED_TESTS.md, plan docBrokerPollSystemDrainTest(RED→GREEN characterisation),DrainingMemberRebalanceIT,LatestResetTailNudgeIT; two quarantined tests re-enabledmaven.ymlis byte-identical to master. A stale "concede" mechanism that would have let a self-hosted result satisfy the required GitHub-hosted gate (rejected from PR ci: self-hosted high-CPU fast-feedback workflow + PR-scoped advisory PIT #75 as a gate-spoof) had ridden in via an old merge and has been removed here.🤖 Generated with Claude Code
https://claude.ai/code/session_01VbtaCN1Re14pJ99VFW4R18