Repository navigation
fix: enforce cooperative invocation deadlines #783
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f6ccf79
5141c9c
6aed226
67eec85
8c22a52
72c175d
9c39c78
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| """One monotonic time budget for cooperative invocation interruption.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import time | ||
| from dataclasses import dataclass | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from aws_durable_execution_sdk_python.exceptions import InvocationDeadlineExceeded | ||
|
|
||
| if TYPE_CHECKING: | ||
| from aws_durable_execution_sdk_python.threading import CompletionEvent | ||
| from aws_durable_execution_sdk_python.types import LambdaContext | ||
|
|
||
| # Stop admitting work before the platform deadline to leave time for cooperative | ||
| # cleanup. This is not a bound on arbitrary user code, clients, or plugin hooks. | ||
| _CLEANUP_MARGIN_SECONDS = 0.1 | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class InvocationDeadline: | ||
| """Internal invocation-local deadline; None keeps standalone state unbounded.""" | ||
|
|
||
| expires_at: float | None = None | ||
|
|
||
| @classmethod | ||
| def from_lambda_context(cls, context: LambdaContext) -> InvocationDeadline: | ||
| # Capture once: wall-clock adjustments cannot extend this invocation. | ||
| started_at = time.monotonic() | ||
| remaining_seconds = context.get_remaining_time_in_millis() / 1000 | ||
| return cls(started_at + max(0.0, remaining_seconds - _CLEANUP_MARGIN_SECONDS)) | ||
|
|
||
| def remaining(self, timeout: float | None = None) -> float | None: | ||
| """Clamp an existing wait to the same budget, without resetting it.""" | ||
| if self.expires_at is None: | ||
| return timeout | ||
| remaining = max(0.0, self.expires_at - time.monotonic()) | ||
| return remaining if timeout is None else min(timeout, remaining) | ||
|
|
||
| def check(self) -> None: | ||
| if self.remaining() == 0: | ||
| raise InvocationDeadlineExceeded("Lambda invocation time budget exhausted") | ||
|
|
||
| def wait(self, event: CompletionEvent, timeout: float | None = None) -> bool: | ||
| """Do not turn an acknowledged checkpoint into an error after its wait.""" | ||
| completed = event.wait(self.remaining(timeout)) | ||
| if not completed: | ||
| self.check() | ||
| return completed |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| from aws_durable_execution_sdk_python.context import DurableContext | ||
| from aws_durable_execution_sdk_python.deadline import InvocationDeadline | ||
| from aws_durable_execution_sdk_python.exceptions import ( | ||
| BackgroundThreadError, | ||
| BotoClientError, | ||
|
|
@@ -193,6 +194,8 @@ def durable_execution( | |
| plugin_executor = PluginExecutor(configured_plugins) | ||
|
|
||
| def wrapper(event: Any, context: LambdaContext) -> MutableMapping[str, Any]: | ||
| deadline = InvocationDeadline.from_lambda_context(context) | ||
| deadline.check() | ||
| invocation_input: DurableExecutionInvocationInput | ||
| service_client: DurableServiceClient | ||
|
|
||
|
|
@@ -228,6 +231,7 @@ def wrapper(event: Any, context: LambdaContext) -> MutableMapping[str, Any]: | |
| service_client=service_client, | ||
| plugin_executor=plugin_executor, | ||
| updated_operation_ids=invocation_input.updated_operation_ids, | ||
| invocation_deadline=deadline, | ||
| ) | ||
|
|
||
| try: | ||
|
|
@@ -288,6 +292,7 @@ def invoke_handler() -> MutableMapping[str, Any]: | |
| ) | ||
| try: | ||
| # Background checkpointing errors will propagate through CompletionEvent.wait() as BackgroundThreadError | ||
| deadline.check() | ||
| result = func(input_event, durable_context) | ||
|
|
||
| # done with userland | ||
|
|
@@ -493,6 +498,7 @@ def wait_for_checkpoint() -> None: | |
| ) | ||
| execution_operation = execution_state.get_execution_operation() | ||
|
|
||
| deadline.check() | ||
| # execute the plugins | ||
| plugin_executor.on_invocation_start( | ||
| execution_arn=invocation_input.durable_execution_arn, | ||
|
|
@@ -518,6 +524,7 @@ def wait_for_checkpoint() -> None: | |
| output = plugin_executor._run_in_invocation_context( | ||
| invoke_handler | ||
| ) | ||
| execution_state.check_invocation_result() | ||
| # The ExitStack has now closed execution_state and joined the | ||
| # checkpoint thread. So any revocation observed while draining | ||
| # in-flight checkpoint work is visible here. A terminal status | ||
|
|
@@ -561,7 +568,11 @@ def wait_for_checkpoint() -> None: | |
| else: | ||
| # Preserve the original fresh-worker context without plugins. | ||
| invocation_future = executor.submit(invoke_worker) | ||
| return invocation_future.result() | ||
| result = invocation_future.result() | ||
| # Cleanup hooks and all owned workers have returned. A hook can consume | ||
| # the remaining budget; do not call End again when this check fails. | ||
| execution_state.check_invocation_result() | ||
|
Comment on lines
+572
to
+574
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Codex AI review · Finding [P2] Keep invocation-end status consistent with the wrapper outcome For a small successful result, no terminal checkpoint is sent. If an To suppress repeats, reply |
||
| return result | ||
|
|
||
| return wrapper | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Codex AI review · Finding
arf_v1_lee6rhbf3w2hdig4iho3ezcsim[P2] Preserve permanent checkpoint failures after deadline settlement
If a terminal checkpoint remains in flight past the cutoff and then raises a nonretryable
CheckpointError, the settlement wait delivers that error andinvoke_handler()correctly producesFAILED. This check then sees no terminal acknowledgment and replaces the classified failure withInvocationDeadlineExceeded. A permanent API rejection therefore triggers another Lambda retry and loses its error in the returned outcome. Preserve nonretryable API failures through both final deadline checks, and extend the terminal-transport test withDurableApiErrorCategory.EXECUTION; its currentOSErrorcase only covers retryable failures.To suppress repeats, reply
/ai verdict deferred,/ai verdict false-positive, or/ai verdict out-of-scopeon the first nonblank line. Add rationale on later lines. A new/ai verdict acceptedallows future repeats again.