Skip to content

[coordinator] streaming pull: bound lease renewal to one batch per tick - #69

Merged
cmttt merged 3 commits into
mainfrom
ls/coordinator-lease-renewal-fix
Jun 22, 2026
Merged

[coordinator] streaming pull: bound lease renewal to one batch per tick#69
cmttt merged 3 commits into
mainfrom
ls/coordinator-lease-renewal-fix

Conversation

@cmttt

@cmttt cmttt commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Overview

The coordinator's rules-sink consumer could wedge into a multi-million-message backlog that neither more pods nor restarts would drain — only falling traffic did. Root cause is a self-reinforcing lease-renewal vs. receive starvation spiral. This change bounds lease renewal so a renewal burst can't starve message reception.

The bug

  • handle_renew_leases drained every renewable message in an unbounded loop each tick. In the biased select!, renewal outranks the streaming-pull receive branch, so a modack burst monopolizes the loop and starves receive → messages age past their lease → more renewals → even less receive. It spirals, pins per-stream throughput regardless of pod count, and is restart-immune.
  • Prod signature at onset: modack ~37×, streaming_pull_response_count halved, with no reconnects and no pod change.

Fix

  • Renew at most one batch (ACK_IDS_MAX_BATCH_SIZE) per tick, then yield. Safe because collect_ack_ids_that_need_to_be_renewed pops and reschedules each item forward, so successive ticks cover the rest; the working set is flow-control-bounded. Renewal logic is otherwise unchanged.

Validation

  • cargo check + rustfmt clean.
  • Built the coordinator binary (with the canonical action-proto overlay), ran it against the local pubsub emulator: an action flows received → decoded → held → renewed via the patched path, with no decode errors / panics / nack-storms.
  • 6 unit tests for the one-batch-per-tick coverage / no-starvation property at the prod 2500 batch size — mutation-verified (re-inserting at now fails 4 of 6).
  • Not covered: handle_renew_leases itself (would need a pubsub-client mock); its only change is the loop removal, exercised live e2e.

Follow-ups (not in this PR)

  • Config mitigation: PUBSUB_MIN_LEASE_EXTENSION_SECS 30 → 120 to cut renewal frequency.
  • Optional hardening: move lease renewal off the hot select! path entirely.

handle_renew_leases drained every renewable message in an unbounded loop on
each lease-renewal tick. Inside the manager's biased select! loop this lets a
modack burst monopolize the loop and starve the lowest-priority streaming-pull
receive branch. The starvation is self-reinforcing: fewer messages received ->
in-flight/on-hold messages age past their lease -> more need renewal -> more
modack -> even less receive, a spiral that pins per-stream throughput
regardless of pod count and only unwinds when upstream publish volume drops.

Renew at most one batch (ACK_IDS_MAX_BATCH_SIZE) per tick and yield back to the
loop. collect_ack_ids_that_need_to_be_renewed re-schedules each collected
message's next renewal forward, so successive ticks cycle through the rest of
the expired set; the per-stream working set is bounded by flow control, so one
batch per LEASE_RENEWAL_INTERVAL keeps leases alive.
@cmttt
cmttt requested a review from haileyok as a code owner June 20, 2026 01:04
cmttt added 2 commits June 20, 2026 13:40
…arvation

Adversarial tests for the property the one-batch-per-tick fix relies on:
collect_ack_ids_that_need_to_be_renewed pops expired items and reschedules each
forward, so renewing one batch per LEASE_RENEWAL_INTERVAL covers the whole
working set across ticks without starving the tail or double-renewing.

Covers: single-tick cap == max_chunk_size; rescheduled items aren't recollected
at the same instant; 5001 msgs / 2500-batch drain in exactly 3 ticks, each
renewed once (no starvation, no duplication); renewal recurs after the lease
window; acked messages are never renewed; empty queue renews nothing.

Verified the suite bites: mutating the reschedule to reinsert at `now` fails 4
of the 6 tests. Adds tokio test-util dev-dependency for paused-time control.
@cmttt
cmttt merged commit 287256b into main Jun 22, 2026
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.

1 participant