Context
The current timeout() helper in statemachine.contrib.timeout is implemented as a synchronous IInvoke handler that uses ctx.cancelled.wait(timeout=duration) (a threading.Event.wait()).
When used with the async engine, including alongside coroutine-function invoke targets (now supported after #611), the timeout handler runs in the default asyncio thread pool via loop.run_in_executor(None, handler.run, ctx) (statemachine/invoke.py:479). This works correctly, but each active timeout consumes a thread pool slot for the entire duration.
Proposal
Add an async-native timeout path using asyncio.sleep() and CancelledError, so that on the async engine:
- No thread pool slot is held while the timeout is pending.
- Cancellation is native (task cancellation at the next
await) rather than the current cooperative threading.Event signalling.
The sync engine keeps the current thread-based implementation.
One implementation option: make _Timeout expose an async def run() in addition to the sync one, and let the engine's existing _has_async_run detection pick the async path on the async engine. The sync engine would keep using the sync run().
Related
Context
The current
timeout()helper instatemachine.contrib.timeoutis implemented as a synchronousIInvokehandler that usesctx.cancelled.wait(timeout=duration)(athreading.Event.wait()).When used with the async engine, including alongside coroutine-function invoke targets (now supported after #611), the timeout handler runs in the default asyncio thread pool via
loop.run_in_executor(None, handler.run, ctx)(statemachine/invoke.py:479). This works correctly, but each active timeout consumes a thread pool slot for the entire duration.Proposal
Add an async-native timeout path using
asyncio.sleep()andCancelledError, so that on the async engine:await) rather than the current cooperativethreading.Eventsignalling.The sync engine keeps the current thread-based implementation.
One implementation option: make
_Timeoutexpose anasync def run()in addition to the sync one, and let the engine's existing_has_async_rundetection pick the async path on the async engine. The sync engine would keep using the syncrun().Related
docs/timeout.md, current timeout documentation (would benefit from a short section on coroutine composition)