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

Virtual Lines with Asyncio

Virtual Lines with Asyncio

PyConES2015 talk given by @igalarzab and @maraujop about how to build a virtual line software with Python 3.5, websockets, Redis and of course asyncio.

A virtual line is a software that allows people to wait in line for accessing an online resource, which can be anything we want to protect from high traffic spikes, such as a purchase process, or when we want people to access it in a specific order or controlled way.

Ticketea Engineering

November 22, 2015
Tweet

More Decks by Ticketea Engineering

Other Decks in Programming

Transcript

  1. Virtual Lines

    with

    asyncIO
    Jose Ignacio Galarza

    @igalarzab
    Miguel Araujo

    @maraujop

    View Slide

  2. @ticketeaeng

    View Slide

  3. View Slide

  4. virtual line

    View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  8. ticketea
    Why Python?
    ✤ Because we are not in the RubyCON :D
    ✤ Python 3 fixes some issues with concurrency
    ✤ This is a typical problem to be solved with async I/O
    ✤ Python scales without problems ;)

    View Slide

  9. ticketea
    Why asyncIO?
    ✤ This problem is I/O bound
    ✤ It’s bundled in the standard library
    ✤ It has a powerful community behind it
    ✤ One of the most readable concurrent paradigms in Python

    View Slide

  10. ticketea
    Why websockets?
    ✤ Two-ways communication between user and server
    ✤ We need almost real-time communication
    ✤ There is no-overhead in the payloads
    ✤ It’s possible to update from HTTP to Websocket
    ✤ Easy to pass through firewalls

    View Slide

  11. ticketea
    Why Redis?
    ✤ We need a cache to store counters atomically
    ✤ We don’t need a huge transactional engine
    ✤ It’s pretty fast
    ✤ It has a lot of libraries (also for asyncIO)

    View Slide

  12. how it

    works

    View Slide

  13. View Slide

  14. View Slide

  15. VLMn
    RESOURCE
    prepare_purchase with token signed
    access_resource
    pay
    leaves_resource
    CLIENTS_IN_RESOURCE++
    CLIENTS_IN_RESOURCE - 1

    View Slide

  16. show me

    the code

    View Slide

  17. ticketea
    New keywords
    async def name:
    await coro
    @asyncio.coroutine
    def name:
    yield from coro
    Python 3.4 Python 3.5

    View Slide

  18. ticketea
    HTTPServer
    class HTTPServer:
    def __init__(self, loop, host='127.0.0.1', port=8080)
    async def start(self)
    async def _start_http_server(self)
    async def _upgrade_to_websocket(self, request)
    async def _access_resource(self, request)
    async def _leave_resource(self, request)
    async def _config_resource(self, request)

    View Slide

  19. ticketea
    Where everything almost begins
    class HTTPServer:
    async def start(self):
    await self.redis.start()
    await self._start_http_server()
    async def _start_http_server(self):
    return await self.loop.create_server(
    self.app.make_handler(),
    self.host,
    self.port
    )

    View Slide

  20. ticketea
    Where everything begins
    if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    server = HTTPServer(loop, host, port)
    loop.run_until_complete(server.start())
    print('Listening to {0}:{1}'.format(host, port))
    try:
    loop.run_forever()
    except KeyboardInterrupt:
    loop.call_soon(loop.stop)
    print('Stopping server...')

    View Slide

  21. ticketea
    HTTPServer: From HTTP to Websocket
    async def _upgrade_to_websocket(self, request):
    websocket = web.WebSocketResponse()
    await websocket.prepare(request)
    resource_id = request.match_info.get('resource_id')
    vlm = await self._get_vlm(resource_id)
    ws_handler = WebsocketHandler(self.loop, websocket, vlm)
    await ws_handler.process_connection()

    View Slide

  22. ticketea
    VirtualLineManager
    class VirtualLineManager(object):
    def __init__(self, loop, redis, resource_id)
    async def init_resource(self)
    def start_updating_clients(self)
    async def keep_clients_updated(self)
    async def let_clients_pass(self)
    def add_connection(self, turn, handler)
    async def take_turn(self)

    View Slide

  23. ticketea
    VirtualLineManager
    async def init_resource(self):
    await self.redis.init_turn_granted_access(self.rid)
    await self.redis.init_clients_in_resource(self.rid)
    await self.redis.init_max_clients_in_resource(self.rid)
    def start_updating_clients(self):
    self.loop.create_task(self.keep_clients_updated())

    View Slide

  24. ticketea
    VirtualLineManager
    async def keep_clients_updated(self):
    await self.let_clients_pass()
    turn = await self.redis.get_turn_granted_access(self.resource_id)
    message = json.dumps({"granted_access": "true", "sig": ""})
    while self.conn_handlers:
    if int(self.conn_handlers[0]['turn']) <= int(turn):
    conn = self.conn_handlers.pop()
    conn['handler'].send_message(message)
    else:
    break
    message = json.dumps({'last_turn': turn})
    connections = deque()
    for conn_handler in self.conn_handlers:
    conn_handler['handler'].send_message(message)
    connections.append(conn_handler)
    self.conn_handlers = connections
    self.loop.call_later(
    self.UPDATE_INTERVAL, lambda: self.start_updating_clients())

    View Slide

  25. ticketea
    RedisHandler
    class RedisHandler:
    def __init__(self, host='127.0.0.1', port=6379, poolsize=10)
    async def start(self)
    async def access_resource(self, rid)
    async def leave_resource(self, rid)
    async def get_last_given_turn(self, rid)
    async def get_clients_in_resource(self, rid)
    async def init_clients_in_resource(self, rid)
    async def set_clients_in_resource(self, rid, clients)
    async def get_max_clients_in_resource(self, rid)
    async def init_max_clients_in_resource(self, rid)
    async def set_max_clients_in_resource(self, rid, max_clients)
    async def get_turn_granted_access(self, rid)
    async def init_turn_granted_access(self, rid)
    async def inc_turn_granted_access(self, rid, incr_quantity)
    async def take_turn(self, rid):
    return await self.redis.hincrby(rid, self.LAST_GIVEN_TURN, 1)

    View Slide

  26. CPU bound

    processes

    View Slide

  27. http://bit.ly/1MCDoKH

    View Slide

  28. View Slide

  29. DEMO TIME

    View Slide

  30. https://www.youtube.com/watch?v=7g0iBWKAvBQ

    View Slide

  31. ticketea
    questions?

    View Slide

  32. thanks!
    Jose Ignacio Galarza

    @igalarzab
    Miguel Araujo

    @maraujop

    View Slide