Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion statemachine/invoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,11 @@ def _cancel(self, invokeid: str):
self._debug("%s Error in on_cancel for %s", self._log_id, invokeid, exc_info=True)

# 3) Cancel the async task (raises CancelledError at next await).
if invocation.task is not None and not invocation.task.done():
if (
invocation.task is not None
and invocation.task is not asyncio.current_task()
and not invocation.task.done()
):
invocation.task.cancel()

# 4) Wait for the sync thread to actually finish (skip if we ARE
Expand Down
59 changes: 59 additions & 0 deletions tests/test_invoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,65 @@ class SM(StateChart):
assert "cancelled_state" in sm.configuration_values


class TestInvokeSelfCancelOnDone:
"""Self-cancel — an invoke whose done.invoke exits the owning state must
not cancel itself when it completes after the initial loop releases."""

async def test_async_invoke_no_self_cancel_on_done(self):
import asyncio

from tests.conftest import SMRunner

sm_runner = SMRunner(is_async=True)
reached = []

class SM(StateChart):
loading = State(initial=True)
ready = State(final=True)
done_invoke_loading = loading.to(ready)

async def on_invoke_loading(self, **kwargs):
await asyncio.sleep(0.05)
return "ok"

async def on_enter_ready(self, **kwargs):
# Suspension point so a pending self-cancel surfaces here.
await asyncio.sleep(0)
reached.append(True)

sm = await sm_runner.start(SM)
await sm_runner.sleep(0.15)
await sm_runner.processing_loop(sm)

assert reached == [True]
assert "ready" in sm.configuration_values

async def test_sync_invoke_no_self_cancel_on_done(self):
from tests.conftest import SMRunner

sm_runner = SMRunner(is_async=False)
reached = []

class SM(StateChart):
loading = State(initial=True)
ready = State(final=True)
done_invoke_loading = loading.to(ready)

def on_invoke_loading(self, **kwargs):
time.sleep(0.05)
return "ok"

def on_enter_ready(self, **kwargs):
reached.append(True)

sm = await sm_runner.start(SM)
await sm_runner.sleep(0.15)
await sm_runner.processing_loop(sm)

assert reached == [True]
assert "ready" in sm.configuration_values


class TestInvokeErrorHandling:
"""Error in invoker → error.execution event."""

Expand Down
Loading