diff --git a/cognite/extractorutils/unstable/core/base.py b/cognite/extractorutils/unstable/core/base.py index 6e38b9c9..61972b05 100644 --- a/cognite/extractorutils/unstable/core/base.py +++ b/cognite/extractorutils/unstable/core/base.py @@ -637,6 +637,14 @@ def _dispatch_single_action(self, action: Action) -> None: token = self._running_action_tokens.get(action.external_id) if token is not None: token.cancel() + else: + # No in-flight run for this external_id: either never dispatched by this process, or + # already finished and cleaned up. Safe to confirm cancellation immediately — Odin + # would otherwise wait on an ack that never comes until this integration's next + # startup. + self._checkin_worker.queue_action_update( + ActionUpdate(external_id=action.external_id, status=ActionStatus.canceled) + ) return actionable_tasks = [t for t in self._tasks if isinstance(t, ACTIONABLE_TASK_TYPES)] diff --git a/tests/test_unstable/test_action_dispatch.py b/tests/test_unstable/test_action_dispatch.py index 635f1e6a..94216274 100644 --- a/tests/test_unstable/test_action_dispatch.py +++ b/tests/test_unstable/test_action_dispatch.py @@ -645,13 +645,16 @@ def cancellable(ctx: TaskContext) -> None: assert statuses[-1] == ActionStatus.canceled -def test_cancel_pending_unknown_action_is_a_no_op() -> None: +def test_cancel_pending_unknown_action_confirms_canceled() -> None: extractor = _make_extractor() extractor._dispatch_single_action( _make_action("act-unknown", "does not matter", status=ActionStatus.cancel_pending) ) - assert _queued_updates(extractor) == [] + updates = _queued_updates(extractor) + assert len(updates) == 1 + assert updates[0].external_id == "act-unknown" + assert updates[0].status == ActionStatus.canceled def test_custom_action_reports_canceled_when_target_raises_action_error_after_cancellation() -> None: