I recently worked through Temporal issue #11539, which involved deleting Worker Deployment Versions when the parent deployment and version workflow had drifted out of sync.
The code fix was small. The interesting part was reproducing the inconsistent state and finding the correct place to make deletion idempotent.
The problem
A Worker Deployment stores version summaries in the parent deployment workflow.
Normally:
Worker Deployment
|
| DeleteWorkerDeploymentVersion
v
Worker Deployment Version workflow
|
| delete succeeds
v
Parent removes version from State.Versions
The bug appears when the parent still lists a version even though the version workflow is already completed or missing:
DescribeWorkerDeployment
-> version still listed
DescribeWorkerDeploymentVersion
-> version not found
Version workflow
-> completed or missing
When DeleteWorkerDeploymentVersion runs, Temporal tries to send a delete-version update to that workflow.
Because the workflow can no longer accept the update, the server returns errors such as:
workflow execution already completed
or:
workflow not found for ID: ...
That error propagates back through the activity.
The parent only removes the version summary after the activity succeeds:
delete(d.State.Versions, args.Version)
So the stale entry remains, and every later delete fails for the same reason.
This matters because the stale version can still count toward deployment limits and can prevent the entire Worker Deployment from being deleted.
Reproducing the bug
I reproduced the issue locally on Temporal main.
Initial state:
parent deployment workflow: RUNNING
version workflow: RUNNING
parent versionSummaries: contains v1
I then tested two scenarios.
1. Version workflow completed
I terminated only the version workflow while keeping the parent running.
parent deployment: still lists v1
version workflow: TERMINATED
Deleting the version failed, with the server logs showing:
workflow execution already completed
The parent summary remained.
2. Version workflow missing
I then removed the version workflow completely.
DescribeWorkerDeployment
-> v1 still listed
DescribeWorkerDeploymentVersion
-> not found
workflow describe
-> workflow not found
Deleting the version failed again, this time with:
workflow not found for ID: ...
The important result was that retention was not required to reproduce the bug.
The failure exists whenever the parent version summary outlives the version workflow.
Choosing the fix
There were several possible places to handle the problem.
The frontend could swallow the NotFound error, but that would only hide the failure. The parent workflow would still keep the stale entry.
The parent could periodically reconcile missing version workflows, but that would introduce a broader cleanup mechanism for something deletion can handle directly.
The better question was:
Where does
NotFoundmean the delete operation has already reached its desired state?
The answer was Activities.DeleteWorkerDeploymentVersion.
If the version workflow is already gone, the version-side goal of deletion is already satisfied.
So deletion can safely treat that specific condition as success:
outcome, err := updateWorkflow(...)
if err != nil {
var notFound *serviceerror.NotFound
if errors.As(err, ¬Found) {
return nil
}
return err
}
I intentionally left the generic updateWorkflow helper unchanged.
NotFound does not mean success for every Temporal update. It only has that meaning here because this is a delete operation.
Once the activity succeeds, the existing parent workflow logic can remove the stale version summary normally.
Regression testing
Before changing the production code, I added tests for:
version workflow completed -> delete should succeed
version workflow missing -> delete should succeed
unrelated service error -> delete should still fail
The first two failed before the fix, while the unrelated error still behaved normally.
I also added a functional regression test that:
- Creates a deployment and version.
- Closes the version workflow.
- Keeps the parent deployment running.
- Calls the public delete API.
- Verifies that the version disappears from the parent's summaries.
After adding the NotFound handling, the regression tests passed without changing unrelated deletion behavior.
What happened upstream
I opened PR #11638 with the idempotent deletion change and regression coverage.
While it was under review, a Temporal maintainer merged PR #11696, which applies the same core behavior: treat a missing Worker Deployment Version as already deleted.
My PR was therefore not the commit that landed upstream, but the issue was ultimately resolved using the same underlying approach.
Takeaway
The fix itself was only a few lines.
The real work was:
reproduce inconsistent state
↓
trace the failed state transition
↓
find the correct idempotency boundary
↓
write a failing regression test
↓
make the smallest semantic change
The bug was not really about a missing workflow.
It was about two durable pieces of state completing the same delete operation at different times.
Once the child-side state is already gone, retrying deletion should converge toward the desired final state instead of requiring the deleted object to still exist.
Top comments (0)