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
f3243ca
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, | ||
|
|
@@ -196,6 +197,8 @@ def durable_execution( | |
|
|
||
| @functools.wraps(func) | ||
| def wrapper(event: Any, context: LambdaContext) -> MutableMapping[str, Any]: | ||
| deadline = InvocationDeadline.from_lambda_context(context) | ||
| deadline.check() | ||
| # Only the factory list outlives an invocation. The worker below owns | ||
| # this fresh executor and all plugin lifecycle hooks. | ||
| plugin_executor = PluginExecutor(configured_plugins) | ||
|
|
@@ -234,6 +237,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: | ||
|
|
@@ -294,6 +298,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 | ||
|
|
@@ -496,6 +501,7 @@ def wait_for_checkpoint() -> None: | |
| resources.callback(execution_state.close) | ||
| execution_operation = execution_state.get_execution_operation() | ||
|
|
||
| deadline.check() | ||
| # execute the plugins | ||
| plugin_executor.on_invocation_start( | ||
| execution_arn=invocation_input.durable_execution_arn, | ||
|
|
@@ -519,6 +525,7 @@ def wait_for_checkpoint() -> None: | |
| execution_state.checkpoint_batches_forever | ||
| ) | ||
| output = invoke_handler() | ||
| execution_state.check_invocation_result() | ||
|
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] Preserve permanent checkpoint failures after deadline settlement If a terminal checkpoint remains in flight past the cutoff and then raises a nonretryable To suppress repeats, reply |
||
| # 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 | ||
|
|
@@ -571,7 +578,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
+582
to
+584
This comment was marked as outdated.
Sorry, something went wrong.
Comment on lines
+582
to
+584
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 | ||
|
|
||
|
|
||
This comment was marked as outdated.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.