Community: Should AsyncMachine continue handling `CancelledErrors` raised by unfinished events silently?
Current Behavior
As of now AsyncMachine will handle CancelledErrors caused by cancelling tasks that are still running while a new event is triggered.
Question
Do you consider this useful and convenient or would you prefer to handle cancellation events yourself via for instance try/except or on_exception?
Example
from transitions.extensions import AsyncMachine
import asyncio
class Model:
def __init__(self):
self.data = []
async def doing_task_1(self):
print("Doing task 1...")
await asyncio.sleep(2)
self.data.append(1)
await self.done()
async def doing_task_2(self):
print("Doing task 2...")
await asyncio.sleep(2)
self.data.append(2)
await self.done()
async def doing_task_3(self):
print("Doing task 3...")
self.data.append(3)
await self.done()
states = ["idle", "busy"]
transitions = [
{"trigger": "do_task_1", "source": "*", "dest": "busy", "after": "doing_task_1"},
{"trigger": "do_task_2", "source": "*", "dest": "busy", "after": "doing_task_2"},
{"trigger": "do_task_3", "source": "*", "dest": "busy", "after": "doing_task_3"},
{"trigger": "done", "source": "busy", "dest": "idle"},
]
model = Model()
m = AsyncMachine(model=model, states=states, transitions=transitions, initial="idle")
async def main():
asyncio.create_task(model.do_task_1()) # won't finish
await asyncio.sleep(0.1)
asyncio.create_task(model.do_task_2()) # won't finish
await asyncio.sleep(0.1)
asyncio.create_task(model.do_task_3()) # does finish
await asyncio.sleep(0.1)
assert model.is_idle()
assert model.data == [3]
asyncio.run(main())
As the source of this discussion, I believe that handling the exeption ourselves would be better. In a state machine context, having a transition from A to B be halfway done and then cancelling it to start a transition from A to C results in nether B or C unless both are aware that it is a possibility that their starting state may be a dirty A and that their transition may get cancelled.
import asyncio
from transitions.extensions.asyncio import AsyncMachine
class Model:
def __init__(self):
self.counter = 0
async def on_enter_a(self):
self.counter = 0
async def on_exit_a(self):
self.counter += 1
await asyncio.sleep(1)
states = ["a", "b", "c"]
transitions = [
{"trigger": "to_b", "source": "a", "dest": "b"},
{"trigger": "to_c", "source": "a", "dest": "c"},
]
model = Model()
m = AsyncMachine(model=model, states=states, initial="a",
transitions=transitions)
async def main():
to_b = asyncio.create_task(model.to_b())
await asyncio.sleep(0.5)
await model.to_c()
assert model.counter == 2
# State A exit was triggered twice despite no transition back to it,
# B and C could have expected counter to be at 1, but it's not.
to_b_res = await to_b
# No exception, only a 'False' which could have been caused by plenty of other issues
asyncio.run(main())
I don't expect the AsyncMachine to handle things differently than a sync Machine, as such I think that a transition should be unitary. In principle, I would have expected the old transitions to raise a CancelledError or the new one to raise a MachineNotFreeError (as long as the transition queue option is not activated of course).