Performance [P3]: Cache Azure managed SDK version lookup - #203
Conversation
Both the sync and async DTS interceptor constructors called
importlib.metadata.version('durabletask-azuremanaged') directly, which walks
distribution metadata on disk. That work was repeated on every interceptor
construction, i.e. on every client and worker construction.
Resolve the version once in a module-level lru_cache'd helper and reuse it.
The "unknown" fallback, the user-agent string format, and the metadata key
names and their order are all unchanged.
Verified with a probe that the number of importlib.metadata.version() calls
across three interceptor constructions drops from 3 to 1.
Fixes #195
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e7499a54-f262-49a6-bf91-274af28543ae
There was a problem hiding this comment.
Pull request overview
This PR improves startup performance for the durabletask.azuremanaged provider by caching the importlib.metadata.version('durabletask-azuremanaged') lookup in a module-level lru_cache helper and reusing the resolved value across sync and async interceptors.
Changes:
- Added
_get_sdk_version()with@lru_cache(maxsize=1)and updated both interceptors to use it. - Added unit tests that verify the version lookup happens once across multiple interceptor constructions, including the
"unknown"fallback behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
durabletask-azuremanaged/durabletask/azuremanaged/internal/durabletask_grpc_interceptor.py |
Introduces cached SDK version helper and uses it to build consistent user-agent metadata without repeated filesystem metadata scans. |
tests/durabletask-azuremanaged/test_durabletask_grpc_interceptor.py |
Adds tests validating the caching behavior and the preserved "unknown" fallback behavior for both sync and async interceptors. |
Comments suppressed due to low confidence (1)
tests/durabletask-azuremanaged/test_durabletask_grpc_interceptor.py:190
- This
mock.patch.objectblock uses a nonstandard hanging indent and closing-paren placement that is inconsistent with the rest of the file and may fail flake8 indentation checks. Reformat to a conventional multi-line call layout.
with mock.patch.object(
durabletask_grpc_interceptor,
"version",
side_effect=PackageNotFoundError('durabletask-azuremanaged')) as mock_version:
|
Thanks for the review. Declining the indentation change, because the stated reason turns out not to hold. The automated comment flagged the hanging indentation (around L162-163 and L187-190) as something that "may fail flake8." I checked rather than assuming: The style in question is the standard PEP 8 form for a wrapped compound-statement header — the continuation lines are indented further than the block body specifically so the two are visually distinguishable: PEP 8 calls this out explicitly as the recommended way to resolve the ambiguity, so changing it would move away from the convention rather than toward it. Separately, this branch is being updated to merge latest |
Resolves conflicts in tests/durabletask-azuremanaged/test_durabletask_grpc_interceptor.py introduced by PR #199 (deferred synchronous token acquisition), keeping both sides: - Imports: kept both main's `from typing import Any` and this branch's `from unittest import mock` / `PackageNotFoundError`. - Test classes: kept main's relocated `_TestTokenCredential` and its new deferred-token tests verbatim; moved this branch's `TestSdkVersionCaching` to sit between `TestDurableTaskGrpcInterceptor` and `TestAccessTokenManagerThreadSafety`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7499a54-f262-49a6-bf91-274af28543ae
Resolves the single conflict in tests/durabletask-azuremanaged/test_durabletask_grpc_interceptor.py introduced by PR #198 (single-flight async Azure token refresh), keeping both sides. Both sides widened an adjacent import into parenthesized form, so git tangled them around the shared closing parenthesis. Kept every import from both: - main's `access_token_manager` import widened to `AccessTokenManager` plus `AsyncAccessTokenManager`. - this branch's `durabletask_grpc_interceptor` module import and its `DTSAsyncDefaultClientInterceptorImpl` / `DTSDefaultClientInterceptorImpl` import. No test class conflicted: #198 appended its async single-flight classes after TestAccessTokenManagerThreadSafety, while TestSdkVersionCaching sits before it. Verified by AST inventory that the merged symbol set equals the union of both parents exactly - 38 symbols, 38 unique, 0 duplicates. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7499a54-f262-49a6-bf91-274af28543ae
…e-managed-sdk-version
Summary
Both the sync (
DTSDefaultClientInterceptorImpl) and async(
DTSAsyncDefaultClientInterceptorImpl) interceptor constructors calledimportlib.metadata.version('durabletask-azuremanaged')directly. That callwalks distribution metadata on disk, so the work was repeated on every
interceptor construction — i.e. on every client and worker construction.
This change resolves the version once in a module-level
functools.lru_cachedhelper (
_get_sdk_version()) and reuses it for every interceptor.Preserved exactly
"unknown"fallback when the version cannot be determined (the helperkeeps the original broad
except Exception, which still coversimportlib.metadata.PackageNotFoundError).durabletask-python/{version}, verbatim.taskhub,x-user-agent, andworkerid(sync worker case only).No public API signature changes, no renames, no removed symbols, no changed
import paths. The only observable difference is the removed repeat filesystem
work.
Note
No changelog entry was added. This is an internal performance change with no
user-visible behavior difference, which the repo guidelines say should not be
documented in the changelog.
Verification
Ran against a session-local virtualenv with both packages installed in
editable mode.
Effectiveness (the actual fix). A probe patched the module-level
versionsymbol and counted calls across three interceptor constructions (sync client,
sync worker, async):
version()callsTests.
TestSdkVersionCaching::test_version_resolved_once_across_interceptor_constructionsasserts the lookup happens exactly once across three constructions, and pins
the full metadata list (keys and order) for all three cases.
TestSdkVersionCaching::test_unknown_fallback_when_version_cannot_be_determinedasserts
PackageNotFoundErrorstill yieldsdurabletask-python/unknownforboth the sync and async interceptors.
setUp/tearDownso no patched valueleaks into other tests.
test_worker_includes_workerid_headerandtest_user_agent_metadata_passed_in_requeststill pass unchanged.Suites.
test_durabletask_grpc_interceptor.py,test_sandboxes_extension.py,test_azuremanaged_grpc_resiliency.py):47 passed.
tests/durabletask, excluding*_e2e.py):681 passed, 7 skipped.
flake8clean on both changed files.pyright(repopyrightconfig.json, strict): 441 errors before and after,none in the changed file — unchanged from baseline.
*_e2e.pysuites andtest_dts_activity_sequence.pyneed a live Durable TaskScheduler endpoint and were not run.
Fixes #195