Add sync and async client parity - #213
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
There was a problem hiding this comment.
Pull request overview
This PR adds sync/async parity across the durabletask sub-clients and the azure-functions-durable client binding surface, removing the need for async→sync “bridge” helpers in Functions scenarios (notably for scheduled tasks and history export).
Changes:
- Added async-first APIs in
durabletaskfor scheduled tasks (AsyncScheduledTaskClient/AsyncScheduleClient) and history export (AsyncExportHistoryClient/AsyncExportHistoryJobClient). - Added a native synchronous Functions durable client (
SyncDurableFunctionsClient) plus a sync binding/decorator path, and updated Functions history-export activities to use it. - Reworked the durable-client binding conversion path so the binding converter returns the raw host config string and the decorator constructs/disposes the requested rich client.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
tests/azure-functions-durable/test_history_export_compat.py |
Updates tests for per-invocation (non-cached) history export context resolution. |
tests/azure-functions-durable/test_decorator_compat.py |
Adds tests validating async vs sync durable-client injection behavior. |
tests/azure-functions-durable/test_converters.py |
Updates tests to reflect durable-client decode now returning raw host configuration. |
durabletask/scheduled/client.py |
Introduces async scheduled-task client façade types. |
durabletask/scheduled/__init__.py |
Exports new async scheduled-task client types. |
durabletask/extensions/history_export/client.py |
Introduces async history-export client façade types. |
durabletask/extensions/history_export/__init__.py |
Exports new async history-export façade types. |
CHANGELOG.md |
Documents new async scheduled-task and history-export APIs in core SDK. |
azure-functions-durable/CHANGELOG.md |
Documents new sync durable client binding/decorator in Functions package. |
azure-functions-durable/azure/durable_functions/internal/history_export_compat.py |
Switches history-export context to use the invocation’s native sync client. |
azure-functions-durable/azure/durable_functions/internal/converters.py |
Adjusts durable-client decode behavior to return raw host config string. |
azure-functions-durable/azure/durable_functions/decorators/durable_app.py |
Adds sync durable-client injection support and wraps functions to build/close the rich client. |
azure-functions-durable/azure/durable_functions/client.py |
Adds SyncDurableFunctionsClient built on TaskHubGrpcClient. |
azure-functions-durable/azure/durable_functions/__init__.py |
Exports SyncDurableFunctionsClient from the package root. |
Comments suppressed due to low confidence (1)
azure-functions-durable/azure/durable_functions/internal/history_export_compat.py:223
- _context_for now expects a native synchronous TaskHubGrpcClient and returns a fresh HistoryExportContext per invocation, but this module still contains (and documents) the old per-process cached context, atexit close path, and async->sync bridge helpers that appear unused after this change. Keeping those stale globals/docs makes the new per-invocation design harder to reason about.
parsed = dt_from_iso(raw) if raw else None
return parsed or datetime.now(timezone.utc)
class FunctionsExportJobEntity(ExportJobEntity):
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (5)
azure-functions-durable/azure/durable_functions/internal/converters.py:220
register_durable_converters()docstring still claims the durable-client binding is decoded to aDurableFunctionsClient, butDurableClientConverter.decode()now returns the raw host configuration string. Updating the docstring will prevent confusion about where the client object is created (converter vs decorator).
def decode(cls, data: meta.Datum, *,
trigger_metadata: _TriggerMetadata) -> Any:
# The decorator turns this host configuration into the requested sync or
# async rich client. Keeping conversion here as a string lets one binding
# type support both client shapes without a hidden sync bridge.
return data.value
durabletask/scheduled/client.py:161
- New async scheduled-task surface area (
AsyncScheduleClient/AsyncScheduledTaskClient) is introduced here, but there are no unit tests exercising the async behaviors (create/update/pause/resume/delete/describe/list) or validating parity with the sync clients' semantics (timeouts, failures, filter semantics). The existing scheduled-task test suite suggests this should be covered.
class AsyncScheduleClient:
"""Asynchronous client for managing a single schedule instance."""
def __init__(self, client: AsyncTaskHubGrpcClient, schedule_id: str,
*, operation_timeout: float = 60):
durabletask/extensions/history_export/client.py:488
- Async history-export APIs (
AsyncExportHistoryClient/AsyncExportHistoryJobClient) are added, but the current history-export client tests appear to cover only the synchronous clients. Adding async tests for create/get/list/wait/delete (and per-job façade behavior) would help ensure parity and prevent regressions.
class AsyncExportHistoryClient:
"""Asynchronous façade for creating and inspecting export jobs."""
def __init__(
self,
azure-functions-durable/azure/durable_functions/decorators/durable_app.py:410
- The binding-registration comment in
durable_client_input()still saysDurableClientConverterconstructs a richDurableFunctionsClientduring decode, but the new wrapper below constructs the rich client from a raw config string per invocation. Updating that comment will avoid confusion about where the client object is created and how it’s closed.
# user function is registered directly and ``.client_function``
# points back at it. Internal callers pass an already-built
# ``FunctionBuilder`` (e.g. history export), which is left untouched.
from ..client import DurableFunctionsClient, SyncDurableFunctionsClient
azure-functions-durable/azure/durable_functions/decorators/durable_app.py:409
- This comment block still claims there is no client middleware wrapper and that the converter builds the rich client during decode. With the new
client_boundwrapper below, that’s no longer true and may mislead future maintenance (e.g., around close semantics).
# user function is registered directly and ``.client_function``
# points back at it. Internal callers pass an already-built
# ``FunctionBuilder`` (e.g. history export), which is left untouched.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (2)
durabletask/scheduled/client.py:237
- The
create_schedulesignature uses an oddly indented continuation line; please format it consistently with the rest of the codebase’s multi-line signatures to avoid flake8 indentation warnings and improve readability.
async def create_schedule(
self, options: ScheduleCreationOptions) -> AsyncScheduleClient:
"""Create a new schedule and return a client for managing it."""
durabletask/scheduled/client.py:251
- The
list_schedulessignature continuation indentation is inconsistent with other multi-line function signatures in this repo (seeExportHistoryClient.list_jobsfor the common pattern). Reformatting helps readability and avoids potential flake8 E12x indentation issues.
async def list_schedules(
self, schedule_query: ScheduleQuery | None = None) -> list[ScheduleDescription]:
"""List schedules matching the given filter criteria."""
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
durabletask/scheduled/client.py:200
AsyncScheduleClient.update()is newly introduced but there is no async E2E test exercising the update path. The sync suite covers update behavior (interval/input changes), but the async client could regress unnoticed without at least oneawait schedule.update(...)test asserting the updated description fields.
async def update(self, options: ScheduleUpdateOptions) -> None:
"""Update this schedule's configuration."""
await self._run_operation(transitions.UPDATE_SCHEDULE, options)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
berndverst
left a comment
There was a problem hiding this comment.
The async tests cover happy paths but not failed/timed-out schedule operations, export wait timeout/not-found behavior, or delete_job handling of gRPC NOT_FOUND. Focused mocked tests would protect these new error contracts.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
azure-functions-durable/azure/durable_functions/decorators/durable_app.py:440
- The durable-client wrapper always replaces the binding value (a host configuration string) with a rich client instance, but
set_client_metadata()copies the original function's annotation forclient_name(or even forces it tostr). This can leave the registered wrapper annotated asstrorDurableFunctionsClienteven when a sync function actually receivesSyncDurableFunctionsClient, which is misleading for introspection and type checking (and contradicts the docstring above). Update the wrapper's annotation to the concrete injected client type based on whether the function is async or sync.
def set_client_metadata(client_bound: Callable[..., Any]) -> None:
annotations = dict(function.__annotations__)
supported_client_types = (
str,
bytes,
DurableFunctionsClient,
SyncDurableFunctionsClient,
)
if annotations.get(client_name) not in supported_client_types:
annotations[client_name] = str
client_bound.__annotations__ = annotations
setattr(client_bound, "client_function", function)
Summary
Closes #180
Closes #181