Skip to content

history_export: resolve activity dependencies per invocation (retire process-global bind_context) - #210

Merged
berndverst merged 1 commit into
mainfrom
andystaples-history-export-per-invocation-deps
Jul 27, 2026
Merged

history_export: resolve activity dependencies per invocation (retire process-global bind_context)#210
berndverst merged 1 commit into
mainfrom
andystaples-history-export-per-invocation-deps

Conversation

@andystaples

Copy link
Copy Markdown
Contributor

Summary

Fixes #182.

The history-export activities previously resolved their runtime dependencies (a durabletask client and a HistoryWriter) from a module-global _context installed once via bind_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)

  • Added HistoryExportContextResolver (a Callable[[], HistoryExportContext]) plus the pure activity bodies run_list_terminal_instances(context, input) and run_export_instance_history(context, input) that take the resolved context explicitly.
  • Added build_activities(resolver) — a factory returning the two worker-registrable activity callables (with the canonical activity names) that invoke resolver() once per activity execution.
  • Changed register(worker, resolver) to register resolver-bound activities.
  • Removed bind_context(), clear_context(), _require_context(), and the module-global context. ExportHistoryClient.register_worker() now captures a HistoryExportContext in the activity closures — no process-global is installed.

Azure Functions adapter

  • Dropped the bind_context shim (_ensure_context_bound). The client-bound wrappers now build a HistoryExportContext from the per-invocation injected durable client (cached once per process) and call the core run_* bodies explicitly — fitting the single core pattern instead of layering Functions-specific plumbing on top of bind_context.

Breaking change

bind_context() / clear_context() (exported in v1.8.0) are removed. Most callers use ExportHistoryClient.register_worker() and are unaffected. Code that called bind_context(HistoryExportContext(client, writer)) directly should register with durabletask.extensions.history_export.activities.register(worker, lambda: HistoryExportContext(client, writer)) (or a lazier resolver). Documented in CHANGELOG.md.

Validation

  • flake8: clean on all changed files.
  • pyright (strict, repo config): 0 errors on the changed source, the full history_export package, and the example.
  • Core history_export tests: 76 passed.
  • azure-functions-durable unit suite: 203 passed (incl. the history-export compat tests).
  • Smoke-tested DFApp.configure_history_export(...) wiring end to end.

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
Copilot AI review requested due to automatic review settings July 27, 2026 17:10

Copilot AI 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.

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, pure run_* bodies, and build_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).

Comment thread CHANGELOG.md
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
berndverst merged commit 2269c44 into main Jul 27, 2026
25 checks passed
@berndverst
berndverst deleted the andystaples-history-export-per-invocation-deps branch July 27, 2026 17:57
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.

history_export: resolve activity dependencies per invocation (retire the process-global bind_context)

3 participants