-
Notifications
You must be signed in to change notification settings - Fork 25
fix(insight): keep one pending export snapshot per execution #726
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
Changes from all commits
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 |
|---|---|---|
|
|
@@ -160,7 +160,7 @@ def _apply_result_override( | |
|
|
||
|
|
||
| class _ExecutionState: | ||
| __slots__ = ("start_time", "parsed_arn", "cached_input", "operations") | ||
| __slots__ = ("start_time", "parsed_arn", "cached_input", "operations", "closed") | ||
|
|
||
| def __init__(self, start_time: Any, parsed_arn: dict[str, str]) -> None: | ||
| self.start_time = start_time | ||
|
|
@@ -169,6 +169,10 @@ def __init__(self, start_time: Any, parsed_arn: dict[str, str]) -> None: | |
| # operation_id -> OperationInfo, adopted verbatim from the SDK's | ||
| # authoritative snapshot (invocation start/end and operation-change). | ||
| self.operations: dict[str, OperationInfo] = {} | ||
| # Set by on_invocation_end before it emits. An operation-change hook | ||
| # from a checkpoint that completes during the end-of-invocation drain | ||
| # is dropped, so no RUNNING snapshot can follow the final record. | ||
| self.closed = False | ||
|
|
||
|
|
||
| class WorkflowInsightPlugin(DurableInstrumentationPlugin): | ||
|
|
@@ -230,6 +234,23 @@ def _discard_state(self, execution_arn: str) -> None: | |
| with self._lock: | ||
| self._state.pop(execution_arn, None) | ||
|
|
||
| def _open_state(self, execution_arn: str) -> _ExecutionState | None: | ||
| """Return the execution's state only while its invocation is open. | ||
|
|
||
| Unlike ``_ensure_state`` this never creates state: an operation-change | ||
| hook always follows an invocation start, so missing state means the | ||
| invocation already ended and its state was discarded. | ||
| """ | ||
| with self._lock: | ||
| state = self._state.get(execution_arn) | ||
| if state is None or state.closed: | ||
| return None | ||
| return state | ||
|
|
||
| def _close_state(self, state: _ExecutionState) -> None: | ||
| with self._lock: | ||
| state.closed = True | ||
|
|
||
| def _adopt_operations( | ||
| self, state: _ExecutionState, operations: dict[str, OperationInfo] | ||
| ) -> None: | ||
|
|
@@ -270,7 +291,11 @@ def on_operation_change(self, info: OperationChangeInfo) -> None: | |
| arn = info.execution_arn | ||
| if not arn or not self._sampled_in(arn): | ||
| return | ||
| state = self._ensure_state(arn) | ||
| state = self._open_state(arn) | ||
|
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 [P1] |
||
| if state is None: | ||
| # The invocation already ended (or is draining its final record); | ||
| # a snapshot from a checkpoint that completed late is stale. | ||
| return | ||
| # Replace state with the full operations snapshot carried by the hook. | ||
| self._adopt_operations(state, info.operations) | ||
| # on-change mode exports an updated RUNNING record on each change so | ||
|
|
@@ -297,6 +322,10 @@ def on_invocation_end(self, info: InvocationEndInfo) -> None: | |
| # Refresh from the fresh end-of-invocation snapshot before emitting so | ||
| # the terminal record reflects the final operation map. | ||
| self._adopt_operations(state, info.operations) | ||
| # Close before emitting: an operation-change hook arriving from a | ||
| # checkpoint that completes during the drain below is rejected, so no | ||
| # RUNNING snapshot can follow (or replace) the final record. | ||
| self._close_state(state) | ||
| status = _STATUS_MAP.get(info.status, "RUNNING") | ||
| is_terminal = status in ("SUCCEEDED", "FAILED") | ||
| is_failure = status == "FAILED" | ||
|
|
||
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_zbiazsubqqmypvsd55kwgxpyqi[P1] After worker startup fails, concurrent drains can all observe
_worker is Noneand independently call_pump; a concurrent schedule can also successfully start a worker before pumping begins. These consumers may export concurrently, and one can flush and signal the shared event while another export is still blocked, causingdrain()to return before delivery. Claim exclusive inline-pump ownership while holding_condition, prevent worker startup during that ownership, and make other drains wait for the owner's event. Cover this with concurrent drains and a blocked inline export.