fix(review): make preview-poll-budget increment safe under concurrent triggers - #7833
Conversation
… triggers recordPreviewPollAttempt did an unguarded read-modify-write against R2: two triggers racing for the same head SHA could both read count=N and both write count=N+1, silently losing one increment and letting the actual poll count exceed MAX_PREVIEW_POLL_ATTEMPTS. Switch the write to a compare-and-swap against the marker's httpEtag (etagMatches for an existing object, etagDoesNotMatch:"*" for the first write), retrying a bounded number of times on a precondition miss so a racing writer's newer count is re-read and both increments land. Preserves the existing fail-open-on-write-failure contract: exhausting the retries degrades to "this attempt didn't count", the same safe direction the module already documents.
|
🚨 Contributor flagged. Click here for more info: Superagent Dashboard |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7833 +/- ##
==========================================
+ Coverage 80.62% 88.86% +8.23%
==========================================
Files 729 89 -640
Lines 74671 21240 -53431
Branches 22791 3684 -19107
==========================================
- Hits 60205 18875 -41330
+ Misses 11654 2187 -9467
+ Partials 2812 178 -2634
Flags with carried forward coverage won't be shown. Click here to find out more.
|
|
Warning ⏸️ LoopOver review result - manual review recommendedReview updated: 2026-07-21 14:38:34 UTC
Review summary Nits — 5 non-blocking
Flagged checks (non-blocking)
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
What & why
Closes #7780.
recordPreviewPollAttempt(src/review/visual/preview-poll-budget.ts) did an unguarded read-modify-write against R2: read the attempt-count marker, computecount + 1, write it back — with no compare-and-swap. The module exists precisely because multiple independent triggers (self-poll job chain, CI-completion webhook,deployment_statuswebhook, sweep pass) can call intobuildCapturefor the same head SHA at nearly the same time. When two race, both read the marker atcount=Nbefore either writes, both writecount=N+1, and one increment is silently lost — letting the real poll count exceedMAX_PREVIEW_POLL_ATTEMPTS.The fix
Switch the increment to a compare-and-swap against the marker's R2
httpEtag:onlyIf: { etagMatches }when a marker already exists, oronlyIf: { etagDoesNotMatch: "*" }for the first write (create-if-absent);null(no write) instead of throwing — re-read the racing writer's newer count and retry, bounded byBUDGET_CAS_MAX_ATTEMPTS = 3.This is R2's native conditional-write primitive; no CAS precedent existed in the codebase to reuse (the cited
actions-fallback.tsmarkers do plain best-effort writes), so the issue's "conditional/compare-and-swap write against R2" path is taken. The existing fail-open-on-write-failure contract is preserved: exhausting the retries under sustained contention degrades to "this attempt didn't count" — the same safe direction the module already documents for a genuine write failure — and a real write error is still swallowed best-effort.Tests
recordPreviewPollAttemptcalls for the same SHA, interleaved via a put barrier so both read before either writes; asserts the final count is 2, not 1 — i.e. both increments land. Verified bug-catching: reverting the CAS makes this fail with count=1.httpEtag+ theonlyIfpreconditions, matching real R2 semantics.Validation
npm run typecheck— cleantest/unit/preview-poll-budget.test.ts(15) + consumer suitesvisual-capture/actions-fallback(228 total) — greengit diff --check— clean; change is scoped to the two files above, no generated-artifact impact