3.6 Sample Application: chatter.py

For an example application, we'll build a little chat application. What's interesting is that none of the application's code deals with network programming at all; instead, an object will hold chat messages, and be magically shared between all the clients through ZEO. I won't present the complete script here; it's included in my ZODB distribution, and you can download it from http://www.amk.ca/zodb/demos/. Only the interesting portions of the code will be covered here.

The basic data structure is the ChatSession object, which provides an add_message() method that adds a message, and a new_messages() method that returns a list of new messages that have accumulated since the last call to new_messages(). Internally, ChatSession maintains a B-tree that uses the time as the key, and stores the message as the corresponding value.

The constructor for ChatSession is pretty simple; it simply creates an attribute containing a B-tree:

class ChatSession(Persistent):
    def __init__(self, name):
        self.name = name
        # Internal attribute: _messages holds all the chat messages.        
        self._messages = BTree.BTree()

add_message() has to add a message to the _messages B-tree. A complication is that it's possible that some other client is trying to add a message at the same time; when this happens, the client that commits first wins, and the second client will get a ConflictError exception when it tries to commit. For this application, ConflictError isn't serious but simply means that the operation has to be retried; other applications might treat it as a fatal error. The code uses try...except...else inside a while loop, breaking out of the loop when the commit works without raising an exception.

    def add_message(self, message):
        """Add a message to the channel.
        message -- text of the message to be added
        """

        while 1:
            try:
                now = time.time()
                self._messages[now] = message
                get_transaction().commit()
            except ConflictError:
                # Conflict occurred; this process should pause and
                # wait for a little bit, then try again.
                time.sleep(.2)
                pass
            else:
                # No ConflictError exception raised, so break
                # out of the enclosing while loop.
                break
        # end while

new_messages() introduces the use of volatile attributes. Attributes of a persistent object that begin with _v_ are considered volatile and are never stored in the database. new_messages() needs to store the last time the method was called, but if the time was stored as a regular attribute, its value would be committed to the database and shared with all the other clients. new_messages() would then return the new messages accumulated since any other client called new_messages(), which isn't what we want.

    def new_messages(self):
        "Return new messages."

        # self._v_last_time is the time of the most recent message
        # returned to the user of this class. 
        if not hasattr(self, '_v_last_time'):
            self._v_last_time = 0

        new = []
        T = self._v_last_time

        for T2, message in self._messages.items():
            if T2 > T:
                new.append(message)
                self._v_last_time = T2

        return new

This application is interesting because it uses ZEO to easily share a data structure; ZEO and ZODB are being used for their networking ability, not primarily for their data storage ability. I can foresee many interesting applications using ZEO in this way: