Skip to content

fix(controller): sort deprecatedVersions so an unchanged status is not rewritten every reconcile - #38

Merged
tczhao merged 2 commits into
v1.6.0-atlanfrom
tianchu/sha-1469-sort-deprecated-versions
Sep 8, 2026
Merged

tczhao merged 2 commits into
v1.6.0-atlanfrom
tianchu/sha-1469-sort-deprecated-versions

Conversation

@tczhao

@tczhao tczhao commented Sep 8, 2026

Copy link
Copy Markdown
Member

Cherry-pick of upstream temporalio#458 (7e13417, first shipped in v1.8.1) onto v1.6.0-atlan, plus a regression test. status.deprecatedVersions is now sorted by build ID before it is assigned, so a TWD whose state has not changed maps to an identical status on every reconcile. Refs SHA-1469.

Why it loops

Reconcile skips its Status().Update only when the mapped status DeepEquals the persisted one (worker_controller.go). deprecatedVersions was built from a range over the Deployments map, so its order changed on every reconcile. For any TWD with several deprecated versions the guard never matched: every reconcile issued a PUT .../status, the write fired a watch event on the TWD, and the controller reconciled it again immediately. The 10s RequeueAfter never gets a chance to matter.

Evidence (2026-09-08)

value
TWD status PUT/s fleet-wide, ~590 tenants ~1,000 (~2.6% of all apiserver 2xx requests)
typical tenant ~2/s, same mechanism at low intensity
three internal test tenants that receive CI builds 16-40/s

On the worst tenant, 3 of 80 TWDs with 12-13 drained versions each produced ~330 of ~405 status writes per 20s (~5.5/s per CR), controller_runtime_reconcile_total was 100% requeue_after at 44/s, and two consecutive GETs of such a CR differed only in the order of deprecatedVersions. Reorder probability grows with the number of entries and sunset.deleteDelay is 24h, so the loop switches on after a deploy burst and off a day later. This is what tripped HighApiServerRequestCount on those tenants.

The change

state_mapper.go: slices.SortStableFunc by BuildID immediately before status.DeprecatedVersions = deprecatedVersions. Byte-identical to upstream apart from the merged import block; the upstream comment is kept verbatim so future rebases see the same text. Build ID is always set and unique per entry, which is why it is the sort key rather than drainedSince.

Test

TestMapToStatusDeprecatedVersionsOrderIsStable maps one current plus twelve drained versions, inserted in descending order, twenty times and asserts ascending build-ID order. Against the unsorted mapper it fails on the first pass with a permutation of the same IDs; with the sort it passes.

Verified locally on this branch: go vet, gofmt, gci, golangci-lint (0 new issues vs v1.6.0-atlan), go build ./..., and go test ./... with envtest 1.27.1.

🤖 Generated with Claude Code

jaypipes and others added 2 commits September 8, 2026 14:15
Status().Update is skipped only when the mapped status DeepEquals the
persisted one. deprecatedVersions was built from a map range, so a TWD
with several drained versions mapped to a differently ordered slice on
every reconcile, rewrote its status, and re-triggered itself through the
watch: ~40 status PUT/s on the worst tenants, ~1,000/s fleet-wide.

(cherry picked from commit 7e13417)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fails against the unsorted mapper on the first pass with a permutation of
the same build IDs and passes with the sort, so the DeepEqual status-write
skip cannot regress into a reconcile loop again. Refs SHA-1469.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@linear

linear Bot commented Sep 8, 2026

Copy link
Copy Markdown

SHA-1469

@tczhao
tczhao merged commit 26f4fdd into v1.6.0-atlan Sep 8, 2026
16 checks passed
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.

2 participants