Skip to content

fix(controller): record pinned executions before force-deleting versions on teardown - #34

Open
tczhao wants to merge 1 commit into
v1.6.0-atlanfrom
tianchu/arun-1232
Open

tczhao wants to merge 1 commit into
v1.6.0-atlanfrom
tianchu/arun-1232

Conversation

@tczhao

@tczhao tczhao commented Sep 1, 2026

Copy link
Copy Markdown
Member

TWD teardown can permanently strand in-flight customer workflows, and today it leaves no record of which ones. This makes it leave one.

handleDeletion force-deletes every worker deployment version with SkipDrainage: true. That flag bypasses the one server-side guard Temporal provides against deleting a version that still has open pinned executions. Those workflows are then unrecoverable without manual intervention: their tasks route by exact deployment-version match, and a task on a base (non-sticky) task queue has no schedule-to-start timeout, so once the version is gone nothing dispatches them and nothing expires them. They sit at attempt: 1 forever.

This fired on 2026-08-31 and stranded six customer workflows. One had been stranded since 08-27 and nobody noticed, because there was nothing to notice.

What this changes

Before each force-delete, query visibility for open executions pinned to that version:

ExecutionStatus="Running" AND TemporalWorkerDeploymentVersion="<deployment>:<buildID>"

If the count is non-zero, log the count, a sample of workflow IDs, and the query itself, then emit a Warning Event (PinnedExecutionsStranded).

The delete still proceeds. Honouring drainage here would hold the TWD in Terminating for as long as the longest pinned workflow runs - a 72-hour Snowflake crawl blocks the namespace and any reinstall for 72 hours. That trades a silent data-integrity failure for a loud availability failure. This PR does not make that trade; it only stops the failure being silent. Bounded drainage is ARUN-1232 item 2 and is deliberately out of scope here.

Why the log line, not a status condition

ARUN-1232 proposes setting a status condition. That does not work on this path and the ticket should be read with this correction:

  • This runs inside the finalizer. On success the finalizer is removed and the API server deletes the TWD immediately, so a condition written here survives for microseconds.
  • Cluster Events expire on the apiserver's event-ttl, commonly 1h. Against an incident that went unnoticed for 20 hours and 5 days, that is weak.
  • Controller logs are already scraped into otel_logs.filelogs_deployment and are exactly where the DISTR-947 detection queries look.

So the log line is the contract. The Event is the human-facing nudge. The status condition belongs to item 2, where the object stays around to carry it.

Grep target for detection:

msg = "force-deleting worker deployment version with open pinned executions"

Deliberate choices

  • Visibility failure never blocks teardown. It reports that the check could not run (PinnedExecutionCheckFailed) and returns. Proceeding keeps teardown reliable at the cost of reintroducing the silent case exactly when Temporal is unhealthy - the alternative risks wedging teardown on an unrelated outage, which is the failure mode ARUN-1182 is already about.
  • planner.go is untouched. The steady-state reaper only deletes VersionStatusDrained, which by definition has no open pinned executions. Adding the check there would be cost with no signal.
  • Query precision. Uses the query validated in DISTR-947 rather than also filtering TemporalWorkflowVersioningBehavior="Pinned". AutoUpgrade workflows on a doomed version are not stranded and will inflate the count, but the extra built-in search attribute may not be registered on every namespace, and a failed query degrades to the check-failed path. Over-reporting on a warning is the safer side to err on.
  • Cost. Two visibility RPCs per version per reconcile attempt. Teardown requeues every 10s while pollers persist (58 attempts over ~20 minutes in the 08-31 incident), so a stuck teardown could issue a few thousand count queries. ES count queries are cheap and this is a rare path, but flagging it explicitly.

Testing

go test ./internal/controller/... - 137 pass. Four new tests in teardown_pinned_test.go cover: open pinned executions logged and evented with IDs; nothing pinned stays silent and skips the list call; count failure proceeds with a warning; list failure still reports the count.

handleDeletion itself is not unit-testable - it dials the client pool directly - so "the delete still proceeds" is guaranteed structurally by recordPinnedExecutions having no return value, not by a test.

make lint-code reports one pre-existing finding, execplan.go:355 cyclomatic complexity, identical on the base branch and in an untouched file.

Scope

No CRD change, no API change, no new flag, no behaviour change to the delete itself. Should rebase cleanly and is a reasonable upstream candidate.

Refs ARUN-1232. The trigger for the accidental teardown is tracked separately in DISTR-947 (Flux remediation.strategy: uninstall), which is still necessary - this PR does not remove the need for it.

🤖 Generated with Claude Code

…ons on teardown

TWD teardown force-deletes every worker deployment version with
SkipDrainage: true, which bypasses the one server-side guard against
deleting a version that still has open pinned executions. Those workflows
are then stranded permanently - their tasks route by exact version match
and a task on a base task queue has no schedule-to-start timeout, so once
the version is gone nothing dispatches them and nothing expires them.
Today this happens with no record of which workflows were lost.

Query visibility for open executions pinned to each version immediately
before the force-delete, then log the count, a sample of workflow IDs, and
the query itself, and emit a Warning Event. The delete still proceeds:
blocking teardown would hold the TWD in Terminating for as long as the
longest pinned workflow runs, trading a data-integrity failure for an
availability one. This converts a silent permanent failure into a
recoverable one with a list to work from.

The log line is the durable record, not the Event or a status condition.
This runs inside the finalizer, so the TWD is deleted moments later and any
condition written here goes with it; cluster Events expire on the
apiserver's event-ttl. Controller logs are already scraped and are where
the ARUN-1232 / DISTR-947 detection queries look.

A visibility failure never blocks teardown - it reports that the check
could not run and proceeds. The steady-state reaper in planner.go is
unchanged: it only deletes Drained versions, which by definition have no
open pinned executions.

Refs ARUN-1232. The trigger that caused the accidental teardown on
2026-08-31 is tracked separately in DISTR-947.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@linear

linear Bot commented Sep 1, 2026

Copy link
Copy Markdown

ARUN-1232

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