Asyncio.gather vs asyncio.wait - python

asyncio.gather and asyncio.wait seem to have similar uses: I have a bunch of async things that I want to execute/wait for (not necessarily waiting for one to finish before the next one starts). They use a different syntax, and differ in some details, but it seems very un-pythonic to me to have 2 functions that have such a huge overlap in functionality. What am I missing?

Although similar in general cases ("run and get results for many tasks"), each function has some specific functionality for other cases:
asyncio.gather()
Returns a Future instance, allowing high level grouping of tasks:
import asyncio
from pprint import pprint
import random
async def coro(tag):
print(">", tag)
await asyncio.sleep(random.uniform(1, 3))
print("<", tag)
return tag
loop = asyncio.get_event_loop()
group1 = asyncio.gather(*[coro("group 1.{}".format(i)) for i in range(1, 6)])
group2 = asyncio.gather(*[coro("group 2.{}".format(i)) for i in range(1, 4)])
group3 = asyncio.gather(*[coro("group 3.{}".format(i)) for i in range(1, 10)])
all_groups = asyncio.gather(group1, group2, group3)
results = loop.run_until_complete(all_groups)
loop.close()
pprint(results)
All tasks in a group can be cancelled by calling group2.cancel() or even all_groups.cancel(). See also .gather(..., return_exceptions=True),
asyncio.wait()
Supports waiting to be stopped after the first task is done, or after a specified timeout, allowing lower level precision of operations:
import asyncio
import random
async def coro(tag):
print(">", tag)
await asyncio.sleep(random.uniform(0.5, 5))
print("<", tag)
return tag
loop = asyncio.get_event_loop()
tasks = [coro(i) for i in range(1, 11)]
print("Get first result:")
finished, unfinished = loop.run_until_complete(
asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED))
for task in finished:
print(task.result())
print("unfinished:", len(unfinished))
print("Get more results in 2 seconds:")
finished2, unfinished2 = loop.run_until_complete(
asyncio.wait(unfinished, timeout=2))
for task in finished2:
print(task.result())
print("unfinished2:", len(unfinished2))
print("Get all other results:")
finished3, unfinished3 = loop.run_until_complete(asyncio.wait(unfinished2))
for task in finished3:
print(task.result())
loop.close()
TaskGroup (Python 3.11+)
Update: Python 3.11 introduces TaskGroups which can "automatically" await more than one task without gather() or await():
# Python 3.11+ ONLY!
async def main():
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(some_coro(...))
task2 = tg.create_task(another_coro(...))
print("Both tasks have completed now.")

asyncio.wait is more low level than asyncio.gather.
As the name suggests, asyncio.gather mainly focuses on gathering the results. It waits on a bunch of futures and returns their results in a given order.
asyncio.wait just waits on the futures. And instead of giving you the results directly, it gives done and pending tasks. You have to manually collect the values.
Moreover, you could specify to wait for all futures to finish or just the first one with wait.

A very important distinction, which is easy to miss, is the default behavior of these two functions, when it comes to exceptions.
I'll use this example to simulate a coroutine that will raise exceptions, sometimes -
import asyncio
import random
async def a_flaky_tsk(i):
await asyncio.sleep(i) # bit of fuzz to simulate a real-world example
if i % 2 == 0:
print(i, "ok")
else:
print(i, "crashed!")
raise ValueError
coros = [a_flaky_tsk(i) for i in range(10)]
await asyncio.gather(*coros) outputs -
0 ok
1 crashed!
Traceback (most recent call last):
File "/Users/dev/PycharmProjects/trading/xxx.py", line 20, in <module>
asyncio.run(main())
File "/Users/dev/.pyenv/versions/3.8.2/lib/python3.8/asyncio/runners.py", line 43, in run
return loop.run_until_complete(main)
File "/Users/dev/.pyenv/versions/3.8.2/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete
return future.result()
File "/Users/dev/PycharmProjects/trading/xxx.py", line 17, in main
await asyncio.gather(*coros)
File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk
raise ValueError
ValueError
As you can see, the coros after index 1 never got to execute. Future returned by gather() is done at that point (unlike wait()) and program terminates, but if you could keep the program alive, other coroutines still would have chance to run:
async def main():
coros = [a_flaky_tsk(i) for i in range(10)]
await asyncio.gather(*coros)
if __name__ == '__main__':
loop = asyncio.new_event_loop()
loop.create_task(main())
loop.run_forever()
# 0 ok
# 1 crashed!
# Task exception was never retrieved
# ....
# 2 ok
# 3 crashed!
# 4 ok
# 5 crashed!
# 6 ok
# 7 crashed!
# 8 ok
# 9 crashed!
But await asyncio.wait(coros) continues to execute tasks, even if some of them fail (Future returned by wait() is not done, unlike gather()) -
0 ok
1 crashed!
2 ok
3 crashed!
4 ok
5 crashed!
6 ok
7 crashed!
8 ok
9 crashed!
Task exception was never retrieved
future: <Task finished name='Task-10' coro=<a_flaky_tsk() done, defined at /Users/dev/PycharmProjects/trading/xxx.py:6> exception=ValueError()>
Traceback (most recent call last):
File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk
raise ValueError
ValueError
Task exception was never retrieved
future: <Task finished name='Task-8' coro=<a_flaky_tsk() done, defined at /Users/dev/PycharmProjects/trading/xxx.py:6> exception=ValueError()>
Traceback (most recent call last):
File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk
raise ValueError
ValueError
Task exception was never retrieved
future: <Task finished name='Task-2' coro=<a_flaky_tsk() done, defined at /Users/dev/PycharmProjects/trading/xxx.py:6> exception=ValueError()>
Traceback (most recent call last):
File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk
raise ValueError
ValueError
Task exception was never retrieved
future: <Task finished name='Task-9' coro=<a_flaky_tsk() done, defined at /Users/dev/PycharmProjects/trading/xxx.py:6> exception=ValueError()>
Traceback (most recent call last):
File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk
raise ValueError
ValueError
Task exception was never retrieved
future: <Task finished name='Task-3' coro=<a_flaky_tsk() done, defined at /Users/dev/PycharmProjects/trading/xxx.py:6> exception=ValueError()>
Traceback (most recent call last):
File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk
raise ValueError
ValueError
Of course, this behavior can be changed for both by using -
asyncio.gather(..., return_exceptions=True)
or,
asyncio.wait([...], return_when=asyncio.FIRST_EXCEPTION)
But it doesn't end here!
Notice:
Task exception was never retrieved
in the logs above.
asyncio.wait() won't re-raise exceptions from the child tasks until you await them individually. (The stacktrace in the logs are just messages, they cannot be caught!)
done, pending = await asyncio.wait(coros)
for tsk in done:
try:
await tsk
except Exception as e:
print("I caught:", repr(e))
Output -
0 ok
1 crashed!
2 ok
3 crashed!
4 ok
5 crashed!
6 ok
7 crashed!
8 ok
9 crashed!
I caught: ValueError()
I caught: ValueError()
I caught: ValueError()
I caught: ValueError()
I caught: ValueError()
On the other hand, to catch exceptions with asyncio.gather(), you must -
results = await asyncio.gather(*coros, return_exceptions=True)
for result_or_exc in results:
if isinstance(result_or_exc, Exception):
print("I caught:", repr(result_or_exc))
(Same output as before)

I also noticed that you can provide a group of coroutines in wait() by simply specifying the list:
result=loop.run_until_complete(asyncio.wait([
say('first hello', 2),
say('second hello', 1),
say('third hello', 4)
]))
Whereas grouping in gather() is done by just specifying multiple coroutines:
result=loop.run_until_complete(asyncio.gather(
say('first hello', 2),
say('second hello', 1),
say('third hello', 4)
))

In addition to all the previous answers, I would like to tell about the different behavior of gather() and wait() in case they are cancelled.
Gather() cancellation
If gather() is cancelled, all submitted awaitables (that have not completed yet) are also cancelled.
Wait() cancellation
If the wait()ing task is cancelled, it simply throws an CancelledError and the waited tasks remain intact.
Simple example:
import asyncio
async def task(arg):
await asyncio.sleep(5)
return arg
async def cancel_waiting_task(work_task, waiting_task):
await asyncio.sleep(2)
waiting_task.cancel()
try:
await waiting_task
print("Waiting done")
except asyncio.CancelledError:
print("Waiting task cancelled")
try:
res = await work_task
print(f"Work result: {res}")
except asyncio.CancelledError:
print("Work task cancelled")
async def main():
work_task = asyncio.create_task(task("done"))
waiting = asyncio.create_task(asyncio.wait({work_task}))
await cancel_waiting_task(work_task, waiting)
work_task = asyncio.create_task(task("done"))
waiting = asyncio.gather(work_task)
await cancel_waiting_task(work_task, waiting)
asyncio.run(main())
Output:
asyncio.wait()
Waiting task cancelled
Work result: done
----------------
asyncio.gather()
Waiting task cancelled
Work task cancelled
Application example
Sometimes it becomes necessary to combine wait() and gather() functionality. For example, we want to wait for the completion of at least one task and cancel the rest pending tasks after that, and if the waiting itself was canceled, then also cancel all pending tasks.
As real examples, let's say we have a disconnect event and a work task. And we want to wait for the results of the work task, but if the connection was lost, then cancel it. Or we will make several parallel requests, but upon completion of at least one response, cancel all others.
It could be done this way:
import asyncio
from typing import Optional, Tuple, Set
async def wait_any(
tasks: Set[asyncio.Future], *, timeout: Optional[int] = None,
) -> Tuple[Set[asyncio.Future], Set[asyncio.Future]]:
tasks_to_cancel: Set[asyncio.Future] = set()
try:
done, tasks_to_cancel = await asyncio.wait(
tasks, timeout=timeout, return_when=asyncio.FIRST_COMPLETED
)
return done, tasks_to_cancel
except asyncio.CancelledError:
tasks_to_cancel = tasks
raise
finally:
for task in tasks_to_cancel:
task.cancel()
async def task():
await asyncio.sleep(5)
async def cancel_waiting_task(work_task, waiting_task):
await asyncio.sleep(2)
waiting_task.cancel()
try:
await waiting_task
print("Waiting done")
except asyncio.CancelledError:
print("Waiting task cancelled")
try:
res = await work_task
print(f"Work result: {res}")
except asyncio.CancelledError:
print("Work task cancelled")
async def check_tasks(waiting_task, working_task, waiting_conn_lost_task):
try:
await waiting_task
print("waiting is done")
except asyncio.CancelledError:
print("waiting is cancelled")
try:
await waiting_conn_lost_task
print("connection is lost")
except asyncio.CancelledError:
print("waiting connection lost is cancelled")
try:
await working_task
print("work is done")
except asyncio.CancelledError:
print("work is cancelled")
async def work_done_case():
working_task = asyncio.create_task(task())
connection_lost_event = asyncio.Event()
waiting_conn_lost_task = asyncio.create_task(connection_lost_event.wait())
waiting_task = asyncio.create_task(wait_any({working_task, waiting_conn_lost_task}))
await check_tasks(waiting_task, working_task, waiting_conn_lost_task)
async def conn_lost_case():
working_task = asyncio.create_task(task())
connection_lost_event = asyncio.Event()
waiting_conn_lost_task = asyncio.create_task(connection_lost_event.wait())
waiting_task = asyncio.create_task(wait_any({working_task, waiting_conn_lost_task}))
await asyncio.sleep(2)
connection_lost_event.set() # <---
await check_tasks(waiting_task, working_task, waiting_conn_lost_task)
async def cancel_waiting_case():
working_task = asyncio.create_task(task())
connection_lost_event = asyncio.Event()
waiting_conn_lost_task = asyncio.create_task(connection_lost_event.wait())
waiting_task = asyncio.create_task(wait_any({working_task, waiting_conn_lost_task}))
await asyncio.sleep(2)
waiting_task.cancel() # <---
await check_tasks(waiting_task, working_task, waiting_conn_lost_task)
async def main():
print("Work done")
print("-------------------")
await work_done_case()
print("\nConnection lost")
print("-------------------")
await conn_lost_case()
print("\nCancel waiting")
print("-------------------")
await cancel_waiting_case()
asyncio.run(main())
Output:
Work done
-------------------
waiting is done
waiting connection lost is cancelled
work is done
Connection lost
-------------------
waiting is done
connection is lost
work is cancelled
Cancel waiting
-------------------
waiting is cancelled
waiting connection lost is cancelled
work is cancelled

Related

FastAPI: Task was destroyed but it is pending

I've been fighting asyncio + FastAPI for the last 48 hours. Any help would be appreciated. I'm registering an on_gift handler that parses gifts I care about and appends them to gift_queue. I'm then pulling them out in a while loop later in the code to send to the websocket.
The goal is to block the main thread until the client disconnects. Then we can do cleanup. It appears that the cleanup is being ran but I'm seeing this error after the first request:
Task was destroyed but it is pending!
task: <Task pending name='Task-12' coro=<WebSocketProtocol13._receive_frame_loop() running at /Users/zane/miniconda3/lib/python3.9/site-packages/tornado/websocket.py:1106> wait_for=<Future pending cb=[IOLoop.add_future.<locals>.<lambda>() at /Users/zane/miniconda3/lib/python3.9/site-packages/tornado/ioloop.py:687, <TaskWakeupMethWrapper object at 0x7f9558a53af0>()]> cb=[IOLoop.add_future.<locals>.<lambda>() at /Users/zane/miniconda3/lib/python3.9/site-packages/tornado/ioloop.py:687]>
#router.websocket('/donations')
async def scan_donations(websocket: WebSocket, username):
await websocket.accept()
client = None
gift_queue = []
try:
client = TikTokLiveClient(unique_id=username,
sign_api_key=api_key)
def on_gift(event: GiftEvent):
gift_cost = 1
print('[DEBUG] Could not find gift cost for {}. Using 1.'.format(
event.gift.extended_gift.name))
try:
if event.gift.streakable:
if not event.gift.streaking:
if gift_cost is not None:
gift_total_cost = gift_cost * event.gift.repeat_count
# if it cost less than 99 coins, skip it
if gift_total_cost < 99:
return
gift_queue.append(json.dumps({
"type": 'tiktok_gift',
"data": dict(name=event.user.nickname)
}))
else:
if gift_cost < 99:
return
gift_queue.append(json.dumps({
'type': 'tiktok_gift',
'data': dict(name=event.user.nickname)
}))
except:
print('[DEBUG] Could not parse gift event: {}'.format(event))
client.add_listener('gift', on_gift)
await client.start()
while True:
if gift_queue:
gift = gift_queue.pop(0)
await websocket.send_text(gift)
del gift
else:
try:
await asyncio.wait_for(websocket.receive_text(), timeout=1)
except asyncio.TimeoutError:
continue
except ConnectionClosedOK:
pass
except ConnectionClosedError as e:
print("[ERROR] TikTok: ConnectionClosedError {}".format(e))
pass
except FailedConnection as e:
print("[ERROR] TikTok: FailedConnection {}".format(e))
pass
except Exception as e:
print("[ERROR] TikTok: {}".format(e))
pass
finally:
print("[DEBUG] TikTok: Stopping listener...")
if client is not None:
print('Stopping TTL')
client.stop()
await websocket.close()
To add:
I can see
[ERROR] TikTok: 1000
[DEBUG] TikTok: Stopping listener...
Stopping TTL
Once I disconnect in Postman. It seems that something is not exiting cleanly. I have a scan_chat function which is nearly identical and I don't get these errors.

How to handle Exceptions and ordering in AsyncIO

Currently Im trying to process list of messages in the same order they arrive. To process it, I'm using python asyncio to execute each message as couroutine/task. So I will be creating the couroutine/task accordingly and I will add to asyncio run forever loop. but In case of corner cases where there can be some exception occur in the coroutine. At the time of exception, I planning to retry those message or handle it differently. but the next couroutine should not be invoked to preserver the order of execution.
Is there any way to handle this ?
from asyncio import get_event_loop, sleep
status = True
async def c(id, sleep_time=2, fail=False):
global status
print(f'started the edge side {id}')
if status:
print('c', sleep_time, fail)
await sleep(sleep_time)
if fail:
status = False
raise Exception('fail')
loop = get_event_loop()
loop.create_task(c(1, sleep_time=1, fail=False))
loop.create_task(c(2, sleep_time=1, fail=False))
loop.create_task(c(3, sleep_time=1, fail=True))
loop.create_task(c(4, sleep_time=1, fail=False))
loop.create_task(c(5, sleep_time=1, fail=False))
loop.run_forever()
small example that I have tried and still it not working as expected... can anyone please suggest, is there any possible way to handle this
Thanks
but the next couroutine should not be invoked to preserver the order
of execution
It sounds like you need a queue and push an item into the queue when the previous one is completed but then all messages will be processed sequentially.
The doc for asyncio.create_task says:
Wrap the coro coroutine into a Task and schedule its execution. Return
the Task object.
You can see this in the following example. All task has been scheduled and when the task no. 3 failed, the remaining ones are not done but those that take less time are already completed because they were already scheduled:
$ python test.py
START: 1
START: 2
START: 3
START: 4
START: 5
END: 1
END: 2
END: 4
DONE: ['1', '2', '3', '4']
PENDING: ['5']
Task: 3 failed (ERROR), re-scheduling...
test.py:
import asyncio
async def worker(i, sleep):
print(f"START: {i}")
await asyncio.sleep(sleep)
if i == 3:
raise Exception("ERROR")
print(f"END: {i}")
async def main():
tasks = []
for i in range(1, 6):
sleep = 4 if i == 3 else i
tasks.append(asyncio.create_task(worker(i, sleep), name=i))
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
print(f"DONE: {sorted(t.get_name() for t in done)}")
print(f"PENDING: {sorted(t.get_name() for t in pending)}")
for task in done:
if task.exception():
print(
f"Task: {task.get_name()} failed ({task.exception()}), re-scheduling..."
)
if __name__ == "__main__":
asyncio.run(main())

Python - Cancel task in asyncio?

I have written code for async pool below. in __aexit__ i'm cancelling the _worker tasks after the tasks get finished. But when i run the code, the worker tasks are not getting cancelled and the code is running forever. This what the task looks like: <Task pending coro=<AsyncPool._worker() running at \async_pool.py:17> wait_for=<Future cancelled>>. The asyncio.wait_for is getting cancelled but not the worker tasks.
class AsyncPool:
def __init__(self,coroutine,no_of_workers,timeout):
self._loop = asyncio.get_event_loop()
self._queue = asyncio.Queue()
self._no_of_workers = no_of_workers
self._coroutine = coroutine
self._timeout = timeout
self._workers = None
async def _worker(self):
while True:
try:
ret = False
queue_item = await self._queue.get()
ret = True
result = await asyncio.wait_for(self._coroutine(queue_item), timeout = self._timeout,loop= self._loop)
except Exception as e:
print(e)
finally:
if ret:
self._queue.task_done()
async def push_to_queue(self,item):
self._queue.put_nowait(item)
async def __aenter__(self):
assert self._workers == None
self._workers = [asyncio.create_task(self._worker()) for _ in range(self._no_of_workers)]
return self
async def __aexit__(self,type,value,traceback):
await self._queue.join()
for worker in self._workers:
worker.cancel()
await asyncio.gather(*self._workers, loop=self._loop, return_exceptions =True)
To use the Asyncpool:
async def something(item):
print("got", item)
await asyncio.sleep(item)
async def main():
async with AsyncPool(something, 5, 2) as pool:
for i in range(10):
await pool.push_to_queue(i)
asyncio.run(main())
The Output in my terminal:
The problem is that your except Exception exception clause also catches cancellation, and ignores it. To add to the confusion, print(e) just prints an empty line in case of a CancelledError, which is where the empty lines in the output come from. (Changing it to print(type(e)) shows what's going on.)
To correct the issue, change except Exception to something more specific, like except asyncio.TimeoutError. This change is not needed in Python 3.8 where asyncio.CancelledError no longer derives from Exception, but from BaseException, so except Exception doesn't catch it.
When you have an asyncio task created and then cancelled, you still have the task alive that need to be "reclaimed". So you want to await worker for it. However, once you await such a cancelled task, as it will never give you back the expected return value, the asyncio.CancelledError will be raised and you need to catch it somewhere.
Because of this behavior, I don't think you should gather them but to await for each of the cancelled tasks, as they are supposed to return right away:
async def __aexit__(self,type,value,traceback):
await self._queue.join()
for worker in self._workers:
worker.cancel()
for worker in self._workers:
try:
await worker
except asyncio.CancelledError:
print("worker cancelled:", worker)
This appears to work. The event is a counting timer and when it expires it cancels the tasks.
import asyncio
from datetime import datetime as dt
from datetime import timedelta as td
import random
import time
class Program:
def __init__(self):
self.duration_in_seconds = 20
self.program_start = dt.now()
self.event_has_expired = False
self.canceled_success = False
async def on_start(self):
print("On Start Event Start! Applying Overrides!!!")
await asyncio.sleep(random.randint(3, 9))
async def on_end(self):
print("On End Releasing All Overrides!")
await asyncio.sleep(random.randint(3, 9))
async def get_sensor_readings(self):
print("getting sensor readings!!!")
await asyncio.sleep(random.randint(3, 9))
async def evauluate_data(self):
print("checking data!!!")
await asyncio.sleep(random.randint(3, 9))
async def check_time(self):
if (dt.now() - self.program_start > td(seconds = self.duration_in_seconds)):
self.event_has_expired = True
print("Event is DONE!!!")
else:
print("Event is not done! ",dt.now() - self.program_start)
async def main(self):
# script starts, do only once self.on_start()
await self.on_start()
print("On Start Done!")
while not self.canceled_success:
readings = asyncio.ensure_future(self.get_sensor_readings())
analysis = asyncio.ensure_future(self.evauluate_data())
checker = asyncio.ensure_future(self.check_time())
if not self.event_has_expired:
await readings
await analysis
await checker
else:
# close other tasks before final shutdown
readings.cancel()
analysis.cancel()
checker.cancel()
self.canceled_success = True
print("cancelled hit!")
# script ends, do only once self.on_end() when even is done
await self.on_end()
print('Done Deal!')
async def main():
program = Program()
await program.main()

Python asyncio blocks on coroutine, but not websockets

Consider the following code:
async def remote_data_retriever():
remote = Remote(sock_path)
while True:
Cached.update_global(remote.get_global())
await asyncio.sleep(RTR_RETR_INTERVAL)
async def on_message(websocket, path):
async for message in websocket:
data = Cached.get_global()
await websocket.send(json.dumps(data.__dict__))
if __name__ == '__main__':
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
ssl_context.load_cert_chain(RTR_CERT_PATH)
app_server = websockets.serve(on_message, RTR_LISTEN_HOST, RTR_LISTEN_PORT, ssl=ssl_context)
try:
asyncio.get_event_loop().run_until_complete(app_server)
print('1')
asyncio.get_event_loop().run_until_complete(remote_data_retriever())
print('2')
asyncio.get_event_loop().run_forever()
except Exception as e:
print(e)
This code will print '1' and then never print '2'. How to correctly schedule a coroutine so it does NOT block on the following call
asyncio.get_event_loop().run_until_complete(remote_data_retriever())
?
run_until_complete(task) starts task at once and waits for its real end, not for await. But your second task uses while True which never ends.
You should rather add task to loop using create_task(task) and start loop later with run_forever().
asyncio.get_event_loop().create_task(app_server)
print('1')
asyncio.get_event_loop().create_task(remote_data_retriever())
print('2')
asyncio.get_event_loop().run_forever()
And then both tasks will run in one loop and await will stop one task to start another task.
Example code which everyone can run
import asyncio
async def task_1():
number = 0
while True:
number += 1
print('task1', number)
await asyncio.sleep(1)
async def task_2():
number = 0
while True:
number += 1
print('task2', number)
await asyncio.sleep(1)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
try:
loop.create_task(task_1())
print('1')
loop.create_task(task_2())
print('2')
loop.run_forever()
except Exception as e:
print(e)
Your exact suggestion doesn't work as create_task() throws exception (correctly) claiming app_server is not a coroutine. However, based on your proposed code, I've been able to get it to work like this:
loop = asyncio.get_event_loop()
try:
asyncio.ensure_future(app_server, loop=loop)
print('1')
loop.create_task(remote_data_retriever())
print('2')
loop.run_forever()
except Exception as e:
print(e)

How to restart all tasks in gather when one raises error?

I have two tasks. When one task raises an error, I wish to restart them both.
Is the following the appropriate way to catch an exception propagated by one task, and restart the gather for the two tasks?
import asyncio
async def foo():
while True:
await asyncio.sleep(1)
print("foo")
async def bar():
for _ in range(3):
await asyncio.sleep(1)
print("bar")
raise ValueError
async def main():
while True:
footask = asyncio.create_task(foo())
bartask = asyncio.create_task(bar())
bothtasks = asyncio.gather(footask, bartask)
try:
await bothtasks
except ValueError:
print("caught ValueError")
try:
footask.cancel()
except asyncio.CancelledError:
pass
asyncio.run(main())
Basically asyncio intentionally doesn't cancel the other tasks in a gather when one task raises an error. So, since I can't think of anything better, I manually cancel the other task(s) with task.cancel() and handle the asyncio.CancelledError myself.
I'm just not convinced this is the intended use of the api, insights appreciated.
Edit:-
In the asyncio-3.7 docs it reads
If gather() is cancelled, all submitted awaitables (that have not completed yet) are also cancelled.
But the behaviour I observe when I replace footask.cancel() with bothtasks.cancel() is that for every iteration of the while loop, an additional foo is awaited, i.e. the foo appears not to be cancelled by cancelling the gather. The output looks something like this:
foo
bar
foo
bar
foo
bar
caught ValueError
foo
foo
bar
foo
foo
bar
foo
foo
bar
caught ValueError
foo
foo
foo
bar
foo
foo
foo
bar
foo
foo
foo
bar
caught ValueError
...
The standard idiom to ensure that the tasks have processed their cancelation is to add a gather(*tasks, return_exceptions=True) following the cancellation. For example:
async def main():
while True:
footask = asyncio.create_task(foo())
bartask = asyncio.create_task(bar())
tasks = (footask, bartask) # or a list comprehension, etc.
try:
await asyncio.gather(*tasks)
except ValueError:
print("caught ValueError")
for t in tasks:
t.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
Note that you might want to do that for all exceptions, not just ValueError, because otherwise a task completing with a non-ValueError exception will still cause other tasks to continue running.
When exceptions happens footask is not cancelled because as you can read in doc:
If return_exceptions is False (default), the first raised exception is
immediately propagated to the task that awaits on gather(). Other
awaitables in the aws sequence won’t be cancelled and will continue to
run.
So we should manually cancel footask and await it was cancelled:
async def main():
while True:
footask = asyncio.create_task(foo())
bartask = asyncio.create_task(bar())
bothtasks = asyncio.gather(footask, bartask)
try:
await bothtasks
except ValueError:
print("caught ValueError")
footask.cancel() # cancel just mark task to be cancelled
try:
await footask # await actually been cancelled
except asyncio.CancelledError:
pass
Upd:
I wrote advanced_gather that acts like gather, but has additional kawrg cancel_on_exception to cancel every task on exception in one of them. Full code:
import asyncio
async def advanced_gather(
*aws,
loop=None,
return_exceptions=False,
cancel_on_exception=False
):
tasks = [
asyncio.ensure_future(aw, loop=loop)
for aw
in aws
]
try:
return await asyncio.gather(
*tasks,
loop=loop,
return_exceptions=return_exceptions
)
except Exception:
if cancel_on_exception:
for task in tasks:
if not task.done():
task.cancel()
await asyncio.gather(
*tasks,
loop=loop,
return_exceptions=True
)
raise
async def foo():
while True:
await asyncio.sleep(1)
print("foo")
async def bar():
for _ in range(3):
await asyncio.sleep(1)
print("bar")
raise ValueError
async def main():
while True:
try:
await advanced_gather(
foo(),
bar(),
cancel_on_exception=True
)
except ValueError:
print("caught ValueError")
asyncio.run(main())
Different cases of what can happen:
import asyncio
from contextlib import asynccontextmanager, suppress
async def test(_id, raise_exc=False):
if raise_exc:
print(f'we raise RuntimeError inside {_id}')
raise RuntimeError('!')
try:
await asyncio.sleep(0.2)
except asyncio.CancelledError:
print(f'cancelledError was raised inside {_id}')
raise
else:
print(f'everything calm inside {_id}')
#asynccontextmanager
async def prepared_stuff(foo_exc=False):
foo = asyncio.create_task(test('foo', raise_exc=foo_exc))
bar = asyncio.create_task(test('bar'))
gather = asyncio.gather(
foo,
bar
)
await asyncio.sleep(0) # make sure everything started
yield (foo, bar, gather)
try:
await gather
except Exception as exc:
print(f'gather raised {type(exc)}')
finally:
# make sure both tasks finished:
await asyncio.gather(
foo,
bar,
return_exceptions=True
)
print('')
# ----------------------------------------------
async def everyting_calm():
async with prepared_stuff() as (foo, bar, gather):
print('everyting_calm:')
async def foo_raises_exception():
async with prepared_stuff(foo_exc=True) as (foo, bar, gather):
print('foo_raises_exception:')
async def foo_cancelled():
async with prepared_stuff() as (foo, bar, gather):
print('foo_cancelled:')
foo.cancel()
async def gather_cancelled():
async with prepared_stuff() as (foo, bar, gather):
print('gather_cancelled:')
gather.cancel()
async def main():
await everyting_calm()
await foo_raises_exception()
await foo_cancelled()
await gather_cancelled()
asyncio.run(main())

Categories