Skip to content

fix(controller): re-apply worker resources when tracked object is missing#445

Open
frossbeamish wants to merge 1 commit into
temporalio:mainfrom
frossbeamish:rb/fix/wrt-reapply-after-sunset
Open

fix(controller): re-apply worker resources when tracked object is missing#445
frossbeamish wants to merge 1 commit into
temporalio:mainfrom
frossbeamish:rb/fix/wrt-reapply-after-sunset

Conversation

@frossbeamish

@frossbeamish frossbeamish commented Jul 17, 2026

Copy link
Copy Markdown

What

Guards the WRT apply hash-skip with an existence check on the rendered object. If the object referenced by an unchanged LastAppliedHash no longer exists, the controller falls through to the SSA apply (create-or-update) instead of skipping. Also extracts the WRT apply block from executePlan into applyWorkerResourceTemplates so it can be unit-tested without a Temporal client.

Why

We hit this in a live cluster running v1.8.0. Sequence:

  1. A worker deployment version is sunset: the controller deletes its versioned Deployment and its WRT-rendered ScaledObject (DeleteWorkerResources).
  2. The WRT status.versions entry for that build ID survives the window (the status update that would drop it can hit an optimistic-concurrency conflict, and the build can be re-registered before the next successful status write, in our case the same build ID was re-registered ~4 minutes after the sunset delete).
  3. The build ID returns as the target version. The render is byte-identical, so RenderedHash == LastAppliedHash and the SSA apply is skipped forever.
  4. The returning version's Deployment is created and scaled to 1 replica (the planner's spec.Replicas == nil path, expecting the scaler resource to own replicas), but the ScaledObject is never re-created. Result: the current version runs a single unscaled pod, which in our case OOM-looped under the full task-queue load and stalled all workflows on that queue.

The hash match tells the controller the render is unchanged; it says nothing about whether the applied object still exists. The existence check is a cache read against an informer the controller already maintains for these GVKs, so the API-server-load rationale for the skip is preserved.

Testing

  • New unit tests in internal/controller/execplan_wrt_test.go:
    • hash-unchanged + resource missing → re-applied, status entry re-recorded
    • hash-unchanged + resource present → still skipped (no Patch calls, verified via interceptor)
  • go test ./internal/controller/ ./internal/planner/ green; go build ./..., go vet, gofmt clean.

…sing

The WRT apply path skips the SSA apply whenever the rendered hash matches
the LastAppliedHash recorded in the WRT status. The hash alone is not a
safe signal: when a version is sunset its rendered resources are deleted,
but the status entry for that build ID can survive (the status update that
would drop it can hit a conflict, or the build can be re-registered before
the next successful status write). If the same build ID returns, the stale
hash matches the unchanged render and the resource is never re-created.

For a WRT rendering a ScaledObject this leaves the returning version's
Deployment with no autoscaler: the controller starts it at 1 replica
expecting the scaler to own replicas, and nothing ever scales it.

Guard the skip with an existence check on the rendered object (a cache
read), falling through to the SSA apply on NotFound or on an
indeterminate error. Extract the WRT apply block into
applyWorkerResourceTemplates so it is unit-testable in isolation.
@frossbeamish
frossbeamish requested review from a team and jlegrone as code owners July 17, 2026 23:35
@CLAassistant

CLAassistant commented Jul 17, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

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