Skip to content
Snippets Groups Projects
Commit c387dc83 authored by Prem Suthar's avatar Prem Suthar Committed by Alberto Paderno
Browse files

Issue #2332725: Lock backend

parent 021ec04e
No related branches found
No related tags found
1 merge request!60Issue #2332725: Lock backend
Pipeline #263412 passed
......@@ -4,3 +4,4 @@ package = Performance and scalability
core = 7.x
files[] = apc.test
files[] = drupal_apc_cache.inc
files[] = drupal_apc_lock.inc
......@@ -5,15 +5,17 @@
* Test classes for the Alternative PHP Cache module.
*/
// phpcs:disable SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator.NullCoalesceOperatorNotUsed
/**
* Methods and properties for all the test classes that write directly to APCu.
*/
trait ApcTestTrait {
trait ApcApcuTestTrait {
/**
* Whether the test should be skipped.
*
* ApcTestTrait::checkRequirements() will set this property to TRUE if the
* ApcApcuTestTrait::checkRequirements() will set this property to TRUE if the
* requirements for the test to run are not met.
*
* @var bool
......@@ -295,14 +297,18 @@ trait ApcTestTrait {
* @param string $key
* The prefix to search for in APCu keys, excluding the regular expression
* delimiters and the ^ anchor.
* @param array $options
* An array of options, which can contain the following keys:
* - message: A custom message to display upon assertion.
*
* @return bool
* TRUE if there are APCu keys matching the prefix, FALSE otherwise.
*/
protected function assertApcuKeysByPrefix($key) {
protected function assertApcuKeysByPrefix($key, $options = array()) {
$escaped_key = preg_quote($key, '/');
$iterator = new APCUIterator("/^$escaped_key/", APC_ITER_KEY);
$message = format_string('There are APCu keys matching @key.', array('@key' => $this->varExport("/^$escaped_key/")));
$message = isset($options['message']) ? $options['message'] : 'There are APCu keys matching @key.';
$message = format_string($message, array('@key' => $this->varExport("/^$escaped_key/")));
return $this->assertTrue($iterator->getTotalCount() > 0, $message);
}
......@@ -313,14 +319,18 @@ trait ApcTestTrait {
* @param string $key
* The prefix to search for in APCu keys, excluding the regular expression
* delimiters and the ^ anchor.
* @param array $options
* An array of options, which can contain the following keys:
* - message: A custom message to display upon assertion.
*
* @return bool
* TRUE if there are no APCu keys matching the prefix, FALSE otherwise.
*/
protected function assertNoApcuKeysByPrefix($key) {
protected function assertNoApcuKeysByPrefix($key, $options = array()) {
$escaped_key = preg_quote($key, '/');
$iterator = new APCUIterator("/^$escaped_key/", APC_ITER_KEY);
$message = format_string('There are no APCu keys matching @key.', array('@key' => $this->varExport("/^$escaped_key/")));
$message = isset($options['message']) ? $options['message'] : 'There are no APCu keys matching @key.';
$message = format_string($message, array('@key' => $this->varExport("/^$escaped_key/")));
return $this->assertTrue($iterator->getTotalCount() === 0, $message);
}
......@@ -330,9 +340,9 @@ trait ApcTestTrait {
/**
* Base class for the test classes that write directly to APCu.
*/
class ApcBaseTestCase extends DrupalUnitTestCase {
class ApcApcuBaseTestCase extends DrupalUnitTestCase {
use ApcTestTrait;
use ApcApcuTestTrait;
/**
* {@inheritdoc}
......@@ -355,7 +365,7 @@ class ApcBaseTestCase extends DrupalUnitTestCase {
/**
* Unit tests for reading and writing the APCu storage.
*/
class ApcApcuStoreAndRetrieveTestCase extends ApcBaseTestCase {
class ApcApcuStoreAndRetrieveTestCase extends ApcApcuBaseTestCase {
/**
* {@inheritdoc}
......@@ -419,9 +429,8 @@ class ApcApcuStoreAndRetrieveTestCase extends ApcBaseTestCase {
$message = 'New items were stored in APCu.';
if ($this->assertTrue(!empty($stored), $message)) {
// Increase the TTL set by ApcTestTrait::storageData() by 2 seconds to
// be sure that non-permanent values are purged when the APCU storage is
// read.
// Halt the tests for 10 seconds, which is 2 seconds more than the
// maximum TTL set by ApcApcuTestTrait::storageData().
$message = format_string('The test will be halted for @delay seconds.', array('@delay' => 10));
$persistent_key_message = '@key persists and it was found.';
$purgeable_key_message = '@key was purgeable and it was not found.';
......@@ -454,6 +463,8 @@ class ApcApcuStoreAndRetrieveTestCase extends ApcBaseTestCase {
*/
trait ApcCacheTestTrait {
use ApcApcuTestTrait;
/**
* The cache bins to store on APCu.
*
......@@ -692,7 +703,6 @@ trait ApcCacheTestTrait {
*/
class ApcCacheBaseTestCase extends DrupalUnitTestCase {
use ApcTestTrait;
use ApcCacheTestTrait;
/**
......@@ -703,7 +713,7 @@ class ApcCacheBaseTestCase extends DrupalUnitTestCase {
parent::setUp();
$conf['cache_backends'][] = drupal_get_path('module', 'apc') . '/' . 'drupal_apc_cache.inc';
$conf['cache_backends'][] = drupal_get_path('module', 'apc') . '/drupal_apc_cache.inc';
foreach ($this->getCacheBins() as $cache_bin) {
$conf["cache_class_$cache_bin"] = 'DrupalApcCache';
......@@ -739,7 +749,6 @@ class ApcCacheBaseTestCase extends DrupalUnitTestCase {
*/
class ApcCacheClearTestCase extends ApcCacheBaseTestCase {
use ApcTestTrait;
use ApcCacheTestTrait;
/**
......@@ -900,7 +909,6 @@ class ApcCacheClearTestCase extends ApcCacheBaseTestCase {
*/
class ApcCacheConflictingKeysTestCase extends ApcCacheBaseTestCase {
use ApcTestTrait;
use ApcCacheTestTrait;
/**
......@@ -1150,8 +1158,9 @@ class ApcCacheStoreAndRetrieveTestCase extends ApcCacheBaseTestCase {
$message = 'New items were stored in the cache.';
if ($this->assertTrue(!empty($stored), $message)) {
// Add 2 seconds to the maximum TTL ApcTestTrait::storageData() sets to
// be sure that non-permanent values are purged when the cache is read.
// Add 2 seconds to the maximum TTL ApcApcuTestTrait::storageData() sets
// to be sure that non-permanent values are purged when the cache is
// read.
$message = format_string('The test will be halted for @delay seconds.', array('@delay' => 10));
$this->pass($message);
......@@ -1186,8 +1195,8 @@ class ApcCacheStoreAndRetrieveTestCase extends ApcCacheBaseTestCase {
foreach ($this->storageData() as $id => $data) {
$cid = "store_retrieve_cache_item_$id";
// The TTL set by ApcTestTrait::storageData() is ignored. All the cache
// items are permanent.
// The TTL set by ApcApcuTestTrait::storageData() is ignored. All the
// cache items are permanent.
if ($this->assertCacheItemSaved($bin, $cid, $data['value'])) {
$stored[] = $cid;
}
......@@ -1227,10 +1236,10 @@ class ApcCacheStoreAndRetrieveTestCase extends ApcCacheBaseTestCase {
$message = 'New items were stored in the cache.';
if ($this->assertTrue(!empty($stored), $message)) {
// 12 is the maximum TTL set by ApcTestTrait::storageData(), plus 2 to
// change the 0 TTL (which means a permanent cache item), plus 2 to be
// sure that all the cached items are purged when cache_get_multiple()
// is called.
// 12 is the maximum TTL set by ApcApcuTestTrait::storageData(), plus 2
// to make the permanent cache items ApcApcuTestTrait::storageData()
// returns purgeable, plus 2 to be sure that all the cached items are
// purged when cache_get_multiple() is called.
$message = format_string('The test will be halted for @delay seconds.', array('@delay' => 12));
$this->pass($message);
......
<?php
/**
* @file
* An APCU-based implementation of a locking mechanism.
*
* @see includes/lock.inc
*/
/**
* Class for the APCu-based locking mechanism implementation.
*
* @internal
*/
class ApcLock {
/**
* The locks acquired in the current request.
*
* @var array
*/
protected static $locks = array();
/**
* The unique ID used to generate the APCu keys for locks.
*
* @var string
*/
protected static $uniqueId = '';
/**
* Initializes the locking system.
*/
public static function initialize() {
self::$uniqueId = drupal_random_bytes(32);
}
/**
* Acquires (or renews) a lock, but it does not block if it fails.
*
* @param string $name
* The name of the lock.
* @param float $timeout
* The number of seconds before the lock expires. The minimum timeout is 1.
*
* @return bool
* TRUE if the lock was acquired, FALSE otherwise.
*/
public static function acquire($name, $timeout = 30.0) {
// Ensure that the timeout is at least one second. APCu works with integer
// TTLs where 0 means the value is persistent.
$timeout = (int) round(max($timeout, 1.0));
$key = self::getLockKey($name);
$expire = microtime(TRUE) + $timeout;
if (isset(self::$locks[$name])) {
// Try to extend the expiration of a lock we already acquired.
$success = apcu_store($key, array('expire' => $expire), $timeout);
if (!$success) {
// The lock was broken.
unset(self::$locks[$name]);
apcu_delete($key);
}
return $success;
}
else {
// Try to acquire the lock.
$retry = FALSE;
do {
$success = apcu_store($key, array('expire' => $expire), $timeout);
if ($success) {
self::$locks[$name] = TRUE;
}
else {
$retry = !$retry && self::maybeAvailable($name);
}
} while ($retry);
}
return isset(self::$locks[$name]);
}
/**
* Checks if a lock acquired by a different process is available.
*
* If an existing lock has expired, it is removed.
*
* @param string $name
* The name of the lock.
*
* @return bool
* TRUE if there is no lock, or it was removed, FALSE otherwise.
*/
public static function maybeAvailable($name) {
$key = self::getLockKey($name);
if (!apcu_exists($key)) {
return TRUE;
}
$lock = apcu_fetch($key, $success);
if (!$success) {
return TRUE;
}
$expire = (float) $lock['expire'];
$now = microtime(TRUE);
if ($now >= $expire) {
apcu_delete($key);
return TRUE;
}
return FALSE;
}
/**
* Waits for a lock to be available.
*
* @param string $name
* The name of the lock.
* @param float $delay
* The maximum number of seconds to wait, as an integer.
*
* @return bool
* FALSE if the lock is available, TRUE otherwise.
*/
public static function waitForLock($name, $delay = 30.0) {
// Pause the process for short periods between calling
// lock_may_be_available(). 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 25
// ms to the wait period each time until it reaches 500 ms. After this
// point, polling will continue every 500 ms 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) round($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 500 ms,
// to reduce the potential for a lock stampede.
$delay = $delay - $sleep;
$sleep = min(500000, $sleep + 25000, $delay);
if (!apcu_exists(self::getLockKey($name))) {
// No longer need to wait.
return FALSE;
}
}
// The caller must still wait longer to get the lock.
return TRUE;
}
/**
* Releases a lock previously acquired by lock_acquire().
*
* This will release the named lock if it is still held by the current
* request.
*
* @param string $name
* The name of the lock.
*/
public static function release($name) {
// The lock is unconditionally removed, since the caller assumes the lock is
// anyway released.
unset(self::$locks[$name]);
apcu_delete(self::getLockKey($name));
}
/**
* Gets the name of the APCu key used to store a lock.
*
* @param string $name
* The name of the lock.
*
* @return string
* The APCu key name.
*/
public static function getLockKey($name) {
// self::$uniqueId is already initialized from ApcLock::initialize(). The
// following lines are added as safeguard, just in case
// ApcLock::initialize() is not called as expected. This should never happen
// because Drupal core calls lock_initialize() in
// _drupal_bootstrap_variables() and lock_initialize() implemented in this
// file is a wrapper for ApcLock::initialize().
if (empty(self::$uniqueId)) {
self::$uniqueId = drupal_random_bytes(32);
}
$hash = hash('sha256', self::$uniqueId . $name, TRUE);
return 'apc_lock::' . drupal_base64_encode($hash);
}
}
/**
* Initializes the locking system.
*
* @see ApcLock::initialize()
*/
function lock_initialize() {
ApcLock::initialize();
}
/**
* Acquires (or renews) a lock, but it does not block if it fails.
*
* @param string $name
* The name of the lock.
* @param float $timeout
* The number of seconds before the lock expires. The minimum timeout is 1.
*
* @return bool
* TRUE if the lock was acquired, FALSE otherwise.
*
* @see ApcLock::acquire()
*/
function lock_acquire($name, $timeout = 30.0) {
return ApcLock::acquire($name, $timeout);
}
/**
* Checks if a lock acquired by a different process is available.
*
* If an existing lock has expired, it is removed.
*
* @param string $name
* The name of the lock.
*
* @return bool
* TRUE if there is no lock, or it was removed, FALSE otherwise.
*
* @see ApcLock::maybeAvailable()
*/
function lock_may_be_available($name) {
return ApcLock::maybeAvailable($name);
}
/**
* Waits for a lock to be available.
*
* This function may be called in a request that fails to acquire a desired
* lock. This will block further execution until the lock is available or the
* specified delay in seconds is reached. This should not be used with locks
* that are acquired very frequently, since the lock is likely to be acquired
* again by a different request while waiting.
*
* @param string $name
* The name of the lock.
* @param float $delay
* The maximum number of seconds to wait, as an integer.
*
* @return bool
* FALSE if the lock is available, TRUE otherwise.
*
* @see ApcLock::waitForLock()
*/
function lock_wait($name, $delay = 30.0) {
return ApcLock::waitForLock($name, $delay);
}
/**
* Releases a lock previously acquired by lock_acquire().
*
* This will release the named lock if it is still held by the current request.
*
* @param string $name
* The name of the lock.
*
* @see ApcLock::release()
*/
function lock_release($name) {
ApcLock::release($name);
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment