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: