mirror of
https://github.com/moodle/moodle.git
synced 2025-04-14 13:02:07 +02:00
Merge branch 'MDL-67020_master' of https://github.com/marxjohnson/moodle
This commit is contained in:
commit
c209719811
25
cache/classes/definition.php
vendored
25
cache/classes/definition.php
vendored
@ -208,6 +208,12 @@ class cache_definition {
|
||||
*/
|
||||
protected $requirelockingwrite = false;
|
||||
|
||||
/**
|
||||
* Gets set to true if this definition requires a lock to be acquired before a write is attempted.
|
||||
* @var bool
|
||||
*/
|
||||
protected $requirelockingbeforewrite = false;
|
||||
|
||||
/**
|
||||
* Gets set to true if this definition requires searchable stores.
|
||||
* @since Moodle 2.4.4
|
||||
@ -357,6 +363,7 @@ class cache_definition {
|
||||
$requiremultipleidentifiers = false;
|
||||
$requirelockingread = false;
|
||||
$requirelockingwrite = false;
|
||||
$requirelockingbeforewrite = false;
|
||||
$requiresearchable = ($mode === cache_store::MODE_SESSION) ? true : false;
|
||||
$maxsize = null;
|
||||
$overrideclass = null;
|
||||
@ -395,7 +402,14 @@ class cache_definition {
|
||||
if (array_key_exists('requirelockingwrite', $definition)) {
|
||||
$requirelockingwrite = (bool)$definition['requirelockingwrite'];
|
||||
}
|
||||
$requirelocking = $requirelockingwrite || $requirelockingread;
|
||||
if (array_key_exists('requirelockingbeforewrite', $definition)) {
|
||||
$requirelockingbeforewrite = (bool)$definition['requirelockingbeforewrite'];
|
||||
}
|
||||
if ($requirelockingbeforewrite && ($requirelockingwrite || $requirelockingread)) {
|
||||
throw new coding_exception('requirelockingbeforewrite cannot be set with requirelockingread or requirelockingwrite
|
||||
in a cache definition, as this will result in conflicting locks.');
|
||||
}
|
||||
$requirelocking = $requirelockingwrite || $requirelockingbeforewrite || $requirelockingread;
|
||||
|
||||
if (array_key_exists('requiresearchable', $definition)) {
|
||||
$requiresearchable = (bool)$definition['requiresearchable'];
|
||||
@ -523,6 +537,7 @@ class cache_definition {
|
||||
$cachedefinition->requirelocking = $requirelocking;
|
||||
$cachedefinition->requirelockingread = $requirelockingread;
|
||||
$cachedefinition->requirelockingwrite = $requirelockingwrite;
|
||||
$cachedefinition->requirelockingbeforewrite = $requirelockingbeforewrite;
|
||||
$cachedefinition->requiresearchable = $requiresearchable;
|
||||
$cachedefinition->maxsize = $maxsize;
|
||||
$cachedefinition->overrideclass = $overrideclass;
|
||||
@ -741,6 +756,14 @@ class cache_definition {
|
||||
return $this->requirelockingwrite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this definition requires a lock to be aquired before a write is attempted.
|
||||
* @return bool
|
||||
*/
|
||||
public function require_locking_before_write() {
|
||||
return $this->requirelockingbeforewrite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this definition allows local storage to be used for caching.
|
||||
* @since Moodle 3.1.0
|
||||
|
2
cache/classes/helper.php
vendored
2
cache/classes/helper.php
vendored
@ -382,6 +382,7 @@ class cache_helper {
|
||||
'misses' => 0,
|
||||
'sets' => 0,
|
||||
'iobytes' => cache_store::IO_BYTES_NOT_SUPPORTED,
|
||||
'locks' => 0,
|
||||
)
|
||||
)
|
||||
);
|
||||
@ -392,6 +393,7 @@ class cache_helper {
|
||||
'misses' => 0,
|
||||
'sets' => 0,
|
||||
'iobytes' => cache_store::IO_BYTES_NOT_SUPPORTED,
|
||||
'locks' => 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
68
cache/classes/loaders.php
vendored
68
cache/classes/loaders.php
vendored
@ -1581,12 +1581,24 @@ class cache_application extends cache implements cache_loader_with_locking {
|
||||
*/
|
||||
protected $requirelockingwrite = false;
|
||||
|
||||
/**
|
||||
* Gets set to true if the cache writes (set|delete) must have a manual lock created first
|
||||
* @var bool
|
||||
*/
|
||||
protected $requirelockingbeforewrite = false;
|
||||
|
||||
/**
|
||||
* Gets set to a cache_store to use for locking if the caches primary store doesn't support locking natively.
|
||||
* @var cache_lock_interface
|
||||
*/
|
||||
protected $cachelockinstance;
|
||||
|
||||
/**
|
||||
* Store a list of locks acquired by this process.
|
||||
* @var array
|
||||
*/
|
||||
protected $locks;
|
||||
|
||||
/**
|
||||
* Overrides the cache construct method.
|
||||
*
|
||||
@ -1603,6 +1615,7 @@ class cache_application extends cache implements cache_loader_with_locking {
|
||||
$this->requirelocking = true;
|
||||
$this->requirelockingread = $definition->require_locking_read();
|
||||
$this->requirelockingwrite = $definition->require_locking_write();
|
||||
$this->requirelockingbeforewrite = $definition->require_locking_before_write();
|
||||
}
|
||||
|
||||
$this->handle_invalidation_events();
|
||||
@ -1648,13 +1661,24 @@ class cache_application extends cache implements cache_loader_with_locking {
|
||||
* @return bool Returns true if the lock could be acquired, false otherwise.
|
||||
*/
|
||||
public function acquire_lock($key) {
|
||||
global $CFG;
|
||||
$key = $this->parse_key($key);
|
||||
$before = microtime(true);
|
||||
if ($this->nativelocking) {
|
||||
return $this->get_store()->acquire_lock($key, $this->get_identifier());
|
||||
$lock = $this->get_store()->acquire_lock($key, $this->get_identifier());
|
||||
} else {
|
||||
$this->ensure_cachelock_available();
|
||||
return $this->cachelockinstance->lock($key, $this->get_identifier());
|
||||
$lock = $this->cachelockinstance->lock($key, $this->get_identifier());
|
||||
}
|
||||
$after = microtime(true);
|
||||
if ($lock) {
|
||||
$this->locks[$key] = $lock;
|
||||
if (defined('MDL_PERF') || !empty($CFG->perfdebug)) {
|
||||
\core\lock\timing_wrapper_lock_factory::record_lock_data($after, $before,
|
||||
$this->get_definition()->get_id(), $key, $lock, $this->get_identifier() . $key);
|
||||
}
|
||||
}
|
||||
return $lock;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1666,6 +1690,9 @@ class cache_application extends cache implements cache_loader_with_locking {
|
||||
*/
|
||||
public function check_lock_state($key) {
|
||||
$key = $this->parse_key($key);
|
||||
if (!empty($this->locks[$key])) {
|
||||
return true; // Shortcut to save having to make a call to the cache store if the lock is held by this process.
|
||||
}
|
||||
if ($this->nativelocking) {
|
||||
return $this->get_store()->check_lock_state($key, $this->get_identifier());
|
||||
} else {
|
||||
@ -1683,11 +1710,18 @@ class cache_application extends cache implements cache_loader_with_locking {
|
||||
public function release_lock($key) {
|
||||
$key = $this->parse_key($key);
|
||||
if ($this->nativelocking) {
|
||||
return $this->get_store()->release_lock($key, $this->get_identifier());
|
||||
$released = $this->get_store()->release_lock($key, $this->get_identifier());
|
||||
} else {
|
||||
$this->ensure_cachelock_available();
|
||||
return $this->cachelockinstance->unlock($key, $this->get_identifier());
|
||||
$released = $this->cachelockinstance->unlock($key, $this->get_identifier());
|
||||
}
|
||||
if ($released && array_key_exists($key, $this->locks)) {
|
||||
unset($this->locks[$key]);
|
||||
if (defined('MDL_PERF') || !empty($CFG->perfdebug)) {
|
||||
\core\lock\timing_wrapper_lock_factory::record_lock_released_data($this->get_identifier() . $key);
|
||||
}
|
||||
}
|
||||
return $released;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1719,6 +1753,10 @@ class cache_application extends cache implements cache_loader_with_locking {
|
||||
* @return bool True on success, false otherwise.
|
||||
*/
|
||||
protected function set_implementation($key, int $version, $data, bool $setparents = true): bool {
|
||||
if ($this->requirelockingbeforewrite && !$this->check_lock_state($key)) {
|
||||
throw new coding_exception('Attempted to set cache key "' . $key . '" without a lock. '
|
||||
. 'Locking before writes is required for ' . $this->get_definition()->get_id());
|
||||
}
|
||||
if ($this->requirelockingwrite && !$this->acquire_lock($key)) {
|
||||
return false;
|
||||
}
|
||||
@ -1753,6 +1791,15 @@ class cache_application extends cache implements cache_loader_with_locking {
|
||||
* ... if they care that is.
|
||||
*/
|
||||
public function set_many(array $keyvaluearray) {
|
||||
if ($this->requirelockingbeforewrite) {
|
||||
foreach ($keyvaluearray as $id => $pair) {
|
||||
if (!$this->check_lock_state($pair['key'])) {
|
||||
debugging('Attempted to set cache key "' . $pair['key'] . '" without a lock. '
|
||||
. 'Locking before writes is required for ' . $this->get_definition()->get_name(), DEBUG_DEVELOPER);
|
||||
unset($keyvaluearray[$id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($this->requirelockingwrite) {
|
||||
$locks = array();
|
||||
foreach ($keyvaluearray as $id => $pair) {
|
||||
@ -1809,10 +1856,11 @@ class cache_application extends cache implements cache_loader_with_locking {
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function get_many(array $keys, $strictness = IGNORE_MISSING) {
|
||||
$locks = [];
|
||||
if ($this->requirelockingread) {
|
||||
foreach ($keys as $id => $key) {
|
||||
$lock =$this->acquire_lock($key);
|
||||
if (!$lock) {
|
||||
$locks[$key] = $this->acquire_lock($key);
|
||||
if (!$locks[$key]) {
|
||||
if ($strictness === MUST_EXIST) {
|
||||
throw new coding_exception('Could not acquire read locks for all of the items being requested.');
|
||||
} else {
|
||||
@ -1823,7 +1871,13 @@ class cache_application extends cache implements cache_loader_with_locking {
|
||||
|
||||
}
|
||||
}
|
||||
return parent::get_many($keys, $strictness);
|
||||
$result = parent::get_many($keys, $strictness);
|
||||
if ($this->requirelockingread) {
|
||||
foreach ($locks as $key => $lock) {
|
||||
$this->release_lock($key);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
20
cache/disabledlib.php
vendored
20
cache/disabledlib.php
vendored
@ -200,6 +200,26 @@ class cache_disabled extends cache {
|
||||
public function purge() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pretend that we got a lock to avoid errors.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function acquire_lock(string $key) : bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pretend that we released a lock to avoid errors.
|
||||
*
|
||||
* @param string $key
|
||||
* @return void
|
||||
*/
|
||||
public function release_lock(string $key) : bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
5
cache/stores/file/addinstanceform.php
vendored
5
cache/stores/file/addinstanceform.php
vendored
@ -62,5 +62,10 @@ class cachestore_file_addinstance_form extends cachestore_addinstance_form {
|
||||
$form->addElement('checkbox', 'asyncpurge', get_string('asyncpurge', 'cachestore_file'));
|
||||
$form->setType('asyncpurge', PARAM_BOOL);
|
||||
$form->addHelpButton('asyncpurge', 'asyncpurge', 'cachestore_file');
|
||||
|
||||
$form->addElement('text', 'lockwait', get_string('lockwait', 'cachestore_file'));
|
||||
$form->setDefault('lockwait', 60);
|
||||
$form->setType('lockwait', PARAM_INT);
|
||||
$form->addHelpButton('lockwait', 'lockwait', 'cachestore_file');
|
||||
}
|
||||
}
|
@ -32,6 +32,8 @@ $string['asyncpurge'] = 'Asynchronously purge directory';
|
||||
$string['asyncpurge_help'] = 'If enabled, the new directory is created with cache revision and the old directory will be deleted asynchronously via a scheduled task.';
|
||||
$string['autocreate'] = 'Auto create directory';
|
||||
$string['autocreate_help'] = 'If enabled the directory specified in path will be automatically created if it does not already exist.';
|
||||
$string['lockwait'] = 'Maximum lock wait time';
|
||||
$string['lockwait_help'] = 'The maximum amount of time in seconds to wait for an exclusive lock before reading or writing a cache key. This is only used for cache definitions that have read or write locking required.';
|
||||
$string['path'] = 'Cache path';
|
||||
$string['path_help'] = 'The directory that should be used to store files for this cache store. If left blank (default) a directory will be automatically created in the moodledata directory. This can be used to point a file store towards a directory on a better performing drive (such as one in memory).';
|
||||
$string['pluginname'] = 'File cache';
|
||||
|
107
cache/stores/file/lib.php
vendored
107
cache/stores/file/lib.php
vendored
@ -37,7 +37,8 @@
|
||||
* @copyright 2012 Sam Hemelryk
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class cachestore_file extends cache_store implements cache_is_key_aware, cache_is_configurable, cache_is_searchable {
|
||||
class cachestore_file extends cache_store implements cache_is_key_aware, cache_is_configurable, cache_is_searchable,
|
||||
cache_is_lockable {
|
||||
|
||||
/**
|
||||
* The name of the store.
|
||||
@ -128,6 +129,23 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
|
||||
*/
|
||||
private $cfg = null;
|
||||
|
||||
/** @var int Maximum number of seconds to wait for a lock before giving up. */
|
||||
protected $lockwait = 60;
|
||||
|
||||
/**
|
||||
* Instance of file_lock_factory configured to create locks in the cache directory.
|
||||
*
|
||||
* @var \core\lock\file_lock_factory $lockfactory
|
||||
*/
|
||||
protected $lockfactory = null;
|
||||
|
||||
/**
|
||||
* List of current locks.
|
||||
*
|
||||
* @var array $locks
|
||||
*/
|
||||
protected $locks = [];
|
||||
|
||||
/**
|
||||
* Constructs the store instance.
|
||||
*
|
||||
@ -193,6 +211,21 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
|
||||
} else {
|
||||
$this->asyncpurge = false;
|
||||
}
|
||||
|
||||
// Leverage cachelock_file to provide native locking, to avoid duplicating logic.
|
||||
// This will store locks alongside the cache, so local cache uses local locks.
|
||||
$lockdir = $path . '/filelocks';
|
||||
if (!file_exists($lockdir)) {
|
||||
make_writable_directory($lockdir);
|
||||
}
|
||||
if (array_key_exists('lockwait', $configuration)) {
|
||||
$this->lockwait = (int)$configuration['lockwait'];
|
||||
}
|
||||
$this->lockfactory = new \core\lock\file_lock_factory('cachestore_file', $lockdir);
|
||||
if (!$this->lockfactory->is_available()) {
|
||||
// File locking is disabled in config, fall back to default lock factory.
|
||||
$this->lockfactory = \core\lock\lock_config::get_lock_factory('cachestore_file');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -675,6 +708,9 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
|
||||
if (isset($data->asyncpurge)) {
|
||||
$config['asyncpurge'] = $data->asyncpurge;
|
||||
}
|
||||
if (isset($data->lockwait)) {
|
||||
$config['lockwait'] = $data->lockwait;
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
@ -702,6 +738,9 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
|
||||
if (isset($config['asyncpurge'])) {
|
||||
$data['asyncpurge'] = (bool)$config['asyncpurge'];
|
||||
}
|
||||
if (isset($config['lockwait'])) {
|
||||
$data['lockwait'] = (int)$config['lockwait'];
|
||||
}
|
||||
$editform->set_data($data);
|
||||
}
|
||||
|
||||
@ -924,4 +963,70 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
|
||||
$result->sd = sqrt($squarediff);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use lock factory to determine the lock state.
|
||||
*
|
||||
* @param string $key Lock identifier
|
||||
* @param string $ownerid Cache identifier
|
||||
* @return bool|null
|
||||
*/
|
||||
public function check_lock_state($key, $ownerid) : ?bool {
|
||||
if (!array_key_exists($key, $this->locks)) {
|
||||
return null; // Lock does not exist.
|
||||
}
|
||||
if (!array_key_exists($ownerid, $this->locks[$key])) {
|
||||
return false; // Lock exists, but belongs to someone else.
|
||||
}
|
||||
if ($this->locks[$key][$ownerid] instanceof \core\lock\lock) {
|
||||
return true; // Lock exists, and we own it.
|
||||
}
|
||||
// Try to get the lock with an immediate timeout. If this succeeds, the lock does not currently exist.
|
||||
$lock = $this->lockfactory->get_lock($key, 0);
|
||||
if ($lock) {
|
||||
// Lock was not already held.
|
||||
$lock->release();
|
||||
return null;
|
||||
} else {
|
||||
// Lock is held by someone else.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use lock factory to acquire a lock.
|
||||
*
|
||||
* @param string $key Lock identifier
|
||||
* @param string $ownerid Cache identifier
|
||||
* @return bool
|
||||
* @throws cache_exception
|
||||
*/
|
||||
public function acquire_lock($key, $ownerid) : bool {
|
||||
$lock = $this->lockfactory->get_lock($key, $this->lockwait);
|
||||
if ($lock) {
|
||||
$this->locks[$key][$ownerid] = $lock;
|
||||
}
|
||||
return (bool)$lock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use lock factory to release a lock.
|
||||
*
|
||||
* @param string $key Lock identifier
|
||||
* @param string $ownerid Cache identifier
|
||||
* @return bool
|
||||
*/
|
||||
public function release_lock($key, $ownerid) : bool {
|
||||
if (!array_key_exists($key, $this->locks)) {
|
||||
return false; // No lock to release.
|
||||
}
|
||||
if (!array_key_exists($ownerid, $this->locks[$key])) {
|
||||
return false; // Tried to release someone else's lock.
|
||||
}
|
||||
$unlocked = $this->locks[$key][$ownerid]->release();
|
||||
if ($unlocked) {
|
||||
unset($this->locks[$key]);
|
||||
}
|
||||
return $unlocked;
|
||||
}
|
||||
}
|
||||
|
12
cache/stores/file/tests/store_test.php
vendored
12
cache/stores/file/tests/store_test.php
vendored
@ -33,6 +33,7 @@ require_once($CFG->dirroot.'/cache/stores/file/lib.php');
|
||||
* @package cachestore_file
|
||||
* @copyright 2013 Sam Hemelryk
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \cachestore_file
|
||||
*/
|
||||
class store_test extends \cachestore_tests {
|
||||
/**
|
||||
@ -99,4 +100,15 @@ class store_test extends \cachestore_tests {
|
||||
]);
|
||||
$this->assertEquals(21, $store->get_last_io_bytes());
|
||||
}
|
||||
|
||||
public function test_lock() {
|
||||
$store = new \cachestore_file('Test');
|
||||
|
||||
$this->assertTrue($store->acquire_lock('lock', '123'));
|
||||
$this->assertTrue($store->check_lock_state('lock', '123'));
|
||||
$this->assertFalse($store->check_lock_state('lock', '321'));
|
||||
$this->assertNull($store->check_lock_state('notalock', '123'));
|
||||
$this->assertFalse($store->release_lock('lock', '321'));
|
||||
$this->assertTrue($store->release_lock('lock', '123'));
|
||||
}
|
||||
}
|
||||
|
49
cache/tests/cache_test.php
vendored
49
cache/tests/cache_test.php
vendored
@ -2111,6 +2111,55 @@ class cache_test extends \advanced_testcase {
|
||||
$this->assertFalse($cache->has('a'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test requiring a lock before attempting to set a key.
|
||||
*
|
||||
* @covers ::set_implementation
|
||||
*/
|
||||
public function test_application_locking_before_write() {
|
||||
$instance = cache_config_testing::instance(true);
|
||||
$instance->phpunit_add_definition('phpunit/test_application_locking', array(
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
'component' => 'phpunit',
|
||||
'area' => 'test_application_locking',
|
||||
'staticacceleration' => true,
|
||||
'staticaccelerationsize' => 1,
|
||||
'requirelockingbeforewrite' => true
|
||||
));
|
||||
$cache = cache::make('phpunit', 'test_application_locking');
|
||||
$this->assertInstanceOf(cache_application::class, $cache);
|
||||
|
||||
$cache->acquire_lock('a');
|
||||
$this->assertTrue($cache->set('a', 'A'));
|
||||
$cache->release_lock('a');
|
||||
|
||||
$this->expectExceptionMessage('Attempted to set cache key "b" without a lock. '
|
||||
. 'Locking before writes is required for phpunit/test_application_locking');
|
||||
$this->assertFalse($cache->set('b', 'B'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test that invalid lock setting combinations are caught.
|
||||
*
|
||||
* @covers ::make
|
||||
*/
|
||||
public function test_application_conflicting_locks() {
|
||||
$instance = cache_config_testing::instance(true);
|
||||
$instance->phpunit_add_definition('phpunit/test_application_locking', array(
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
'component' => 'phpunit',
|
||||
'area' => 'test_application_locking',
|
||||
'staticacceleration' => true,
|
||||
'staticaccelerationsize' => 1,
|
||||
'requirelockingwrite' => true,
|
||||
'requirelockingbeforewrite' => true,
|
||||
));
|
||||
|
||||
$this->expectException('coding_exception');
|
||||
cache::make('phpunit', 'test_application_locking');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the static cache_helper method purge_stores_used_by_definition.
|
||||
*/
|
||||
|
8
cache/upgrade.txt
vendored
8
cache/upgrade.txt
vendored
@ -1,6 +1,14 @@
|
||||
This files describes API changes in /cache/stores/* - cache store plugins.
|
||||
Information provided here is intended especially for developers.
|
||||
|
||||
=== 4.1 ===
|
||||
* Added new `requirelockingbeforewrite` option for cache definitions. This will check that a lock for a given cache key already
|
||||
exists before it will perform a `set()` on that key. A `coding_exception` is thrown if the lock has not been acquired.
|
||||
* Added native locking to cachestore_file. This will use an instance of file_lock_factory pointing at a subdirectory in the same
|
||||
location as the cache instance, meaning a local file cache will have its locks stored locally. If file locks are disabled
|
||||
globally, it will fall back to use the default lock factory, which may not be in the same location as the cache. cachestore_file
|
||||
includes an additional setting to control how long it will wait for a lock before giving up, default is 60 seconds.
|
||||
|
||||
=== 4.0 ===
|
||||
* Cache stores may implement new optional function cache_store::get_last_io_bytes() to provide
|
||||
information about the size of data transferred (shown in footer if performance info enabled).
|
||||
|
@ -1068,7 +1068,7 @@ abstract class base {
|
||||
$changed = $needrebuild = false;
|
||||
foreach ($defaultoptions as $key => $value) {
|
||||
if (isset($records[$key])) {
|
||||
if (array_key_exists($key, $data) && $records[$key]->value !== $data[$key]) {
|
||||
if (array_key_exists($key, $data) && $records[$key]->value != $data[$key]) {
|
||||
$DB->set_field('course_format_options', 'value',
|
||||
$data[$key], array('id' => $records[$key]->id));
|
||||
$changed = true;
|
||||
|
@ -58,12 +58,15 @@ class file_lock_factory implements lock_factory {
|
||||
* Create this lock factory.
|
||||
*
|
||||
* @param string $type - The type, e.g. cron, cache, session
|
||||
* @param string|null $lockdirectory - Optional path to the lock directory, to override defaults.
|
||||
*/
|
||||
public function __construct($type) {
|
||||
public function __construct($type, ?string $lockdirectory = null) {
|
||||
global $CFG;
|
||||
|
||||
$this->type = $type;
|
||||
if (!isset($CFG->file_lock_root)) {
|
||||
if (!is_null($lockdirectory)) {
|
||||
$this->lockdirectory = $lockdirectory;
|
||||
} else if (!isset($CFG->file_lock_root)) {
|
||||
$this->lockdirectory = $CFG->dataroot . '/lock';
|
||||
} else {
|
||||
$this->lockdirectory = $CFG->file_lock_root;
|
||||
@ -100,7 +103,7 @@ class file_lock_factory implements lock_factory {
|
||||
global $CFG;
|
||||
$preventfilelocking = !empty($CFG->preventfilelocking);
|
||||
$lockdirisdataroot = true;
|
||||
if (!empty($CFG->file_lock_root) && strpos($CFG->file_lock_root, $CFG->dataroot) !== 0) {
|
||||
if (strpos($this->lockdirectory, $CFG->dataroot) !== 0) {
|
||||
$lockdirisdataroot = false;
|
||||
}
|
||||
return !$preventfilelocking || !$lockdirisdataroot;
|
||||
|
@ -70,40 +70,67 @@ class timing_wrapper_lock_factory implements lock_factory {
|
||||
* @return \core\lock\lock|boolean - An instance of \core\lock\lock if the lock was obtained, or false.
|
||||
*/
|
||||
public function get_lock($resource, $timeout, $maxlifetime = 86400) {
|
||||
global $PERF;
|
||||
|
||||
$before = microtime(true);
|
||||
|
||||
$result = $this->factory->get_lock($resource, $timeout, $maxlifetime);
|
||||
|
||||
$after = microtime(true);
|
||||
$duration = $after - $before;
|
||||
if (empty($PERF->locks)) {
|
||||
$PERF->locks = [];
|
||||
}
|
||||
$lockdata = (object) [
|
||||
'type' => $this->type,
|
||||
'resource' => $resource,
|
||||
'wait' => $duration,
|
||||
'success' => (bool)$result
|
||||
];
|
||||
self::record_lock_data($after, $before, $this->type, $resource, (bool)$result, $result);
|
||||
if ($result) {
|
||||
$lockdata->lock = $result;
|
||||
$lockdata->timestart = $after;
|
||||
$result->init_factory($this);
|
||||
}
|
||||
$PERF->locks[] = $lockdata;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a lock that was previously obtained with @lock.
|
||||
* Records statistics about a lock to the performance data.
|
||||
*
|
||||
* @param float $after The time after the lock was achieved.
|
||||
* @param float $before The time before the lock was requested.
|
||||
* @param string $type The type of lock.
|
||||
* @param string $resource The resource being locked.
|
||||
* @param bool $result Whether the lock was successful.
|
||||
* @param lock|string $lock A value uniquely identifying the lock.
|
||||
* @return void
|
||||
*/
|
||||
public static function record_lock_data(float $after, float $before, string $type, string $resource, bool $result, $lock) {
|
||||
global $PERF;
|
||||
$duration = $after - $before;
|
||||
if (empty($PERF->locks)) {
|
||||
$PERF->locks = [];
|
||||
}
|
||||
$lockdata = (object) [
|
||||
'type' => $type,
|
||||
'resource' => $resource,
|
||||
'wait' => $duration,
|
||||
'success' => $result
|
||||
];
|
||||
if ($result) {
|
||||
$lockdata->lock = $lock;
|
||||
$lockdata->timestart = $after;
|
||||
}
|
||||
$PERF->locks[] = $lockdata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a lock that was previously obtained with {@see get_lock}.
|
||||
*
|
||||
* @param lock $lock - The lock to release.
|
||||
* @return boolean - True if the lock is no longer held (including if it was never held).
|
||||
*/
|
||||
public function release_lock(lock $lock) {
|
||||
self::record_lock_released_data($lock);
|
||||
return $this->factory->release_lock($lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the lock in the performance info and update it with the time held.
|
||||
*
|
||||
* @param lock|string $lock A value uniquely identifying the lock.
|
||||
* @return void
|
||||
*/
|
||||
public static function record_lock_released_data($lock) {
|
||||
global $PERF;
|
||||
|
||||
// Find this lock in the list of locks we got, looking backwards since it is probably
|
||||
@ -117,8 +144,6 @@ class timing_wrapper_lock_factory implements lock_factory {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->factory->release_lock($lock);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -251,6 +251,7 @@ $definitions = array(
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
'simplekeys' => true,
|
||||
'canuselocalstore' => true,
|
||||
'requirelockingbeforewrite' => true
|
||||
),
|
||||
// This is the session user selections cache.
|
||||
// It's a special cache that is used to record user selections that should persist for the lifetime of the session.
|
||||
|
@ -475,17 +475,7 @@ class course_modinfo {
|
||||
// partial rebuild logic sometimes sets the $coursemodinfo->cacherev to -1 which is an
|
||||
// indicator that it needs rebuilding.
|
||||
if ($coursemodinfo === false || ($course->cacherev > $coursemodinfo->cacherev)) {
|
||||
$lock = self::get_course_cache_lock($course->id);
|
||||
try {
|
||||
// Only actually do the build if it's still needed after getting the lock (not if
|
||||
// somebody else, who might have been holding the lock, built it already).
|
||||
$coursemodinfo = $cachecoursemodinfo->get_versioned($course->id, $course->cacherev);
|
||||
if ($coursemodinfo === false || ($course->cacherev > $coursemodinfo->cacherev)) {
|
||||
$coursemodinfo = self::inner_build_course_cache($course, $lock);
|
||||
}
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
$coursemodinfo = self::build_course_cache($course);
|
||||
}
|
||||
|
||||
// Set initial values
|
||||
@ -631,34 +621,6 @@ class course_modinfo {
|
||||
return $compressedsections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a lock for rebuilding the cache of a single course.
|
||||
*
|
||||
* Caller must release the returned lock.
|
||||
*
|
||||
* This is used to ensure that the cache rebuild doesn't happen multiple times in parallel.
|
||||
* This function will wait up to 1 minute for the lock to be obtained. If the lock cannot
|
||||
* be obtained, it throws an exception.
|
||||
*
|
||||
* @param int $courseid Course id
|
||||
* @return \core\lock\lock Lock (must be released!)
|
||||
* @throws moodle_exception If the lock cannot be obtained
|
||||
*/
|
||||
protected static function get_course_cache_lock($courseid) {
|
||||
// Get database lock to ensure this doesn't happen multiple times in parallel. Wait a
|
||||
// reasonable time for the lock to be released, so we can give a suitable error message.
|
||||
// In case the system crashes while building the course cache, the lock will automatically
|
||||
// expire after a (slightly longer) period.
|
||||
$lockfactory = \core\lock\lock_config::get_lock_factory('core_modinfo');
|
||||
$lock = $lockfactory->get_lock('build_course_cache_' . $courseid,
|
||||
self::COURSE_CACHE_LOCK_WAIT, self::COURSE_CACHE_LOCK_EXPIRY);
|
||||
if (!$lock) {
|
||||
throw new moodle_exception('locktimeout', '', '', null,
|
||||
'core_modinfo/build_course_cache_' . $courseid);
|
||||
}
|
||||
return $lock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and stores in MUC object containing information about course
|
||||
* modules and sections together with cached fields from table course.
|
||||
@ -676,32 +638,43 @@ class course_modinfo {
|
||||
throw new coding_exception('Object $course is missing required property \id\'');
|
||||
}
|
||||
|
||||
$lock = self::get_course_cache_lock($course->id);
|
||||
$cachecoursemodinfo = cache::make('core', 'coursemodinfo');
|
||||
$cachekey = $course->id;
|
||||
$cachecoursemodinfo->acquire_lock($cachekey);
|
||||
try {
|
||||
return self::inner_build_course_cache($course, $lock, $partialrebuild);
|
||||
// Only actually do the build if it's still needed after getting the lock (not if
|
||||
// somebody else, who might have been holding the lock, built it already).
|
||||
$coursemodinfo = $cachecoursemodinfo->get_versioned($course->id, $course->cacherev);
|
||||
if ($coursemodinfo === false || ($course->cacherev > $coursemodinfo->cacherev)) {
|
||||
$coursemodinfo = self::inner_build_course_cache($course);
|
||||
}
|
||||
} finally {
|
||||
$lock->release();
|
||||
$cachecoursemodinfo->release_lock($cachekey);
|
||||
}
|
||||
return $coursemodinfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to build course cache when there is already a lock obtained.
|
||||
*
|
||||
* @param stdClass $course object from DB table course
|
||||
* @param \core\lock\lock $lock Lock object - not actually used, just there to indicate you have a lock
|
||||
* @param bool $partialrebuild Indicate if it's partial course cache rebuild or not
|
||||
* @return stdClass Course object that has been stored in MUC
|
||||
*/
|
||||
protected static function inner_build_course_cache(\stdClass $course, \core\lock\lock $lock,
|
||||
bool $partialrebuild = false): \stdClass {
|
||||
protected static function inner_build_course_cache(\stdClass $course, bool $partialrebuild = false): \stdClass {
|
||||
global $DB, $CFG;
|
||||
require_once("{$CFG->dirroot}/course/lib.php");
|
||||
|
||||
$cachekey = $course->id;
|
||||
$cachecoursemodinfo = cache::make('core', 'coursemodinfo');
|
||||
if (!$cachecoursemodinfo->check_lock_state($cachekey)) {
|
||||
throw new coding_exception('You must acquire a lock on the course ID before calling inner_build_course_cache');
|
||||
}
|
||||
|
||||
// Always reload the course object from database to ensure we have the latest possible
|
||||
// value for cacherev.
|
||||
$course = $DB->get_record('course', ['id' => $course->id],
|
||||
implode(',', array_merge(['id'], self::$cachedfields)), MUST_EXIST);
|
||||
|
||||
// Retrieve all information about activities and sections.
|
||||
$coursemodinfo = new stdClass();
|
||||
$coursemodinfo->modinfo = self::get_array_of_activities($course, $partialrebuild);
|
||||
@ -710,8 +683,7 @@ class course_modinfo {
|
||||
$coursemodinfo->$key = $course->$key;
|
||||
}
|
||||
// Set the accumulated activities and sections information in cache, together with cacherev.
|
||||
$cachecoursemodinfo = cache::make('core', 'coursemodinfo');
|
||||
$cachecoursemodinfo->set_versioned($course->id, $course->cacherev, $coursemodinfo);
|
||||
$cachecoursemodinfo->set_versioned($cachekey, $course->cacherev, $coursemodinfo);
|
||||
return $coursemodinfo;
|
||||
}
|
||||
|
||||
@ -724,19 +696,20 @@ class course_modinfo {
|
||||
public static function purge_course_section_cache_by_id(int $courseid, int $sectionid): void {
|
||||
$course = get_course($courseid);
|
||||
$cache = cache::make('core', 'coursemodinfo');
|
||||
$cache->acquire_lock($course->id);
|
||||
$coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
|
||||
$cachekey = $course->id;
|
||||
$cache->acquire_lock($cachekey);
|
||||
$coursemodinfo = $cache->get_versioned($cachekey, $course->cacherev);
|
||||
if ($coursemodinfo !== false) {
|
||||
foreach ($coursemodinfo->sectioncache as $sectionno => $sectioncache) {
|
||||
if ($sectioncache->id == $sectionid) {
|
||||
$coursemodinfo->cacherev = -1;
|
||||
unset($coursemodinfo->sectioncache[$sectionno]);
|
||||
$cache->set_versioned($course->id, $course->cacherev, $coursemodinfo);
|
||||
$cache->set_versioned($cachekey, $course->cacherev, $coursemodinfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$cache->release_lock($course->id);
|
||||
$cache->release_lock($cachekey);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -748,14 +721,15 @@ class course_modinfo {
|
||||
public static function purge_course_section_cache_by_number(int $courseid, int $sectionno): void {
|
||||
$course = get_course($courseid);
|
||||
$cache = cache::make('core', 'coursemodinfo');
|
||||
$cache->acquire_lock($course->id);
|
||||
$coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
|
||||
$cachekey = $course->id;
|
||||
$cache->acquire_lock($cachekey);
|
||||
$coursemodinfo = $cache->get_versioned($cachekey, $course->cacherev);
|
||||
if ($coursemodinfo !== false && array_key_exists($sectionno, $coursemodinfo->sectioncache)) {
|
||||
$coursemodinfo->cacherev = -1;
|
||||
unset($coursemodinfo->sectioncache[$sectionno]);
|
||||
$cache->set_versioned($course->id, $course->cacherev, $coursemodinfo);
|
||||
$cache->set_versioned($cachekey, $course->cacherev, $coursemodinfo);
|
||||
}
|
||||
$cache->release_lock($course->id);
|
||||
$cache->release_lock($cachekey);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -767,16 +741,17 @@ class course_modinfo {
|
||||
public static function purge_course_module_cache(int $courseid, int $cmid): void {
|
||||
$course = get_course($courseid);
|
||||
$cache = cache::make('core', 'coursemodinfo');
|
||||
$cache->acquire_lock($course->id);
|
||||
$coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
|
||||
$cachekey = $course->id;
|
||||
$cache->acquire_lock($cachekey);
|
||||
$coursemodinfo = $cache->get_versioned($cachekey, $course->cacherev);
|
||||
$hascache = ($coursemodinfo !== false) && array_key_exists($cmid, $coursemodinfo->modinfo);
|
||||
if ($hascache) {
|
||||
$coursemodinfo->cacherev = -1;
|
||||
unset($coursemodinfo->modinfo[$cmid]);
|
||||
$cache->set_versioned($course->id, $course->cacherev, $coursemodinfo);
|
||||
$coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
|
||||
$cache->set_versioned($cachekey, $course->cacherev, $coursemodinfo);
|
||||
$coursemodinfo = $cache->get_versioned($cachekey, $course->cacherev);
|
||||
}
|
||||
$cache->release_lock($course->id);
|
||||
$cache->release_lock($cachekey);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -970,6 +945,7 @@ class course_modinfo {
|
||||
* @param int $courseid Course id
|
||||
*/
|
||||
public static function purge_course_cache(int $courseid): void {
|
||||
increment_revision_number('course', 'cacherev', 'id = :id', array('id' => $courseid));
|
||||
$cachemodinfo = cache::make('core', 'coursemodinfo');
|
||||
$cachemodinfo->delete($courseid);
|
||||
}
|
||||
|
@ -274,7 +274,9 @@ class modinfolib_test extends advanced_testcase {
|
||||
$prevcacherev = $cacherev;
|
||||
|
||||
// Little trick to check that cache is not rebuilt druing the next step - substitute the value in MUC and later check that it is still there.
|
||||
$cache->acquire_lock($course->id);
|
||||
$cache->set_versioned($course->id, $cacherev, (object)array_merge((array)$cachedvalue, array('secretfield' => 1)));
|
||||
$cache->release_lock($course->id);
|
||||
|
||||
// Clear static cache and call get_fast_modinfo() again (pretend we are in another request). Cache should not be rebuilt.
|
||||
course_modinfo::clear_instance_cache();
|
||||
|
@ -9,6 +9,9 @@ Declaration is as follow:
|
||||
$deprecatedcapabilities = [
|
||||
'fake/access:fakecapability' => ['replacement' => '', 'message' => 'This capability should not be used anymore.']
|
||||
];
|
||||
* coursemodinfo cache uses the new `requirelockingbeforewrite` option, and rebuilding the cache now uses the cache lock API, rather
|
||||
than using the core lock factory directly. This allows the locks to be stored locally if the cache is stored locally, and
|
||||
avoids the risk of delays and timeouts when multiple nodes need to rebuild the cache locally, but are waiting for a central lock.
|
||||
|
||||
=== 4.1 ===
|
||||
|
||||
|
@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2022101800.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2022101800.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '4.1dev+ (Build: 20221018)'; // Human-friendly version name
|
||||
|
Loading…
x
Reference in New Issue
Block a user