Skip to content

🤖 feat: reuse the earlier import when a CoderTemplate Update is retried - #132

Draft
ThomasK33 wants to merge 6 commits into
mainfrom
feat/template-update-version-reuse
Draft

ThomasK33 wants to merge 6 commits into
mainfrom
feat/template-update-version-reuse

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Part of #117.

What changed

Retrying the same CoderTemplate Update now waits for, or activates, the template version an earlier attempt started, instead of starting a new import every time. Before this, an import that outlasted the 34-second write budget could never become active through the API: each retry created another version, timed out again, and left one more unused version behind.

How it works:

  1. Name. An Update with changed files names its version k8s- plus 20 hex digits of a hash over the template ID and the exact source zip. Attempt n ≥ 2 is <name>-<n>. The name is recomputed from the request, so it also works after a restart. Two templates with identical files never share a version.
  2. Lookup. The Update first uploads the source (Coder returns the existing file ID for identical bytes from the same user). It then finds the latest attempt with that name (TemplateVersionByName; an exponential probe plus binary search, about 2·log₂(n) lookups).
    • Built from that same file and pending or running: wait for it. No new version.
    • Built from that same file, succeeded and not archived: activate it.
    • Failed, canceled, being canceled, archived, or built from a different file: create the next attempt. Version names are caller-controlled in Coder, so a version that only matches the name is never reused.
  3. Races. If another request creates the same name first, Coder returns 409 with a name validation error and creates no job. The Update looks up again and waits on the winner's version, never returning AlreadyExists.
  4. Bound per request. At most 48 lookups (then 503, nothing created). A deadline or cancellation during the lookups returns 504, nothing created. There is no permanent cap on attempts.
  5. Unchanged: Create, and 🤖 fix: apply CoderTemplate metadata only after the new source imports #129's order (metadata only after activation, and the UID/resourceVersion re-check after the wait).

docs/reference/aggregated-api-behavior.md gets an "Update retries" section and keeps the "not idempotent" warning for Create. It also folds in two small wording fixes from the #128 review.

Why only Update

Coder has no API to look up a version that is not attached to a template yet, so reuse on Create can only be best-effort. That part is deferred. The Coder behaviors this PR relies on (name rules, the duplicate-name 409 and its single transaction, lookup by name) were checked in the Coder v2.37.2 source first, and the 409 was confirmed live.

Tests

Review round 2 added the file check: TestTemplateStorageUpdateNeverReusesSquattedName builds a version with the exact derived name from other source (succeeded, pending, running); the Update creates <name>-2 and never touches the squatter. Removing the file check fails all of its cases. A live KIND check on Coder v2.37.2 (real attempt renamed away, squatter created with the derived name) confirmed that repeated kubectl apply activated <name>-2 built from the right file and never the squatter.

template_update_reuse_test.go adds 19 tests, all passing: name derivation; timeout then retry on the same and on a restarted server (exactly one CreateTemplateVersion); a succeeded attempt promoted with no new version and no upload; failed, canceled, being-canceled and archived attempts each lead to -2; 40 earlier failures lead to -41 in ≤12 lookups; a lost duplicate-name race; lost responses (version created or promotion applied, reply dropped); no sharing across templates; the 48-lookup limit (503); cancellation mid-scan (504, no writes). The full internal/aggregated/... suite passes.

Mutations: always creating a new version, treating the 409 as an error, raising the lookup limit, dropping the template ID from the name, a linear scan, treating archived as reusable, and dropping the in-flight context mapping each make at least one test fail. One more mutation (no context check before each lookup) is equivalent to the last one and changes no behavior.

Gates on the head: make test, make test-integration, make build, make verify-vendor, make lint, make docs-check, all exit 0.

Live KIND run against Coder v2.37.2

Recorded on an image built from the same code before a docs-only rebase (the pod's image ID matched the built image).

Step 2: Coder itself rejects a second version with the same name on one template (409 with a name validation error), and the rejected request creates no import job.

Step 2: Coder returns 409 for a duplicate version name and starts no extra import

Step 3: an Update to a source that takes about 102 s to import, applied repeatedly with kubectl apply: 504, 504, then 200. Coder ends with one new version, k8s-aa38de892b445b2e6016, now active, and one new import job.

Step 3: three applies converge on one version and one import

Step 4: an import that fails. Two applies return 400 and create k8s-6130… and then k8s-6130…-2.

Step 4: a failed attempt leads to the next attempt name

Accelerated replay of the recorded run (14 s; idle gaps trimmed):

live-kind-run-accelerated.webm

Not verified

  • Truly concurrent Updates on a live cluster. The 409 path is covered by a unit test and by the live Coder 409 in step 2.
  • Coder versions other than v2.37.2.
  • Versions created before this change keep random names and are never reused (documented).

Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: xhigh • Cost: $1680.17

A CoderTemplate Update with changed spec.files created a new template
version on every request. When the import outlasted the request, each
retry started another import and none of them ever became active.

Update now names the version after the target template and the exact
source zip ("k8s-" + 20 hex digits of a hash). Before creating a
version it looks up the latest attempt with that name. A pending,
running or succeeded attempt is reused, so a retry waits on or promotes
it, also after a server restart. A failed, canceled or archived attempt
is skipped and the next attempt gets "<name>-<n>". If another request
creates the same name first, Coder answers 409 and the lookup runs again
instead of failing.

Finding the latest attempt uses an exponential probe and a binary
search, so skipped attempts cost O(log n) lookups. Each request stops
after 48 lookups (503) or when its context ends (504), and creates
nothing in that case. Create is unchanged.

Part of #117

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Change-Id: Id70c6acd7411b73e7bf26b71c035d896c2e209d8
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Update retries with the same files now wait on or activate the version
an earlier request started, and a failed attempt gets the next name.
Document that for Update only; Create retries still import again.

Also point the Template builds summary to the 34-second budget section
for imports that finish just before a timeout, and say that the maximum
poll interval must be greater than 0.

Part of #117

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

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

chatgpt-codex-connector Bot commented Sep 24, 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-09-24T13:50:35.818204Z 8576174 Manual request
🔒 Security Review ✅ Completed 2026-09-24T13:49:41.094986Z 8576174 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: 36d2bb6af1

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.

The docs-quality job flagged "resends" in the Update retries section.

Part of #117

Signed-off-by: Thomas Kosiewski <tk@coder.com>

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Change-Id: I9a3fa5d2fb48bc71da74cc62fba2d7a6cbdbb3be
@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: d3c3d68d0e

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: d3c3d68d0e

ℹ️ 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/aggregated/storage/template_version_reuse.go Outdated
Comment thread docs/reference/aggregated-api-behavior.md Outdated
… file

Coder lets callers choose or change template version names, so a
version with the derived name could hold other source. Reusing it on
the name alone could wait on and activate the wrong source.

Update now uploads the source first (Coder returns the existing file ID
for the same bytes from the same user) and reuses an attempt only when
its job was built from that file ID. A name match with another file
counts as unusable, so the next attempt name is used. The lookup bound
and context handling are unchanged. The mock deduplicates uploads like
coderd.

Part of #117

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Change-Id: I631bb729f759c21b3cb1e0eb88ebd9a6f8ba8a83
Signed-off-by: Thomas Kosiewski <tk@coder.com>
List canceling with the unusable attempt states, and explain that a
version with the derived name is reused only when it was built from the
same uploaded file, so versions with other source or from another Coder
user are never reused.

Part of #117

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Change-Id: I94f127c0633e1ab2b842f22c29a03b1f9361b4cd
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: 8576174bed

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: 8576174bed

ℹ️ 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 docs/reference/aggregated-api-behavior.md Outdated
Comment thread internal/aggregated/storage/template_version_reuse.go
Comment on lines +78 to +79
if version.Archived || version.Job.FileID != fileID {
return false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify every import input before reusing a version

Fresh evidence beyond the earlier source-mismatch report is that CreateTemplateVersionRequest accepts import-affecting inputs such as ProvisionerTags and UserVariableValues, while this predicate verifies only the deduplicated file ID. A caller using the same Coder user can create or rename a version built from that same file but with different variable values or provisioner routing; a retry then treats it as its own and can activate build results that the Kubernetes Update would never have created. Validate all relevant creation inputs or use an immutable ownership marker, and cover the same-file/different-input case.

AGENTS.md reference: AGENTS.md:L107-L108

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid, and not fixable with the current Coder API. Coder v2.37.2 does not expose a template version's provisioner type or user variable values (codersdk.ProvisionerJob has no provisioner field, and the job input is decoded to TemplateVersionID/WorkspaceBuildID/Error only), and the expected tags depend on those variables. So there is no fail-closed check that an earlier attempt was built with the same inputs. This PR is deferred until Coder offers a request-scoped idempotency key or exposes the job's creation inputs. Leaving this thread open.

A lookup that fails with 503 or 504 creates no template version or
import, but the file it uploaded first can remain in Coder. Say that
instead of "creates nothing".

Retries converge only while the k8s-... versions are not renamed or
changed outside coder-k8s: renaming one breaks the contiguous attempt
names, and a later retry starts another import. Document that and add
a test that pins the behavior: one extra import, and only the new
attempt built from the retry's own upload is activated.

Part of #117

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Change-Id: I2be9fbd85d5b2f0efa24fc4a323a73a77324a86b
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33
ThomasK33 marked this pull request as draft September 24, 2026 14:26
@ThomasK33

Copy link
Copy Markdown
Member Author

Deferred, and converted to draft.

The last review found that reuse checks the version name and the uploaded file, but not the other inputs that affect an import: provisioner type, user variable values and provisioner tags. A version built from the same file with different inputs could be waited on and activated. Coder v2.37.2 does not expose the provisioner type or the user variable values of a template version, and the expected tags depend on those variables, so there is no fail-closed way to prove that an earlier attempt matches the request. Anything that trusts the name and file alone changes the contract this PR claims.

What stays on this branch for later: the naming and lookup code, the file-ID check against name squatting, the tests (including out-of-band rename and squatting), and the docs. The latest commit (7123a09) fixes the two smaller findings; it does not fix the input-equivalence finding, which stays open.

main already has the 34-second budget docs (#128) and the safer Update ordering (#129). This PR can come back if Coder adds a request-scoped idempotency key for template version creation, or exposes a version's creation inputs (provisioner type and variable values). Tracked on #117.


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

This branch has not been deployed

No deployments
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