Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Advanced asyncio: Solving Real-world Production...

Advanced asyncio: Solving Real-world Production Problems

Everyone’s talking about it. Everyone’s using it. But most likely, they’re doing it wrong, just like we did.

By building a simplified chaos monkey service, we will walk through how to create a good foundation for an asyncio-based service, including graceful shutdowns, proper exception handling, and testing asynchronous code. We’ll get into the hairier topics as well, covering topics like working with synchronous code, debugging and profiling, and working with threaded code. We’ll learn how to approach asynchronous and concurrent programming with Python’s asyncio library, take away some best practices, and learn what pitfalls to avoid.

Lynn Root

May 04, 2019
Tweet

More Decks by Lynn Root

Other Decks in Programming

Transcript

  1. • Initial setup of Mayhem Mandrill • Development best practices

    • Testing, debugging, and profiling slides: rogue.ly/adv-aio agenda
  2. Python 3.7.0 (default, Jul 6 2018, 11:30:06) [Clang 9.1.0 (clang-902.0.39.2)]

    on darwin Type "help", "copyright", "credits" or "license" for more information. >>> import asyncio, datetime >>> async def hello(): ... print(f'[{datetime.datetime.now()}] Hello...') ... await asyncio.sleep(1) # some I/O-intensive work ... print(f'[{datetime.datetime.now()}] ...World!') ... >>> asyncio.run(hello()) [2018-07-07 10:45:55.559856] Hello... [2018-07-07 10:45:56.568737] ...World!
  3. Python 3.7.0 (default, Jul 6 2018, 11:30:06) [Clang 9.1.0 (clang-902.0.39.2)]

    on darwin Type "help", "copyright", "credits" or "license" for more information. >>> import asyncio, datetime >>> async def hello(): ... print(f'[{datetime.datetime.now()}] Hello...') ... await asyncio.sleep(1) # some I/O-intensive work ... print(f'[{datetime.datetime.now()}] ...World!') ... >>> asyncio.run(hello()) [2018-07-07 10:45:55.559856] Hello... [2018-07-07 10:45:56.568737] ...World! FAKE NEWS
  4. Python 3.7.0 (default, Jul 6 2018, 11:30:06) [Clang 9.1.0 (clang-902.0.39.2)]

    on darwin Type "help", "copyright", "credits" or "license" for more information. >>> import asyncio, datetime >>> async def hello(): ... print(f'[{datetime.datetime.now()}] Hello...') ... await asyncio.sleep(1) # some I/O-intensive work ... print(f'[{datetime.datetime.now()}] ...World!') ... >>> asyncio.run(hello()) [2018-07-07 10:45:55.559856] Hello... [2018-07-07 10:45:56.568737] ...World! FAKE NEWS
  5. Python 3.7.0 (default, Jul 6 2018, 11:30:06) [Clang 9.1.0 (clang-902.0.39.2)]

    on darwin Type "help", "copyright", "credits" or "license" for more information. >>> import asyncio, datetime >>> async def hello(): ... print(f'[{datetime.datetime.now()}] Hello...') ... await asyncio.sleep(1) # some I/O-intensive work ... print(f'[{datetime.datetime.now()}] ...World!') ... >>> asyncio.run(hello()) [2018-07-07 10:45:55.559856] Hello... [2018-07-07 10:45:56.568737] ...World! FAKE NEWS
  6. async def publish(queue): choices = string.ascii_lowercase + string.digits while True:

    host_id = "".join(random.choices(choices, k=4)) msg = Message( msg_id=str(uuid.uuid4()), inst_name=f"cattle-{host_id}" ) asyncio.create_task(queue.put(msg)) logging.info(f"Published {msg}") # simulate randomness of publishing messages await asyncio.sleep(random.random()) concurrently publish messages
  7. concurrently publish messages async def publish(queue): choices = string.ascii_lowercase +

    string.digits while True: host_id = "".join(random.choices(choices, k=4)) msg = Message( msg_id=str(uuid.uuid4()), inst_name=f"cattle-{host_id}" ) asyncio.create_task(queue.put(msg)) logging.info(f"Published {msg}") # simulate randomness of publishing messages await asyncio.sleep(random.random())
  8. async def publish(queue): choices = string.ascii_lowercase + string.digits while True:

    host_id = "".join(random.choices(choices, k=4)) msg = Message( msg_id=str(uuid.uuid4()), inst_name=f"cattle-{host_id}" ) await queue.put(msg) logging.info(f"Published {msg}") # simulate randomness of publishing messages await asyncio.sleep(random.random()) concurrently publish messages
  9. concurrently publish messages async def publish(queue): choices = string.ascii_lowercase +

    string.digits while True: host_id = "".join(random.choices(choices, k=4)) msg = Message( msg_id=str(uuid.uuid4()), inst_name=f"cattle-{host_id}" ) await queue.put(msg) # lines below are blocked logging.info(f"Published {msg}") # simulate randomness of publishing messages await asyncio.sleep(random.random())
  10. async def publish(queue): choices = string.ascii_lowercase + string.digits while True:

    host_id = "".join(random.choices(choices, k=4)) msg = Message( msg_id=str(uuid.uuid4()), inst_name=f"cattle-{host_id}" ) asyncio.create_task(queue.put(msg)) logging.info(f"Published {msg}") # simulate randomness of publishing messages await asyncio.sleep(random.random()) concurrently publish messages
  11. async def consume(queue): while True: msg = await queue.get() logging.info(f"Consumed

    {msg}") # unhelpful simulation of an i/o operation await asyncio.sleep(random.random()) concurrently consume messages
  12. async def consume(queue): while True: msg = await queue.get() #

    <-- does not block loop logging.info(f"Consumed {msg}") # unhelpful simulation of an i/o operation await asyncio.sleep(random.random()) concurrently consume messages
  13. async def consume(queue): while True: msg = await queue.get() #

    <-- only blocks coro scope logging.info(f"Consumed {msg}") # unhelpful simulation of an i/o operation await asyncio.sleep(random.random()) concurrently consume messages
  14. async def consume(queue): while True: msg = await queue.get() logging.info(f"Consumed

    {msg}") await restart_host(msg) async def restart_host(msg): # unhelpful simulation of i/o work await asyncio.sleep(random.random()) msg.restarted = True logging.info(f"Restarted {msg.hostname}") concurrently consume messages
  15. async def consume(queue): while True: msg = await queue.get() logging.info(f"Consumed

    {msg}") asyncio.create_task(restart_host(msg)) async def restart_host(msg): # unhelpful simulation of i/o work await asyncio.sleep(random.random()) msg.restarted = True logging.info(f"Restarted {msg.hostname}") concurrently consume messages
  16. concurrently consume messages async def consume(queue): while True: msg =

    await queue.get() logging.info(f"Consumed {msg}") asyncio.create_task(restart_host(msg)) async def restart_host(msg): # unhelpful simulation of i/o work await asyncio.sleep(random.random()) msg.restarted = True logging.info(f"Restarted {msg.hostname}")
  17. async def restart_host(msg): # unhelpful simulation of i/o work await

    asyncio.sleep(random.random()) msg.restarted = True logging.info(f"Restarted {msg.hostname}") concurrent work
  18. async def restart_host(msg): # unhelpful simulation of i/o work await

    asyncio.sleep(random.random()) msg.restarted = True logging.info(f"Restarted {msg.hostname}") async def save(msg): # unhelpful simulation of i/o work await asyncio.sleep(random.random()) msg.saved = True logging.info(f"Saved {msg} into database") concurrent work
  19. async def restart_host(msg): ... async def save(msg): ... async def

    consume(queue): while True: msg = await queue.get() logging.info(f"Pulled {msg}") asyncio.create_task(save(msg)) asyncio.create_task(restart_host(msg)) concurrent work
  20. async def restart_host(msg): ... async def save(msg): ... async def

    consume(queue): while True: msg = await queue.get() logging.info(f'Pulled {msg}') await save(msg) await restart_host(msg) concurrent work
  21. async def restart_host(msg): ... async def save(msg): ... async def

    consume(queue): while True: msg = await queue.get() logging.info(f'Pulled {msg}') await save(msg) last_restart = await last_restart_date(msg) if today - last_restart > max_days: await restart_host(msg) block when needed
  22. async def handle_message(msg): await save(msg) last_restart = await last_restart_date(msg) if

    today - last_restart > max_days: asyncio.create_task(restart_host(msg)) async def consume(queue): while True: msg = await queue.get() logging.info(f"Pulled {msg}") asyncio.create_task(handle_message(msg)) block when needed
  23. async def handle_message(msg): asyncio.create_task(save(msg)) asyncio.create_task(restart_host(msg)) async def consume(queue): while True:

    msg = await queue.get() logging.info(f"Pulled {msg}") asyncio.create_task(handle_message(msg)) block when needed
  24. def cleanup(msg): msg.acked = True logging.info(f"Done. Acked {msg}") async def

    handle_message(msg): asyncio.create_task(save(msg)) asyncio.create_task(restart_host(msg)) unblocking: finalization tasks
  25. def cleanup(msg): msg.acked = True logging.info(f"Done. Acked {msg}") async def

    handle_message(msg): await save(msg) await restart_host(msg) cleanup(msg) unblocking: finalization tasks
  26. def cleanup(msg, fut): msg.acked = True logging.info(f"Done. Acked {msg}") async

    def handle_message(msg): g_future = asyncio.gather(save(msg), restart_host(msg)) callback = functools.partial(cleanup, msg) g_future.add_done_callback(callback) await g_future unblocking: finalization tasks
  27. 13:15:31,250 INFO: Pulled Message(inst_name='cattle-zpsk') 13:15:31,286 INFO: Restarted cattle-zpsk.example.net 13:15:31,347 INFO:

    Pulled Message(inst_name='cattle-998c') 13:15:31,486 INFO: Saved Message(inst_name='cattle-zpsk') into database 13:15:31,486 INFO: Done. Acked Message(inst_name='cattle-zpsk') 13:15:31,811 INFO: Pulled Message(inst_name='cattle-j9bu') 13:15:31,863 INFO: Saved Message(inst_name='cattle-998c') into database 13:15:31,903 INFO: Pulled Message(inst_name='cattle-vk5l') 13:15:32,149 INFO: Pulled Message(inst_name='cattle-1lf2') 13:15:32,239 INFO: Restarted cattle-vk5l.example.net 13:15:32,245 INFO: Restarted cattle-998c.example.net 13:15:32,245 INFO: Done. Acked Message(inst_name='cattle-998c') 13:15:32,267 INFO: Saved Message(inst_name='cattle-j9bu') into database 13:15:32,478 INFO: Pulled Message(inst_name='cattle-mflk') 13:15:32,481 INFO: Restarted cattle-j9bu.example.net 13:15:32,482 INFO: Done. Acked Message(inst_name='cattle-j9bu') 13:15:32,505 INFO: Pulled Message(inst_name='cattle-t7tv') unblocking: finalization tasks
  28. unblocking: finalization tasks 13:15:31,250 INFO: Pulled Message(inst_name='cattle-zpsk') 13:15:31,286 INFO: Restarted

    cattle-zpsk.example.net 13:15:31,347 INFO: Pulled Message(inst_name='cattle-998c') 13:15:31,486 INFO: Saved Message(inst_name='cattle-zpsk') into database 13:15:31,486 INFO: Done. Acked Message(inst_name='cattle-zpsk') 13:15:31,811 INFO: Pulled Message(inst_name='cattle-j9bu') 13:15:31,863 INFO: Saved Message(inst_name='cattle-998c') into database 13:15:31,903 INFO: Pulled Message(inst_name='cattle-vk5l') 13:15:32,149 INFO: Pulled Message(inst_name='cattle-1lf2') 13:15:32,239 INFO: Restarted cattle-vk5l.example.net 13:15:32,245 INFO: Restarted cattle-998c.example.net 13:15:32,245 INFO: Done. Acked Message(inst_name='cattle-998c') 13:15:32,267 INFO: Saved Message(inst_name='cattle-j9bu') into database 13:15:32,478 INFO: Pulled Message(inst_name='cattle-mflk') 13:15:32,481 INFO: Restarted cattle-j9bu.example.net 13:15:32,482 INFO: Done. Acked Message(inst_name='cattle-j9bu') 13:15:32,505 INFO: Pulled Message(inst_name='cattle-t7tv')
  29. unblocking: finalization tasks 13:15:31,250 INFO: Pulled Message(inst_name='cattle-zpsk') 13:15:31,286 INFO: Restarted

    cattle-zpsk.example.net 13:15:31,347 INFO: Pulled Message(inst_name='cattle-998c') 13:15:31,486 INFO: Saved Message(inst_name='cattle-zpsk') into database 13:15:31,486 INFO: Done. Acked Message(inst_name='cattle-zpsk') 13:15:31,811 INFO: Pulled Message(inst_name='cattle-j9bu') 13:15:31,863 INFO: Saved Message(inst_name='cattle-998c') into database 13:15:31,903 INFO: Pulled Message(inst_name='cattle-vk5l') 13:15:32,149 INFO: Pulled Message(inst_name='cattle-1lf2') 13:15:32,239 INFO: Restarted cattle-vk5l.example.net 13:15:32,245 INFO: Restarted cattle-998c.example.net 13:15:32,245 INFO: Done. Acked Message(inst_name='cattle-998c') 13:15:32,267 INFO: Saved Message(inst_name='cattle-j9bu') into database 13:15:32,478 INFO: Pulled Message(inst_name='cattle-mflk') 13:15:32,481 INFO: Restarted cattle-j9bu.example.net 13:15:32,482 INFO: Done. Acked Message(inst_name='cattle-j9bu') 13:15:32,505 INFO: Pulled Message(inst_name='cattle-t7tv')
  30. def cleanup(msg, fut): msg.acked = True logging.info(f"Done. Acked {msg}") async

    def handle_message(msg): g_future = asyncio.gather(save(msg), restart_host(msg)) callback = functools.partial(cleanup, msg) g_future.add_done_callback(callback) await g_future unblocking: finalization tasks
  31. async def cleanup(msg): msg.acked = True logging.info(f"Done. Acked {msg}") async

    def handle_message(msg): await asyncio.gather(save(msg), restart_host(msg)) await cleanup(msg) unblocking: finalization tasks
  32. def main(): queue = asyncio.Queue() loop = asyncio.get_event_loop() try: loop.create_task(publish(queue))

    loop.create_task(consume(queue)) loop.run_forever() finally: logging.info("Cleaning up") loop.close() responding to signals
  33. def main(): queue = asyncio.Queue() loop = asyncio.get_event_loop() try: loop.create_task(publish(queue))

    loop.create_task(consume(queue)) loop.run_forever() except KeyboardInterrupt: logging.info("Process interrupted") finally: logging.info("Cleaning up") loop.close() responding to signals
  34. responding to signals def main(): queue = asyncio.Queue() loop =

    asyncio.get_event_loop() try: loop.create_task(publish(queue)) loop.create_task(consume(queue)) loop.run_forever() except KeyboardInterrupt: # <-- a.k.a. SIGINT logging.info("Process interrupted") finally: logging.info("Cleaning up") loop.close()
  35. $ python mayhem.py $ pkill -INT -f "python mayhem.py" 19:11:25,321

    INFO: Pulled Message(inst_name='cattle-lrnm') 19:11:25,321 INFO: Done. Acked Message(inst_name='cattle-lrnm') 19:11:25,700 INFO: Pulled Message(inst_name='cattle-m0f6') 19:11:25,700 INFO: Done. Acked Message(inst_name='cattle-m0f6') 19:11:25,740 INFO: Saved Message(inst_name='cattle-m0f6') into database 19:11:25,840 INFO: Saved Message(inst_name='cattle-lrnm') into database 19:11:26,144 INFO: Process interrupted 19:11:26,144 INFO: Cleaning up responding to signals
  36. responding to signals $ python mayhem.py $ pkill -INT -f

    "python mayhem.py" 19:11:25,321 INFO: Pulled Message(inst_name='cattle-lrnm') 19:11:25,321 INFO: Done. Acked Message(inst_name='cattle-lrnm') 19:11:25,700 INFO: Pulled Message(inst_name='cattle-m0f6') 19:11:25,700 INFO: Done. Acked Message(inst_name='cattle-m0f6') 19:11:25,740 INFO: Saved Message(inst_name='cattle-m0f6') into database 19:11:25,840 INFO: Saved Message(inst_name='cattle-lrnm') into database 19:11:26,144 INFO: Process interrupted 19:11:26,144 INFO: Cleaning up
  37. $ python mayhem.py $ pkill -TERM -f "python mayhem.py" 19:08:25,553

    INFO: Pulled Message(inst_name='cattle-npww') 19:08:25,554 INFO: Done. Acked Message(inst_name='cattle-npww') 19:08:25,655 INFO: Pulled Message(inst_name='cattle-rm7n') 19:08:25,655 INFO: Done. Acked Message(inst_name='cattle-rm7n') 19:08:25,790 INFO: Saved Message(inst_name='cattle-rm7n') into database 19:08:25,831 INFO: Saved Message(inst_name='cattle-npww') into database [1] 78851 terminated python mandrill/mayhem.py responding to signals
  38. responding to signals $ python mayhem.py $ pkill -TERM -f

    "python mayhem.py" 19:08:25,553 INFO: Pulled Message(inst_name='cattle-npww') 19:08:25,554 INFO: Done. Acked Message(inst_name='cattle-npww') 19:08:25,655 INFO: Pulled Message(inst_name='cattle-rm7n') 19:08:25,655 INFO: Done. Acked Message(inst_name='cattle-rm7n') 19:08:25,790 INFO: Saved Message(inst_name='cattle-rm7n') into database 19:08:25,831 INFO: Saved Message(inst_name='cattle-npww') into database [1] 78851 terminated python mandrill/mayhem.py
  39. responding to signals def main(): queue = asyncio.Queue() loop =

    asyncio.get_event_loop() try: loop.create_task(publish(queue)) loop.create_task(consume(queue)) loop.run_forever() except KeyboardInterrupt: logging.info("Process interrupted") finally: logging.info("Cleaning up") loop.close()
  40. responding to signals def main(): queue = asyncio.Queue() loop =

    asyncio.get_event_loop() # <-- could happen here or earlier try: loop.create_task(publish(queue)) loop.create_task(consume(queue)) loop.run_forever() except KeyboardInterrupt: logging.info("Process interrupted") # <-- could happen here finally: logging.info("Cleaning up") # <-- could happen here loop.close() # <-- could happen here
  41. async def shutdown(signal, loop): logging.info(f"Received exit signal {signal.name}...") logging.info("Closing database

    connections") logging.info("Nacking outstanding messages") tasks = [ t for t in asyncio.all_tasks() if t is not asyncio.current_task() ] [task.cancel() for task in tasks] logging.info(f"Cancelling {len(tasks)} outstanding tasks") await asyncio.gather(*tasks, return_exceptions=True) logging.info("Flushing metrics”) loop.stop() logging.info("Shutdown complete.") signal handler
  42. async def shutdown(signal, loop): logging.info(f"Received exit signal {signal.name}...") logging.info("Closing database

    connections") logging.info("Nacking outstanding messages") tasks = [ t for t in asyncio.all_tasks() if t is not asyncio.current_task() ] [task.cancel() for task in tasks] logging.info(f"Cancelling {len(tasks)} outstanding tasks") await asyncio.gather(*tasks, return_exceptions=True) logging.info("Flushing metrics”) loop.stop() logging.info("Shutdown complete.") signal handler
  43. signal handler async def shutdown(signal, loop): logging.info(f"Received exit signal {signal.name}...")

    logging.info("Closing database connections") logging.info("Nacking outstanding messages") tasks = [ t for t in asyncio.all_tasks() if t is not asyncio.current_task() ] [task.cancel() for task in tasks] logging.info(f"Cancelling {len(tasks)} outstanding tasks") await asyncio.gather(*tasks, return_exceptions=True) logging.info("Flushing metrics") loop.stop() logging.info("Shutdown complete.")
  44. def main(): queue = asyncio.Queue() loop = asyncio.get_event_loop() try: loop.create_task(publish(queue))

    loop.create_task(consume(queue)) loop.run_forever() except KeyboardInterrupt: logging.info("Process interrupted") finally: logging.info("Cleaning up") loop.close() signal handler
  45. def main(): loop = asyncio.get_event_loop() signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT)

    for s in signals: loop.add_signal_handler( s, lambda s=s: asyncio.create_task(shutdown(s, loop))) queue = asyncio.Queue() try: loop.create_task(publish(queue)) loop.create_task(consume(queue)) loop.run_forever() finally: logging.info("Cleaning up") loop.close() signal handler
  46. def main(): loop = asyncio.get_event_loop() signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT)

    for s in signals: loop.add_signal_handler( s, lambda s=s: asyncio.create_task(shutdown(s, loop))) queue = asyncio.Queue() try: loop.create_task(publish(queue)) loop.create_task(consume(queue)) loop.run_forever() finally: logging.info("Cleaning up") loop.close() signal handler
  47. $ python mayhem.py # or -HUP or -INT $ pkill

    -TERM -f "python mayhem.py" 19:11:25,321 INFO: Pulled Message(inst_name='cattle-lrnm') 19:11:25,321 INFO: Done. Acked Message(inst_name='cattle-lrnm') 19:11:25,700 INFO: Pulled Message(inst_name='cattle-m0f6') 19:11:25,700 INFO: Done. Acked Message(inst_name='cattle-m0f6') 19:11:25,740 INFO: Saved Message(inst_name='cattle-m0f6') into database 19:11:25,840 INFO: Saved Message(inst_name='cattle-lrnm') into database 19:11:26,143 INFO: Received exit signal SIGTERM... 19:11:26,143 INFO: Closing database connections 19:11:26,144 INFO: Cancelling 19 outstanding tasks 19:11:26,144 INFO: Flushing metrics 19:11:26,145 INFO: Cleaning up signal handler
  48. signal handler $ python mayhem.py # or -HUP or -INT

    $ pkill -TERM -f "python mayhem.py" 19:11:25,321 INFO: Pulled Message(inst_name='cattle-lrnm') 19:11:25,321 INFO: Done. Acked Message(inst_name='cattle-lrnm') 19:11:25,700 INFO: Pulled Message(inst_name='cattle-m0f6') 19:11:25,700 INFO: Done. Acked Message(inst_name='cattle-m0f6') 19:11:25,740 INFO: Saved Message(inst_name='cattle-m0f6') into database 19:11:25,840 INFO: Saved Message(inst_name='cattle-lrnm') into database 19:11:26,143 INFO: Received exit signal SIGTERM... 19:11:26,143 INFO: Closing database connections 19:11:26,144 INFO: Cancelling 19 outstanding tasks 19:11:26,144 INFO: Flushing metrics 19:11:26,145 INFO: Cleaning up
  49. which signals to care about Hard Exit Graceful Reload/Restart nginx

    TERM, INT QUIT HUP Apache TERM WINCH HUP uWSGI INT, QUIT HUP, TERM Gunicorn INT, QUIT TERM HUP Docker KILL TERM
  50. async def cant_stop_me(): ... def main(): loop = asyncio.get_event_loop() signals

    = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT) for s in signals: loop.add_signal_handler( s, lambda s=s: asyncio.create_task(shutdown(s, loop))) shielded_coro = asyncio.shield(cant_stop_me()) try: loop.run_until_complete(shielded_coro) finally: logging.info("Cleaning up") loop.close() ungraceful shutdown: asyncio.shield
  51. ungraceful shutdown: asyncio.shield async def cant_stop_me(): ... def main(): loop

    = asyncio.get_event_loop() signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT) for s in signals: loop.add_signal_handler( s, lambda s=s: asyncio.create_task(shutdown(s, loop))) shielded_coro = asyncio.shield(cant_stop_me()) try: loop.run_until_complete(shielded_coro) finally: logging.info("Cleaning up") loop.close()
  52. 13:24:20,105 INFO: Hold on... ^C13:24:21,156 INFO: Received exit signal SIGINT...

    13:24:21,156 INFO: Cancelling 2 outstanding tasks 13:24:21,156 INFO: Coroutine cancelled 13:24:21,157 INFO: Cleaning up Traceback (most recent call last): File "examples/shield_test.py", line 62, in loop.run_until_complete(shielded_coro) File "/Users/lynn/.pyenv/versions/3.7.0/lib/python3.7/asyncio/base_events.p return future.result() concurrent.futures._base.CancelledError ungraceful shutdown: asyncio.shield
  53. ungraceful shutdown: asyncio.shield async def cant_stop_me(): logging.info("Hold on...") await asyncio.sleep(60)

    logging.info("Done!") async def main(): await asyncio.shield(cant_stop_me()) asyncio.run(main())
  54. 18:27:17,587 INFO: Hold on... ^C18:27:18,982 INFO: Cleaning up Traceback (most

    recent call last): File "shield_test_no_shutdown.py", line 23, in <module> loop.run_until_complete(shielded_coro) File "/Users/lynn/.pyenv/versions/3.6.2/lib/python3.6/asyncio/base_events.p self.run_forever() File "/Users/lynn/.pyenv/versions/3.6.2/lib/python3.6/asyncio/base_events.p self._run_once() File "/Users/lynn/.pyenv/versions/3.6.2/lib/python3.6/asyncio/base_events.p event_list = self._selector.select(timeout) File "/Users/lynn/.pyenv/versions/3.6.2/lib/python3.6/selectors.py", line 5 kev_list = self._kqueue.control(None, max_ev, timeout) KeyboardInterrupt ungraceful shutdown: asyncio.shield
  55. ungraceful shutdown: asyncio.shield async def cant_stop_me(): logging.info("Hold on...") await asyncio.sleep(60)

    logging.info("Done!") async def imma_let_you_speak(task_to_cancel): await asyncio.sleep(2) logging.info(f"interrupting {task_to_cancel}”) task_to_cancel.cancel() async def main(): shielded = asyncio.shield(cant_stop_me()) cancel_coro = imma_let_you_speak(shielded) await asyncio.gather(shielded, cancel_coro) asyncio.run(main())
  56. ungraceful shutdown: asyncio.shield async def cant_stop_me(): logging.info("Hold on...") await asyncio.sleep(60)

    logging.info("Done!") async def imma_let_you_speak(task_to_cancel): await asyncio.sleep(2) logging.info(f"interrupting {task_to_cancel}”) task_to_cancel.cancel() async def main(): shielded = asyncio.shield(cant_stop_me()) cancel_coro = imma_let_you_speak(shielded) await asyncio.gather(shielded, cancel_coro) asyncio.run(main())
  57. 18:43:53,729 INFO: Hold on... 18:43:55,730 INFO: killing <Future pending cb=[gather.<locals>._done_callback

    python3.7/asyncio/tasks.py:660]> Traceback (most recent call last): File "shield_test_no_shutdown.py", line 38, in <module> asyncio.run(main()) File "/Users/lynn/.pyenv/versions/3.7.0/lib/python3.7/asyncio/runners.py", return loop.run_until_complete(main) File "/Users/lynn/.pyenv/versions/3.7.0/lib/python3.7/asyncio/base_events.p return future.result() concurrent.futures._base.CancelledError ungraceful shutdown: asyncio.shield
  58. • try/except/finally isn’t enough • Define desired shutdown behavior •

    Use signal handlers • Listen for appropriate signals graceful shutdown: tl;dr
  59. async def restart_host(msg): # unhelpful simulation of i/o work await

    asyncio.sleep(random.random()) msg.restarted = True logging.info(f"Restarted {msg.hostname}") global handler
  60. async def restart_host(msg): # unhelpful simulation of i/o work await

    asyncio.sleep(random.random()) rand_int = random.randrange(1, 5) if rand_int == 2: raise Exception(f"Could not restart {msg.hostname}!") msg.restarted = True logging.info(f"Restarted {msg.hostname}") global handler
  61. $ python mayhem.py 13:49:33,524 INFO: Pulled Message(inst_name='cattle-hvy0') 13:49:33,924 INFO: Pulled

    Message(inst_name='cattle-5i2f') 13:49:33,925 ERROR: Task exception was never retrieved future: <Task finished coro=<restart_host() done, defined at mayhem_ex_handling.py:56> exception=Exception('Could not restart cattle-5i2f.example.net')> Traceback (most recent call last): File "mayhem_ex_handling.py", line 60, in restart_host raise Exception(f"Could not restart {msg.hostname}") Exception: Could not restart cattle-5i2f.example.net 13:49:34,247 INFO: Pulled Message(inst_name='cattle-e086') 13:49:34,432 INFO: Saved Message(inst_name='cattle-hvy0') into database 13:49:34,517 INFO: Restarted cattle-hvy0.example.net global handler
  62. global handler $ python mayhem.py 13:49:33,524 INFO: Pulled Message(inst_name='cattle-hvy0') 13:49:33,924

    INFO: Pulled Message(inst_name='cattle-5i2f') 13:49:33,925 ERROR: Task exception was never retrieved future: <Task finished coro=<restart_host() done, defined at mayhem_ex_handling.py:56> exception=Exception('Could not restart cattle-5i2f.example.net')> Traceback (most recent call last): File "mayhem_ex_handling.py", line 60, in restart_host raise Exception(f"Could not restart {msg.hostname}") Exception: Could not restart cattle-5i2f.example.net 13:49:34,247 INFO: Pulled Message(inst_name='cattle-e086') 13:49:34,432 INFO: Saved Message(inst_name='cattle-hvy0') into database 13:49:34,517 INFO: Restarted cattle-hvy0.example.net
  63. global handler $ python mayhem.py 13:49:33,524 INFO: Pulled Message(inst_name='cattle-hvy0') 13:49:33,924

    INFO: Pulled Message(inst_name='cattle-5i2f') 13:49:33,925 ERROR: Task exception was never retrieved future: <Task finished coro=<restart_host() done, defined at mayhem_ex_handling.py:56> exception=Exception('Could not restart cattle-5i2f.example.net')> Traceback (most recent call last): File "mayhem_ex_handling.py", line 60, in restart_host raise Exception(f"Could not restart {msg.hostname}") Exception: Could not restart cattle-5i2f.example.net 13:49:34,247 INFO: Pulled Message(inst_name='cattle-e086') 13:49:34,432 INFO: Saved Message(inst_name='cattle-hvy0') into database 13:49:34,517 INFO: Restarted cattle-hvy0.example.net
  64. def exception_handler(loop, context): logging.error(f"Caught exception: {context['exception']}") def main(): loop =

    asyncio.get_event_loop() # <-- snip —> loop.set_exception_handler(exception_handler) # <-- snip --> global handler
  65. global handler def exception_handler(loop, context): logging.error(f"Caught exception: {context['exception']}") def main():

    loop = asyncio.get_event_loop() # <-- snip —> loop.set_exception_handler(exception_handler) # <-- snip -->
  66. $ python mayhem.py 14:01:56,187 INFO: Pulled Message(instance_name='cattle-i490') 14:01:56,192 INFO: Restarted

    cattle-i490.example.net 14:01:56,241 INFO: Pulled Message(instance_name='cattle-31is') 14:01:56,331 INFO: Saved Message(instance_name='cattle-31is') into database 14:01:56,535 INFO: Pulled Message(instance_name='cattle-sx7f') 14:01:56,535 ERROR: Caught exception: Could not restart cattle-sx7f.example.n 14:01:56,730 INFO: Pulled Message(instance_name='cattle-hsh9') 14:01:56,731 INFO: Saved Message(instance_name='cattle-sx7f') into database 14:01:56,759 INFO: Pulled Message(instance_name='cattle-g20p') 14:01:56,800 INFO: Restarted cattle-31is.example.net 14:01:57,26 INFO: Saved Message(instance_name='cattle-i490') into database 14:01:57,45 INFO: Saved Message(instance_name='cattle-hsh9') into database 14:01:57,181 INFO: Saved Message(instance_name='cattle-g20p') into database 14:01:57,194 INFO: Restarted cattle-g20p.example.net global handler
  67. global handler $ python mayhem.py 14:01:56,187 INFO: Pulled Message(instance_name='cattle-i490') 14:01:56,192

    INFO: Restarted cattle-i490.example.net 14:01:56,241 INFO: Pulled Message(instance_name='cattle-31is') 14:01:56,331 INFO: Saved Message(instance_name='cattle-31is') into database 14:01:56,535 INFO: Pulled Message(instance_name='cattle-sx7f') 14:01:56,535 ERROR: Caught exception: Could not restart cattle-sx7f 14:01:56,730 INFO: Pulled Message(instance_name='cattle-hsh9') 14:01:56,731 INFO: Saved Message(instance_name='cattle-sx7f') into database 14:01:56,759 INFO: Pulled Message(instance_name='cattle-g20p') 14:01:56,800 INFO: Restarted cattle-31is.example.net 14:01:57,26 INFO: Saved Message(instance_name='cattle-i490') into database 14:01:57,45 INFO: Saved Message(instance_name='cattle-hsh9') into database 14:01:57,181 INFO: Saved Message(instance_name='cattle-g20p') into database 14:01:57,194 INFO: Restarted cattle-g20p.example.net
  68. async def handle_message(msg): saved, restarted = await asyncio.gather( save(msg), restart_host(msg),

    return_exceptions=True) to_ack = True if isinstance(restarted, Exception): to_ack = False await cleanup(msg, to_ack) specific handlers
  69. specific handlers async def handle_message(msg): saved, restarted = await asyncio.gather(

    save(msg), restart_host(msg), return_exceptions=True) to_ack = True if isinstance(restarted, Exception): to_ack = False await cleanup(msg, to_ack)
  70. specific handlers async def handle_message(msg): saved, restarted = await asyncio.gather(

    save(msg), restart_host(msg), return_exceptions=True) to_ack = True if isinstance(restarted, Exception): to_ack = False await cleanup(msg, to_ack)
  71. specific handlers async def handle_message(msg): saved, restarted = await asyncio.gather(

    save(msg), restart_host(msg), return_exceptions=True) to_ack = True if isinstance(restarted, Exception): to_ack = False await cleanup(msg, to_ack)
  72. specific handlers async def handle_message(msg): saved, restarted = await asyncio.gather(

    save(msg), restart_host(msg), return_exceptions=True) to_ack = True if isinstance(restarted, Exception): to_ack = False await cleanup(msg, to_ack)
  73. running coroutines from other threads def threaded_consume(): threaded_pubsub_client.subscribe(TOPIC, handle_message_sync) def

    handle_message_sync(msg): msg = Message(**msg.json_data) logging.info(f"Pulled {msg}") asyncio.create_task(handle_message(msg))
  74. def threaded_consume(): threaded_pubsub_client.subscribe(TOPIC, handle_message_sync) def handle_message_sync(msg): msg = Message(**msg.json_data) logging.info(f"Pulled

    {msg}") asyncio.create_task(handle_message(msg)) async def run(): loop = asyncio.get_running_loop() executor = concurrent.futures.ThreadPoolExecutor() await loop.run_in_executor(executor, threaded_consume, loop) running coroutines from other threads
  75. running coroutines from other threads def threaded_consume(): threaded_pubsub_client.subscribe(TOPIC, handle_message_sync) def

    handle_message_sync(msg): msg = Message(**msg.json_data) logging.info(f"Pulled {msg}") asyncio.create_task(handle_message(msg)) async def run(): loop = asyncio.get_running_loop() executor = concurrent.futures.ThreadPoolExecutor() await loop.run_in_executor(executor, threaded_consume, loop)
  76. running coroutines from other threads 16:45:36,833 INFO: Pulled Message(inst_name='cattle-hvy0') 16:45:36,833

    ERROR: Top-level exception occurred in callback while processing a message Traceback (most recent call last): File "/Users/lynn/.pyenv/versions/ep18-37/lib/python3.7/site-packages/ google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py", line 63, in _wrap_callback_errors callback(message) File "mayhem.py", line 115, in callback asyncio.create_task(handle_message(data)) File "/Users/lynn/.pyenv/versions/3.7.0/lib/python3.7/asyncio/tasks.py", line 320, in create_task loop = events.get_running_loop() RuntimeError: no running event loop
  77. running coroutines from other threads 16:45:36,833 INFO: Pulled Message(inst_name='cattle-hvy0') 16:45:36,833

    ERROR: Top-level exception occurred in callback while processing a message Traceback (most recent call last): File "/Users/lynn/.pyenv/versions/ep18-37/lib/python3.7/site-packages/ google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py", line 63, in _wrap_callback_errors callback(message) File "mayhem.py", line 115, in callback asyncio.create_task(handle_message(data)) File "/Users/lynn/.pyenv/versions/3.7.0/lib/python3.7/asyncio/tasks.py", line 320, in create_task loop = events.get_running_loop() RuntimeError: no running event loop
  78. def threaded_consume(): threaded_pubsub_client.subscribe(TOPIC, handle_message_sync) def handle_message_sync(msg): msg = Message(**msg.json_data) logging.info(f"Pulled

    {msg}") asyncio.create_task(handle_message(msg)) async def run(): loop = asyncio.get_running_loop() executor = concurrent.futures.ThreadPoolExecutor() await loop.run_in_executor(executor, threaded_consume, loop) running coroutines from other threads
  79. def threaded_consume(loop): callback = functools.partial(handle_message_sync, loop) threaded_pubsub_client.subscribe(TOPIC, callback) def handle_message_sync(loop,

    msg): msg = Message(**msg.json_data) logging.info(f"Pulled {msg}") loop.create_task(handle_message(msg)) async def run(): loop = asyncio.get_running_loop() executor = concurrent.futures.ThreadPoolExecutor() await loop.run_in_executor(executor, threaded_consume, loop) running coroutines from other threads
  80. running coroutines from other threads def threaded_consume(loop): callback = functools.partial(handle_message_sync,

    loop) threaded_pubsub_client.subscribe(TOPIC, callback) def handle_message_sync(loop, msg): msg = Message(**msg.json_data) logging.info(f"Pulled {msg}") loop.create_task(handle_message(msg)) async def run(): loop = asyncio.get_running_loop() executor = concurrent.futures.ThreadPoolExecutor() await loop.run_in_executor(executor, threaded_consume, loop)
  81. 18:08:10,543 INFO: Pulled Message(inst_name='xbci') 18:08:10,543 INFO: Pulled Message(inst_name='e8x5') 18:08:10,544 INFO:

    Running something else 18:08:10,721 INFO: Saved Message(inst_name='e8x5') into database 18:08:10,828 INFO: Saved Message(inst_name='xbci') into database 18:08:10,828 ERROR: Caught exception: Could not restart xbci.example.net 18:08:11,549 INFO: Restarted e8x5.example.net 18:08:11,821 INFO: Done. Message(inst_name='e8x5') 18:08:12,108 INFO: Running something else 18:08:12,276 INFO: Done. Message(inst_name='xbci') running coroutines from other threads
  82. running coroutines from other threads def threaded_consume(loop): callback = functools.partial(handle_message_sync,

    loop) threaded_pubsub_client.subscribe(TOPIC, callback) def handle_message_sync(loop, msg): msg = Message(**msg.json_data) logging.info(f"Pulled {msg}") loop.create_task(handle_message(msg)) async def run(): loop = asyncio.get_running_loop() executor = concurrent.futures.ThreadPoolExecutor() await loop.run_in_executor(executor, threaded_consume, loop)
  83. running coroutines from other threads def threaded_consume(loop): callback = functools.partial(handle_message_sync,

    loop) threaded_pubsub_client.subscribe(TOPIC, callback) def handle_message_sync(loop, msg): msg = Message(**msg.json_data) logging.info(f"Pulled {msg}") asyncio.run_coroutine_threadsafe(handle_message(data), loop) async def run(): loop = asyncio.get_running_loop() executor = concurrent.futures.ThreadPoolExecutor() await loop.run_in_executor(executor, threaded_consume, loop)
  84. • ThreadPoolExecutor: calling threaded code from the 
 main event

    loop • asyncio.run_coroutine_threadsafe: running a 
 coroutine on the main event loop from another thread
 threads and asyncio
  85. async def save(msg): # unhelpful simulation of i/o work await

    asyncio.sleep(random.random()) msg.saved = True logging.info(f"Saved {msg} into database") simple testing with pytest
  86. @pytest.fixture def message(): return mayhem.Message(msg_id="1234", instance_name="mayhem_test") def test_save(message): assert not

    message.saved # sanity check asyncio.run(mayhem.save(message)) assert message.saved simple testing with pytest
  87. simple testing with pytest @pytest.fixture def message(): return mayhem.Message(msg_id="1234", instance_name="mayhem_test")

    def test_save(message): assert not message.saved # sanity check asyncio.run(mayhem.save(message)) assert message.saved
  88. simple testing with pytest @pytest.fixture def message(): return mayhem.Message(msg_id="1234", instance_name="mayhem_test")

    def test_save(message): assert not message.saved # sanity check loop = asyncio.get_event_loop() loop.run_until_complete(mayhem.save(message)) loop.close() assert message.saved
  89. simple testing with pytest @pytest.fixture def message(): return mayhem.Message(msg_id="1234", instance_name="mayhem_test")

    def test_save(message): assert not message.saved # sanity check loop = asyncio.get_event_loop() loop.run_until_complete(mayhem.save(message)) loop.close() assert message.saved
  90. simple testing with pytest @pytest.fixture def message(): return mayhem.Message(msg_id="1234", instance_name="mayhem_test")

    @pytest.mark.asyncio async def test_save(message): assert not message.saved # sanity check await mayhem.save(message) assert message.saved
  91. simple testing with pytest @pytest.fixture def message(): return mayhem.Message(msg_id="1234", instance_name="mayhem_test")

    @pytest.mark.asyncio async def test_save(message): assert not message.saved # sanity check await mayhem.save(message) assert message.saved
  92. simple testing with pytest @pytest.fixture def message(): return mayhem.Message(msg_id="1234", instance_name="mayhem_test")

    @pytest.mark.asyncio async def test_save(message): assert not message.saved # sanity check await mayhem.save(message) assert message.saved
  93. async def save(msg): # unhelpful simulation of i/o work await

    asyncio.sleep(random.random()) msg.saved = True logging.info(f"Saved {msg} into database") mocking coroutines
  94. mocking coroutines async def save(msg): # unhelpful simulation of i/o

    work await asyncio.sleep(random.random()) msg.saved = True logging.info(f"Saved {msg} into database")
  95. @pytest.fixture def create_coro_mock(mocker, monkeypatch): def _create_mock_patch_coro(to_patch=None): mock = mocker.Mock() async

    def _coro(*args, **kwargs): return mock(*args, **kwargs) if to_patch: # <-- may not need/want to patch anything monkeypatch.setattr(to_patch, _coro) return mock, _coro return _create_mock_patch_coro mocking coroutines
  96. mocking coroutines @pytest.fixture def create_coro_mock(mocker, monkeypatch): def _create_mock_patch_coro(to_patch=None): mock =

    mocker.Mock() async def _coro(*args, **kwargs): return mock(*args, **kwargs) if to_patch: # <-- may not need/want to patch anything monkeypatch.setattr(to_patch, _coro) return mock, _coro return _create_mock_patch_coro
  97. mocking coroutines @pytest.fixture def create_coro_mock(mocker, monkeypatch): def _create_mock_patch_coro(to_patch=None): mock =

    mocker.Mock() async def _coro(*args, **kwargs): return mock(*args, **kwargs) if to_patch: # <-- may not need/want to patch anything monkeypatch.setattr(to_patch, _coro) return mock, _coro return _create_mock_patch_coro
  98. mocking coroutines @pytest.fixture def create_coro_mock(mocker, monkeypatch): def _create_mock_patch_coro(to_patch=None): mock =

    mocker.Mock() async def _coro(*args, **kwargs): return mock(*args, **kwargs) if to_patch: # <-- may not need/want to patch anything monkeypatch.setattr(to_patch, _coro) return mock, _coro return _create_mock_patch_coro
  99. mocking coroutines @pytest.fixture def create_coro_mock(mocker, monkeypatch): def _create_mock_patch_coro(to_patch=None): mock =

    mocker.Mock() async def _coro(*args, **kwargs): return mock(*args, **kwargs) if to_patch: # <-- may not need/want to patch anything monkeypatch.setattr(to_patch, _coro) return mock, _coro return _create_mock_patch_coro
  100. @pytest.fixture def mock_sleep(create_coro_mock): mock, _ = create_coro_mock("mayhem.asyncio.sleep") return mock @pytest.mark.asyncio

    async def test_save(mock_sleep, message): assert not message.saved # sanity check await mayhem.save(message) assert message.saved assert 1 == mock_sleep.call_count mocking coroutines
  101. mocking coroutines @pytest.fixture def mock_sleep(create_coro_mock): mock, _ = create_coro_mock("mayhem.asyncio.sleep") return

    mock @pytest.mark.asyncio async def test_save(mock_sleep, message): assert not message.saved # sanity check await mayhem.save(message) assert message.saved assert 1 == mock_sleep.call_count
  102. mocking coroutines @pytest.fixture def mock_sleep(create_coro_mock): mock, _ = create_coro_mock("mayhem.asyncio.sleep") return

    mock @pytest.mark.asyncio async def test_save(mock_sleep, message): assert not message.saved # sanity check await mayhem.save(message) assert message.saved assert 1 == mock_sleep.call_count
  103. async def consume(queue): while True: msg = await queue.get() logging.info(f"Pulled

    {msg}") asyncio.create_task(handle_message(msg)) testing create_task
  104. @pytest.fixture def mock_queue(mocker, monkeypatch): queue = mocker.Mock() monkeypatch.setattr(mayhem.asyncio, "Queue", queue)

    return queue.return_value @pytest.fixture def mock_get(mock_queue, create_coro_mock): mock, coro = create_coro_mock() mock_queue.get = coro return mock testing create_task
  105. testing create_task @pytest.fixture def mock_queue(mocker, monkeypatch): queue = mocker.Mock() monkeypatch.setattr(mayhem.asyncio,

    "Queue", queue) return queue.return_value @pytest.fixture def mock_get(mock_queue, create_coro_mock): mock, coro = create_coro_mock() mock_queue.get = coro return mock
  106. @pytest.mark.asyncio async def test_consume(mock_get, mock_queue, message, create_coro_mock): mock_handle_message, _ =

    create_coro_mock("mayhem.handle_message") mock_get.side_effect = [message, Exception("break while loop")] with pytest.raises(Exception, match="break while loop"): await mayhem.consume(mock_queue) mock_handle_message.assert_called_once_with(message) testing create_task
  107. testing create_task @pytest.mark.asyncio async def test_consume(mock_get, mock_queue, message, create_coro_mock): mock_handle_message,

    _ = create_coro_mock("mayhem.handle_message") mock_get.side_effect = [message, Exception("break while loop")] with pytest.raises(Exception, match="break while loop"): await mayhem.consume(mock_queue) mock_handle_message.assert_called_once_with(message)
  108. testing create_task @pytest.mark.asyncio async def test_consume(mock_get, mock_queue, message, create_coro_mock): mock_handle_message,

    _ = create_coro_mock("mayhem.handle_message") mock_get.side_effect = [message, Exception("break while loop")] with pytest.raises(Exception, match="break while loop"): await mayhem.consume(mock_queue) mock_handle_message.assert_called_once_with(message)
  109. testing create_task @pytest.mark.asyncio async def test_consume(mock_get, mock_queue, message, create_coro_mock): mock_handle_message,

    _ = create_coro_mock("mayhem.handle_message") mock_get.side_effect = [message, Exception("break while loop")] with pytest.raises(Exception, match="break while loop"): await mayhem.consume(mock_queue) mock_handle_message.assert_called_once_with(message)
  110. testing create_task @pytest.mark.asyncio async def test_consume(mock_get, mock_queue, message, create_coro_mock): mock_handle_message,

    _ = create_coro_mock("mayhem.handle_message") mock_get.side_effect = [message, Exception("break while loop")] with pytest.raises(Exception, match="break while loop"): await mayhem.consume(mock_queue) mock_handle_message.assert_called_once_with(message)
  111. ============================= FAILURES ============================= ___________________________ test_consume ___________________________ <--snip--> @pytest.mark.asyncio async def

    test_consume(mock_get, mock_queue, message, create_coro_mock): mock_get.side_effect = [message, Exception("break while loop")] mock_handle_message = create_coro_mock("mayhem.handle_message") with pytest.raises(Exception, match="break while loop"): await mayhem.consume(mock_queue) > mock_handle_message.assert_called_once_with(message) E AssertionError: Expected 'mock' to be called once. Called 0 times. test_mayhem.py:230: AssertionError testing create_task
  112. @pytest.mark.asyncio async def test_consume(mock_get, mock_queue, message, create_coro_mock): mock_handle_message, _ =

    create_coro_mock("mandrill.handle_message") mock_get.side_effect = [message, Exception("break while loop")] with pytest.raises(Exception, match="break while loop"): await mayhem.consume(mock_queue) ret_tasks = [ t for t in asyncio.all_tasks() if t is not asyncio.current_task() ] assert 1 == len(ret_tasks) mock_handle_message.assert_not_called() # <-- sanity check await asyncio.gather(*ret_tasks) mock_handle_message.assert_called_once_with(message) testing create_task
  113. testing create_task @pytest.mark.asyncio async def test_consume(mock_get, mock_queue, message, create_coro_mock): mock_handle_message,

    _ = create_coro_mock(“mandrill.handle_message") mock_get.side_effect = [message, Exception("break while loop")] with pytest.raises(Exception, match="break while loop"): await mayhem.consume(mock_queue) ret_tasks = [ t for t in asyncio.all_tasks() if t is not asyncio.current_task() ] assert 1 == len(ret_tasks) mock_handle_message.assert_not_called() # <-- sanity check await asyncio.gather(*ret_tasks) mock_handle_message.assert_called_once_with(message)
  114. testing create_task @pytest.mark.asyncio async def test_consume(mock_get, mock_queue, message, create_coro_mock): mock_handle_message,

    _ = create_coro_mock(“mandrill.handle_message") mock_get.side_effect = [message, Exception("break while loop")] with pytest.raises(Exception, match="break while loop"): await mayhem.consume(mock_queue) ret_tasks = [ t for t in asyncio.all_tasks() if t is not asyncio.current_task() ] assert 1 == len(ret_tasks) mock_handle_message.assert_not_called() # <-- sanity check await asyncio.gather(*ret_tasks) mock_handle_message.assert_called_once_with(message)
  115. def main(): loop = asyncio.get_event_loop() for s in (signal.SIGHUP, signal.SIGTERM,

    signal.SIGINT): loop.add_signal_handler( s, lambda s=s: asyncio.create_task(shutdown(s, loop)) ) loop.set_exception_handler(exception_handler) queue = asyncio.Queue() try: loop.create_task(publish(queue)) loop.create_task(consume(queue)) loop.run_forever() finally: logging.info("Cleaning up") loop.close() testing the event loop
  116. testing the event loop @pytest.fixture def event_loop(event_loop, mocker): new_loop =

    asyncio.get_event_loop_policy().new_event_loop() asyncio.set_event_loop(new_loop) new_loop._close = new_loop.close new_loop.close = mocker.Mock() yield new_loop new_loop._close()
  117. testing the event loop @pytest.fixture def event_loop(event_loop, mocker): new_loop =

    asyncio.get_event_loop_policy().new_event_loop() asyncio.set_event_loop(new_loop) new_loop._close = new_loop.close new_loop.close = mocker.Mock() yield new_loop new_loop._close()
  118. def test_main(create_mock_coro, event_loop, mock_queue): mock_consume, _ = create_mock_coro("mayhem.consume") mock_publish, _

    = create_mock_coro("mayhem.publish") mock_shutdown_gather, _ = create_mock_coro("mayhem.asyncio.gather") def _send_signal(): time.sleep(0.1) os.kill(os.getpid(), signal.SIGTERM) thread = threading.Thread(target=_send_signal, daemon=True) thread.start() mandrill.main() # <--snip--> testing the event loop
  119. testing the event loop def test_main(create_mock_coro, event_loop, mock_queue): mock_consume, _

    = create_mock_coro("mayhem.consume") mock_publish, _ = create_mock_coro("mayhem.publish") mock_shutdown_gather, _ = create_mock_coro("mayhem.asyncio.gather") def _send_signal(): time.sleep(0.1) os.kill(os.getpid(), signal.SIGTERM) thread = threading.Thread(target=_send_signal, daemon=True) thread.start() mandrill.main() # <--snip-->
  120. testing the event loop def test_main(create_mock_coro, event_loop, mock_queue): mock_consume, _

    = create_mock_coro("mayhem.consume") mock_publish, _ = create_mock_coro("mayhem.publish") mock_shutdown_gather, _ = create_mock_coro("mayhem.asyncio.gather") def _send_signal(): time.sleep(0.1) os.kill(os.getpid(), signal.SIGTERM) thread = threading.Thread(target=_send_signal, daemon=True) thread.start() mandrill.main() # <--snip-->
  121. testing the event loop def test_main(create_mock_coro, event_loop, mock_queue): mock_consume, _

    = create_mock_coro("mayhem.consume") mock_publish, _ = create_mock_coro("mayhem.publish") mock_shutdown_gather, _ = create_mock_coro("mayhem.asyncio.gather") def _send_signal(): time.sleep(0.1) os.kill(os.getpid(), signal.SIGTERM) thread = threading.Thread(target=_send_signal, daemon=True) thread.start() mandrill.main() # <--snip-->
  122. def test_main(create_mock_coro, event_loop, mock_queue): # <--snip--> mayhem.main() assert signal.SIGTERM in

    event_loop._signal_handlers assert mayhem.handle_exception == event_loop.get_exception_handler() mock_consume.assert_called_once_with(mock_queue) mock_publish.assert_called_once_with(mock_queue) mock_shutdown_gather.assert_called_once_with() # asserting the loop is stopped but not closed assert not event_loop.is_running() assert not event_loop.is_closed() event_loop.close.assert_called_once_with() testing the event loop
  123. testing the event loop def test_main(create_mock_coro, event_loop, mock_queue): # <--snip-->

    mayhem.main() assert signal.SIGTERM in event_loop._signal_handlers assert mayhem.handle_exception == event_loop.get_exception_handler() mock_consume.assert_called_once_with(mock_queue) mock_publish.assert_called_once_with(mock_queue) mock_shutdown_gather.assert_called_once_with() # asserting the loop is stopped but not closed assert not event_loop.is_running() assert not event_loop.is_closed() event_loop.close.assert_called_once_with()
  124. testing the event loop def test_main(create_mock_coro, event_loop, mock_queue): # <--snip-->

    mayhem.main() assert signal.SIGTERM in event_loop._signal_handlers assert mayhem.handle_exception == event_loop.get_exception_handler() mock_consume.assert_called_once_with(mock_queue) mock_publish.assert_called_once_with(mock_queue) mock_shutdown_gather.assert_called_once_with() # asserting the loop is stopped but not closed assert not event_loop.is_running() assert not event_loop.is_closed() event_loop.close.assert_called_once_with()
  125. def test_main(create_mock_coro, event_loop, mock_queue): mock_consume, _ = create_mock_coro("mayhem.consume") mock_publish, _

    = create_mock_coro("mayhem.publish") mock_shutdown_gather, _ = create_mock_coro("mayhem.asyncio.gather") def _send_signal(): time.sleep(0.1) os.kill(os.getpid(), signal.SIGTERM) thread = threading.Thread(target=_send_signal, daemon=True) thread.start() mandrill.main() # <--snip--> testing the event loop
  126. @pytest.mark.parametrize("sig_to_test", ("SIGINT", "SIGTERM", "SIGHUP")) def test_main(sig_to_test, create_mock_coro, event_loop, mock_queue): mock_consume,

    _ = create_mock_coro("mayhem.consume") mock_publish, _ = create_mock_coro("mayhem.publish") mock_shutdown_gather, _ = create_mock_coro("mayhem.asyncio.gather") def _send_signal(): time.sleep(0.1) os.kill(os.getpid(), sig_to_test) thread = threading.Thread(target=_send_signal, daemon=True) thread.start() mandrill.main() assert sig_to_test in event_loop._signal_handlers # <--snip--> testing the event loop
  127. testing the event loop @pytest.mark.parametrize("sig_to_test", ("SIGINT", "SIGTERM", "SIGHUP")) def test_main(sig_to_test,

    create_mock_coro, event_loop, mock_queue): mock_consume, _ = create_mock_coro("mayhem.consume") mock_publish, _ = create_mock_coro("mayhem.publish") mock_shutdown_gather, _ = create_mock_coro("mayhem.asyncio.gather") def _send_signal(): time.sleep(0.1) os.kill(os.getpid(), sig_to_test) thread = threading.Thread(target=_send_signal, daemon=True) thread.start() mandrill.main() assert sig_to_test in event_loop._signal_handlers # <--snip-->
  128. • pytest-asyncio + mocked coroutines • asynctest for the former

    Java developers stdlib’s 
 unittest testing asyncio code
  129. async def monitor_tasks(): while True: tasks = [ t for

    t in asyncio.all_tasks() if t is not asyncio.current_task() ] [t.print_stack() for t in tasks] await asyncio.sleep(2) manual debugging
  130. manual debugging async def monitor_tasks(): while True: tasks = [

    t for t in asyncio.all_tasks() if t is not asyncio.current_task() ] [t.print_stack() for t in tasks] await asyncio.sleep(2)
  131. Stack for <Task pending coro=<handle_message() running at mayhem.py:107> wait File

    "mayhem.py", line 107, in handle_message save_coro, restart_coro, return_exceptions=True Stack for <Task pending coro=<handle_message() running at mayhem.py:107> wait File "mayhem.py", line 107, in handle_message save_coro, restart_coro, return_exceptions=True Stack for <Task pending coro=<cleanup() running at mayhem.py:78> wait_for=<Fu File "mayhem.py", line 78, in cleanup await asyncio.sleep(random.random()) Stack for <Task pending coro=<consume() running at mayhem.py:115> wait_for=<F File "mayhem.py", line 115, in consume msg = await queue.get() Stack for <Task pending coro=<restart_host() running at mayhem.py:62> wait_fo File "mayhem.py", line 62, in restart_host await asyncio.sleep(random.randrange(1, 3)) manual debugging
  132. Stack for <Task pending coro=<handle_message() running at mayhem.py:107> wait File

    "mayhem.py", line 107, in handle_message save_coro, restart_coro, return_exceptions=True Stack for <Task pending coro=<handle_message() running at mayhem.py:107> wait File "mayhem.py", line 107, in handle_message save_coro, restart_coro, return_exceptions=True Stack for <Task pending coro=<cleanup() running at mayhem.py:78> wait_for=<Fu File "mayhem.py", line 78, in cleanup await asyncio.sleep(random.random()) Stack for <Task pending coro=<consume() running at mayhem.py:115> wait_for=<F File "mayhem.py", line 115, in consume msg = await queue.get() Stack for <Task pending coro=<restart_host() running at mayhem.py:62> wait_fo File "mayhem.py", line 62, in restart_host await asyncio.sleep(random.randrange(1, 3)) manual debugging
  133. manual debugging async def monitor_tasks(): while True: tasks = [

    t for t in asyncio.all_tasks() if t is not asyncio.current_task() ] [t.print_stack(limit=5) for t in tasks] await asyncio.sleep(2)
  134. 12:57:52,830 ERROR: Task exception was never retrieved future: <Task finished

    coro=<handle_message() done, defined at mayhem.py:97> exception=Exception('Could not restart cattle- ykdc.example.net') created at /Users/lynn/.pyenv/versions/3.7.2/lib/ python3.7/asyncio/tasks.py:325> using debug mode: traceback context
  135. 12:57:52,830 ERROR: Task exception was never retrieved future: <Task finished

    ...> source_traceback: Object created at (most recent call last): File "mayhem.py", line 164, in <module> main() File "mayhem.py", line 157, in main loop.run_forever() File "/Users/lynn/.pyenv/versions/3.7.2/lib/python3.7/asyncio/base_events.p self._run_once() File "/Users/lynn/.pyenv/versions/3.7.2/lib/python3.7/asyncio/base_events.p handle._run() File "/Users/lynn/.pyenv/versions/3.7.2/lib/python3.7/asyncio/events.py", l self._context.run(self._callback, *self._args) File "mayhem.py", line 117, in consume asyncio.create_task(handle_message(msg)) File "/Users/lynn/.pyenv/versions/3.7.2/lib/python3.7/asyncio/tasks.py", li return loop.create_task(coro) using debug mode: traceback context
  136. 12:57:52,830 ERROR: Task exception was never retrieved future: <Task finished

    ...> source_traceback: Object created at ... Traceback (most recent call last): File "mayhem.py", line 107, in handle_message save_coro, restart_coro File "mayhem.py", line 60, in restart_host raise Exception(f"Could not restart {msg.hostname}") Exception: Could not restart cattle-ykdc.example.net using debug mode: traceback context
  137. $ PYTHONASYNCIODEBUG=1 python mayhem.py
 
 20:21:59,954 ERROR: Top-level exception occurred

    in callback while processing a message Traceback (most recent call last): File "/Users/lynn/.pyenv/versions/pycon19/lib/python3.7/site-packages/ google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py", line 63, in _wrap_callback_errors callback(message) File "mayhem.py", line 174, in callback loop.create_task(handle_message(pubsub_msg)) # <-- snip --> RuntimeError: Non-thread-safe operation invoked on an event loop other than the current one using debug mode: thread safety
  138. using debug mode: thread safety $ PYTHONASYNCIODEBUG=1 python mayhem.py
 


    20:21:59,954 ERROR: Top-level exception occurred in callback while processing a message Traceback (most recent call last): File "/Users/lynn/.pyenv/versions/pycon19/lib/python3.7/site-packages/ google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py", line 63, in _wrap_callback_errors callback(message) File "mayhem.py", line 174, in callback loop.create_task(handle_message(pubsub_msg)) # <-- snip --> RuntimeError: Non-thread-safe operation invoked on an event loop other than the current one
  139. using debug mode: slow coros async def save(msg): time.sleep(1 +

    random.random()) msg.saved = True logging.info(f"Saved {msg} into database")
  140. $ PYTHONASYNCIODEBUG=1 python mayhem.py
 
 20:00:57,781 INFO: Pulled Message(inst_name='cattle-okxa') 20:00:57,782

    INFO: Extended deadline 3s for Message(inst_name='cattle-okxa') 20:00:59,416 INFO: Saved Message(inst_name='cattle-okxa') into database 20:00:59,417 WARNING: Executing <Task finished coro=<save() done, defined at mayhem.py:68> result=None created at /Users/lynn/.pyenv/versions/3.7.2/lib/python3.7/asyncio/ tasks.py:719> took 1.634 seconds 20:00:59,418 INFO: Pulled Message(instance_name='cattle-pmbv') using debug mode: slow coros
  141. using debug mode: slow coros $ PYTHONASYNCIODEBUG=1 python mayhem.py
 


    20:00:57,781 INFO: Pulled Message(inst_name='cattle-okxa') 20:00:57,782 INFO: Extended deadline 3s for Message(inst_name='cattle-okxa') 20:00:59,416 INFO: Saved Message(inst_name='cattle-okxa') into database 20:00:59,417 WARNING: Executing <Task finished coro=<save() done, defined at mayhem.py:68> result=None created at /Users/lynn/.pyenv/versions/3.7.2/lib/python3.7/asyncio/ tasks.py:719> took 1.634 seconds 20:00:59,418 INFO: Pulled Message(instance_name='cattle-pmbv')
  142. def main(): queue = asyncio.Queue() loop = asyncio.get_event_loop() # float,

    in seconds loop.slow_callback_duration = 0.5 # <-- snip --> using debug mode: slow coros
  143. using debug mode: slow coros def main(): queue = asyncio.Queue()

    loop = asyncio.get_event_loop() # float, in seconds loop.slow_callback_duration = 0.5 # <-- snip -->
  144. # <-- snip --> from aiodebug import log_slow_callbacks # <--

    snip --> def main(): queue = asyncio.Queue() loop = asyncio.get_event_loop() log_slow_callbacks.enable(0.05) debugging in production
  145. debugging in production # <-- snip --> from aiodebug import

    log_slow_callbacks # <-- snip --> def main(): queue = asyncio.Queue() loop = asyncio.get_event_loop() log_slow_callbacks.enable(0.05)
  146. debugging in production # <-- snip --> from aiodebug import

    log_slow_callbacks # <-- snip --> def main(): queue = asyncio.Queue() loop = asyncio.get_event_loop() log_slow_callbacks.enable(0.05)
  147. debugging in production # <-- snip --> from aiodebug import

    log_slow_callbacks from aiodebug import monitor_loop_lag # <-- snip --> def main(): queue = asyncio.Queue() loop = asyncio.get_event_loop() log_slow_callbacks.enable(0.05) monitor_loop_lag.enable(my_statsd_client)
  148. • manual: task.print_stack() • proper: built-in debug mode • f-it,

    we’ll do it live: aiodebug debugging asyncio code
  149. $ timeout -s INT 5s python -m cProfile -s tottime

    mayhem.py ncalls tottime percall ... filename:lineno(function) 134 4.785 0.036 ... {method 'control' of 'select.kqueue' objects} 17 0.007 0.000 ... {built-in method _imp.create_dynamic} 62 0.007 0.000 ... {built-in method marshal.loads} 132 0.003 0.000 ... base_events.py:1679(_run_once) 217/216 0.003 0.000 ... {built-in method builtins.__build_class__} 361 0.003 0.000 ... {built-in method posix.stat} 62 0.002 0.000 ... <frozen importlib._bootstrap_external>:914(ge 42 0.002 0.000 ... {built-in method builtins.compile} 195 0.001 0.000 ... <frozen importlib._bootstrap_external>:1356(f 50 0.001 0.000 ... {method 'write' of '_io.TextIOWrapper' object 122 0.001 0.000 ... _make.py:1217(__repr__) 50 0.001 0.000 ... __init__.py:293(__init__) 18 0.001 0.000 ... enum.py:134(__new__) 72/15 0.001 0.000 ... sre_parse.py:475(_parse) 62 0.001 0.000 ... {method 'read' of '_io.FileIO' objects} cProfile
  150. $ timeout -s INT 5s python -m cProfile -s tottime

    mayhem.py ncalls tottime percall ... filename:lineno(function) 134 4.785 0.036 ... {method 'control' of 'select.kqueue' objects} 17 0.007 0.000 ... {built-in method _imp.create_dynamic} 62 0.007 0.000 ... {built-in method marshal.loads} 132 0.003 0.000 ... base_events.py:1679(_run_once) 217/216 0.003 0.000 ... {built-in method builtins.__build_class__} 361 0.003 0.000 ... {built-in method posix.stat} 62 0.002 0.000 ... <frozen importlib._bootstrap_external>:914(ge 42 0.002 0.000 ... {built-in method builtins.compile} 195 0.001 0.000 ... <frozen importlib._bootstrap_external>:1356(f 50 0.001 0.000 ... {method 'write' of '_io.TextIOWrapper' object 122 0.001 0.000 ... _make.py:1217(__repr__) 50 0.001 0.000 ... __init__.py:293(__init__) 18 0.001 0.000 ... enum.py:134(__new__) 72/15 0.001 0.000 ... sre_parse.py:475(_parse) 62 0.001 0.000 ... {method 'read' of '_io.FileIO' objects} cProfile
  151. $ timeout -s INT 5s python -m cProfile -s filename

    mayhem.py ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 4.704 4.704 mayhem.py:141(main) 18 0.000 0.000 0.002 0.000 mayhem.py:56(restart_host) 22 0.000 0.000 0.003 0.000 mayhem.py:67(save) 33 0.000 0.000 0.003 0.000 mayhem.py:74(cleanup) 22 0.000 0.000 0.002 0.000 mayhem.py:83(extend) 11 0.000 0.000 0.000 0.000 mayhem.py:91(handle_results) 12 0.000 0.000 0.002 0.000 mayhem.py:41(publish) 22 0.000 0.000 0.002 0.000 mayhem.py:97(handle_message) 12 0.000 0.000 0.003 0.000 mayhem.py:114(consume) 11 0.000 0.000 0.000 0.000 mayhem.py:37(__attrs_post_init_ 1 0.000 0.000 0.000 0.000 mayhem.py:26(Message) 1 0.000 0.000 0.000 0.000 mayhem.py:130(<listcomp>) 2 0.000 0.000 0.001 0.000 mayhem.py:126(shutdown) 1 0.000 0.000 0.000 0.000 mayhem.py:148(<lambda>) cProfile
  152. cProfile $ timeout -s INT 5s python -m cProfile -s

    filename mayhem.py ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 4.704 4.704 mayhem.py:141(main) 18 0.000 0.000 0.002 0.000 mayhem.py:56(restart_host) 22 0.000 0.000 0.003 0.000 mayhem.py:67(save) 33 0.000 0.000 0.003 0.000 mayhem.py:74(cleanup) 22 0.000 0.000 0.002 0.000 mayhem.py:83(extend) 11 0.000 0.000 0.000 0.000 mayhem.py:91(handle_results) 12 0.000 0.000 0.002 0.000 mayhem.py:41(publish) 22 0.000 0.000 0.002 0.000 mayhem.py:97(handle_message) 12 0.000 0.000 0.003 0.000 mayhem.py:114(consume) 11 0.000 0.000 0.000 0.000 mayhem.py:37(__attrs_post_init_ 1 0.000 0.000 0.000 0.000 mayhem.py:26(Message) 1 0.000 0.000 0.000 0.000 mayhem.py:130(<listcomp>) 2 0.000 0.000 0.001 0.000 mayhem.py:126(shutdown) 1 0.000 0.000 0.000 0.000 mayhem.py:148(<lambda>)
  153. $ timeout -s INT 5s python -m cProfile -o mayhem.prof

    mayhem.py cProfile with (K|Q)CacheGrind
  154. $ timeout -s INT 5s python -m cProfile -o mayhem.prof

    mayhem.py
 
 $ pyprof2calltree --kcachegrind -i mayhem.prof cProfile with (K|Q)CacheGrind
  155. @profile async def save(msg): # unhelpful simulation of i/o work

    await asyncio.sleep(random.random()) msg.saved = True logging.info(f"Saved {msg} into database") line_profiler
  156. line_profiler @profile async def save(msg): # unhelpful simulation of i/o

    work await asyncio.sleep(random.random()) msg.saved = True logging.info(f"Saved {msg} into database")
  157. $ timeout -s INT 5s kernprof -o mayhem.prof --line-by-line mayhem.py

    $ python -m line_profiler mayhem.prof line_profiler
  158. $ timeout -s INT 5s kernprof -o mayhem.prof --line-by-line mayhem.py

    $ python -m line_profiler mayhem.prof Timer unit: 1e-06 s Total time: 0.002202 s File: mayhem.py Function: save at line 69 Line # Hits Time Per Hit % Time Line Contents ======================================================= 69 @profile 70 async def save(msg): 71 8 259.0 32.4 11.8 await asyncio.sleep(random.rand 72 8 26.0 3.2 1.2 msg.saved = True 73 8 1917.0 239.6 87.1 logging.info(f"Saved {msg} into line_profiler
  159. $ timeout -s INT 5s kernprof -o mayhem.prof --line-by-line mayhem.py

    $ python -m line_profiler mayhem.prof Timer unit: 1e-06 s Total time: 0.002202 s File: mayhem.py Function: save at line 69 Line # Hits Time Per Hit % Time Line Contents ======================================================= 69 @profile 70 async def save(msg): 71 8 259.0 32.4 11.8 await asyncio.sleep(random.rand 72 8 26.0 3.2 1.2 msg.saved = True 73 8 1917.0 239.6 87.1 logging.info(f"Saved {msg} into line_profiler
  160. $ timeout -s INT 5s kernprof -o mayhem.prof --line-by-line mayhem.py

    $ python -m line_profiler mayhem.prof Timer unit: 1e-06 s Total time: 0.002202 s File: mayhem.py Function: save at line 69 Line # Hits Time Per Hit % Time Line Contents ======================================================= 69 @profile 70 async def save(msg): 71 8 259.0 32.4 11.8 await asyncio.sleep(random.rand 72 8 26.0 3.2 1.2 msg.saved = True 73 8 1917.0 239.6 87.1 logging.info(f"Saved {msg} into line_profiler
  161. $ timeout -s INT 5s kernprof -o mayhem.prof --line-by-line mayhem.py

    $ python -m line_profiler mayhem.prof Timer unit: 1e-06 s Total time: 0.0011 s File: mayhem.py Function: save at line 69 Line # Hits Time Per Hit % Time Line Contents ======================================================= 69 @profile 70 async def save(msg): 71 7 269.0 38.4 24.5 await asyncio.sleep(random.rand 72 5 23.0 4.6 2.1 msg.saved = True 73 5 808.0 161.6 73.5 await logger.info(f"Saved {msg} line_profiler
  162. $ timeout -s INT 5s kernprof -o mayhem.prof --line-by-line mayhem.py

    $ python -m line_profiler mayhem.prof Timer unit: 1e-06 s Total time: 0.0011 s File: mayhem.py Function: save at line 69 Line # Hits Time Per Hit % Time Line Contents ======================================================= 69 @profile 70 async def save(msg): 71 7 269.0 38.4 24.5 await asyncio.sleep(random.rand 72 5 23.0 4.6 2.1 msg.saved = True 73 5 808.0 161.6 73.5 await logger.info(f"Saved {msg} line_profiler
  163. $ timeout -s INT 5s kernprof -o mayhem.prof --line-by-line mayhem.py

    $ python -m line_profiler mayhem.prof Timer unit: 1e-06 s Total time: 0.0011 s File: mayhem.py Function: save at line 69 Line # Hits Time Per Hit % Time Line Contents ======================================================= 69 @profile 70 async def save(msg): 71 7 269.0 38.4 24.5 await asyncio.sleep(random.rand 72 5 23.0 4.6 2.1 msg.saved = True 73 5 808.0 161.6 73.5 await logger.info(f"Saved {msg} line_profiler
  164. • No idea? Visualize with cProfile with 
 pyprof2calltree +

    KCacheGrind • Some idea? line_profiler • f-it, we’ll do it live: profiling live-profiler profiling asyncio code