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. 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 ;)
  2. 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
  3. 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
  4. 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)
  5. 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)
  6. 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 )
  7. 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...')
  8. 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()
  9. 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)
  10. 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())
  11. 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": "<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())
  12. 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)