Skip to content

Performance [P2]: Defer synchronous Azure token acquisition #187

Description

@berndverst

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    performance / optimizationUsed for issues or PRs purely focused on performance and optimizations, not bugs.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions