Performance [P3]: Lazily load sandbox runtime API exports - #208
Merged
Conversation
Importing durabletask.azuremanaged.preview.sandboxes eagerly imported both the sandbox client and the sandbox worker. The worker module pulls in ManagedIdentityCredential from azure-identity plus the core worker graph, so merely touching the sandboxes namespace paid that cost at startup. Resolve the package's public names through a PEP 562 module __getattr__ instead, with TYPE_CHECKING imports so type checkers and IDEs still see them. __all__, dir(), star-imports, submodule attributes, and every import path are unchanged. Fixes #197 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9df32033-0d12-4be2-ad8c-3fea7890376f
Contributor
There was a problem hiding this comment.
Pull request overview
This PR reduces import-time overhead for the Azure Managed sandbox preview surface by switching durabletask.azuremanaged.preview.sandboxes from eager re-exports to PEP 562 lazy exports, so heavy dependencies (notably azure-identity via the sandbox worker runtime) are only imported when actually needed.
Changes:
- Replaced eager symbol imports in
...preview.sandboxes.__init__with lazy__getattr__+ caching and a__dir__implementation to preserve discoverability. - Added subprocess-based tests to validate that importing the package does not eagerly import the worker/runtime modules while preserving
__all__,dir(), star-import behavior, and attribute errors. - Documented the user-visible import-time improvement in the Azure Managed changelog.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| tests/durabletask-azuremanaged/test_sandboxes_extension.py | Adds isolated subprocess tests validating lazy-import behavior and API compatibility for the sandboxes package. |
| durabletask-azuremanaged/durabletask/azuremanaged/preview/sandboxes/init.py | Implements PEP 562 lazy exports for sandbox preview public names while preserving __all__ and submodule attribute access. |
| durabletask-azuremanaged/CHANGELOG.md | Notes the user-facing reduction in eager imports and improved import performance under ## Unreleased. |
andystaples
requested changes
Jul 27, 2026
andystaples
left a comment
Contributor
There was a problem hiding this comment.
Needs discussion, see my review on #209
…ox-exports # Conflicts: # durabletask-azuremanaged/CHANGELOG.md
The subprocess-isolated lazy-import probes ran with no timeout, so a hung child (import deadlock or environment issue) would stall CI indefinitely. Pass an explicit 60s bound - generous for an import-time probe - and turn subprocess.TimeoutExpired into a readable pytest failure that reports the bound and any partial output, rather than letting it propagate raw. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9df32033-0d12-4be2-ad8c-3fea7890376f
…ox-exports # Conflicts: # tests/durabletask-azuremanaged/test_sandboxes_extension.py
…ox-exports # Conflicts: # tests/durabletask-azuremanaged/test_sandboxes_extension.py
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
Importing
durabletask.azuremanaged.preview.sandboxeseagerly imported both the sandboxclient and the sandbox worker. The worker module pulls in
ManagedIdentityCredentialfromazure-identityplus the core worker graph, so merely touching the sandboxes namespace — evento declare a worker profile or reference a type — paid that entire cost at process startup.
This replaces the eager re-exports in the package initializer with PEP 562 lazy exports: a
module-level
__getattr__resolves each public name from its defining submodule on firstaccess and caches it in the module globals.
TYPE_CHECKINGimports keep type checkers, IDEs,and
pyright --strictfully aware of the names.Backwards compatibility is preserved exactly:
__all__is byte-for-byte identical, sofrom ... import *still exports the same six names.dir()still lists the public names (via a module-level__dir__).client,helpers,worker,worker_profiles, ...) remain reachable as packageattributes, matching the attribute binding the eager imports used to produce as a side effect.
AttributeError.Before / after
Measured on the same machine with
durabletaskanddurabletask.azuremanagedinstallededitable, median of 9 runs each,
HEAD~1vsHEAD:-X importtimecumulative fordurabletask.azuremanaged.preview.sandboxespython -c "import ...sandboxes"processlen(sys.modules)after importRaw
-X importtimetail lines:azure.identityalone accounts for ~890 ms of cumulative import time on this machine, and it isno longer loaded unless
SandboxWorkeris actually used:Interaction with #196
#196 makes the analogous lazy-export change to the core
durabletask/__init__.py. Because thesandboxes package is reached through
durabletask, there are two independent doors to theworker graph, and closing only one leaves the other open. Measured here as a full 2x2 on
import durabletask.azuremanaged.preview.sandboxes— module counts are deterministic, times arethe median of 7 runs of the summed
-X importtimeself-times (which include the ~87 msinterpreter floor):
main)Two honest conclusions from that table:
removed by this PR was pulling the core worker graph in directly, bypassing the core
initializer's laziness entirely. This PR is the load-bearing change for the sandboxes path.
grpc/protobufand friends, still pulled in bythe parent
durabletaskpackage. That is Performance [P2]: Avoid eager worker imports from Azure managed package #196's scope and is deliberately untouched here.With both changes landed the import drops to 84 modules — only 6 above the bare-interpreter
floor of 78, i.e. from 470 modules of overhead down to 6.
The two PRs are therefore complementary rather than additive, and they touch disjoint files
(verified: no path overlap between the branches). These figures were reproduced independently in
this worktree; the module counts match the #196 author's measurements exactly.
Verification
tests/durabletask-azuremanaged/test_sandboxes_extension.py. Because thatmodule imports the sandbox worker at module scope, the lazy-import assertions run in a
subprocess:
test_sandbox_package_defers_worker_runtime_imports— asserts...sandboxes.worker,...sandboxes.client,...sandboxes.transport, andazure.identityare absent fromsys.modulesafter importing the package, then asserts__all__,dir(), that everyexported name resolves to the same object as a direct import, and that submodule attributes
still work.
test_sandbox_package_star_import_matches_all— assertsfrom ... import *yields exactlythe
__all__names and does not leak the lazy-import plumbing.test_sandbox_package_raises_attribute_error_for_unknown_attributes— asserts a properAttributeErrorand correcthasattrbehavior.client,transport,worker, andazure.identityas eagerly imported).test_sandboxes_extension.py+test_durabletask_grpc_interceptor.py— 45 passed (42 before this change, plus the 3 new tests).
pytest tests/durabletask --ignore-glob="*_e2e.py"— 681 passed,7 skipped, no regressions against the recorded baseline.
TYPE_CHECKINGimports are resolvedproperly rather than suppressed — pyflakes treats names listed in
__all__as used, so nonoqawas needed.pyrightconfig.json, strict): zero errors in the changed file and zeroanywhere under
durabletask-azuremanaged/durabletask.--diffis empty for both changed files.Fixes #197