Skip to content

fix(fetch): Undo the claim when submitting to the push pool fails - #794

Open
enochtangg wants to merge 4 commits into
mainfrom
revert-claim-on-push-queue-timeout
Open

enochtangg wants to merge 4 commits into
mainfrom
revert-claim-on-push-queue-timeout

Conversation

@enochtangg

@enochtangg enochtangg commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

** Description

When the fetch thread claims an activation but cannot hand it to the push pool, the row is left in the Claimed state with nothing holding it. The claim query only selects Pending rows, so nothing re-picks it and it sits until handle_claim_expiration reverts it.

This inflates the SLO metric. pending_activation.max_lag.sec counts Claimed rows, so one stranded activation drives it up until the lease expires. This symptom can be observed consistently in process-segments-push in s4s2: DD link. During this time, throughput, occupancy and AlloyDB latency were all normal, so the metric was tracking a single stuck row rather than real pipeline latency.

When workers briefly go unavailable (due to a deployment), failed pushes each revert their own claim, which is correct, but the retried work fills the push queue to its cap. Activations that then time out submitting to the push queue are not reverted. The fetch thread only logs them.

**Fix

Release the claim when a submit to the push pool fails, for both Timeout and Closed.

  • Submit without parking the activation. The fetch thread uses try_send in a retry loop instead of send_async wrapped in a timeout. A failed try_send hands the activation back, so an error proves the push pool never received it.
  • Release the rest of the batch. The first failure releases the current activation and everything left in the batch in one query, then stops. Before, a closed queue stranded the rest of the batch, and a timeout made each remaining activation wait out its own timeout.
  • ActivationStore::release_claims only releases rows that are still Claimed with the claim_expires_at they were fetched with. Every claim writes a new expiry, so a release can never undo a later claim on the same row. A row already marked Processing is also left alone.

This is safe because only activations that never left the broker are released, so no worker can be running them. A submit failure now costs roughly the fetch backoff instead of the full claim lease. The push thread already reverted inline after a failed gRPC push. That logic moved to ActivationStore::undo_claims, so the push thread gets the same fenced release.

@enochtangg
enochtangg requested a review from a team as a code owner September 17, 2026 21:18

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4d91332. Configure here.

Comment thread src/fetch/thread.rs
Comment thread src/store/traits.rs Outdated
enochtangg and others added 2 commits September 18, 2026 11:48
`send_async` moves the activation onto a flume waiter that a push thread
can take at any moment. Dropping that future on timeout neither returns
the activation nor reports whether it was delivered, so a submit timeout
could not tell a full queue from a late delivery. The fetch thread then
undid a claim for an activation a worker had already started, and the row
became claimable a second time.

`try_send` keeps the activation on this thread and hands it back in
`TrySendError::Full`, so every non-Ok exit proves the push pool never saw
it. The cost is polling every millisecond while the queue is full instead
of parking on the channel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@enochtangg
enochtangg force-pushed the revert-claim-on-push-queue-timeout branch from 2ed9f71 to 012ad05 Compare September 18, 2026 20:06
Comment thread src/fetch/thread.rs
self.store.undo_claim(&id, "fetch.undo_claim").await;

// We cannot recover from a closed channel
return false;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if we abort early here, then the remaining ids in the batch are still stuck in Claimed, right?

also isn't this fundamentally racy? if the claim expires before we get to this line, we might undo somebody else's claim.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

just tested it and fable was able to find the same thing (and more things i didn't verify)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catches, tried to fix with the following:

  • When a submit fails, the fetch thread now releases the current activation and the rest of the batch in one query.
  • Releases also check claim_expires_at now. A release happens if the row still has the expiry we claimed it with, so it can't undo someone else's newer claim. I tested this end to end locally.

A release matched on id and status alone, so one that arrived after its
lease expired could undo a later claim on the same row. The row went back
to pending while the new claimant was delivering it, and the task could
run twice. Releases now also match on the claim_expires_at the activation
was fetched with. Every claim writes a fresh expiry, so a stale release
no longer matches.

On a submit failure the fetch thread released only the current
activation. A closed queue returned early and stranded the rest of the
batch until lease expiry, and a timeout made each remaining activation
wait out its own timeout. The first failure now releases the current
activation and the rest of the batch in one query.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants