From b704bb7d0a154a5fefe3386ec764a238f8d8f008 Mon Sep 17 00:00:00 2001 From: vikramlc Date: Thu, 10 Sep 2026 20:30:08 +0530 Subject: [PATCH] fix(odin): Confirm cancellation for a cancel_pending action never dispatched locally If a cancel_pending action has no registered CancellationToken (never dispatched by this process, or already finished), the extractor silently did nothing. Odin would then wait on an acknowledgement that never comes until the integration's next startup. Queue an explicit canceled ActionUpdate instead. --- cognite/extractorutils/unstable/core/base.py | 8 ++++++++ tests/test_unstable/test_action_dispatch.py | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cognite/extractorutils/unstable/core/base.py b/cognite/extractorutils/unstable/core/base.py index d01b1f3b..b70067cc 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 1d29dddc..75c7ac71 100644 --- a/tests/test_unstable/test_action_dispatch.py +++ b/tests/test_unstable/test_action_dispatch.py @@ -635,13 +635,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: