What is the issue?
Constructing a synchronous Azure managed interceptor performs a potentially blocking credential request before any RPC is made.
What is the impact?
Client and worker startup latency includes an authentication round trip even when the instance is never used. This especially affects short-lived tools, tests, and applications that construct clients eagerly. For normal long-lived clients, this moves rather than eliminates first-request latency.
Details about the issue including code reference
Relevant code:
|
def __init__(self, token_credential: TokenCredential | None, refresh_interval_seconds: int = 600): |
|
self._scope = "https://durabletask.io/.default" |
|
self._refresh_interval_seconds = refresh_interval_seconds |
|
self._logger = shared.get_logger("token_manager") |
|
|
|
self._credential = token_credential |
|
self._refresh_lock = Lock() |
|
|
|
if self._credential is not None: |
|
self._token = self._credential.get_token(self._scope) |
|
self.expiry_time = datetime.fromtimestamp(self._token.expires_on, tz=timezone.utc) |
|
self._token_manager = None |
|
if token_credential is not None: |
|
self._token_credential = token_credential |
|
self._token_manager = AccessTokenManager(token_credential=self._token_credential) |
|
access_token = self._token_manager.get_access_token() |
|
if access_token is not None: |
|
self._upsert_authorization_header(access_token.token) |
AccessTokenManager.__init__() calls credential.get_token(). DTSDefaultClientInterceptorImpl.__init__() creates that manager and immediately calls get_access_token().
A potential or proposed solution
Defer token acquisition until the first intercepted RPC and retain cached refresh-before-expiry behavior. Consider an explicit warm-up option if fail-fast authentication during construction is an intended contract, because lazy acquisition changes when authentication failures surface.
Cold-start relevance
This is the primary direct startup cost for synchronous Azure managed clients and workers: construction invokes the credential before the first RPC. With DefaultAzureCredential or ManagedIdentityCredential, that can include environment probing or a network token request. SandboxWorker passes its managed-identity credential through this same synchronous construction path.
Relevant code:
|
worker_id = f"{socket.gethostname()}:{os.getpid()}:{uuid.uuid4()}" |
|
resolved_interceptors: list[shared.ClientInterceptor] = ( |
|
list(interceptors) if interceptors is not None else [] |
|
) |
|
resolved_interceptors.append( |
|
DTSDefaultClientInterceptorImpl(token_credential, taskhub, worker_id=worker_id) |
|
) |
|
|
|
# We pass in None for the metadata so we don't construct an additional interceptor in the parent class |
|
# Since the parent class doesn't use anything metadata for anything else, we can set it as None |
|
super().__init__( |
|
host_address=host_address, |
|
channel=channel, |
|
secure_channel=secure_channel, |
|
metadata=None, |
|
log_handler=log_handler, |
|
log_formatter=log_formatter, |
|
interceptors=resolved_interceptors, |
|
channel_options=channel_options, |
|
resiliency_options=resiliency_options, |
|
concurrency_options=concurrency_options, |
|
# DTS natively supports long timers so chunking is unnecessary |
|
maximum_timer_interval=None, |
|
payload_store=payload_store, |
|
data_converter=data_converter |
|
) |
|
def __init__(self) -> None: |
|
resolved_host_address = _resolve_host_address() |
|
resolved_taskhub = _resolve_taskhub() |
|
resolved_secure_channel = _resolve_secure_channel(resolved_host_address) |
|
resolved_token_credential = _resolve_token_credential() |
|
resolved_max_concurrent_activities = _resolve_max_concurrent_activities() |
|
resolved_dts_sandbox_identifier = _resolve_dts_sandbox_identifier() |
|
resolved_sandbox_provider = _resolve_sandbox_provider() |
|
concurrency_options = ConcurrencyOptions( |
|
maximum_concurrent_activity_work_items=resolved_max_concurrent_activities) |
|
|
|
self._sandbox_host_address = resolved_host_address |
|
self._sandbox_secure_channel = resolved_secure_channel |
|
self._sandbox_token_credential = resolved_token_credential |
|
self._sandbox_logger = shared.get_logger("worker") |
|
|
|
super().__init__( |
|
host_address=resolved_host_address, |
|
taskhub=resolved_taskhub, |
|
token_credential=resolved_token_credential, |
|
secure_channel=resolved_secure_channel, |
|
concurrency_options=concurrency_options) |
What is the issue?
Constructing a synchronous Azure managed interceptor performs a potentially blocking credential request before any RPC is made.
What is the impact?
Client and worker startup latency includes an authentication round trip even when the instance is never used. This especially affects short-lived tools, tests, and applications that construct clients eagerly. For normal long-lived clients, this moves rather than eliminates first-request latency.
Details about the issue including code reference
Relevant code:
durabletask-python/durabletask-azuremanaged/durabletask/azuremanaged/internal/access_token_manager.py
Lines 17 to 27 in 55d8e0b
durabletask-python/durabletask-azuremanaged/durabletask/azuremanaged/internal/durabletask_grpc_interceptor.py
Lines 44 to 50 in 55d8e0b
AccessTokenManager.__init__()callscredential.get_token().DTSDefaultClientInterceptorImpl.__init__()creates that manager and immediately callsget_access_token().A potential or proposed solution
Defer token acquisition until the first intercepted RPC and retain cached refresh-before-expiry behavior. Consider an explicit warm-up option if fail-fast authentication during construction is an intended contract, because lazy acquisition changes when authentication failures surface.
Cold-start relevance
This is the primary direct startup cost for synchronous Azure managed clients and workers: construction invokes the credential before the first RPC. With DefaultAzureCredential or ManagedIdentityCredential, that can include environment probing or a network token request. SandboxWorker passes its managed-identity credential through this same synchronous construction path.
Relevant code:
durabletask-python/durabletask-azuremanaged/durabletask/azuremanaged/worker.py
Lines 92 to 117 in 55d8e0b
durabletask-python/durabletask-azuremanaged/durabletask/azuremanaged/preview/sandboxes/worker.py
Lines 43 to 64 in 55d8e0b