Две асинхронные операции с одним тайм-аутом - PullRequest
0 голосов
/ 25 сентября 2018

В основном я хочу:

await action1()
await action2()
return result

с одним тайм-аутом для обоих действий и - это важно - с сообщением об ошибке, сообщающим, какое действие истекло.

Для сравнения только с однимaction:

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

Теперь с двумя действиями у меня есть это, и оно мне не нравится.

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())

Я считаю, что действительно нелогично иметь:

except asyncio.TimeoutError:
     pass

где обработка тайм-аута является основной функциональностью.Можете ли вы предложить лучший способ?

Ответы [ 2 ]

0 голосов
/ 26 сентября 2018
Модуль

async_timeout , изначально разработанный для aiohttp, может быть именно тем, что вам нужно.Это трассировка содержит строку, которая вызвала таймаут.

Установка:

pip install async_timeout

Использование:

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())

Вывод:

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

Во второй и третьей строках указывается место, где произошел тайм-аут:

  File "C:\Users\gmn\main.py", line 74, in main
    await asyncio.sleep(1)  # second
0 голосов
/ 25 сентября 2018

Можете ли вы предложить лучший способ?

Ваш код верен, но если вы ищете что-то более элегантное, возможно, контекстный менеджер подойдет вам:

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()

Можно использовать его следующим образом:

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())
...