Skip to content

Commit

Permalink
ENHANCEMENT Added Database->getLock() and Database->releaseLock() for…
Browse files Browse the repository at this point in the history
… application-level advisory locks
  • Loading branch information
chillu committed Oct 7, 2011
1 parent 67568b0 commit 8302af1
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 0 deletions.
53 changes: 53 additions & 0 deletions model/Database.php
Expand Up @@ -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;
}
}

/**
Expand Down
28 changes: 28 additions & 0 deletions model/MySQLDatabase.php
Expand Up @@ -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));
}
}

/**
Expand Down
40 changes: 40 additions & 0 deletions tests/model/DatabaseTest.php
Expand Up @@ -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 {
Expand Down

0 comments on commit 8302af1

Please sign in to comment.