Skip to content

Bump Temporal Go SDK to v1.16.0 and add a workflow replay test - #793

Merged
emmanuelmaduwuba-lyft merged 2 commits into
mainfrom
temporal-sdk-1.16
Oct 8, 2026
Merged

emmanuelmaduwuba-lyft merged 2 commits into
mainfrom
temporal-sdk-1.16

Conversation

@emmanuelmaduwuba-lyft

@emmanuelmaduwuba-lyft emmanuelmaduwuba-lyft commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Why

The Deploy workflow (one per repo and root) and the PR workflow run for a long time and never call ContinueAsNew, so their histories grow without bound. A large history can leave a workflow unable to make progress after a worker restart, because replaying it exceeds the SDK's 1-second deadlock detection. Past Temporal's hard history limit, the server terminates the workflow and any queued deploys are lost. The fix, in a follow-up PR, continues these workflows as new once their history passes a threshold. That needs workflow.GetInfo(ctx).GetCurrentHistoryLength(), which the SDK added in v1.16.0.

What changes

  • go.temporal.io/sdk v1.15.0 → v1.16.0, go.temporal.io/api v1.8.0 → v1.11.0, github.com/gogo/status v1.1.0 → v1.1.1. No workflow code changes.
  • TestReplayHistories replays exported workflow histories against the current code, registered the way the worker registers it. Histories hold production workflow inputs, so they're read from ATLANTIS_REPLAY_HISTORY_DIR and never checked in. The test skips when the variable isn't set.
  • The Deploy workflow and its plugins move into deployWorkflow(), so the worker and the replay test share one definition.
  • Current Temporal CLI exports use enum names like EVENT_TYPE_WORKFLOW_EXECUTION_STARTED and include fields newer than API v1.11. The test rewrites the enum names and ignores unknown fields, which is what the worker does when it receives history as protobuf.

v1.16.0 release notes worth a look

Testing

  • go build ./... succeeds with the existing replace google.golang.org/grpc => v1.45.0 pin, and go vet ./server/neptune/... is clean.
  • go test ./... passes except two legacy e2e tests that need a released conftest binary on PATH; they fail the same way on main

The deploy and PR workflows run for a long time and never continue as new,
so their histories grow without bound. Fixing that needs
workflow.GetInfo(ctx).GetCurrentHistoryLength(), which the SDK added in
v1.16.0. This bumps go.temporal.io/sdk v1.15.0 -> v1.16.0 (and
go.temporal.io/api v1.8.0 -> v1.11.0, gogo/status v1.1.0 -> v1.1.1) with no
workflow code changes.

An SDK bump or workflow change that alters the commands a workflow emits
breaks workflows that are already running. TestReplayHistories replays
exported histories against the current code to catch that. Histories hold
production workflow inputs, so the test reads them from the directory in
ATLANTIS_REPLAY_HISTORY_DIR and skips when it isn't set.

The test registers workflows the way the worker does. The Deploy workflow
and its plugins are moved into deployWorkflow() so the worker and the test
share one definition. Current Temporal CLI exports name enums like
EVENT_TYPE_WORKFLOW_EXECUTION_STARTED and include fields newer than this
API version, so the test rewrites enum names and ignores unknown fields,
as the worker does when it receives history as protobuf.
gosec G703 flagged os.ReadFile on a path built from ATLANTIS_REPLAY_HISTORY_DIR.
Opening the directory with os.OpenRoot confines every read to it, so a
history name can't reach outside the directory.
@emmanuelmaduwuba-lyft
emmanuelmaduwuba-lyft added this pull request to stack #795 October 8, 2026 15:57
@emmanuelmaduwuba-lyft

Copy link
Copy Markdown
Contributor Author

/ptal @yusyuan-lyft @jfernandoz

@emmanuelmaduwuba-lyft
emmanuelmaduwuba-lyft merged commit b6877aa into main Oct 8, 2026
3 checks passed
@emmanuelmaduwuba-lyft
emmanuelmaduwuba-lyft deleted the temporal-sdk-1.16 branch October 8, 2026 17:19
emmanuelmaduwuba-lyft added a commit that referenced this pull request Oct 8, 2026
**Why**

The Deploy workflow (one per repo and root) only exits when its queue is
empty (`deploy/workflow.go`, `OnTimeout`). 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:
 

1. **Past Temporal's hard history limit, the server terminates the
workflow.** The queue and lock live only in workflow memory
(`queue/worker.go`: "we don't persist lock state anywhere"), so queued
deploys are dropped without any notice.
2. **A large history can stop a workflow from making progress.** After a
worker restart, replaying it can exceed the SDK's 1-second deadlock
detection, and every workflow task then fails.

**What changes**

The workflow continues as new at an idle point and carries its in-memory
state into the next run:

- **When:**
  - its history passes 10,000 events (behind `workflow.GetVersion`); or
- it receives the new `continue-as-new` signal
(`workflows.DeployContinueAsNewSignalName`, no input).
- **Only when idle:** no deployment is running and the queue can't pop
anything. The worker reports `working` while it waits on a locked queue
with items, so idleness uses a new `Worker.IsDeploying()` plus
`Queue.CanPop()` rather than `GetState()`.
- **What's carried over** (`deploy.Request.ContinuedState`):
  - queued deployments, in pop order;
  - lock state;
- check run cache entries, so queued deployments keep updating their
existing check runs instead of creating duplicates.
- **Before continuing:** revision and unlock signals that arrived but
weren't processed are handled, so they're carried over too.
- **Metrics:** the workflow interceptor counts continue-as-new
separately instead of as a failure, plus a `continue_as_new` counter
tagged with the reason.

**Why there's a signal as well as the history check**

`GetVersion` returns `DefaultVersion` when it's first called during
replay, and keeps that for the rest of the execution (sdk-go v1.16.0
`internal_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:

- Registering the new signal channel and evaluating `Idle()`,
`HasPending()` and the history length are pure.
- `GetVersion` returns the default during replay.
- Restoring state is a no-op when `ContinuedState` is 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:
  - continues as new on a request while idle;
  - on a request while busy, waits for the next idle timeout;
  - never continues while never idle;
  - continues on history over the limit while idle;
- doesn't continue over the limit on a workflow started before the
change (`GetVersion` mocked to default);
  - doesn't continue under the limit;
  - doesn't continue over the limit while deploying.

Each case decodes the continue-as-new input and checks the carried state
and the original repo and root.
- `queueContinuer.Snapshot` drains a pending revision signal into the
queue and applies a pending unlock.
- `restoreQueue` restores order and check run IDs, and is a no-op
without state.
- `Worker.IsDeploying` is false for a worker that hasn't deployed, since
`InProgressStatus` is the zero value.

`go test ./...` passes except two legacy e2e tests that need a released
`conftest` on `PATH`, which fail the same way on `main`. Before merging,
run `TestReplayHistories` from #793 against exported production Deploy
histories, including a long-running one.

**Rollout**

1. Deploy to staging, then production.
2. New deploy workflows continue as new automatically once they pass
10,000 events.
3. Send `continue-as-new` once to each existing long-running deploy
workflow, e.g. `temporal workflow signal --workflow-id '<repo>||<root>'
--name continue-as-new`. Then check the new run's start input carries
the expected queue, and that queued check runs are updated, not
duplicated.

**Rollback**

After a workflow has continued as new, code without this change would
ignore `ContinuedState` on the next start and drop that queue. Fix
forward rather than reverting once any workflow has continued.
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