history_export: resolve activity dependencies per invocation (retire process-global bind_context) - #210
Merged
Conversation
Retire the process-global `bind_context` from the history-export extension in favor of a per-invocation resolver/factory pattern so any hosting model (including multi-process ones like Azure Functions) can supply the export client and writer without a process-global. Core: - Add `HistoryExportContextResolver` plus pure activity bodies `run_list_terminal_instances`/`run_export_instance_history` that take an explicit `HistoryExportContext`. - Add `build_activities(resolver)` factory and change `register(worker, resolver)` to register resolver-bound activities. - Remove `bind_context`/`clear_context`/`_require_context` and the module-global context. `ExportHistoryClient.register_worker` now captures a context in the activity closures instead of installing a global. Azure Functions adapter: - Drop the `bind_context` shim; build the `HistoryExportContext` from the per-invocation injected client (cached per process) and call the core bodies explicitly. Fixes #182 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f91b67b0-d282-4744-b744-4b2a8e1294be
Contributor
There was a problem hiding this comment.
Pull request overview
This PR refactors the durabletask.extensions.history_export extension to stop relying on a process-global activity context (bind_context) and instead resolve dependencies (durable client + HistoryWriter) per activity invocation via a resolver/factory pattern. This aligns history export with host-driven, multi-process execution models (e.g., Azure Functions) where “the process that created the job” may not be “the process that executes the activity”.
Changes:
- Introduces resolver-based activity registration (
HistoryExportContextResolver, purerun_*bodies, andbuild_activities(resolver)), and removes the module-global context binding model. - Updates
ExportHistoryClient.register_worker()and the Azure Functions adapter to use per-invocation context resolution/caching rather than process-global binding. - Updates tests to use a per-test mutable context holder + resolver, and documents the breaking change in
CHANGELOG.md.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| durabletask/extensions/history_export/activities.py | Replaces process-global context with per-invocation resolver; adds pure run_* activity bodies and activity factory/registration helpers. |
| durabletask/extensions/history_export/client.py | Wires worker registration through resolver-bound activities (no process-global binding). |
| durabletask/extensions/history_export/init.py | Updates public exports to include resolver types and pure run_*/factory building blocks (and remove bind/clear exports). |
| azure-functions-durable/azure/durable_functions/internal/history_export_compat.py | Removes bind_context usage; builds/caches a per-process HistoryExportContext from the injected client + configured writer and calls pure bodies. |
| CHANGELOG.md | Adds/updates Unreleased notes for the new resolver API and the breaking removal of process-global binding. |
| tests/durabletask/extensions/history_export/_test_helpers.py | Adds a MutableContextHolder to support resolver-based activity registration in tests. |
| tests/durabletask/extensions/history_export/test_activities.py | Switches E2E activity tests to resolver+holder pattern; updates assertions/messages accordingly. |
| tests/durabletask/extensions/history_export/test_client.py | Removes teardown that cleared a now-removed process-global context. |
| tests/durabletask/extensions/history_export/test_orchestrator.py | Updates orchestrator E2E tests to register activities with a resolver and swap contexts per test. |
| tests/azure-functions-durable/test_history_export_compat.py | Updates compat tests to pass explicit contexts and validate new caching/closure behavior. |
| tests/azure-functions-durable/e2e/apps/dtask_style/history_export_routes.py | Updates E2E app documentation to reflect per-invocation resolution (no process-wide context binding). |
| CHANGED | ||
|
|
||
| - **Breaking:** `FailureDetails.error_type` — and the `errorType` value sent over the wire — is now the fully-qualified type name (`module.ClassName`, e.g. `builtins.ValueError`, `durabletask.task.TaskFailedError`) instead of the bare class name, matching the .NET and Java SDKs. Code that compared `error_type` against a bare name (for example `== "ValueError"`) must be updated to the qualified name or, preferably, switched to `FailureDetails.is_caused_by()`. Because this value is persisted and crosses the orchestration boundary, failures produced by older workers may still carry a bare name; `is_caused_by()` accepts both. | ||
| - **Breaking:** Retired the process-global history-export context. `durabletask.extensions.history_export.bind_context()` and `clear_context()` have been removed; the export activities now resolve their `HistoryExportContext` per invocation via a resolver captured at registration. `ExportHistoryClient.register_worker()` continues to wire this up automatically, so most callers are unaffected. Code that called `bind_context(HistoryExportContext(client, writer))` directly should instead register the activities with `durabletask.extensions.history_export.activities.register(worker, lambda: HistoryExportContext(client, writer))` (or a lazier resolver), or use `ExportHistoryClient.register_worker()`. |
berndverst
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
Fixes #182.
The history-export activities previously resolved their runtime dependencies (a durabletask client and a
HistoryWriter) from a module-global_contextinstalled once viabind_context(HistoryExportContext(...)). That "activities run in-process within the worker that registered them" assumption holds for a standalone worker but breaks in host-driven, multi-process models such as Azure Functions, where the process that starts an export job is often not the worker process that runs an export activity — so_require_context()raised.This PR retires the process-global and moves the core extension to a per-invocation resolver / factory pattern, so any hosting model can supply the dependencies lazily at execution time.
Core changes (
durabletask.extensions.history_export)HistoryExportContextResolver(aCallable[[], HistoryExportContext]) plus the pure activity bodiesrun_list_terminal_instances(context, input)andrun_export_instance_history(context, input)that take the resolved context explicitly.build_activities(resolver)— a factory returning the two worker-registrable activity callables (with the canonical activity names) that invokeresolver()once per activity execution.register(worker, resolver)to register resolver-bound activities.bind_context(),clear_context(),_require_context(), and the module-global context.ExportHistoryClient.register_worker()now captures aHistoryExportContextin the activity closures — no process-global is installed.Azure Functions adapter
bind_contextshim (_ensure_context_bound). The client-bound wrappers now build aHistoryExportContextfrom the per-invocation injected durable client (cached once per process) and call the corerun_*bodies explicitly — fitting the single core pattern instead of layering Functions-specific plumbing on top ofbind_context.Breaking change
bind_context()/clear_context()(exported in v1.8.0) are removed. Most callers useExportHistoryClient.register_worker()and are unaffected. Code that calledbind_context(HistoryExportContext(client, writer))directly should register withdurabletask.extensions.history_export.activities.register(worker, lambda: HistoryExportContext(client, writer))(or a lazier resolver). Documented inCHANGELOG.md.Validation
flake8: clean on all changed files.pyright(strict, repo config): 0 errors on the changed source, the fullhistory_exportpackage, and the example.history_exporttests: 76 passed.azure-functions-durableunit suite: 203 passed (incl. the history-export compat tests).DFApp.configure_history_export(...)wiring end to end.