Performance [P2]: Reduce orchestration history replay allocations - #204
Merged
Merged
Conversation
Long orchestration histories were copied and rescanned several times per work item before and during replay: - `TaskHubGrpcWorker._execute_orchestrator()` built `list(req.pastEvents) + list(req.newEvents)` (three throwaway lists sized by the full history) purely to locate the first `executionStarted` event. It now iterates `itertools.chain(...)` and still breaks at the first match. - `_OrchestrationExecutor.execute()` materialized every `executionStarted` event in the committed history but only read element `[0]`. It now uses `next(...)` over a generator, short-circuiting at the first match and falling back to `"<unknown>"` exactly as before. - `_OrchestrationExecutor._build_rewind_result()` built a combined `list(old_events) + list(new_events)` for its two passes. Both inputs are sequences, so each pass now chains them lazily in the same order. Replay order, rewind handling, non-determinism detection, log messages and exception types/messages are all unchanged; this is an allocation-only change with no observable behavior difference. Adds regression tests covering both branches of the orchestration-name lookup (resolved from committed history, and the "<unknown>" fallback). Fixes #185 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d6a6454c-5bdc-4bc7-9059-a2789a65e271
Contributor
There was a problem hiding this comment.
Pull request overview
This PR reduces unnecessary allocations during orchestration history replay by replacing several “copy-to-list then scan” patterns with lazy iteration, while preserving event ordering and existing replay/rewind semantics.
Changes:
- Avoids materializing combined history lists in
TaskHubGrpcWorker._execute_orchestrator()by scanningpastEvents+newEventsviaitertools.chain. - Optimizes
_OrchestrationExecutor.execute()to resolve the orchestration name usingnext(..., "<unknown>")rather than collecting all matchingexecutionStartedevents. - Removes the combined
all_eventslist in_OrchestrationExecutor._build_rewind_result()by using separate lazy chains for the two passes. - Adds targeted regression tests to cover orchestration-name lookup behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| durabletask/worker.py | Replaces several full-history list materializations with lazy iteration to reduce replay allocations while keeping ordering/behavior intact. |
| tests/durabletask/test_orchestration_executor.py | Adds regression tests validating orchestration-name resolution from committed history and the <unknown> fallback path. |
andystaples
approved these changes
Jul 27, 2026
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.
Summary
Long orchestration histories were copied and rescanned several times per work item, before and during replay. Each of these allocated throwaway lists sized by the full history, which is pure GC pressure and cache churn on every single work item. This PR removes those copies without changing any behavior.
Changes
1.
TaskHubGrpcWorker._execute_orchestrator()Built
list(req.pastEvents) + list(req.newEvents)— three throwaway lists sized by the entire history — purely to locate the firstexecutionStartedevent and thenbreak. Now iteratesitertools.chain(req.pastEvents, req.newEvents), which allocates a constant-size iterator instead. Iteration order (past events, then new events) and thebreakon first match are unchanged.2.
_OrchestrationExecutor.execute()Materialized every
executionStartedevent in the committed history via a list comprehension, then read only element[0]:Replaced with
next(<generator>, "<unknown>"), which short-circuits at the first match instead of scanning and collecting the whole history. The resolved name is identical in both branches: first match when one exists,"<unknown>"otherwise.3.
_OrchestrationExecutor._build_rewind_result()Built a combined
all_events = list(old_events) + list(new_events)to feed its two passes. Both parameters are already typedSequence[pb.HistoryEvent](and the function already relies on that vialen()/indexing), so each pass now chains them lazily with its own fresh iterator. Order is unchanged (old events first, then new events), and the first pass fully completes before the second begins — no generator-exhaustion hazard.Deliberately not changed
The rewind detection in
execute()already short-circuits correctly:Both are lazy generators, and the
andmeansold_eventsis only scanned when a rewind is actually in flight. Fusing theexecutionCompletedscan into theexecutionStartedscan — as the issue's proposed solution hints at — would force a full O(N) walk of the committed history on every normal work item, making the common path measurably worse. Left as-is on purpose.Compatibility
No public API signature changes, no renames, no removed symbols, no changed import paths. Replay order, rewind handling, non-determinism detection, log messages, and exception types/messages are all preserved. This is an allocation-only change with no observable behavior difference, so per the repo's changelog policy it does not get a
CHANGELOG.mdentry.Tests
Added two regression tests in
tests/durabletask/test_orchestration_executor.pycovering both branches of the orchestration-name lookup, which previously had no direct coverage:test_orchestration_name_resolved_from_committed_history— name is taken from the firstexecutionStartedevent when it is not the first event in the committed history.test_orchestration_name_unknown_without_committed_history— falls back to<unknown>when the committed history carries noexecutionStartedevent.Verification
All run against a session-local venv with both packages installed editable from this worktree.
pytest tests/durabletask -q --ignore-glob="*_e2e.py"pytest tests/durabletask-azuremanaged/{test_sandboxes_extension,test_durabletask_grpc_interceptor,test_azuremanaged_grpc_resiliency}.pyflake8 durabletask/worker.py tests/durabletask/test_orchestration_executor.pypyright durabletask/worker.pyNotes on the baseline:
test_client.py::test_sync_client_recreate_cooldown_prevents_immediate_repeated_recreation, a timing-sensitive test unrelated to this change; it passes after the change too.tests/durabletask/test_orchestration_executor.py(171 tests incl. the executor and tracing suites) andtests/durabletask/test_build_rewind_result.py(the 13 rewind tests) pass in full — these are the primary guards for replay correctness and rewind behavior.pyrightreports 0 errors ondurabletask/worker.pyboth before and after. The new test diagnostics are the samereportPrivateUsage/reportUnknown*kinds the test file already produces 433 times, matching the established convention intest_build_rewind_result.py.tests/durabletask-azuremanagedfiles (test_dts_activity_sequence.py,test_dts_batch_actions.py,entities/test_dts_entity_failure_handling.py) require a live Durable Task Scheduler endpoint and fail identically on a clean baseline in this environment.Fixes #185