Skip to content

Preserve Workflow Streams after workflow closure - #1994

Merged
tconley1428 merged 2 commits into
mainfrom
codex/preserve-workflow-streams-after-closure
Oct 28, 2026
Merged

tconley1428 merged 2 commits into
mainfrom
codex/preserve-workflow-streams-after-closure

Conversation

@brianstrauch

@brianstrauch brianstrauch commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

What was changed

Active subscriptions continue polling through Updates. After Describe confirms completion, failure, cancellation, termination, or timeout, subscriptions query the confirmed run for all remaining retained pages and finish after final delivery. The synchronous __temporal_workflow_stream_poll Query shares PollInput/PollResult and read-only batch collection with the Update. It returns immediately and clamps stale offsets to the retained base.

Accepted-run tracking is local to each subscription attempt. Query errors, failed descriptions, and unconfirmed closure remain errors; terminal reads stay pinned across workflow ID reuse. Existing truncation, draining, cancellation, conversion, and continue-as-new behavior is preserved.

Why?

Subscribing after closure, or closing a run during an accepted poll, could previously end the subscription before its unread retained tail was delivered. Query draining makes retained stream contents available after closure without adding history events.

Checklist

  1. How was this tested:
  • Workflow Streams integration/unit suite and focused malformed-Describe regressions passed.
  • poe lint passed.
  • Covered all five terminal statuses, cold replay with unchanged history, pagination/filtering/truncation, ID reuse, concurrent subscriptions, codecs/raw conversion, query rejection, and missing handlers.
  • Python-client/TypeScript-worker completed-stream interoperability smoke passed with cold replay and unchanged history.
  1. Any docs updates needed?

Updated the stream guide/API comments and release notes. Workers must be upgraded before upgraded clients use closed-stream reads; retained history and an available compatible worker are required.

  1. Breaking changes / Server PR:

Subscription signatures and existing wire types are unchanged. No Server, Core, or protobuf change is required. Upgraded clients surface missing poll Query errors from older workers.

Coordinated SDK PRs

@moetemp moetemp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a pass through the closed-stream drain. Looks clean. Left a few questions, mostly around the Update-to-Query hand-off and the cost of draining a large tail. Nothing blocking from my side.

Comment thread temporalio/contrib/workflow_streams/_stream.py Outdated
Comment thread temporalio/contrib/workflow_streams/_client.py Outdated
Comment thread temporalio/contrib/workflow_streams/_client.py
Comment thread tests/contrib/workflow_streams/test_closed_streams.py

def _collect_batch(self, payload: PollInput) -> PollResult:
"""Read retained items immediately without changing stream state."""
log_offset = max(0, payload.from_offset - self._base_offset)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each page is one query. On a closed run the worker replays history to answer every query. So a large retained tail turns into one full replay per page. Is matching the 1MB Update cap worth it for closed-run reads, or could a drain read use a bigger single-shot budget to cut the replays? We can defer if the retained tail stays small in practice.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend keeping the current ~1 MB cap for this PR. I ran 600 local dev-server drains using the existing native build: 10 workloads × 0.5/1/1.5 MB estimated budgets × 20 repetitions, varying tail/history size, caching, filtering, and AES-GCM encoding. Every measured Query page recreated the workflow. For a Signal-backed ~5 MB tail, 1.5 MB pages reduced median drain time from 1.42 s to 1.06 s (six replays to four), but increased first-item latency from 247 ms to 279 ms.

The larger budget also exposed missing JSON/envelope overhead in the estimator: with 16-byte items, the 1.5 MB estimate produced a 2.38 MB response and failed with WorkflowQueryFailedError: Blob data size exceeds limit; the current budget succeeded. I'd defer a larger Query-specific budget until response-size accounting and codec headroom are addressed.

@tconley1428
tconley1428 merged commit d30ecbe into main Oct 28, 2026
20 checks passed
@tconley1428
tconley1428 deleted the codex/preserve-workflow-streams-after-closure branch October 28, 2026 17:51
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.

3 participants