Skip to content

fix(preprod): Reap stuck PROCESSING snapshot comparisons#116708

Merged
NicoHinderling merged 1 commit into
masterfrom
nico/reap-stuck-snapshot-comparisons
Jun 2, 2026
Merged

fix(preprod): Reap stuck PROCESSING snapshot comparisons#116708
NicoHinderling merged 1 commit into
masterfrom
nico/reap-stuck-snapshot-comparisons

Conversation

@NicoHinderling

@NicoHinderling NicoHinderling commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Extend the hourly detect_expired_preprod_artifacts reaper to also mark PreprodSnapshotComparison rows stuck in PROCESSING for more than 30 minutes as FAILED + TIMEOUT, mirroring the existing PreprodArtifactSizeComparison handling.

When a compare_snapshots task is hard-killed mid-run — SIGKILL after a deploy/worker SIGTERM, or OOM — the except BaseException cleanup that would set the row to FAILED never runs, so the row is left stuck in PROCESSING. The task's retry guard only re-runs PENDING/FAILED rows, and the task is only enqueued on a build upload or a staff recompare — never on page load/poll. So a stuck comparison is skipped forever (skipping, comparison not in retryable state (state=1)) and spins indefinitely for the customer.

Marking it FAILED makes the row retryable again on the next upload or recompare, and turns the perpetual spinner into an honest failed state. This matches how the sibling PreprodArtifactSizeComparison PROCESSING rows are already recovered.

This is fix #3 of a two-PR Graphite stack. The follow-up PR (#1) parallelizes the odiff comparison phase so large comparisons finish well within the deadline — that's the durable fix for the underlying slowness; this PR stops affected comparisons from getting permanently wedged in the meantime.

@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Jun 2, 2026
Comment thread docs/superpowers/specs/2026-06-02-reap-stuck-snapshot-comparisons-design.md Outdated
When a compare_snapshots task is hard-killed mid-run (SIGKILL after a
deploy/worker SIGTERM, or OOM), the cleanup that marks the comparison
FAILED never runs, leaving the row stuck in PROCESSING. The task's
retry guard only re-runs PENDING or FAILED rows, and the task is only
enqueued on build upload or staff recompare, so a stuck comparison is
skipped forever and spins indefinitely for the customer.

Extend the existing hourly detect_expired_preprod_artifacts reaper to
also mark PreprodSnapshotComparison rows stuck in PROCESSING for more
than 30 minutes as FAILED with a TIMEOUT error code, mirroring the
existing PreprodArtifactSizeComparison handling. This makes the row
retryable again on the next upload or recompare.

Co-Authored-By: Claude <noreply@anthropic.com>
@NicoHinderling NicoHinderling force-pushed the nico/reap-stuck-snapshot-comparisons branch from 5775663 to bca2b07 Compare June 2, 2026 17:53
@NicoHinderling NicoHinderling marked this pull request as ready for review June 2, 2026 18:10
@NicoHinderling NicoHinderling requested a review from a team as a code owner June 2, 2026 18:10
@NicoHinderling NicoHinderling merged commit c7da4d4 into master Jun 2, 2026
64 checks passed
@NicoHinderling NicoHinderling deleted the nico/reap-stuck-snapshot-comparisons branch June 2, 2026 18:15
NicoHinderling added a commit that referenced this pull request Jun 2, 2026
Parallelize the preprod snapshot odiff comparison phase. The single
serial `OdiffServer` is replaced with a pool of N persistent servers
(tunable via the new `preprod.snapshots.odiff-worker-count` option,
default 4) driven by a `ContextPropagatingThreadPoolExecutor`. The
per-batch fetch → compare → upload work is extracted into
`_process_batch`, which checks a server out of a `queue.Queue`, runs the
batch, uploads its diff masks, and returns partial results that are
merged single-threaded in the main loop.

**Why:** for large builds the comparison diffs thousands of changed
image pairs one at a time against one node process, taking 3+ minutes —
long enough to hit the task's 300s `processing_deadline`
(`ProcessingDeadlineExceeded`) or be interrupted by a deploy SIGTERM. A
real build (~6,000 changed pairs across 431 batches) was timing out this
way. Spreading the batches across N servers brings it comfortably under
the deadline.

**Notes for reviewers:**
- Concurrency safety: each `OdiffServer` is only ever used by one thread
(checked out of the pool); the objectstore `Session` is thread-safe for
`get`/`put`; each batch's `compare_images_batch` uses its own tempdir,
so there are no cross-thread filename collisions. All result
accumulation happens in the main thread — workers mutate no shared
state.
- A mid-batch failure is isolated: already-processed pairs are preserved
and only the unprocessed ones are marked `errored`, so a single bad
batch doesn't fail the whole comparison.
- Memory scales with N (each worker can hold a pair's decoded/padded
images), which is why N is an `FLAG_AUTOMATOR_MODIFIABLE` option — it
can be dialed down in prod without a deploy. `MAX_DIFF_PIXELS` still
caps any single pair.
- Adds a `compare_snapshots: odiff phase complete` summary log
(duration, throughput, slowest batch, worker count) for post-deploy
diagnosis, and the first end-to-end tests for `compare_snapshots` (which
previously had no task-level coverage).

This is the durable follow-up to #116708 (which stopped killed
comparisons from getting stuck in `PROCESSING`); together they address
the timeout end-to-end.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resolved errors disappears from the list

2 participants