5

Basically I want:

await action1()
await action2()
return result

with a single timeout for both actions and - that's important - with an error message telling which action timed out.

For comparison, with just one action:

try:
    await asyncio.wait_for(action(), timeout=1.0)
except asyncio.TimeoutError:
    raise RuntimeError("Problem")

Now with two actions I have this and don't like it.

import asyncio

async def a2():
    try:
        await asyncio.sleep(1.0)
    except asyncio.CancelledError:
        raise RuntimeError("Problem 1") from None
    try:
        await asyncio.sleep(1.0)
    except asyncio.CancelledError:
        raise RuntimeError("Problem 2") from None
    return True


async def test():
    loop = asyncio.get_event_loop()
    action_task = loop.create_task(a2())
    # timeouts: 0.5 -> Problem1 exc; 1.5 -> Problem2 exc; 2.5 -> OK
    try:
        await asyncio.wait_for(action_task, timeout=0.5)
    except asyncio.TimeoutError:
        pass
    result = await action_task

asyncio.get_event_loop().run_until_complete(test())

I find it really counter-intuitive to have:

except asyncio.TimeoutError:
     pass

where timeout handling is the main functionality. Can you suggest a better way?

VPfB
  • 14,927
  • 6
  • 41
  • 75
  • Possible duplicate of [Combine awaitables like Promise.all](https://stackoverflow.com/questions/34377319/combine-awaitables-like-promise-all) – bobah Sep 25 '18 at 13:21
  • 2
    @bobah It's not a duplicate, I want to run first action 1 and then action 2. – VPfB Sep 25 '18 at 13:43
  • fair point, I've retracted the close vote – bobah Sep 25 '18 at 14:45

2 Answers2

2

Can you suggest a better way?

Your code is correct, but if you're looking for something more elegant, perhaps a context manager would fit your usage:

class Timeout:
    def __init__(self, tmout):
        self.tmout = tmout
        self._timed_out = False
        self._section = None

    async def __aenter__(self):
        loop = asyncio.get_event_loop()
        self._timer = loop.call_later(self.tmout, self._cancel_task,
                                      asyncio.current_task())
        return self

    def set_section(self, section):
        self._section = section

    def _cancel_task(self, task):
        self._timed_out = True
        task.cancel()

    async def __aexit__(self, t, v, tb):
        if self._timed_out:
            assert t is asyncio.CancelledError
            raise RuntimeError(self._section) from None
        else:
            self._timer.cancel()

One would use it as follows:

async def main():
    # raises RuntimeError("second sleep")
    async with Timeout(1) as tmout:
        tmout.set_section("first sleep")
        # increase above 1.0 and "first sleep" is raised
        await asyncio.sleep(0.8)
        tmout.set_section("second sleep")
        await asyncio.sleep(0.5)

asyncio.get_event_loop().run_until_complete(main())
user4815162342
  • 141,790
  • 18
  • 296
  • 355
-1

async_timeout module originally developed for aiohttp may be exact thing you need. It's traceback contains line which caused timeout.

Installation:

pip install async_timeout

Usage:

import asyncio
from async_timeout import timeout


async def main():
    with timeout(1.5) as t:

        await asyncio.sleep(1)  # first

        await asyncio.sleep(1)  # second

        await asyncio.sleep(1)  # third

        print('not timeout')


if __name__ ==  '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Output:

Traceback (most recent call last):
  File "C:\Users\gmn\main.py", line 74, in main
    await asyncio.sleep(1)  # second
  File "C:\Users\gmn\AppData\Local\Programs\Python\Python37\lib\asyncio\tasks.py", line 564, in sleep
    return await future
concurrent.futures._base.CancelledError

During handling of the above exception, another exception occurred:
  ...

Second and third lines here tell you place where timeout happened:

  File "C:\Users\gmn\main.py", line 74, in main
    await asyncio.sleep(1)  # second
Mikhail Gerasimov
  • 36,989
  • 16
  • 116
  • 159
  • `async_timeout` is a nice module, but it doesn't address the OP's problem. The question clearly states there is to be "single timeout for both actions and - **that's important** - with an error message telling which action timed out." `async_timeout` raises the exact same error regardless of which action timed out. – user4815162342 Sep 26 '18 at 16:32
  • @user4815162342 no, error is not "the exact same". Error has same type and message, but different traceback "telling which action timed out". It allows clearly identify place where timeout happen. It fully solves OP's problem if I understood it correctly. – Mikhail Gerasimov Sep 26 '18 at 16:46
  • The OP specified that they want an "**error message** telling which action timed out". In Python in general, and in the OP's code in particular, the error message comes from the exception object and is not something one has to guess from the traceback. The traceback is a nice debugging feature, but it won't help when the lines happen to be identical, or when the `.py` sources are not shipped with the program. Also, to extract the actual message, one would have to _parse_ the traceback. – user4815162342 Sep 26 '18 at 17:17
  • @user4815162342 well, I guess actuality of my answer then depends of what exactly OP need: message in console for user or debug information for developer (I assume later). Also I want to note, that traceback will still help if lines happen to be identical: it contains line number. – Mikhail Gerasimov Sep 26 '18 at 17:35
  • Being the OP, first I'd like to thank for the information about the async_timeout library. I learned something new. Regarding my needs, I prefer a clear human readable information for the log, something I can formulate myself. That's why I think this library is not the right tool for what I'm programming. – VPfB Sep 26 '18 at 18:35