Python since 1.4
OpenEnd (Strakt) since 2001
A set of database updates that we want atomically written.
This is not a transaction in the ACID sense, although many properties apply. The isolation level is equivalent to SQL Read Committed, so you will see changes made by other committed transactions. Multi-document consistency can only be guaranteed if you observe the commit locks when reading (we don't).
The lack of consistency is not a big problem, as any update that depended on an inconsistency should fail to commit, due to incompatible changes.
The Open End collaborative todo system.
Grew out of an earlier advanced helpdesk system
Custom object db on top of SQL
Move from single-process to multiple servers
MongoDB a nice fit for our data model
Moves transaction to application:
Store changes in a commit collection
Apply changes (may fail!)
Profit!
Generate when transaction runs
Diff of documents to change
Data for new documents
List of documents to remove
Lock affected documents (fail)
Apply changes (fail)
Unlock documents
Mark commit as not running
Something changed in the database
Other failure scenarios, not covered
# Colour { '_id' : ObjectId(), 'keys' : [], 'count' : 0, 'colour' : 'red', # 'green', 'any' }
# Commit { '_id' : ObjectId(), # document id 'handled_by' : ObjectId() # process id 'input' : {} 'changes' : { str(docid) : { key : ( old, new ) } } }
def main(): commit = committer.add_key() committer.process(commit) def add_key(self): # Generate a random key colour = random.choice(['red', 'green']) key = random.choice('abcd…') return self.prepare_commit(colour, key)
def prepare_commit(self, colour, key): ckey = self.database.data.find_one( {'colour' : colour}) ccount = ckey['count'] ckeys = ckey['keys'] … commit = { 'handled_by': self.id, 'input' : { 'colour': colour, 'key' : key}, 'changes' : { str(ckey['_id']) : { 'count' : ( ccount, ccount + 1), 'keys' : ( ckeys, ckeys + [key]) }, }
def process(self, commit): while commit: try: self.commit(commit) except Locked: unhandle, if other handlers: return except Conflict: remove and recreate, continue else: remove finally: unhandle commit find and handle next commit
def commit(self, commit): affected = commit['changes'].items() try: lock(affected) for doc, changes in affected: for k, (prev, _) in data.items(): if doc[k] != prev: raise Conflict() for doc, changes in affected: for k, (_, new) in data.items(): doc[k]=new store doc finally: unlock(self.id)
def lock(affected): for docid, _ in affected: doc = self.database.data.find_and_modify( {'_id': ObjectId(docid), '_handled_by': None}, {'$set' : {'_handled_by': self.id}}) if doc is None: raise Locked() def unlock(selfid): self.database.data.update( {'_handled_by': selfid}, {'$unset' : {'_handled_by': True}}, multi=True)
Only changes, no add or delete
No MongoDB error checking
Write changes to a separate commit collection
Apply changes to data
Handle failures
Commits may be delayed in case of lock contention
Consistency in case of failures can be improved