Skip to content

feat(ref): write a scan-state row only when its generation is the expected one - #698

Merged
ThomasK33 merged 6 commits into
mainfrom
issue636-scanstate-if
Nov 6, 2026
Merged

ThomasK33 merged 6 commits into
mainfrom
issue636-scanstate-if

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Why

ThomasK33 asked to fix and build the open issues and land them on main. This is part C of item 2 of #636, the resumable orphan sweep. The sweep will keep a cursor in a job_scan_state row. PutJobScanState is last-writer-wins, and the header of scanstate.go argues that River's unique jobs never race. #636 shows that a stuck sweep can still run beside the next one after the rescue horizon, so a run that lost the row could overwrite the new run's cursor. This PR adds a write that lands only when the row has the generation the writer read. Nothing calls it yet. A later PR of #636 makes the sweep save its cursor with it.

What changes

  • ScanStateStore.PutJobScanStateIf(ctx, job, scope, payload, generation, expectGeneration) writes the row only when its current generation is expectGeneration. An absent row counts as generation 0. Otherwise it writes nothing and returns the new ErrScanStateConflict.
  • PostgreSQL and SQLite: one statement decides it. An UPDATE guarded by the expected generation. For an expected generation 0, an INSERT ... ON CONFLICT DO UPDATE ... WHERE generation = 0 upsert, so an absent row is created, a row at generation 0 is rewritten, and two creates that race do not refuse each other.
  • S3-WAL: the check and the write share one read-modify-write of the side object (scanMutate), so the check holds at the write.
  • The scanstate.go header and internal/ref/README.md name the new method and when to use it.
  • The method's comment states the two rules that make the fence hold: a run that takes a row over writes a generation that differs from the one it read, and a generation never repeats, so a job keeps its row after its work ends. It also says that a second S3-WAL process that loses its CAS race fails with the store's own error, not ErrScanStateConflict.

Scope

  • No caller yet. No schema change, and no change to the existing last-writer-wins PutJobScanState.
  • A mixed fleet is unaffected: old binaries never call the new method.

Verification

  • TestJobScanState_PutIf runs on PostgreSQL, SQLite and S3-WAL. It checks an absent row, a stale generation, the current generation, a row at generation 0, the run that lost the row, and eight concurrent writers that hold the same generation (exactly one wins). It passes on all three backends.
  • TestJobScanState_PutIfCreateRace: six writers expect generation 0 on a new row and write generation 0, eight rounds. All succeed on the three backends. With the earlier two-statement PostgreSQL code it fails in 3 of 3 runs: a writer that expected generation 0 on a row at generation 0 got scan state row changed: conditional write refused.
  • TestJobScanState_PutIfCreateWins: six writers expect generation 0 on a new row and write generation 1. Exactly one creates it, on the three backends. A SQLite upsert guard that also accepts a row already at the new generation fails it: 6 writers created the row, want exactly 1.
  • Mutants of the generation check each fail TestJobScanState_PutIf:
    • PostgreSQL and SQLite, the UPDATE without its generation guard: write with a stale generation = <nil>, want ErrScanStateConflict
    • PostgreSQL and SQLite, the upsert without its guard: create over an existing row = <nil>, want ErrScanStateConflict
    • S3-WAL, the check removed: write that expects a generation on an absent row = <nil>, want ErrScanStateConflict
  • go test -short -race of ./internal/... and ./cmd/..., and go test -race ./internal/ref/ with the container lane: pass. go build ./... and go vet ./...: clean. golangci-lint run ./internal/ref/... (v2.15.0): 0 issues.

Refs #636


Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh

…ected one

PutJobScanState is last-writer-wins. That is exact while River keeps one pass
per job running, but a pass that outlives its rescue horizon can still be
writing when the next pass takes over, and a job that resumes from its row
must not let the older pass overwrite it.

PutJobScanStateIf writes only when the row's generation is the one the caller
read, and returns ErrScanStateConflict otherwise. An absent row counts as
generation 0. It is implemented on PostgreSQL, SQLite and S3-WAL.

Change-Id: I9e767d6dc355a4843f93f089cbe509926ade831b
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I62c1cef09b31114515922ce51816f348fc4ba643
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-06T07:51:53.022937Z 39e48a2 Manual request
🔒 Security Review ✅ Completed 2026-11-06T07:52:49.523365Z 39e48a2 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 629696e711

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/ref/scanstate.go Resolved
@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 629696e711

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Two writers that expected generation 0 on a row that did not exist yet
could both miss the guarded UPDATE, and the loser of the following
INSERT was told that another run took the row over, although the row
never left generation 0. PostgreSQL and SQLite now decide it in one
upsert whose update half carries the generation guard.

Refs #636

Change-Id: Iee2807b64819ac3568fc83eb32fc2e1f31af1f79
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 27f38584de

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 27f38584de

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

…scan state fence safe

Change-Id: Iee454987b8e98be6b7d2053365f539ec6174ebd3
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I2d256db40bbbda3bfe2d907184ffc98ee888c4f5
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 4120f33529

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 4120f33529

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

… own error

Change-Id: I3ee20ce147624db6ec7cc5bfbcfd0dea0f85e4b6
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 39e48a29a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 39e48a29a1

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33
ThomasK33 added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit 1815795 Nov 6, 2026
10 checks passed
@ThomasK33
ThomasK33 deleted the issue636-scanstate-if branch November 6, 2026 14:54
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