8000 fix(reconciler): isolate permanent batcher failures per work unit by VLukiantsev · Pull Request #1201 · google/xmc · GitHub
[go: up one dir, main page]

Skip to content

fix(reconciler): isolate permanent batcher failures per work unit - #1201

Merged
VLukiantsev merged 3 commits into
fix/batcher-requeue-failed-flushesfrom
fix/batcher-isolate-permanent-failures
Oct 13, 2026
Merged

VLukiantsev merged 3 commits into
fix/batcher-requeue-failed-flushesfrom
fix/batcher-isolate-permanent-failures

Conversation

@VLukiantsev
@VLukiantsev VLukiantsev commented Oct 12, 2026 •
Copy link
Copy Markdown
Collaborator

Summary

When a batch that covers more than one work unit fails permanently, resend it one work unit at a time so a single bad work unit no longer drops valid updates for the others. The resending is bounded: at most 32 work units per batch, stopping after 3 work units fail permanently in a row, and aborting and re-queueing on any temporary error or cancellation. Layer 3 of the b/569896494 stack.

Motivation & Context

Storage fails the whole per-experiment transaction when any one work unit is bad. Layer 2a drops the batch in that case. This layer recovers the valid updates.

Design:

  • Per work unit, not per update. Storage's NotFound is per work unit, and all of a work unit's sources (JobSet, Workload, KueueSync) succeed or fail together. Resending per update would let one missing work unit with 3 sources look like "3 failures in a row" and wrongly drop healthy work units. Grouping by work unit also cuts calls by up to 3×.
  • Bounds:
    • Calls per tick: each tick waits for every experiment, so unbounded resending could stall the whole batcher. Each failed batch now costs at most 1 + 32 calls per tick. groupByWorkUnit orders groups by descending max retry attempt (preserving ascending WID on ties) so previously re-queued overflow work units (attempt > 0) are isolated ahead of fresh (attempt == 0) updates on the next flush and cannot be starved behind active low-WID work units.
    • 3 failures in a row: treated as an experiment-wide failure, so the rest of the batch, including work units past the 32 limit, is dropped (reason=permanent). Otherwise, work units past the limit are re-queued and handled over later ticks.

Key Changes

  • cmd/reconciler/shared/batcher_retry.go: new groupByWorkUnit (and maxGroupAttempt) groups updates per work unit ordered by descending retry attempt; new isolatePermanentFailure returns experimentWide; handleFlushFailure applies the single-work-unit fast path, isolation, and overflow rule.
  • docs/flows/reconciliation.md §6: isolation bounds and mermaid diagram (including single-work-unit vs multi-work-unit permanent error branches).

Assumptions

  • Status history: when a batch is resent one work unit at a time, storage writes one status-history row per call, which can add intermediate rows that a single transaction would have folded together. This only happens on the permanent-failure path.
  • Large failing batches: re-queued overflow is charged a backoff attempt. Batches far larger than 32 work units therefore drain over later ticks and can reach the 5-minute budget (retry_budget_exhausted). The most common trigger, deleted work units, is handled without any resending by the next PR (typed WorkUnitNotFoundError).

Verification & Testing

  • Commands Executed:
    • bazel test //cmd/reconciler/...: 7/7 pass.
    • Race-detector run on //cmd/reconciler/shared:shared_test: passes.
    • Pre-commit hook: passes.
  • Tests (TestRetry_PermanentMultiWorkUnitIsolation & TestRetry_IsolationPrioritizesRequeuedOverflow):
    • one missing work unit with 3 sources: work units 2–4 persist, in 1+4 calls;
    • one bad work unit among 3;
    • non-consecutive permanent failures (WID 1, 2, 4 failing, WID 3, 5 succeeding) reset the consecutive streak and try all 5 work units;
    • 3 consecutive permanent failures: 1+3 calls, all dropped;
    • 40 work units, all permanent: overflow dropped;
    • 40 work units: 33 calls, 8 re-queued;
    • TestRetry_IsolationPrioritizesRequeuedOverflow: when WID 40 has a permanent failure and WID 1..32 emit fresh updates on the next tick, re-queued WID 33..40 (attempt = 1) are isolated first so WID 40 is dropped on tick 2;
    • transient abort after WID 1 succeeds: only WID 2 and WID 3 are re-queued;
    • cancelled context: rest re-queued.

Related Issues / Bugs

Part of b/569896494. Stacked on #1200.

TAG=agy
CONV=cbf91f29-980f-4054-9fd9-4a0bf6574811

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a permanent failure isolation mechanism for the update batcher. Instead of dropping an entire batch upon encountering a permanent storage error, the batcher now groups updates by work unit and resends up to 32 work units individually. It handles consecutive permanent failures as experiment-wide issues, dropping remaining units, while transient errors or context cancellations trigger a requeue of the remaining work units. The feedback suggests using context.WithoutCancel(ctx) when calling b.requeue during context cancellation or transient errors to prevent logging and cleanup operations from being skipped due to a cancelled context.

Comment thread cmd/reconciler/shared/batcher_retry.go Resolved
Comment thread cmd/reconciler/shared/batcher_retry.go Resolved
@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a permanent failure isolation mechanism for the update batcher. Instead of dropping an entire batch upon encountering a permanent storage error, the batcher now groups updates by work unit and resends up to 32 work units individually. It handles consecutive permanent failures as experiment-wide issues, dropping remaining units, while transient errors or context cancellations trigger a requeue of the remaining work units. The feedback suggests using context.WithoutCancel(ctx) when calling b.requeue during context cancellation or transient errors to prevent logging and cleanup operations from being skipped due to a cancelled context.

I am having trouble creating individual review comments. Click here to see my feedback.

cmd/reconciler/shared/batcher_retry.go (233-236)

medium

When the context is cancelled (ctx.Err() != nil), passing the cancelled ctx to b.requeue (which subsequently logs via b.recordDropped) can cause logging or other context-bound operations to be skipped or fail, depending on the logging configuration.

Using context.WithoutCancel(ctx) (introduced in Go 1.21) is a safer alternative here. It creates a context that inherits values (like trace IDs for structured logging) from the parent context but discards the cancellation signal, ensuring that cleanup and logging are successfully executed.

		if ctx.Err() != nil {
			b.requeue(context.WithoutCancel(ctx), xid, slices.Concat(groups[i:]...), ctx.Err())
			return false
		}

cmd/reconciler/shared/batcher_retry.go (242-245)

medium

If the transient error err is due to context cancellation or deadline expiration, the parent ctx will be cancelled. Passing a cancelled context to b.requeue can cause logging or other context-bound operations to be skipped or fail.

Using context.WithoutCancel(ctx) ensures that cleanup and logging are successfully executed while still preserving trace context.

		if !IsPermanentStorageError(err) {
			b.requeue(context.WithoutCancel(ctx), xid, slices.Concat(groups[i:]...), err)
			return false
		}

@VLukiantsev
VLukiantsev force-pushed the fix/batcher-isolate-permanent-failures branch 2 times, most recently from 1acb44b to fcc91c8 Compare October 12, 2026 10:46
@VLukiantsev
VLukiantsev force-pushed the fix/batcher-isolate-permanent-failures branch 2 times, most recently from 3388c09 to 093beed Compare October 12, 2026 12:46
@VLukiantsev
VLukiantsev marked this pull request as ready for review October 12, 2026 12:54
@github-actions
github-actions Bot requested a review from pachopepe October 12, 2026 12:54
@VLukiantsev
VLukiantsev force-pushed the fix/batcher-isolate-permanent-failures branch from 093beed to 88a9017 Compare October 12, 2026 16:43
When a multi-work-unit flush fails permanently, resend it one work unit
at a time (all of that work unit's sources in one call), since Storage
reports missing rows per work unit. Isolation is bounded:

- At most 32 work units are isolated per failed batch.
- It stops after 3 consecutive work units fail permanently, treating the
  failure as experiment-wide and dropping the unsent work units.
- It aborts on the first transient error or context cancellation and
  requeues that work unit and all unsent ones.
- Work units beyond the cap are dropped if the failure was
  experiment-wide and requeued otherwise.

A batch covering a single work unit is dropped on a permanent failure.

Part of b/569896494

TAG=agy
CONV=cbf91f29-980f-4054-9fd9-4a0bf6574811
Address review: requeue/logging must still run after the flush context is
cancelled. Covered by the cancelled-ctx isolation test case.

Part of b/569896494

TAG=agy
CONV=cbf91f29-980f-4054-9fd9-4a0bf6574811
@VLukiantsev
VLukiantsev force-pushed the fix/batcher-isolate-permanent-failures branch from 88a9017 to f6e2f28 Compare October 12, 2026 17:10
Comment thread cmd/reconciler/shared/batcher_retry.go Resolved
Comment thread docs/flows/reconciliation.md Resolved
@VLukiantsev
VLukiantsev requested a review from pachopepe October 13, 2026 11:26
@VLukiantsev
VLukiantsev added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit 706e553 Oct 13, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0