Add asyncio.sleep_forever() in some form

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:

  1. Make the float("inf") a.k.a. math.inf a documented feature.
  2. Add a sentinel to asyncio.sleep: asyncio.sleep(asyncio.SLEEP_FOREVER).
  3. Add a new function named sleep_forever() or similar.
1 Like

What pattern necessates such a task

It strikes me as a antipattern

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”.

1 Like

Then that’s the antipattern using a closures to control setup/teardown when you don’t nothing is actually done in the inside of that one

Could you please be more specific what kind of “closure” do you mean?

1 Like

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.

Wait until it’s the future.

await asyncio.Future() # "Tomorrow never comes" -- Vitalstatistix
5 Likes

Or just put the cleanup code after the task group exits. it won’t run until then.

1 Like

The only little imperfection is that the Future is low-level API.

I found about 50 instances in our company github

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.

I prefer to wait until it is the future.

await asyncio.Future()

:slight_smile:

1 Like

Not each one-line function…

2 Likes