Skip to content

🤖 fix: confirm CoderWorkspacePoolMember bind extensions by Coder's deadline and check the start build initiator - #339

Merged
ThomasK33 merged 3 commits into
mainfrom
feat/workspace-pool-member-guard
Oct 14, 2026
Merged

ThomasK33 merged 3 commits into
mainfrom
feat/workspace-pool-member-guard

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Summary

This PR fixes the deadline extension of a ready CoderWorkspacePoolMember and makes its identity read check who started the start build. The reconciler is still dormant (nothing registers it), so no user sees a change.

  1. Bind extension confirmed by Coder's deadline. Before the first extend request, the member stores the deadline it asks for in the new field status.bindExtendDeadline. Only a Coder deadline at or after that target sets bindExtended. So a crash, a lost status write, a 409, a 429, or an uncertain answer never leaves bindExtended=true without an extension. They also never cause a second extension after one that took effect (Codex 4267240452, 4267405687 on 🤖 feat: keep CoderWorkspacePoolMember workspaces warm and extend once at bind #324).
  2. Fresh deadline after the status write. The member computes the deadline from a clock reading after the write that stores the target, so a slow write does not make Coder refuse with 400 (Codex 4267240457 on 🤖 feat: keep CoderWorkspacePoolMember workspaces warm and extend once at bind #324).
  3. Start build provenance before every write. The identity read now also requires that the operator user started the start build with the pinned version, as a start transition. So a status edit of workspaceID and startBuildID cannot point the member at a kwp-<uid> workspace or build that the operator did not start for it. This covers readiness and every extension. The delete step (PR 8c) uses the same check. (🤖 feat: confirm uncertain CoderWorkspacePoolMember creates and apply the readiness rules #322 and 🤖 feat: keep CoderWorkspacePoolMember workspaces warm and extend once at bind #324 final assessors.)
  4. No request that Coder must refuse at the max deadline. The extension never goes past the build's max_deadline. Without room for at least 30 minutes, the member sends no request: an unclaimed member waits with MaxDeadline, a bound member counts the bind as done. Any other 400 (for example clock skew past Coder's 29-minute rule) is a wait with ExtendRefused (advisor note on 🤖 feat: keep CoderWorkspacePoolMember workspaces warm and extend once at bind #324).

Refs #153

Background

This is PR 8a of the CoderWorkspacePool stack (#153). The plan's PR 8 row lists these items with the trigger "fix before PR 12 registers the reconciler". PR 8b (watch rules) and PR 8c (cleanup) follow.

createHadNoEffect is unchanged: #319 owns it. The extend path no longer uses it, because the next read decides every non-final answer.

Implementation

Coder v2.37.3 answers PUT /workspaces/{id}/extend with 409 when the latest build is not a finished start build or the deadline is manual, and with 400 when the new deadline is less than 29 minutes ahead or after max_deadline (coderd/workspaces.go:1640-1700,3057-3061).

keepWarm (coderworkspacepoolmember_extend.go):

Case Result
Bound, bindExtended set No Coder call (unchanged).
No deadline No extension. A bound member sets bindExtended (unchanged).
Bound, bindExtendDeadline set, Coder deadline at or after it Sets bindExtended. No request.
Unclaimed, deadline 15 minutes or more away No extension (unchanged).
Bound, deadline already at or after the new deadline Sets bindExtended. No request, so a bind never shortens the deadline.
The max deadline leaves less than 30 minutes No request. Unclaimed: wait MaxDeadline. Bound: sets bindExtended.
Bound, no target stored yet Stores bindExtendDeadline, then reads the clock again.
Request Deadline = fresh now + max(TTL, 30 min), at most max_deadline, in whole seconds (status stores seconds). A bound member never asks for less than its stored target.
200 Bound: sets bindExtended. Unclaimed: sets lastExtendTime to the clock reading the new deadline counts from (Codex 4267760273).
409 Wait. The next poll reads again.
400 Bound: sets bindExtended, no retry. Unclaimed: wait ExtendRefused.
Other error Wait CoderUnavailable. The next read decides: a bound member retries only when Coder's deadline is still before the target.

readOwnPoolMemberWorkspace (coderworkspacepoolmember_ready.go) now reads the operator user (GET /users/me) and fails with OwnershipUnknown (WorkspaceDeleted=Unknown) when the start build's initiator, transition, or template version is wrong. A claimed Coder prebuild still passes, because its claim build is the operator's start build.

API: one new optional status field, CoderWorkspacePoolMember.status.bindExtendDeadline. The CRD is still in config/crd/dormant/, and the member type has no API reference page yet, so make docs-reference changes nothing.

Validation

Tests (coderworkspacepoolmember_extend_test.go, coderworkspacepoolmember_ready_test.go):

  • TestPoolMemberKeepsUnclaimedWorkspaceWarm: a 90-minute max deadline caps the extension at it, then two polls inside the last 15 minutes send no request (MaxDeadline). A 400 is a wait ExtendRefused that leaves the deadline.
  • TestPoolMemberExtendsOnceAtBind (plan mutation 12): the 409 case now also checks the stored target. A slow target write (clock readings started, started, late) stores started + 30m but sends late + 30m, which Coder accepts. A max deadline too near for an extension ends the bind extension without a request.
  • TestPoolMemberBindExtensionConfirmed (replaces TestPoolMemberBindExtensionAtMostOnce): a lost status write after a successful request (no second request), a crash right after the target write (the next poll extends), a 409 followed by a conflicting status write (the next poll extends), 502 without effect (sent again), 502 after commit (not sent again), 429 (sent again), and a bound 400 (not tried again).
  • TestPoolMemberGuardsEveryExtension and TestPoolMemberReadinessFailures: a status that names a start build the owner started on the member's own workspace fails with OwnershipUnknown and no Coder write.

Mutation checks: I removed one control at a time, and each removal made a named test fail (12 of 12): the confirm-by-read rule, the target write before the request, the fresh deadline after it, the max-deadline cap, the no-room rule (unclaimed and bound), the bound 400 rule, the ExtendRefused reason, the start build initiator check, bindExtended after success, and the earlier mutations 12 and no-shorten.

Gates on the final commit: drift (codegen, manifests, installer, API reference: no diff), make docs-check, dormant-CRD guard, docs lint and spelling, make verify-vendor, make build, make lint, actionlint, make test, make test-integration, make vuln, race. All passed (on the same tree before a rebase onto one unrelated main commit, #335).

Size

319 hand-written lines (113 production, 206 test, counting removals), plus 12 generated lines (deepcopy and the dormant CRD).

Risks

None for users. The reconciler is dormant: nothing registers it.


Generated with mux • Model: anthropic:claude-opus-5-5 • Thinking: high • Cost: $121.59

…line and check the start build initiator

Change-Id: I431d9dab372a21a885caef16659f5e9782a2fad3
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…ne at bind

Change-Id: I5b3d8d99f493c268d3b692baed5be0bec89e39a6
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 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-10-14T07:18:34.522364Z 5be0904 Manual request
🔒 Security Review ✅ Completed 2026-10-14T07:17:14.590473Z 5be0904 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.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

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

Reviewed commit: 10dc11cb15

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.

@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: 10dc11cb15

ℹ️ 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/controller/coderworkspacepoolmember_extend.go Outdated
Change-Id: I17d4226fdbc86196ea985c14a760c32b1917fa73
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 Security Review

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

Reviewed commit: 5be09049db

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.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 5be09049db

ℹ️ 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".

@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 14, 2026
Merged via the queue into main with commit e4fd8fc Oct 14, 2026
10 checks passed
@ThomasK33
ThomasK33 deleted the feat/workspace-pool-member-guard branch October 14, 2026 09:50
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