From f1d814cfd6dc369f103b317ddaeb4f89a2f0cf4f Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Wed, 8 Oct 2008 00:44:16 -0700 Subject: [PATCH] Now that Cherrypy session locking is gone, locking is now performed in controller.Database, but only for the SQLite backend. --- controller/Database.py | 28 +++++++++++++++++++++++++++- controller/test/Test_database.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/controller/Database.py b/controller/Database.py index bde59c7..6b2d837 100644 --- a/controller/Database.py +++ b/controller/Database.py @@ -5,6 +5,7 @@ import sys import sha import cherrypy import random +import threading from model.Persistent import Persistent from model.Notebook import Notebook @@ -18,6 +19,20 @@ class Connection_wrapper( object ): return getattr( self.connection, name ) +def synchronized( method ): + def lock( self, *args, **kwargs ): + if self.lock: + self.lock.acquire() + + try: + return method( self, *args, **kwargs ) + finally: + if self.lock: + self.lock.release() + + return lock + + class Database( object ): ID_BITS = 128 # number of bits within an id ID_DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz" @@ -93,9 +108,10 @@ class Database( object ): self.__connection = \ Connection_wrapper( sqlite.connect( data_filename, detect_types = sqlite.PARSE_DECLTYPES, check_same_thread = False ) ) - + self.__pool = None self.__backend = Persistent.SQLITE_BACKEND + self.lock = threading.Lock() # multiple simultaneous client threads make SQLite angry else: import psycopg2 as psycopg from psycopg2.pool import PersistentConnectionPool @@ -125,6 +141,7 @@ class Database( object ): ) self.__backend = Persistent.POSTGRESQL_BACKEND + self.lock = None # PostgreSQL does its own synchronization self.__cache = cache @@ -151,6 +168,7 @@ class Database( object ): except ImportError: return None + @synchronized def save( self, obj, commit = True ): """ Save the given object to the database. @@ -182,6 +200,7 @@ class Database( object ): # no commit yet, so don't touch the cache connection.pending_saves.append( obj ) + @synchronized def commit( self ): connection = self.__get_connection() connection.commit() @@ -195,6 +214,7 @@ class Database( object ): connection.pending_saves = [] + @synchronized def rollback( self ): connection = self.__get_connection() connection.rollback() @@ -230,6 +250,7 @@ class Database( object ): return obj + @synchronized def select_one( self, Object_type, sql_command, use_cache = False ): """ Execute the given sql_command and return its results in the form of an object of Object_type, @@ -274,6 +295,7 @@ class Database( object ): return obj + @synchronized def select_many( self, Object_type, sql_command ): """ Execute the given sql_command and return its results in the form of a list of objects of @@ -311,6 +333,7 @@ class Database( object ): return [ isinstance( item, str ) and unicode( item, encoding = "utf8" ) or item for item in row ] + @synchronized def execute( self, sql_command, commit = True ): """ Execute the given sql_command. @@ -328,6 +351,7 @@ class Database( object ): if commit: connection.commit() + @synchronized def execute_script( self, sql_commands, commit = True ): """ Execute the given sql_commands. @@ -374,6 +398,7 @@ class Database( object ): return "".join( digits ) + @synchronized def next_id( self, Object_type, commit = True ): """ Generate the next available object id and return it. @@ -404,6 +429,7 @@ class Database( object ): return next_id + @synchronized def close( self ): """ Shutdown the database. diff --git a/controller/test/Test_database.py b/controller/test/Test_database.py index 946d20d..ff514a1 100644 --- a/controller/test/Test_database.py +++ b/controller/test/Test_database.py @@ -1,4 +1,5 @@ from pytz import utc +from threading import Thread from pysqlite2 import dbapi2 as sqlite from datetime import datetime from Stub_object import Stub_object @@ -10,7 +11,7 @@ from controller.Database import Database, Connection_wrapper class Test_database( object ): def setUp( self ): # make an in-memory sqlite database to use in place of PostgreSQL during testing - self.connection = Connection_wrapper( sqlite.connect( ":memory:", detect_types = sqlite.PARSE_DECLTYPES ) ) + self.connection = Connection_wrapper( sqlite.connect( ":memory:", detect_types = sqlite.PARSE_DECLTYPES, check_same_thread = False ) ) self.cache = Stub_cache() cursor = self.connection.cursor() cursor.execute( Stub_object.sql_create_table() ) @@ -190,5 +191,31 @@ class Test_database( object ): assert next_id assert self.database.load( Stub_object, next_id ) + def test_synchronize( self ): + def make_objects(): + for i in range( 50 ): + object_id = self.database.next_id( Stub_object ) + basic_obj = Stub_object( object_id, value = 1 ) + original_revision = basic_obj.revision + + self.database.execute( basic_obj.sql_create() ) + obj = self.database.load( Stub_object, basic_obj.object_id ) + + assert obj.object_id == basic_obj.object_id + assert obj.revision.replace( tzinfo = utc ) == original_revision + assert obj.value == basic_obj.value + + object_id = self.database.next_id( Stub_object ) + + # if synchronization (locking) is working properly, then these two threads should be able to run + # simultaneously without error. without locking, SQLite will raise + thread1 = Thread( target = make_objects ) + thread2 = Thread( target = make_objects ) + thread1.start() + thread2.start() + + thread1.join() + thread2.join() + def test_backend( self ): assert self.database.backend == Persistent.SQLITE_BACKEND