Coverage for async-durable-execution/src/async_durable_execution/composite/wait_for_callback.py: 100%
32 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 16:54 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 16:54 +0000
1"""Composite wait_for_callback operation built from callback, step, and child context."""
3from __future__ import annotations
5import asyncio
6import logging
7from dataclasses import dataclass
8from typing import TYPE_CHECKING, Any
10from ..config import Duration
11from ..context import bind_current_context, get_current_context
12from ..execution import durable_callable
13from ..primitive.base import OperationContext
14from ..primitive.callback import Callback, create_callback
15from ..primitive.child import run_in_child_context
16from ..primitive.step import step
18if TYPE_CHECKING:
19 from collections.abc import Awaitable, Callable
21 from ..serdes import SerDes
23logger = logging.getLogger(__name__)
26@durable_callable
27async def wait_for_callback_handler(
28 submitter: Callable[[], Awaitable[Any]],
29 name: str | None = None,
30 timeout: Duration | None = None,
31 heartbeat_timeout: Duration | None = None,
32 serdes: SerDes | None = None,
33 retry_strategy: Callable[[Exception, int], Duration | None] | None = None,
34) -> Any:
35 """Create a callback, run a submitter, and wait for callback completion."""
36 callback_name = f"{name}-callback" if name is not None else "callback"
37 submitter_step_name = f"{name}-submitter" if name is not None else "submitter"
38 callback: Callback = await create_callback(
39 name=callback_name,
40 timeout=timeout,
41 heartbeat_timeout=heartbeat_timeout,
42 serdes=serdes,
43 )
45 async def submitter_step():
46 step_context = get_current_context()
47 callback_context = WaitForCallbackContext(
48 callback_id=callback.callback_id,
49 execution_state=step_context.execution_state,
50 operation_identifier=step_context.operation_identifier,
51 )
52 with bind_current_context(callback_context):
53 return await submitter()
55 await step(
56 func=submitter_step,
57 name=submitter_step_name,
58 retry_strategy=retry_strategy,
59 serdes=serdes,
60 )
62 return await callback.result()
65def wait_for_callback(
66 submitter: Callable[[], Awaitable[Any]],
67 *,
68 name: str | None = None,
69 timeout: Duration | None = None,
70 heartbeat_timeout: Duration | None = None,
71 serdes: SerDes | None = None,
72 retry_strategy: Callable[[Exception, int], Duration | None] | None = None,
73) -> asyncio.Task[Any]:
74 """Create a callback, run a submitter, then suspend until the callback resolves.
76 Args:
77 submitter: Async callable. Use get_current_context().callback_id inside the
78 submitter to access the callback id.
79 name: Optional durable operation name.
80 timeout: Optional maximum time to wait for callback completion.
81 heartbeat_timeout: Optional maximum time to wait between callback heartbeats.
82 serdes: Optional serializer for callback results and submitter results.
83 retry_strategy: Optional strategy that returns a retry delay or None to stop.
84 """
85 context_name = name if name is not None else getattr(submitter, "__name__", None)
86 logger.debug("wait_for_callback name: %s", context_name)
88 return run_in_child_context(
89 wait_for_callback_handler(
90 submitter,
91 name=context_name,
92 timeout=timeout,
93 heartbeat_timeout=heartbeat_timeout,
94 serdes=serdes,
95 retry_strategy=retry_strategy,
96 ),
97 name=context_name,
98 )
101@dataclass(frozen=True)
102class WaitForCallbackContext(OperationContext):
103 """Context available during wait_for_callback submitter execution."""
105 callback_id: str = ""