Coverage for async-durable-execution/src/async_durable_execution/primitive/wait.py: 100%
41 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"""Implement the durable wait operation."""
3from __future__ import annotations
5import asyncio
6import logging
7from typing import TYPE_CHECKING
9from .base import OperationExecutor
10from .child import get_durable_context
11from ..config import Duration, duration_to_seconds
12from ..exceptions import ValidationError, suspend_with_optional_resume_delay
13from ..models import (
14 Operation,
15 OperationIdentifier,
16 OperationStatus,
17 OperationSubType,
18 OperationUpdate,
19 WaitOptions,
20)
21from ..task import create_eager_task
23if TYPE_CHECKING:
24 from .child import DurableContext
25 from ..state import ExecutionState
27logger = logging.getLogger(__name__)
30class WaitOperationExecutor(OperationExecutor[None]):
31 """Executor for wait operations."""
33 def __init__(
34 self,
35 seconds: int,
36 state: ExecutionState,
37 operation_identifier: OperationIdentifier,
38 ):
39 """Initialize the wait operation executor.
41 Args:
42 seconds: Number of seconds to wait
43 state: The execution state
44 operation_identifier: The operation identifier
45 """
46 super().__init__(state=state, operation_identifier=operation_identifier)
47 self.seconds = seconds
49 async def start(self) -> None:
50 """Start a new wait operation."""
51 operation: OperationUpdate = OperationUpdate.create_wait_start(
52 identifier=self.operation_identifier,
53 wait_options=WaitOptions(wait_seconds=self.seconds),
54 )
55 await self.create_checkpoint(operation, is_sync=True)
57 logger.debug(
58 "Wait checkpoint created for id: %s, name: %s, will suspend",
59 self.operation_identifier.operation_id,
60 self.operation_identifier.name,
61 )
63 return await self.execute()
65 async def replay(self, operation: Operation) -> None:
66 """Replay an existing wait operation from its checkpoint."""
67 if operation.status is OperationStatus.SUCCEEDED:
68 logger.debug(
69 "Wait already completed, skipping wait for id: %s, name: %s",
70 self.operation_identifier.operation_id,
71 self.operation_identifier.name,
72 )
73 return None
75 await self.execute()
77 async def execute(self, operation: Operation | None = None) -> None:
78 """Execute wait by suspending.
80 Wait operations 'execute' by suspending execution until the timer completes.
81 This method never returns normally - it always suspends.
83 Raises:
84 SuspendExecution: Always suspends to wait for timer completion
85 """
86 msg: str = f"Wait for {self.seconds} seconds"
87 suspend_with_optional_resume_delay(msg, self.seconds) # throws suspend
90def wait(duration: Duration, *, name: str | None = None) -> asyncio.Task[None]:
91 """Suspend the durable execution for at least the given duration.
93 Args:
94 duration: Seconds or timedelta to pause. Must be at least one second.
95 name: Optional operation name shown in execution history.
96 """
97 context = get_durable_context("wait")
98 seconds = duration_to_seconds(duration)
99 if seconds < 1:
100 msg = "duration must be at least 1 second"
101 raise ValidationError(msg)
103 with context._replay_aware():
104 operation_id = context.step_counter.create_step_id()
105 operation_identifier = OperationIdentifier(
106 operation_id=operation_id,
107 sub_type=OperationSubType.WAIT,
108 parent_id=context.parent_id,
109 name=name,
110 )
112 return create_eager_task(
113 lambda: _wait(
114 seconds=seconds,
115 context=context,
116 operation_identifier=operation_identifier,
117 ),
118 )
121async def _wait(
122 *,
123 seconds: int,
124 context: DurableContext,
125 operation_identifier: OperationIdentifier,
126) -> None:
127 executor: WaitOperationExecutor = WaitOperationExecutor(
128 seconds=seconds,
129 state=context.execution_state,
130 operation_identifier=operation_identifier,
131 )
132 await executor.process()