Commit 7388171b authored by catch's avatar catch

Issue #1477446 by pounard, amateescu: Move lock backend to PSR-0 code.

parent 86eb32d3
...@@ -2270,8 +2270,7 @@ function _drupal_bootstrap_variables() { ...@@ -2270,8 +2270,7 @@ function _drupal_bootstrap_variables() {
global $conf; global $conf;
// Initialize the lock system. // Initialize the lock system.
require_once DRUPAL_ROOT . '/' . variable_get('lock_inc', 'core/includes/lock.inc'); require_once DRUPAL_ROOT . '/core/includes/lock.inc';
lock_initialize();
// Load variables from the database, but do not overwrite variables set in settings.php. // Load variables from the database, but do not overwrite variables set in settings.php.
$conf = variable_initialize(isset($conf) ? $conf : array()); $conf = variable_initialize(isset($conf) ? $conf : array());
......
...@@ -299,6 +299,16 @@ function install_begin_request(&$install_state) { ...@@ -299,6 +299,16 @@ function install_begin_request(&$install_state) {
require_once DRUPAL_ROOT . '/core/includes/cache.inc'; require_once DRUPAL_ROOT . '/core/includes/cache.inc';
$conf['cache_default_class'] = 'Drupal\Core\Cache\InstallBackend'; $conf['cache_default_class'] = 'Drupal\Core\Cache\InstallBackend';
// The install process cannot use the database lock backend since the database
// is not fully up, so we use a null backend implementation durting the
// installation process. This will also speed up the installation process.
// The site being installed will use the real lock backend when doing AJAX
// requests but, except for a WSOD, there is no chance for a a lock to stall
// (as opposed to the cache backend) so we can afford having a null
// implementation here.
require_once DRUPAL_ROOT . '/core/includes/lock.inc';
$conf['lock_backend'] = 'Drupal\Core\Lock\NullLockBackend';
// Prepare for themed output. We need to run this at the beginning of the // Prepare for themed output. We need to run this at the beginning of the
// page request to avoid a different theme accidentally getting set. (We also // page request to avoid a different theme accidentally getting set. (We also
// need to run it even in the case of command-line installations, to prevent // need to run it even in the case of command-line installations, to prevent
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/** /**
* @file * @file
* A database-mediated implementation of a locking mechanism. * Drupal lock framework procedural proxy implementation.
*/ */
/** /**
...@@ -53,39 +53,38 @@ ...@@ -53,39 +53,38 @@
* *
* lock_acquire() and lock_wait() will automatically break (delete) a lock * lock_acquire() and lock_wait() will automatically break (delete) a lock
* whose duration has exceeded the timeout specified when it was acquired. * whose duration has exceeded the timeout specified when it was acquired.
*
* Alternative implementations of this API (such as APC) may be substituted
* by setting the 'lock_inc' variable to an alternate include filepath. Since
* this is an API intended to support alternative implementations, code using
* this API should never rely upon specific implementation details (for example
* no code should look for or directly modify a lock in the {semaphore} table).
*/ */
/** use Drupal\Core\Lock\DatabaseLockBackend;
* Initialize the locking system. use Drupal\Core\Lock\LockBackendInterface;
*/
function lock_initialize() {
global $locks;
$locks = array();
}
/** /**
* Helper function to get this request's unique id. * Get locking layer instance.
*
* @return Drupal\Core\Lock\LockBackendInterface
*/ */
function _lock_id() { function lock() {
// Do not use drupal_static(). This identifier refers to the current $lock_backend = &drupal_static(__FUNCTION__);
// client request, and must not be changed under any circumstances
// else the shutdown handler may fail to release our locks. if (!isset($lock_backend)) {
static $lock_id; $class_name = variable_get('lock_backend', 'Drupal\Core\Lock\DatabaseLockBackend');
// Do not allow a WSOD here, if the class does not exists use the default
// one.
// @todo: We should log failed class loading for debugging, but for that we
// need an early watchdog function that logs into a file if the database is
// not present.
if (class_exists($class_name)) {
$lock_backend = new $class_name();
}
else {
$lock_backend = new DatabaseLockBackend();
}
if (!isset($lock_id)) { drupal_register_shutdown_function(array($lock_backend, 'releaseAll'));
// Assign a unique id.
$lock_id = uniqid(mt_rand(), TRUE);
// We only register a shutdown function if a lock is used.
drupal_register_shutdown_function('lock_release_all', $lock_id);
} }
return $lock_id;
return $lock_backend;
} }
/** /**
...@@ -98,89 +97,11 @@ function _lock_id() { ...@@ -98,89 +97,11 @@ function _lock_id() {
* *
* @return * @return
* TRUE if the lock was acquired, FALSE if it failed. * TRUE if the lock was acquired, FALSE if it failed.
*/
function lock_acquire($name, $timeout = 30.0) {
global $locks;
// Insure that the timeout is at least 1 ms.
$timeout = max($timeout, 0.001);
$expire = microtime(TRUE) + $timeout;
if (isset($locks[$name])) {
// Try to extend the expiration of a lock we already acquired.
$success = (bool) db_update('semaphore')
->fields(array('expire' => $expire))
->condition('name', $name)
->condition('value', _lock_id())
->execute();
if (!$success) {
// The lock was broken.
unset($locks[$name]);
}
return $success;
}
else {
// Optimistically try to acquire the lock, then retry once if it fails.
// The first time through the loop cannot be a retry.
$retry = FALSE;
// We always want to do this code at least once.
do {
try {
db_insert('semaphore')
->fields(array(
'name' => $name,
'value' => _lock_id(),
'expire' => $expire,
))
->execute();
// We track all acquired locks in the global variable.
$locks[$name] = TRUE;
// We never need to try again.
$retry = FALSE;
}
catch (PDOException $e) {
// Suppress the error. If this is our first pass through the loop,
// then $retry is FALSE. In this case, the insert must have failed
// meaning some other request acquired the lock but did not release it.
// We decide whether to retry by checking lock_may_be_available()
// Since this will break the lock in case it is expired.
$retry = $retry ? FALSE : lock_may_be_available($name);
}
// We only retry in case the first attempt failed, but we then broke
// an expired lock.
} while ($retry);
}
return isset($locks[$name]);
}
/**
* Check if lock acquired by a different process may be available.
* *
* If an existing lock has expired, it is removed. * @deprecated
*
* @param $name
* The name of the lock.
*
* @return
* TRUE if there is no lock or it was removed, FALSE otherwise.
*/ */
function lock_may_be_available($name) { function lock_acquire($name, $timeout = 30.0) {
$lock = db_query('SELECT expire, value FROM {semaphore} WHERE name = :name', array(':name' => $name))->fetchAssoc(); return lock()->acquire($name, $timeout);
if (!$lock) {
return TRUE;
}
$expire = (float) $lock['expire'];
$now = microtime(TRUE);
if ($now > $expire) {
// We check two conditions to prevent a race condition where another
// request acquired the lock and set a new expire time. We add a small
// number to $expire to avoid errors with float to string conversion.
return (bool) db_delete('semaphore')
->condition('name', $name)
->condition('value', $lock['value'])
->condition('expire', 0.0001 + $expire, '<=')
->execute();
}
return FALSE;
} }
/** /**
...@@ -199,41 +120,11 @@ function lock_may_be_available($name) { ...@@ -199,41 +120,11 @@ function lock_may_be_available($name) {
* *
* @return * @return
* TRUE if the lock holds, FALSE if it is available. * TRUE if the lock holds, FALSE if it is available.
*
* @deprecated
*/ */
function lock_wait($name, $delay = 30) { function lock_wait($name, $delay = 30) {
// Pause the process for short periods between calling return lock()->wait($name, $delay);
// lock_may_be_available(). This prevents hitting the database with constant
// database queries while waiting, which could lead to performance issues.
// However, if the wait period is too long, there is the potential for a
// large number of processes to be blocked waiting for a lock, especially
// if the item being rebuilt is commonly requested. To address both of these
// concerns, begin waiting for 25ms, then add 25ms to the wait period each
// time until it reaches 500ms. After this point polling will continue every
// 500ms until $delay is reached.
// $delay is passed in seconds, but we will be using usleep(), which takes
// microseconds as a parameter. Multiply it by 1 million so that all
// further numbers are equivalent.
$delay = (int) $delay * 1000000;
// Begin sleeping at 25ms.
$sleep = 25000;
while ($delay > 0) {
// This function should only be called by a request that failed to get a
// lock, so we sleep first to give the parallel request a chance to finish
// and release the lock.
usleep($sleep);
// After each sleep, increase the value of $sleep until it reaches
// 500ms, to reduce the potential for a lock stampede.
$delay = $delay - $sleep;
$sleep = min(500000, $sleep + 25000, $delay);
if (lock_may_be_available($name)) {
// No longer need to wait.
return FALSE;
}
}
// The caller must still wait longer to get the lock.
return TRUE;
} }
/** /**
...@@ -243,30 +134,20 @@ function lock_wait($name, $delay = 30) { ...@@ -243,30 +134,20 @@ function lock_wait($name, $delay = 30) {
* *
* @param $name * @param $name
* The name of the lock. * The name of the lock.
*
* @deprecated
*/ */
function lock_release($name) { function lock_release($name) {
global $locks; lock()->release($name);
unset($locks[$name]);
db_delete('semaphore')
->condition('name', $name)
->condition('value', _lock_id())
->execute();
} }
/** /**
* Release all previously acquired locks. * Release all previously acquired locks.
*
* @deprecated
*/ */
function lock_release_all($lock_id = NULL) { function lock_release_all($lock_id = NULL) {
global $locks; lock()->releaseAll($lock_id);
$locks = array();
if (empty($lock_id)) {
$lock_id = _lock_id();
}
db_delete('semaphore')
->condition('value', $lock_id)
->execute();
} }
/** /**
......
<?php
/**
* @file
* Definition of Drupal\Core\Lock\DatabaseLockBackend.
*/
namespace Drupal\Core\Lock;
use PDOException;
/**
* Defines the database lock backend. This is the default backend in Drupal.
*/
class DatabaseLockBackend extends LockBackendAbstract {
/**
* Implements Drupal\Core\Lock\LockBackedInterface::acquire().
*/
public function acquire($name, $timeout = 30.0) {
// Insure that the timeout is at least 1 ms.
$timeout = max($timeout, 0.001);
$expire = microtime(TRUE) + $timeout;
if (isset($this->locks[$name])) {
// Try to extend the expiration of a lock we already acquired.
$success = (bool) db_update('semaphore')
->fields(array('expire' => $expire))
->condition('name', $name)
->condition('value', $this->getLockId())
->execute();
if (!$success) {
// The lock was broken.
unset($this->locks[$name]);
}
return $success;
}
else {
// Optimistically try to acquire the lock, then retry once if it fails.
// The first time through the loop cannot be a retry.
$retry = FALSE;
// We always want to do this code at least once.
do {
try {
db_insert('semaphore')
->fields(array(
'name' => $name,
'value' => $this->getLockId(),
'expire' => $expire,
))
->execute();
// We track all acquired locks in the global variable.
$this->locks[$name] = TRUE;
// We never need to try again.
$retry = FALSE;
}
catch (PDOException $e) {
// Suppress the error. If this is our first pass through the loop,
// then $retry is FALSE. In this case, the insert must have failed
// meaning some other request acquired the lock but did not release it.
// We decide whether to retry by checking lock_may_be_available()
// Since this will break the lock in case it is expired.
$retry = $retry ? FALSE : $this->lockMayBeAvailable($name);
}
// We only retry in case the first attempt failed, but we then broke
// an expired lock.
} while ($retry);
}
return isset($this->locks[$name]);
}
/**
* Implements Drupal\Core\Lock\LockBackedInterface::lockMayBeAvailable().
*/
public function lockMayBeAvailable($name) {
$lock = db_query('SELECT expire, value FROM {semaphore} WHERE name = :name', array(':name' => $name))->fetchAssoc();
if (!$lock) {
return TRUE;
}
$expire = (float) $lock['expire'];
$now = microtime(TRUE);
if ($now > $expire) {
// We check two conditions to prevent a race condition where another
// request acquired the lock and set a new expire time. We add a small
// number to $expire to avoid errors with float to string conversion.
return (bool) db_delete('semaphore')
->condition('name', $name)
->condition('value', $lock['value'])
->condition('expire', 0.0001 + $expire, '<=')
->execute();
}
return FALSE;
}
/**
* Implements Drupal\Core\Lock\LockBackedInterface::release().
*/
public function release($name) {
unset($this->locks[$name]);
db_delete('semaphore')
->condition('name', $name)
->condition('value', $this->getLockId())
->execute();
}
/**
* Implements Drupal\Core\Lock\LockBackedInterface::releaseAll().
*/
public function releaseAll($lock_id = NULL) {
$this->locks = array();
if (empty($lock_id)) {
$lock_id = $this->getLockId();
}
db_delete('semaphore')
->condition('value', $lock_id)
->execute();
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Lock\LockBackendAbstract.
*/
namespace Drupal\Core\Lock;
/**
* Non backend related common methods implementation for lock backends.
*/
abstract class LockBackendAbstract implements LockBackendInterface {
/**
* Current page lock token identifier.
*
* @var string
*/
protected $lockId;
/**
* Existing locks for this page.
*
* @var array
*/
protected $locks = array();
/**
* Implements Drupal\Core\Lock\LockBackedInterface::wait().
*/
public function wait($name, $delay = 30) {
// Pause the process for short periods between calling
// lock_may_be_available(). This prevents hitting the database with constant
// database queries while waiting, which could lead to performance issues.
// However, if the wait period is too long, there is the potential for a
// large number of processes to be blocked waiting for a lock, especially
// if the item being rebuilt is commonly requested. To address both of these
// concerns, begin waiting for 25ms, then add 25ms to the wait period each
// time until it reaches 500ms. After this point polling will continue every
// 500ms until $delay is reached.
// $delay is passed in seconds, but we will be using usleep(), which takes
// microseconds as a parameter. Multiply it by 1 million so that all
// further numbers are equivalent.
$delay = (int) $delay * 1000000;
// Begin sleeping at 25ms.
$sleep = 25000;
while ($delay > 0) {
// This function should only be called by a request that failed to get a
// lock, so we sleep first to give the parallel request a chance to finish
// and release the lock.
usleep($sleep);
// After each sleep, increase the value of $sleep until it reaches
// 500ms, to reduce the potential for a lock stampede.
$delay = $delay - $sleep;
$sleep = min(500000, $sleep + 25000, $delay);
if ($this->lockMayBeAvailable($name)) {
// No longer need to wait.
return FALSE;
}
}
// The caller must still wait longer to get the lock.
return TRUE;
}
/**
* Implements Drupal\Core\Lock\LockBackedInterface::getLockId().
*/
public function getLockId() {
if (!isset($this->lockId)) {
$this->lockId = uniqid(mt_rand(), TRUE);
}
return $this->lockId;
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Lock\LockBackendInterface.
*/
namespace Drupal\Core\Lock;
/**
* Lock backend interface.
*/
interface LockBackendInterface {
/**
* Acquires a lock.
*
* @param string $name
* Lock name.
* @param float $timeout = 30.0
* (optional) Lock lifetime in seconds.
*
* @return bool
*/
public function acquire($name, $timeout = 30.0);
/**
* Checks if a lock is available for acquiring.
*
* @param string $name
* Lock to acquire.
*
* @return bool
*/
public function lockMayBeAvailable($name);
/**
* Waits a short amount of time before a second lock acquire attempt.
*
* While this method is subject to have a generic implementation in abstract
* backend implementation, some backends may provide non blocking or less I/O
* intensive wait mecanism: this is why this method remains on the backend
* interface.
*
* @param string $name
* Lock name currently being locked.
* @param int $delay = 30
* Miliseconds to wait for.
*
* @return bool
* TRUE if the wait operation was successful and lock may be available. You
* still need to acquire the lock manually and it may fail again.
*/
public function wait($name, $delay = 30);
/**
* Releases the given lock.
*
* @param string $name
*/
public function release($name);
/**
* Releases all locks for the given lock token identifier.
*
* @param string $lockId
* (optional) If none given, remove all locks from the current page.
* Defaults to NULL.
*/
public function releaseAll($lockId = NULL);
/**
* Gets the unique page token for locks. Locks will be wipeout at each end of
* page request on a token basis.
*
* @return string
*/
public function getLockId();
}
<?php
/**
* @file
* Definition of Drupal\Core\Lock\NullLockBackend.
*/
namespace Drupal\Core\Lock;
/**
* Defines a Null lock backend.
*
* This implementation won't actually lock anything and will always succeed on
* lock attempts.
*/
class NullLockBackend implements LockBackendInterface {
/**
* Current page lock token identifier.
*
* @var string
*/
protected $lockId;
/**
* Implements Drupal\Core\Lock\LockBackedInterface::acquire().
*/
public function acquire($name, $timeout = 30.0) {
return TRUE;
}
/**
* Implements Drupal\Core\Lock\LockBackedInterface::lockMayBeAvailable().
*/
public function lockMayBeAvailable($name) {
return TRUE;
}
/**
* Implements Drupal\Core\Lock\LockBackedInterface::wait().
*/
public function wait($name, $delay = 30) {}
/**
* Implements Drupal\Core\Lock\LockBackedInterface::release().
*/
public function release($name) {}
/**
* Implements Drupal\Core\Lock\LockBackedInterface::releaseAll().
*/
public function releaseAll($lock_id = NULL) {}
/**
* Implements Drupal\Core\Lock\LockBackedInterface::getLockId().
*/
public function getLockId() {
if (!isset($this->lockId)) {
$this->lockId = uniqid(mt_rand(), TRUE);
}
return $this->lockId;