cardstories part 12: long polling server

The poll method has been added server side to implement a notification mechanism inspired by long polling.

long polling

A specific implementation of long polling was chosen over a more generic one. The full state of a given game can be retrieved at any time by the client, as well as the lobby / list of games for a given player. Instead of using long polling to simulate a generic bidirectional communication channel, it is applied to an event notification mechanism telling the client that it needs to request information from the server.
The general idea is that the client sends a request asking the server if anything happened after a given timestamp, in milliseconds. For instance:

$ curl 'http://localhost:4923/resource?action=poll&player_id=loic&modified=1302779947401'
{"game_id": [16], "player_id": [1], "modified": [1302780247404]}

asks if anything changed for player loic after 1302779947401. The call will block because there is nothing new. At 1302780247404 something happens and the call returns with the new timestamp. It also hints that the event is related to game_id number 16. When receiving this answer, the client is expected to immediately issue a new request like:

$ curl 'http://localhost:4923/resource?action=poll&player_id=loic&modified=1302780247404'

It will block again and timeout if nothing happens for more than –poll-timeout seconds (as defined in tap.py). When it timesout, the map returned to the client contains the keyword timeout as in the following example:

{"player_id": [1], "modified": [1302780247404], "timeout": [1302780278453]}

The logic of this notification queue was implemented and tested in the pollable class. It contains two methods: poll that returns a deferred that will fire when the touch method is called triggers all the deferred created by previous calls to the poll method.

CardstoriesGame and CardstoriesPlayer objects

The server process relied on the database to store all information. There was no in core representation of the player or the game and this made for a simpler implementation. Although it would be possible to implement the notification queue described above using information stored in the database, it would have been inconvenient and useless since it does not need to persist when the process dies.
A CardstoriesGame and a CardstoriesPlayer class were implemented and tested .
The CardstoriesPlayer class is simple minded and derived from the pollable class.The CardstoriesGame object does a little more : it keeps an in core image of the game players and the pending invitations, if applicable. Each method of the CardstoriesGame class that has a side effect (such as inviting new players or picking a card) will call the touch method of the pollable base class. It allows to implement the following use case:

  • an incoming poll request is received for game number 20
  • nothing happened yet: the poll method is called on game 20 to acquire a deferred that will fire when something happens
  • a callback is attached to the deferred : it will return the incoming poll request when the deferred is fired
  • later on, a player on game 20 picks a card
  • as a conquence, the touch method is called on game 20 and triggers the deferred previously acquired via the poll method
  • the previously attached callback is called and completes the request that returns to the client, informing it that something needs attention

CardstoriesService re-architecture

This class previously held all the game logic and now delegates all of it to the new CardstoriesGame class after checking for the required arguments. It is a container of all games, dispatching the requests to the relevant game and implementing the lobby / list of games.
The poll action was added to the list of supported actions with the following semantic:

  • If player_id is set, return when a game in which the designated player is involved has been modified. It is best suited when displaying the list of game in which a player is involved.
  • If game_id is set, return when the designated game changes state. It is best suited when displaying a single game.

For each game that is not in the complete state, an in-core representation of the game (CardstoriesGame) is created and persists until the game moves to the complete state. For consistency, a short lived in-core representation is created when a client requests information about a completed game. It is destroyed as soon as the request is answered.
The CardstoriesPlayer objects are only created when a poll request is made with a player_id argument. They do not exist if no poll is waiting on them, even if a player is involved in a number of games. There would be no reason to keep it up to date. A CardstoriesPlayer object will be destroyed after –poll-timeout * 2 seconds unless it receives a new poll request.
When the CardstoriesService is started, the list of games in progress and their associated players are read from the database and the corresponding in-core objects created. For each CardstoriesGame object, the CardstoriesService object asks to be notified (registering the poll_notify_player callback with the poll function) whenever its state is changed. When it knows that a CardstoriesGame changed, it gets the list of players involved (method get_players) and calls the touch method for each corresponding CardstoriesPlayer instance it will find in the self.players map.

    def poll_notify_players(self, args):
        if args == None:
            return False
        game_id = int(args['game_id'][0])
        if not self.games.has_key(game_id):
            return False
        game = self.games[game_id]
        for player_id in game.get_players():
            if self.players.has_key(player_id):
                self.players[player_id].touch(args)
        d = game.poll(args)
        d.addCallback(self.poll_notify_players)
        return True