ENHANCEMENT Added Database->getLock() and Database->releaseLock() for application-level advisory locks

This commit is contained in:
Ingo Schommer 2011-09-22 16:28:58 +02:00
parent 67568b08a3
commit 8302af1ea8
3 changed files with 121 additions and 0 deletions

View File

@ -828,6 +828,59 @@ abstract class SS_Database {
* 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;
}
}
/**

View File

@ -1039,6 +1039,34 @@ class MySQLDatabase extends SS_Database {
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));
}
}
/**

View File

@ -82,7 +82,47 @@ class DatabaseTest extends SapphireTest {
$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 {