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

Using Tasks in Your Asyncio Web App

Feihong Hsu
September 09, 2016

Using Tasks in Your Asyncio Web App

In this talk, I cover how to implement different types of tasks in an asyncio-based web application, including how to start them, stop them, and send incremental data to a web frontend using websockets. I will also spend a little time reviewing asyncio concepts.

Feihong Hsu

September 09, 2016
Tweet

More Decks by Feihong Hsu

Other Decks in Technology

Transcript

  1. Using Tasks in Your Asyncio Web App ChiPy September 8,

    2016 Feihong Hsu github.com/feihong
  2. This talk is on GitHub https:/ /github.com/feihong/asyncio-tasks-talk I will be

    sharing a fair amount of code during this talk. If you are not in an ideal viewing position you may want to refer to the GitHub repo so that you can read the code.
  3. What In this talk, I cover how to implement different

    types of tasks in an asyncio-based web application, including how to start them, stop them, and send incremental data to a web frontend using websockets. I will also spend a little time reviewing asyncio concepts.
  4. Why You could just run tasks in Celery or Django

    Channels, so why bother with asyncio? Depending on your circumstances, using asyncio can lead to • A simpler architecture • Less code • Better performance
  5. What is a coroutine object? An "object [representing] a computation

    or an I/O operation (usually a combination) that will complete eventually" Originally, coroutine objects were essentially glorified generators. Python 3.5 introduced native coroutine objects, which are created by coroutine functions using the async def syntax.
  6. What is a coroutine function? Coroutine functions are functions that

    define a coroutine, using either the async def syntax or the @asyncio.coroutine decorator. If you don't need to worry about backwards compatibility, you should almost always use new-style coroutine functions that return native coroutine objects.
  7. A very simple coroutine function import asyncio async def hello():

    print('Hello Task!') coroutine = hello() asyncio.get_event_loop().run_until_complete(coroutine)
  8. Return values of coroutine functions import asyncio async def add(x,

    y): await asyncio.sleep(2) return x + y async def main(): return_value = await add(4, 7) print(return_value) asyncio.get_event_loop().run_until_complete(main())
  9. What is an asyncio task? In an event-loop-based program, you

    primarily use tasks instead of threads to implement concurrency. You should not directly create Task objects: use the ensure_future() function or the AbstractEventLoop.create_task() method.
  10. A simple task import asyncio async def hello(): print('Hello Task!')

    asyncio.ensure_future(hello()) asyncio.get_event_loop().run_forever()
  11. Types of tasks we'll talk about today • Asynchronous •

    Synchronous (using ThreadPoolExecutor) • Inside web socket handler • Inside a separate process
  12. Asynchronous task • Simplest way to implement concurrency in asyncio

    web app. • Define logic using a coroutine function, schedule it using the asyncio.ensure_future() function. • Send messages to the client by using the websocket object in your coroutine function.
  13. Task function async def long_task(writer): total = 150 for i

    in range(1, total+1): writer.write(type='progress', value=i, total=total) print(i) await asyncio.sleep(0.05)
  14. Muffin web app boilerplate import muffin from muffin_playground import Application,

    WebSocketWriter app = Application() app.register_special_static_route()
  15. The web socket request handler @app.register('/websocket/') async def websocket(request): ws

    = muffin.WebSocketResponse() await ws.prepare(request) writer = WebSocketWriter(ws) async for msg in ws: if msg.data == 'start': coroutine = long_task(writer) task = asyncio.ensure_future(coroutine) return ws
  16. Client code, boilerplate jq = window.jQuery ws = new WebSocket('ws://localhost:5000/websocket/')

    def on_click(evt): ws.send('start') jq('button.start').on('click', on_click)
  17. Client code, websocket message handler def on_message(evt): obj = JSON.parse(evt.data)

    print(obj) percent = obj['value'] / obj['total'] * 100 jq('progress').val(percent) jq('.percent').text(str.format('{}%', percent.toFixed(0))) ws.onmessage = on_message
  18. Adding the ability to cancel the task Great, we can

    now start a long-running task from a web page! But, what if we want to give the user the ability to cancel the task? The asyncio.Task class has a cancel() method, but if we want to use it we must keep a reference to the task object and add some cleanup logic when the task completes. This necessitates some refactoring.
  19. New web socket request handler (1) @app.register('/websocket/') class WSHandler(WebSocketHandler): async

    def on_open(self): self.task = None self.writer = WebSocketWriter(self.websocket) def task_done_callback(self, future): self.task = None # next slide
  20. New web socket request handler (2) @app.register('/websocket/') class WSHandler(WebSocketHandler): #

    previous slide async def on_message(self, msg): print(msg) if msg.data == 'start' and not self.task: self.task = asyncio.ensure_future(long_task(self.writer)) self.task.add_done_callback(self.task_done_callback) elif msg.data == 'stop' and self.task: self.task.cancel()
  21. Updated client code from wsclient import WsClient class MyClient(WsClient): url

    = '/websocket/' auto_dispatch = True def on_progress(self, obj): print(obj) percent = obj['value'] / obj['total'] * 100 jq('progress').val(percent) jq('.percent').text(str.format('{}%', percent.toFixed(0)))
  22. Synchronous task • Run synchronous code that cannot be directly

    run by the asyncio event loop. • Define logic inside a normal function. • Asyncio will execute the function using a thread pool. • Be careful about writing to websockets because most asyncio functions are not thread safe.
  23. Task function def long_task(writer, stop_event: threading.Event): total = 150 for

    i in range(1, total+1): if stop_event.is_set(): return writer.write(type='progress', value=i, total=total) print(i) time.sleep(0.05)
  24. Web socket request handler (1) @app.register('/websocket/') class WSHandler(WebSocketHandler): async def

    on_open(self): self.future = None self.writer = ThreadSafeWebSocketWriter(self.websocket) self.stop_event = threading.Event() def future_done_callback(self, future): self.future = None self.stop_event.clear() # next slide
  25. ThreadSafeWebSocketWriter class ThreadSafeWebSocketWriter: def __init__(self, wsresponse): self.resp = wsresponse self.loop

    = asyncio.get_event_loop() def write(self, **kwargs): if not self.resp.closed: data = json.dumps(kwargs) self.loop.call_soon_threadsafe(self.resp.send_str, data)
  26. Web socket request handler (2) @app.register('/websocket/') class WSHandler(WebSocketHandler): # previous

    slide async def on_message(self, msg): print(msg) if msg.data == 'start' and not self.future: loop = asyncio.get_event_loop() self.future = loop.run_in_executor( None, long_task, self.writer, self.stop_event) self.future.add_done_callback(self.future_done_callback) elif msg.data == 'stop' and self.future: self.stop_event.set()
  27. Web socket handler as a task • Web socket handler

    is essentially a long-running task anyway • Implement the logic after the web socket connection is opened • Makes sense if the task doesn't need to run for very long • Note that a web socket handler cannot simultaneously read and write to the socket at the same time
  28. Web socket handler @app.register('/websocket/') async def websocket(request): ws = muffin.WebSocketResponse()

    await ws.prepare(request) writer = WebSocketWriter(ws) total = 150 for i in range(1, total+1): writer.write(type='progress', value=i, total=total) print(i) await asyncio.sleep(0.05) await ws.close() return ws
  29. Client code client = None jq('button.start').on('click', def(evt): nonlocal client client

    = MyClient() ) jq('button.stop').on('click', def(evt): if client is not None: client.close() )
  30. Separate process as a task • Task runs in separate

    program • Asyncio provides subprocess-like API for running commands • You web app now needs two web socket handlers: • Collect messages from task processes • Push messages to browser clients
  31. Task program, boilerplate import sys import time import json import

    websocket def main(): url, name = sys.argv[1:] long_task(url, name)
  32. Task program, useful function def long_task(ws_url, name): ws = websocket.WebSocket()

    ws.connect(ws_url) total = 150 for i in range(1, total+1): # print(i) data = dict(type='progress', name=name, value=i, total=total) ws.send(json.dumps(data)) time.sleep(0.05) ws.close()
  33. Start task request handler @app.register('/start-task/') async def start_task(request): name =

    '%s-%d' % (random.choice(NAMES), app.task_id) app.task_id += 1 proc = await asyncio.create_subprocess_exec( 'python', 'long_task.py', 'ws://localhost:5000/collect/', name) return str(proc)
  34. Browser client web socket handler app.sockets = set() @app.register('/progress/') async

    def websocket(request): ws = muffin.WebSocketResponse() await ws.prepare(request) app.sockets.add(ws) async for msg in ws: pass app.sockets.remove(ws) return ws
  35. Task process web socket handler @app.register('/collect/') async def websocket(request): ws

    = muffin.WebSocketResponse() await ws.prepare(request) async for msg in ws: for ws in app.sockets: ws.send_str(msg.data) return ws
  36. Client code The client code for this example is a

    bit long, so I won't show it on these slides. The basic difference with previous examples is that progress bars are be added as tasks start and progress bars are removed as tasks end.
  37. Conclusion • Asyncio in Python 3.5 is powerful and easy

    to use (especially in comparison to previous efforts) • The asyncio module contains many excellent tools to help you implement various types of concurrency in your application. • The asyncio ecosystem includes a number of viable options for web development, including aiohttp and Muffin.