Repository navigation
Continue the deploy workflow as new to bound its history - #794
Merged
Merged
Conversation
emmanuelmaduwuba-lyft
added this pull request to stack #795
October 8, 2026 15:57
yusyuan-lyft
approved these changes
Oct 8, 2026
The Deploy workflow (one per repo and root) only exits when its queue is empty. A queue that is locked with merged revisions waiting keeps the workflow alive indefinitely, and each idle hour adds a timer and a workflow task to its history. Some of these histories are approaching Temporal's hard limit, where the server terminates the workflow. The queue and lock live only in workflow memory, so termination drops queued deploys. A large history can also stop a workflow from making progress after a worker restart, when replaying it exceeds the SDK's deadlock detection. The workflow now continues as new at an idle point (no deployment running and nothing the queue can start), carrying its queue, lock state, and check run IDs into the next run's request. Restored check run IDs keep queued deployments updating their existing check runs instead of creating duplicates. Before continuing, it handles revision and unlock signals that arrived but weren't processed yet, so they're carried over too. It continues as new when either: - its history passes 10,000 events, using GetCurrentHistoryLength(). This is behind workflow.GetVersion, so histories recorded before this change replay unchanged. - it receives the new "continue-as-new" signal (exported as workflows.DeployContinueAsNewSignalName). A workflow started before this change replays its old timeouts first, which fixes the version at its default, so the history check never applies to it. The signal moves those workflows over once. Their histories contain no such signal, so adding it doesn't affect replay. The worker's state stays "working" while it waits on a locked queue that still has items, so idleness is judged by the new Worker.IsDeploying() plus Queue.CanPop() instead. The workflow interceptor counted any returned error as a failure, including continue-as-new. It now counts continue-as-new separately.
emmanuelmaduwuba-lyft
force-pushed
the
deploy-continue-as-new
branch
from
October 8, 2026 17:19
2125525 to
d4907ce
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The Deploy workflow only exits when its queue is empty. A queue that is locked with merged revisions waiting keeps the workflow alive indefinitely, and each idle hour adds a timer and a workflow task to its history, roughly 125 events a day. Two things go wrong as it grows:
queue/worker.go: "we don't persist lock state anywhere"), so queued deploys are dropped without any notice.What changes
The workflow continues as new at an idle point and carries its in-memory state into the next run:
workflow.GetVersion); orcontinue-as-newsignal (workflows.DeployContinueAsNewSignalName, no input).workingwhile it waits on a locked queue with items, so idleness uses a newWorker.IsDeploying()plusQueue.CanPop()rather thanGetState().deploy.Request.ContinuedState):continue_as_newcounter tagged with the reason.Why there's a signal as well as the history check
GetVersionreturnsDefaultVersionwhen it's first called during replay, and keeps that for the rest of the execution (sdk-go v1.16.0internal_event_handlers.go,GetVersion). A workflow started before this change replays its old timeouts first, so the history check never applies to it. The signal moves those workflows over once. Their histories contain no such signal, so listening for it doesn't affect replay. New runs, including those created by the signal, use the history check from then on.Replay safety
On histories recorded before this change, nothing new emits a command:
Idle(),HasPending()and the history length are pure.GetVersionreturns the default during replay.ContinuedStateis nil.The post-deploy PR revision child workflow is started with
PARENT_CLOSE_POLICY_ABANDON, so it survives the parent continuing as new. The Terraform child workflow only runs while a deploy is in progress, which the idle check excludes.Testing
New tests:
Runner:
GetVersionmocked to default);Each case decodes the continue-as-new input and checks the carried state and the original repo and root.
queueContinuer.Snapshotdrains a pending revision signal into the queue and applies a pending unlock.restoreQueuerestores order and check run IDs, and is a no-op without state.Worker.IsDeployingis false for a worker that hasn't deployed, sinceInProgressStatusis the zero value.Rollback
After a workflow has continued as new, code without this change would ignore
ContinuedStateon the next start and drop that queue. Fix forward rather than reverting once any workflow has continued.