Sometimes a task needs to wait for its cancellation without doing anything else. Now, when we’ve got TaskGroups, it happens more often, I think. The functionality is not missing. In contrary, there are several ways to achieve it and thus none of them became an idiom.
Currently people use:
while True:
await asyncio.sleep(999999)
# -- or --
await asyncio.Future()
# -- or --
await asyncio.Event().wait()
Further, asyncio.sleep(float("inf")) seems to work, but I could not find it mentioned anywhere except on some uvloop related websites.
I’d like to submit for discussion few alternatives:
Make the float("inf") a.k.a. math.inf a documented feature.
Add a sentinel to asyncio.sleep: asyncio.sleep(asyncio.SLEEP_FOREVER).
Add a new function named sleep_forever() or similar.
My typical usage is in application’s main task group. I use to have a “control task” and a “worker task” (often one for each “service”). A cancellation from the group is a start signal for the shutdown & cleanup procedure. In a “control task” I use the sleep until cancelled (i.e. “forever”) as a divider between pre-shutdown and cleanup code. I could even create a dedicated cleanup task to minimalize the chance it could fail while the application is doing its regular work. The sleep_forever would be then its first await.
I don’t know if this is a “pattern”; I would be surprised if it is an “anti-pattern”.
Waiting on a termination event is the cleanest option if you don’t want to just wait on the task group itself closing. The patterns I typically use are:
wait on a termination event, cancel the task group if the event gets set; or
process requests from a queue, cancel the task group if None is received
The reason I prefer these is because exposing the task group itself provides the ability to spawn new tasks in the task group, while exposing just an event or queue gives a more controlled interface.
I don’t use much asyncio but I do use trio and I have found a couple use cases for trio.sleep_forever(). I wanted to create my own contextmanager that did not exit the context block until the tasks died. This is part of a larger program that manages mpv instances.
@asynccontextmanager
def open_mpv(socket_file):
"""
Create a new MPV instance and wait for it to close.
Yields:
MPV
Arguments:
socket_file (str): path to the IPC socket
"""
async with trio.open_nursery() as nursery:
mpv = MPV(socket_file)
with trio.fail_after(3):
await mpv.start(nursery)
try:
yield mpv
finally:
try:
await trio.sleep_forever() # Sleeping until the underline tasks die.
except trio.Cancelled:
mpv.stop()
## Usage
async with open_mpv('mpv_socket') as mpv:
# Do something with mpv
mpv_instances.append(mpv)
# Now leaving the context. Wait around...
Why not simply subclass TaskGroup and add your shutdown code in the __aexit__ function (before or after the superclass cleanup, whichever makes most sense for your application)? Having cleanup code is exactly what a with statement is designed for.
FWIW, I frequently use await asyncio.Event().wait().
Being able to instead write await asyncio.sleep_forever() would be nice. This could potentially be beneficial over await asyncio.Event().wait(), as static analysis could determine that sleep_forever() will never return.
I wouldn’t want to have to pass inf or some other constant to asyncio.sleep() but I wouldn’t be against that becoming possible/documented if others would prefer it.