GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — fix(coderd/database): lock the workspace row when claiming a prebuild by kevinh-canva · Pull Request #31468 · coder/coder

Conversation

@kevinh-canva

@kevinh-canva kevinh-canva commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Context

ClaimPrebuiltWorkspace picks a prebuild with FOR UPDATE OF p SKIP LOCKED, where p is the workspace_prebuilds view. That view is built from CTEs. Postgres does not push row-level locks into a CTE, so the clause locks nothing.

When several users claim from the same preset at once, two claims can select the same prebuild. The second claim waits on the first claim's row lock in the outer UPDATE. Once the first commits, the second reassigns the workspace to its own user. The first user's next build on that workspace fails with 409 A workspace build is already active.

We hit this in load testing for a training session with hundreds of users claiming at once. More than half of the claims returned 409, even with client-side retries.

Intent

Lock the underlying workspaces row (FOR UPDATE OF pw SKIP LOCKED) instead of the view, so concurrent claims skip a prebuild that is already being claimed.

Also filter on pw.owner_id being the prebuilds system user. Postgres rechecks conditions on a locked row in READ COMMITTED mode. A prebuild that a concurrent transaction has just claimed then drops out instead of being claimed again.

This changes only the query text. There is no schema or migration change, and dump.sql is unchanged.

Testing

TestClaimPrebuiltWorkspaceConcurrent holds one claim's transaction open and runs a second claim against the same preset. It polls pg_stat_activity to tell "returned" apart from "blocked on a lock", so it does not depend on wall-clock timing.

Before the fix (test only):

--- FAIL: TestClaimPrebuiltWorkspaceConcurrent (0.41s)
    querier_test.go:7975: 
                Error Trace:    /tmp/coder-src/coder/coderd/database/querier_test.go:7975
                Error:          claim B blocked on claim A's lock
                Test:           TestClaimPrebuiltWorkspaceConcurrent
                Messages:       claim B returned {ID:c2d820d4-6977-4137-a9cc-35c257e47c89 Name:claimed-by-cheerful_gottlieb11}, <nil> after claim A committed
FAIL
FAIL    github.com/coder/coder/v2/coderd/database       0.468s
FAIL

After the fix:

=== RUN   TestClaimPrebuiltWorkspaceConcurrent
--- PASS: TestClaimPrebuiltWorkspaceConcurrent (0.40s)
PASS
ok      github.com/coder/coder/v2/coderd/database       0.447s

Also run against Postgres, on both main and v2.38.3:

  • TestClaimPrebuiltWorkspaceConcurrent with -count=10 and with -race.
  • ./coderd/database/...
  • ./enterprise/coderd/prebuilds/...
  • Prebuild, claim, and create tests in ./enterprise/coderd and ./enterprise/cli.
  • golangci-lint v2.0.2 reports 0 issues.
  • coderd/database/generate.sh with the pinned sqlc fork changes only the query const in queries.sql.go.

Notes

The same patch applies cleanly to v2.38.3 and v2.38.4. Both still have the bug.

Note

This PR was written with help from an AI coding agent (Claude). I reviewed the change and the verification output above.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

chatgpt-codex-connector[bot] reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
@kevinh-canva @forge-canva
fix(coderd/database): lock the workspace row when claiming a prebuild
6366d30
ClaimPrebuiltWorkspace locked the workspace_prebuilds view with
FOR UPDATE OF p SKIP LOCKED. The view is built from CTEs, and Postgres
does not push row locks into a CTE, so the clause locked nothing.

Concurrent claims could pick the same prebuild. The second claim waited
on the first claim's row lock, then reassigned the workspace to its own
user. The first user's next build then failed with a 409.

Join and lock the underlying workspaces row instead. Also check its
owner_id, because Postgres rechecks conditions on a locked row. That
excludes a prebuild that a concurrent transaction has just claimed.

Co-authored-by: Forge <[email protected]>
@github-actions github-actions Bot added the community Pull Requests and issues created by the community. label Oct 28, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

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-10-28T22:42:38.518523Z 6366d30 PR opened
ℹ️ 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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@github-actions github-actions Bot assigned kevinh-canva Oct 28, 2026
@dylanhuff-at-coder @claude
fix(coderd/database): claim prebuilds with FOR NO KEY UPDATE and skip…
eda5d07
… deleted rows

FOR UPDATE is stronger than the FOR NO KEY UPDATE lock the outer UPDATE
takes. It also blocks FK KEY SHARE checks from inserts that reference the
workspace, so a reconciler delete build racing a claim can deadlock with the
claim's own build insert. FOR NO KEY UPDATE still conflicts between claims,
so SKIP LOCKED behaves the same.

Also exclude soft-deleted rows on the locked workspace so the row recheck
rejects a prebuild deleted while the claim is in flight, and add a test that
a claim racing an open claim gets a different prebuild without blocking.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@dylanhuff-at-coder

dylanhuff-at-coder commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Hey @kevinh-canva, thanks for putting this together. I pushed to this branch with some changes, please don't force push and overwrite them.

My commit switches the lock to FOR NO KEY UPDATE so it can’t deadlock with the prebuild reconciler, adds NOT pw.deleted on the locked row, and adds a test that two concurrent claims get different prebuilds.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@dylanhuff-at-coder dylanhuff-at-coder mentioned this pull request Oct 29, 2026
fix(coderd/database): lock the workspaces row when claiming a prebuild #31518
Merged
dylanhuff-at-coder added a commit that referenced this pull request Oct 30, 2026
@dylanhuff-at-coder @kevinh-canva @claude
fix(coderd/database): lock the workspaces row when claiming a prebuild (
5557383
#31518)

`ClaimPrebuiltWorkspace` locked the `workspace_prebuilds` view with `FOR
UPDATE ... SKIP LOCKED`, but the view is built from CTEs, so nothing got
locked. Concurrent claims picked the same prebuild and the losers got a
409 (a customer saw 28 of 30 fail).

This locks the `workspaces` row directly (`FOR NO KEY UPDATE SKIP
LOCKED`), rechecks owner and deleted on it, and checks eligibility with
per-workspace index lookups instead of the views. It uses `=` instead of
`IN` so one claim can't take two prebuilds, filters on the preset's
template so other templates' prebuilds aren't scanned, and claims oldest
first so prebuilds are used before they hit the preset TTL. Query-only,
no migration, applies cleanly to release/2.38. Builds on #31468 by Kevin
Ha (Canva).

Benchmarks (Postgres 17, 20k users, 320k builds, 600 ready prebuilds):
~12 ms per claim (was ~640 ms), and 500 concurrent claims got distinct
prebuilds in ~1.5 s.

Known limitations:

- Each claim still checks every prebuild of the preset's template (other
presets included) before sorting. That's a few ms at hundreds of
prebuilds per template.
- Pre-existing: a reconciler delete during a claim can still 409.
Follow-up is a `deleting_at` marker the claim skips.

---------

Co-authored-by: Kevin Ha <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
@dylanhuff-at-coder

dylanhuff-at-coder commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Closing this PR, we merged a different one to address this issue

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@dylanhuff-at-coder dylanhuff-at-coder closed this Nov 2, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Reviewers

No reviews

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@kevinh-canva kevinh-canva

Labels

community Pull Requests and issues created by the community.

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@kevinh-canva @dylanhuff-at-coder
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.