Skip to content

Fix the queue populator LogConsumer drain deadlock on rebalance - #2787

Open
delthas wants to merge 1 commit into
development/9.4from
improvement/BB-825/logconsumer-drain-deadlock
Open

Fix the queue populator LogConsumer drain deadlock on rebalance#2787
delthas wants to merge 1 commit into
development/9.4from
improvement/BB-825/logconsumer-drain-deadlock

Conversation

@delthas

@delthas delthas commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

The problem

When a consumer group rebalances (typically because a backbeat pod restarts during a rolling update), every consumer must release its partitions before the group can resume. The queue populator's LogConsumer refused to release until its last stored offsets were committed — but librdkafka cannot complete a commit while the rebalance is waiting for the release, and it commits the stored offsets itself as part of releasing anyway. The consumer was waiting on something that can only happen after the wait ends.

So whenever a rebalance arrived within the ~5 seconds following a batch:

  • the consumer held its partitions for 299s (maxPollIntervalMs − 1000). The whole group waits for its slowest member, so the entire oplog pipeline — replication entries, bucket notifications, lifecycle — froze for ~5 minutes;
  • when the wait timed out, it disconnected the consumer and nothing ever reconnected: the pod consumed nothing at all until the liveness probe restarted it;
  • pod shutdown goes through the same wait, so stopping a pod could overrun the 30s kubernetes grace period, get SIGKILLed, and leave a stale group member behind — which is exactly what triggers the next unlucky rebalance.

Impact on Zenko CI

This was the single largest source of CTST end2end flakiness over the past two months: the two Replication scenarios (object stuck PENDING past the test timeout while the populator was frozen or dead) failed 49 of 257 runs across 20 branches, and the Bucket notifications filter scenario (events delivered ~5 minutes late, after all 4 in-test retries) failed 7 runs — each red job costing a full ~4h end2end attempt. Traced examples: run 29589236290 attempt 1 (populator frozen 15:38:20→15:43:19, then dead until the liveness restart) and run 29082783060 attempt 1 (all 4 retries' notifications delivered in one batch, 44s after the last retry gave up). Merging this should remove both flake clusters.

The fix

Same approach as BackbeatConsumer's rebalance handling (#2731 / #2739 / #2740):

  • on rebalance, wait only for the batch currently being processed (a few seconds at most), not for a commit acknowledgment: storeOffsets() now signals batch completion directly, and the _pendingCommit flag is removed;
  • commit the stored offsets right before releasing the partitions, best-effort: if the commit cannot go through, librdkafka commits stored offsets during the release anyway, and at worst the next partition owner re-consumes a few records — the populator is at-least-once by design;
  • cap the wait at 30s (was 299s) and keep the deliberate disconnect when it expires: a batch stuck that long means the populator is wedged, and a healthcheck-driven restart is the intended recovery.

A new functional suite (tests/functional/lib/queuePopulator/LogConsumer.js, runs under ft_test:lib) exercises a real two-consumer rebalance against kafka: partitions released within seconds of the revoke, committed offsets on the broker matching the stored ones, the consumer still alive and consuming once the group settles, and the timeout/disconnect path.

Issue: BB-825

@bert-e

bert-e commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Hello delthas,

My role is to assist you with the merge of this
pull request. Please type @bert-e help to get information
on this process, or consult the user documentation.

Available options
name description privileged authored
/after_pull_request Wait for the given pull request id to be merged before continuing with the current one.
/bypass_author_approval Bypass the pull request author's approval
/bypass_build_status Bypass the build and test status
/bypass_commit_size Bypass the check on the size of the changeset TBA
/bypass_incompatible_branch Bypass the check on the source branch prefix
/bypass_jira_check Bypass the Jira issue check
/bypass_peer_approval Bypass the pull request peers' approval
/bypass_leader_approval Bypass the pull request leaders' approval
/approve Instruct Bert-E that the author has approved the pull request. ✍️
/create_pull_requests Allow the creation of integration pull requests.
/create_integration_branches Allow the creation of integration branches.
/no_octopus Prevent Wall-E from doing any octopus merge and use multiple consecutive merge instead
/unanimity Change review acceptance criteria from one reviewer at least to all reviewers
/wait Instruct Bert-E not to run until further notice.
Available commands
name description privileged
/help Print Bert-E's manual in the pull request.
/status Print Bert-E's current status in the pull request TBA
/clear Remove all comments from Bert-E from the history TBA
/retry Re-start a fresh build TBA
/build Re-start a fresh build TBA
/force_reset Delete integration branches & pull requests, and restart merge process from the beginning.
/reset Try to remove integration branches unless there are commits on them which do not appear on the source branch.

Status report is not available.

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.62%. Comparing base (b58b9cf) to head (be5e136).

Additional details and impacted files

Impacted file tree graph

Files with missing lines Coverage Δ
lib/queuePopulator/KafkaLogConsumer/LogConsumer.js 96.61% <100.00%> (+5.85%) ⬆️

... and 6 files with indirect coverage changes

Components Coverage Δ
Bucket Notification 80.22% <ø> (ø)
Core Library 80.87% <100.00%> (-0.50%) ⬇️
Ingestion 70.63% <ø> (-0.61%) ⬇️
Lifecycle 79.06% <ø> (ø)
Oplog Populator 85.83% <ø> (ø)
Replication 59.63% <ø> (-0.04%) ⬇️
Bucket Scanner 85.76% <ø> (ø)
@@                 Coverage Diff                 @@
##           development/9.4    #2787      +/-   ##
===================================================
- Coverage            74.88%   74.62%   -0.27%     
===================================================
  Files                  201      201              
  Lines                13754    13753       -1     
===================================================
- Hits                 10300    10263      -37     
- Misses                3444     3480      +36     
  Partials                10       10              
Flag Coverage Δ
api:retry 9.09% <0.00%> (+<0.01%) ⬆️
api:routes 8.91% <0.00%> (+<0.01%) ⬆️
bucket-scanner 85.76% <ø> (ø)
ft_test:queuepopulator 9.11% <5.55%> (-1.86%) ⬇️
ingestion 12.55% <5.55%> (-0.05%) ⬇️
lib 8.85% <94.44%> (+0.99%) ⬆️
lifecycle 19.04% <0.00%> (+0.10%) ⬆️
notification 1.01% <0.00%> (+<0.01%) ⬆️
oplogPopulator 0.14% <0.00%> (+<0.01%) ⬆️
replication 18.64% <0.00%> (+<0.01%) ⬆️
unit 51.59% <100.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 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.

@scality scality deleted a comment from bert-e Jul 28, 2026
@bert-e

bert-e commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Request integration branches

Waiting for integration branch creation to be requested by the user.

To request integration branches, please comment on this pull request with the following command:

/create_integration_branches

Alternatively, the /approve and /create_pull_requests commands will automatically
create the integration branches.

On revoke, _drainAndUnassign() refused to unassign while a commit was
pending, waiting for a DRAINED event that only a successful commit
callback could emit. librdkafka parks its timed auto-commit during a
rebalance and only commits stored offsets when serving the unassign,
so the commit the drain waited for could not happen until the drain
released: the consumer held its partitions for maxPollIntervalMs-1000
(299s by default), freezing the whole group, then disconnected with no
reconnect path, leaving a zombie pod until the liveness probe restarted
it. close() funnels through the same drain, making graceful shutdowns
overrun the k8s grace period.

Rework the drain to mirror BackbeatConsumer's revoke path:
- gate only on the in-flight batch; storeOffsets() now emits DRAINED
  on every path and the _pendingCommit flag is removed;
- commit stored offsets explicitly (best-effort, never gating) before
  unassigning, with librdkafka's removal-time auto-commit as fallback;
- cap the drain timeout at 30s and keep the deliberate disconnect so
  the healthcheck restarts a populator with a genuinely stuck batch.

Add a functional suite driving a real two-consumer rebalance, asserting
prompt release, broker-side committed offsets matching the stored ones,
continued consumption after the rebalance, and the timeout path.

Issue: BB-825
@delthas
delthas force-pushed the improvement/BB-825/logconsumer-drain-deadlock branch from c8748c8 to be5e136 Compare July 28, 2026 09:50
@delthas
delthas requested review from a team, SylvainSenechal and benzekrimaha July 28, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants