From 8302af1ea8e31e5435f5543c1d21d151f3638e8e Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 22 Sep 2011 16:28:58 +0200 Subject: [PATCH] ENHANCEMENT Added Database->getLock() and Database->releaseLock() for application-level advisory locks --- model/Database.php | 53 ++++++++++++++++++++++++++++++++++++ model/MySQLDatabase.php | 28 +++++++++++++++++++ tests/model/DatabaseTest.php | 40 +++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/model/Database.php b/model/Database.php index dd47efcd786..8d443ebb8d3 100644 --- a/model/Database.php +++ b/model/Database.php @@ -828,6 +828,59 @@ abstract function transactionRollback($savepoint=false); * Commit everything inside this transaction so far */ abstract function transactionEnd(); + + /** + * Determines if the used database supports application-level locks, + * which is different from table- or row-level locking. + * See {@link getLock()} for details. + * + * @return boolean + */ + function supportsLocks() { + return false; + } + + /** + * Returns if the lock is available. + * See {@link supportsLocks()} to check if locking is generally supported. + * + * @return Boolean + */ + function canLock($name) { + return false; + } + + /** + * Sets an application-level lock so that no two processes can run at the same time, + * also called a "cooperative advisory lock". + * + * Return FALSE if acquiring the lock fails; otherwise return TRUE, if lock was acquired successfully. + * Lock is automatically released if connection to the database is broken (either normally or abnormally), + * making it less prone to deadlocks than session- or file-based locks. + * Should be accompanied by a {@link releaseLock()} call after the logic requiring the lock has completed. + * Can be called multiple times, in which case locks "stack" (PostgreSQL, SQL Server), + * or auto-releases the previous lock (MySQL). + * + * Note that this might trigger the database to wait for the lock to be released, delaying further execution. + * + * @param String + * @param Int Timeout in seconds + * @return Boolean + */ + function getLock($name, $timeout = 5) { + return false; + } + + /** + * Remove an application-level lock file to allow another process to run + * (if the execution aborts (e.g. due to an error) all locks are automatically released). + * + * @param String + * @return Boolean + */ + function releaseLock($name) { + return false; + } } /** diff --git a/model/MySQLDatabase.php b/model/MySQLDatabase.php index 9238776c989..ed85f8344ae 100644 --- a/model/MySQLDatabase.php +++ b/model/MySQLDatabase.php @@ -1039,6 +1039,34 @@ function datetimeDifferenceClause($date1, $date2) { return "UNIX_TIMESTAMP($date1) - UNIX_TIMESTAMP($date2)"; } + + function supportsLocks() { + return true; + } + + function canLock($name) { + $id = $this->getLockIdentifier($name); + return (bool)DB::query(sprintf("SELECT IS_FREE_LOCK('%s')", $id))->value(); + } + + function getLock($name, $timeout = 5) { + $id = $this->getLockIdentifier($name); + + // MySQL auto-releases existing locks on subsequent GET_LOCK() calls, + // in contrast to PostgreSQL and SQL Server who stack the locks. + + return (bool)DB::query(sprintf("SELECT GET_LOCK('%s', %d)", $id, $timeout))->value(); + } + + function releaseLock($name) { + $id = $this->getLockIdentifier($name); + return (bool)DB::query(sprintf("SELECT RELEASE_LOCK('%s')", $id))->value(); + } + + protected function getLockIdentifier($name) { + // Prefix with database name + return Convert::raw2sql($this->database . '_' . Convert::raw2sql($name)); + } } /** diff --git a/tests/model/DatabaseTest.php b/tests/model/DatabaseTest.php index 7728c723438..e40b831d358 100644 --- a/tests/model/DatabaseTest.php +++ b/tests/model/DatabaseTest.php @@ -82,7 +82,47 @@ function testHasTable() { $this->assertTrue(DB::getConn()->hasTable('DatabaseTest_MyObject')); $this->assertFalse(DB::getConn()->hasTable('asdfasdfasdf')); } + + function testGetAndReleaseLock() { + $db = DB::getConn(); + + if(!$db->supportsLocks()) { + return $this->markTestSkipped('Tested database doesn\'t support application locks'); + } + $this->assertTrue($db->getLock('DatabaseTest'), 'Can aquire lock'); + // $this->assertFalse($db->getLock('DatabaseTest'), 'Can\'t repeatedly aquire the same lock'); + $this->assertTrue($db->getLock('DatabaseTest'), 'The same lock can be aquired multiple times in the same connection'); + + $this->assertTrue($db->getLock('DatabaseTestOtherLock'), 'Can aquire different lock'); + $db->releaseLock('DatabaseTestOtherLock'); + + // Release potentially stacked locks from previous getLock() invocations + $db->releaseLock('DatabaseTest'); + $db->releaseLock('DatabaseTest'); + + $this->assertTrue($db->getLock('DatabaseTest'), 'Can aquire lock after releasing it'); + $db->releaseLock('DatabaseTest'); + } + + function testCanLock() { + $db = DB::getConn(); + + if(!$db->supportsLocks()) { + return $this->markTestSkipped('Database doesn\'t support locks'); + } + + if($db instanceof MSSQLDatabase) { + return $this->markTestSkipped('MSSQLDatabase doesn\'t support inspecting locks'); + } + + $this->assertTrue($db->canLock('DatabaseTest'), 'Can lock before first aquiring one'); + $db->getLock('DatabaseTest'); + $this->assertFalse($db->canLock('DatabaseTest'), 'Can\'t lock after aquiring one'); + $db->releaseLock('DatabaseTest'); + $this->assertTrue($db->canLock('DatabaseTest'), 'Can lock again after releasing it'); + } + } class DatabaseTest_MyObject extends DataObject implements TestOnly {