first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+196
View File
@@ -0,0 +1,196 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\lock;
use coding_exception;
/**
* This is a db record locking factory.
*
* This lock factory uses record locks relying on sql of the form "SET XXX where YYY" and checking if the
* value was set. It supports timeouts, autorelease and can work on any DB. The downside - is this
* will always be slower than some shared memory type locking function.
*
* @package core
* @category lock
* @copyright Damyon Wiese 2013
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class db_record_lock_factory implements lock_factory {
/** @var \moodle_database $db Hold a reference to the global $DB */
protected $db;
/** @var string $type Used to prefix lock keys */
protected $type;
/** @var array $openlocks - List of held locks - used by auto-release */
protected $openlocks = array();
/**
* Is available.
* @return boolean - True if this lock type is available in this environment.
*/
public function is_available() {
return true;
}
/**
* Almighty constructor.
* @param string $type - Used to prefix lock keys.
*/
public function __construct($type) {
global $DB;
$this->type = $type;
// Save a reference to the global $DB so it will not be released while we still have open locks.
$this->db = $DB;
\core_shutdown_manager::register_function(array($this, 'auto_release'));
}
/**
* Return information about the blocking behaviour of the lock type on this platform.
* @return boolean - True
*/
public function supports_timeout() {
return true;
}
/**
* Will this lock type will be automatically released when a process ends.
*
* @return boolean - True (shutdown handler)
*/
public function supports_auto_release() {
return true;
}
/**
* @deprecated since Moodle 3.10.
*/
public function supports_recursion() {
throw new coding_exception('The function supports_recursion() has been removed, please do not use it anymore.');
}
/**
* This function generates a unique token for the lock to use.
* It is important that this token is not solely based on time as this could lead
* to duplicates in a clustered environment (especially on VMs due to poor time precision).
*/
protected function generate_unique_token() {
return \core\uuid::generate();
}
/**
* Create and get a lock
* @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
* @param int $timeout - The number of seconds to wait for a lock before giving up.
* @param int $maxlifetime - Unused by this lock type.
* @return boolean - true if a lock was obtained.
*/
public function get_lock($resource, $timeout, $maxlifetime = 86400) {
$token = $this->generate_unique_token();
$now = time();
$giveuptime = $now + $timeout;
$expires = $now + $maxlifetime;
$resourcekey = $this->type . '_' . $resource;
if (!$this->db->record_exists('lock_db', array('resourcekey' => $resourcekey))) {
$record = new \stdClass();
$record->resourcekey = $resourcekey;
$result = $this->db->insert_record('lock_db', $record);
}
$params = array('expires' => $expires,
'token' => $token,
'resourcekey' => $resourcekey,
'now' => $now);
$sql = 'UPDATE {lock_db}
SET
expires = :expires,
owner = :token
WHERE
resourcekey = :resourcekey AND
(owner IS NULL OR expires < :now)';
do {
$now = time();
$params['now'] = $now;
$this->db->execute($sql, $params);
$countparams = array('owner' => $token, 'resourcekey' => $resourcekey);
$result = $this->db->count_records('lock_db', $countparams);
$locked = $result === 1;
if (!$locked && $timeout > 0) {
usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
}
// Try until the giveup time.
} while (!$locked && $now < $giveuptime);
if ($locked) {
$this->openlocks[$token] = 1;
return new lock($token, $this);
}
return false;
}
/**
* Release a lock that was previously obtained with @lock.
* @param lock $lock - a lock obtained from this factory.
* @return boolean - true if the lock is no longer held (including if it was never held).
*/
public function release_lock(lock $lock) {
$params = array('noexpires' => null,
'token' => $lock->get_key(),
'noowner' => null);
$sql = 'UPDATE {lock_db}
SET
expires = :noexpires,
owner = :noowner
WHERE
owner = :token';
$result = $this->db->execute($sql, $params);
if ($result) {
unset($this->openlocks[$lock->get_key()]);
}
return $result;
}
/**
* @deprecated since Moodle 3.10.
*/
public function extend_lock() {
throw new coding_exception('The function extend_lock() has been removed, please do not use it anymore.');
}
/**
* Auto release any open locks on shutdown.
* This is required, because we may be using persistent DB connections.
*/
public function auto_release() {
// Called from the shutdown handler. Must release all open locks.
foreach ($this->openlocks as $key => $unused) {
$lock = new lock($key, $this);
$lock->release();
}
}
}
+185
View File
@@ -0,0 +1,185 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\lock;
use coding_exception;
/**
* Flock based file locking factory.
*
* The file lock factory returns file locks locked with the flock function. Works OK, except on some
* NFS, exotic shared storage and exotic server OSes (like windows). On windows, a second attempt to get a
* lock will block indefinitely instead of timing out.
*
* @package core
* @category lock
* @copyright Damyon Wiese 2013
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class file_lock_factory implements lock_factory {
/** @var string $type - The type of lock, e.g. cache, cron, session. */
protected $type;
/** @var string $lockdirectory - Full system path to the directory used to store file locks. */
protected $lockdirectory;
/** @var boolean $verbose - If true, debugging info about the owner of the lock will be written to the lock file. */
protected $verbose;
/**
* 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, ?string $lockdirectory = null) {
global $CFG;
$this->type = $type;
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;
}
$this->verbose = false;
if ($CFG->debugdeveloper) {
$this->verbose = true;
}
}
/**
* Return information about the blocking behaviour of the lock type on this platform.
* @return boolean - False if attempting to get a lock will block indefinitely.
*/
public function supports_timeout() {
global $CFG;
return $CFG->ostype !== 'WINDOWS';
}
/**
* This lock type will be automatically released when a process ends.
* @return boolean - True
*/
public function supports_auto_release() {
return true;
}
/**
* Is available.
* @return boolean - True if preventfilelocking is not set - or the file_lock_root is not in dataroot.
*/
public function is_available() {
global $CFG;
$preventfilelocking = !empty($CFG->preventfilelocking);
$lockdirisdataroot = true;
if (strpos($this->lockdirectory, $CFG->dataroot) !== 0) {
$lockdirisdataroot = false;
}
return !$preventfilelocking || !$lockdirisdataroot;
}
/**
* @deprecated since Moodle 3.10.
*/
public function supports_recursion() {
throw new coding_exception('The function supports_recursion() has been removed, please do not use it anymore.');
}
/**
* Get some info that might be useful for debugging.
* @return boolean - string
*/
protected function get_debug_info() {
return 'host:' . php_uname('n') . ', pid:' . getmypid() . ', time:' . time();
}
/**
* Get a lock within the specified timeout or return false.
* @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
* @param int $timeout - The number of seconds to wait for a lock before giving up.
* @param int $maxlifetime - Unused by this lock type.
* @return boolean - true if a lock was obtained.
*/
public function get_lock($resource, $timeout, $maxlifetime = 86400) {
$giveuptime = time() + $timeout;
$hash = md5($this->type . '_' . $resource);
$lockdir = $this->lockdirectory . '/' . substr($hash, 0, 2);
if (!check_dir_exists($lockdir, true, true)) {
return false;
}
$lockfilename = $lockdir . '/' . $hash;
$filehandle = fopen($lockfilename, "wb");
// Could not open the lock file.
if (!$filehandle) {
return false;
}
do {
// Will block on windows. So sad.
$wouldblock = false;
$locked = flock($filehandle, LOCK_EX | LOCK_NB, $wouldblock);
if (!$locked && $wouldblock && $timeout > 0) {
usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
}
// Try until the giveup time.
} while (!$locked && $wouldblock && time() < $giveuptime);
if (!$locked) {
fclose($filehandle);
return false;
}
if ($this->verbose) {
fwrite($filehandle, $this->get_debug_info());
}
return new lock($filehandle, $this);
}
/**
* Release a lock that was previously obtained with @lock.
* @param lock $lock - A lock obtained from this factory.
* @return boolean - true if the lock is no longer held (including if it was never held).
*/
public function release_lock(lock $lock) {
$handle = $lock->get_key();
if (!$handle) {
// We didn't have a lock.
return false;
}
$result = flock($handle, LOCK_UN);
fclose($handle);
return $result;
}
/**
* @deprecated since Moodle 3.10.
*/
public function extend_lock() {
throw new coding_exception('The function extend_lock() has been removed, please do not use it anymore.');
}
}
@@ -0,0 +1,111 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\lock;
use coding_exception;
/**
* Lock factory for use during installation.
*
* @package core
* @category lock
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class installation_lock_factory implements lock_factory {
/**
* Create this lock factory.
*
* @param string $type - The type, e.g. cron, cache, session
*/
public function __construct($type) {
}
/**
* Return information about the blocking behaviour of the lock type on this platform.
*
* @return boolean - False if attempting to get a lock will block indefinitely.
*/
public function supports_timeout() {
return true;
}
/**
* This lock type will be automatically released when a process ends.
*
* @return boolean - True
*/
public function supports_auto_release() {
return true;
}
/**
* This lock factory is only available during the initial installation.
* To use it at any other time would be potentially dangerous.
*
* @return boolean
*/
public function is_available() {
return during_initial_install();
}
/**
* @deprecated since Moodle 3.10.
*/
public function supports_recursion() {
throw new coding_exception('The function supports_recursion() has been removed, please do not use it anymore.');
}
/**
* Get some info that might be useful for debugging.
* @return boolean - string
*/
protected function get_debug_info() {
return 'host:' . php_uname('n') . ', pid:' . getmypid() . ', time:' . time();
}
/**
* Get a lock within the specified timeout or return false.
*
* @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
* @param int $timeout - The number of seconds to wait for a lock before giving up.
* @param int $maxlifetime - Unused by this lock type.
* @return boolean - true if a lock was obtained.
*/
public function get_lock($resource, $timeout, $maxlifetime = 86400) {
return new lock($resource, $this);
}
/**
* Release a lock that was previously obtained with @lock.
*
* @param lock $lock - A lock obtained from this factory.
* @return boolean - true if the lock is no longer held (including if it was never held).
*/
public function release_lock(lock $lock) {
return true;
}
/**
* @deprecated since Moodle 3.10.
*/
public function extend_lock() {
throw new coding_exception('The function extend_lock() has been removed, please do not use it anymore.');
}
}
+124
View File
@@ -0,0 +1,124 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\lock;
use coding_exception;
/**
* Class representing a lock
*
* The methods available for a specific lock type are only known by it's factory.
*
* @package core
* @category lock
* @copyright Damyon Wiese 2013
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class lock {
/** @var string|int $key A unique key representing a held lock */
protected $key = '';
/** @var lock_factory $factory The factory that generated this lock */
protected $factory;
/** @var bool $released Has this lock been released? If a lock falls out of scope without being released - show a warning. */
protected $released;
/** @var string $caller Where was this called from? Stored for when a warning is shown */
protected $caller = 'unknown';
/**
* Construct a lock containing the unique key required to release it.
* @param mixed $key - The lock key. The type of this is up to the lock_factory being used.
* For file locks this is a file handle. For MySQL this is a string.
* @param lock_factory $factory - The factory that generated this lock.
*/
public function __construct($key, $factory) {
$this->factory = $factory;
$this->key = $key;
$this->released = false;
$caller = debug_backtrace(true, 2)[1];
if ($caller && array_key_exists('file', $caller ) ) {
$this->caller = $caller['file'] . ' on line ' . $caller['line'];
} else if ($caller && array_key_exists('class', $caller)) {
$this->caller = $caller['class'] . $caller['type'] . $caller['function'];
}
}
/**
* Sets the lock factory that owns a lock. This function should not be called under normal use.
* It is intended only for cases like {@see timing_wrapper_lock_factory} where we wrap a lock
* factory.
*
* When used, it should be called immediately after constructing the lock.
*
* @param lock_factory $factory New lock factory that owns this lock
*/
public function init_factory(lock_factory $factory): void {
$this->factory = $factory;
}
/**
* Return the unique key representing this lock.
* @return string|int lock key.
*/
public function get_key() {
return $this->key;
}
/**
* @deprecated since Moodle 3.10.
*/
public function extend() {
throw new coding_exception('The function extend() has been removed, please do not use it anymore.');
}
/**
* Release this lock
* @return bool
*/
public function release() {
$this->released = true;
if (empty($this->factory)) {
return false;
}
$result = $this->factory->release_lock($this);
// Release any held references to the factory.
unset($this->factory);
$this->factory = null;
$this->key = '';
return $result;
}
/**
* Print debugging if this lock falls out of scope before being released.
*/
public function __destruct() {
if (!$this->released && defined('PHPUNIT_TEST')) {
$key = $this->key;
$this->release();
throw new \coding_exception("A lock was created but not released at:\n" .
$this->caller . "\n\n" .
" Code should look like:\n\n" .
" \$factory = \core\lock\lock_config::get_lock_factory('type');\n" .
" \$lock = \$factory->get_lock($key);\n" .
" \$lock->release(); // Locks must ALWAYS be released like this.\n\n");
}
}
}
+104
View File
@@ -0,0 +1,104 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Lock configuration class, used to get an instance of the currently configured lock factory.
*
* @package core
* @category lock
* @copyright Damyon Wiese 2013
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\lock;
defined('MOODLE_INTERNAL') || die();
/**
* Lock configuration class, used to get an instance of the currently configured lock factory.
*
* @package core
* @category lock
* @copyright Damyon Wiese 2013
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class lock_config {
/**
* Get the currently configured locking subclass.
*
* @return string class name
* @throws \coding_exception
*/
public static function get_lock_factory_class(): string {
global $CFG, $DB;
if (during_initial_install()) {
$lockfactoryclass = '\core\lock\installation_lock_factory';
} else if (isset($CFG->lock_factory) && $CFG->lock_factory != 'auto') {
if (!class_exists($CFG->lock_factory)) {
// In this case I guess it is not safe to continue. Different cluster nodes could end up using different locking
// types because of an installation error.
throw new \coding_exception('Lock factory set in $CFG does not exist: ' . $CFG->lock_factory);
}
$lockfactoryclass = $CFG->lock_factory;
} else {
$dbtype = clean_param($DB->get_dbfamily(), PARAM_ALPHA);
// DB Specific lock factory is preferred - should support auto-release.
$lockfactoryclass = "\\core\\lock\\{$dbtype}_lock_factory";
if (!class_exists($lockfactoryclass)) {
$lockfactoryclass = '\core\lock\file_lock_factory';
}
// Test if the auto chosen lock factory is available.
$lockfactory = new $lockfactoryclass('test');
if (!$lockfactory->is_available()) {
// Final fallback - DB row locking.
$lockfactoryclass = '\core\lock\db_record_lock_factory';
}
}
return $lockfactoryclass;
}
/**
* Get an instance of the currently configured locking subclass.
*
* @param string $type - Unique namespace for the locks generated by this factory. e.g. core_cron
* @return \core\lock\lock_factory
* @throws \coding_exception
*/
public static function get_lock_factory(string $type): \core\lock\lock_factory {
global $CFG;
$lockfactoryclass = self::get_lock_factory_class();
$lockfactory = new $lockfactoryclass($type);
if (!$lockfactory->is_available()) {
throw new \coding_exception("Lock factory class $lockfactoryclass is not available.");
}
// If tracking performance, insert a timing wrapper to keep track of lock delays.
if (MDL_PERF || (!empty($CFG->perfdebug) && $CFG->perfdebug > 7)) {
$wrapper = new timing_wrapper_lock_factory($type, $lockfactory);
$lockfactory = $wrapper;
}
return $lockfactory;
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Defines abstract factory class for generating locks.
*
* @package core
* @copyright Damyon Wiese 2013
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\lock;
defined('MOODLE_INTERNAL') || die();
/**
* Defines abstract factory class for generating locks.
*
* @package core
* @category lock
* @copyright Damyon Wiese 2013
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface lock_factory {
/**
* Define the constructor signature required by the lock_config class.
*
* @param string $type - The type this lock is used for (e.g. cron, cache)
*/
public function __construct($type);
/**
* Return information about the blocking behaviour of the locks on this platform.
*
* @return boolean - False if attempting to get a lock will block indefinitely.
*/
public function supports_timeout();
/**
* Will this lock be automatically released when the process ends.
* This should never be relied upon in code - but is useful in the case of
* fatal errors. If a lock type does not support this auto release,
* the max lock time parameter must be obeyed to eventually clean up a lock.
*
* @return boolean - True if this lock type will be automatically released when the current process ends.
*/
public function supports_auto_release();
/**
* Is available.
*
* @return boolean - True if this lock type is available in this environment.
*/
public function is_available();
/**
* Get a lock within the specified timeout or return false.
*
* @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
* @param int $timeout - The number of seconds to wait for a lock before giving up.
* Not all lock types will support this.
* @param int $maxlifetime - The number of seconds to wait before reclaiming a stale lock.
* Not all lock types will use this - e.g. if they support auto releasing
* a lock when a process ends.
* @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);
/**
* Release a lock that was previously obtained with @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);
}
+76
View File
@@ -0,0 +1,76 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\lock;
use core\progress\base;
/**
* Lock utilities.
*
* @package core
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class lock_utils {
/**
* Start a progress bar and attempt to get a lock, updating the bar until a lock is achieved.
*
* This will make multiple attempts at getting the lock using a short timeout set by $progressupdatetime. After
* each failed attempt, it will update the progress bar and try again, until $timeout is reached.
*
* @param lock_factory $lockfactory The factory to use to get the lock
* @param string $resource The resource key we will try to get a lock on
* @param base $progress The progress bar
* @param int $timeout The maximum time in seconds to wait for a lock
* @param string $message Optional message to display on the progress bar
* @param int $progressupdatetime The number of seconds to wait for each lock attempt before updating the progress bar.
* @param int $maxlifetime The maxlifetime to set on the lock, if supported.
* @return lock|false A lock if successful, or false if the timeout expires.
* @throws \coding_exception
*/
public static function wait_for_lock_with_progress(
lock_factory $lockfactory,
string $resource,
\core\progress\base $progress,
int $timeout,
string $message = '',
int $progressupdatetime = 10,
int $maxlifetime = DAYSECS,
) {
if ($progressupdatetime < 1) {
throw new \invalid_parameter_exception('Progress bar cannot update more than once per second. ' .
'$progressupdate time must be at least 1.');
}
if ($progressupdatetime > $timeout) {
throw new \invalid_parameter_exception('$timeout must be greater than $progressupdatetime.');
}
$lockattempts = 0;
$maxattempts = $timeout / $progressupdatetime;
$lock = false;
$progress->start_progress($message);
while (!$lock && $lockattempts < $maxattempts) {
$lock = $lockfactory->get_lock($resource, $progressupdatetime, $maxlifetime);
if (!$lock) {
$progress->progress();
$lockattempts++;
}
}
$progress->end_progress();
return $lock;
}
}
+171
View File
@@ -0,0 +1,171 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\lock;
use coding_exception;
/**
* MySQL / MariaDB locking factory.
*
* @package core
* @category lock
* @copyright Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mysql_lock_factory implements lock_factory {
/** @var string $dbprefix - used as a namespace for these types of locks */
protected $dbprefix = '';
/** @var \moodle_database $db Hold a reference to the global $DB */
protected $db;
/** @var array $openlocks - List of held locks - used by auto-release */
protected $openlocks = [];
/**
* Return a unique prefix based on the database name and prefix.
* @param string $type - Used to prefix lock keys.
* @return string
*/
protected function get_unique_db_prefix($type) {
global $CFG;
$prefix = $CFG->dbname . ':';
if (isset($CFG->prefix)) {
$prefix .= $CFG->prefix;
}
$prefix .= '_' . $type . '_';
return $prefix;
}
/**
* Lock constructor.
* @param string $type - Used to prefix lock keys.
*/
public function __construct($type) {
global $DB;
$this->dbprefix = $this->get_unique_db_prefix($type);
// Save a reference to the global $DB so it will not be released while we still have open locks.
$this->db = $DB;
\core_shutdown_manager::register_function([$this, 'auto_release']);
}
/**
* Is available.
* @return boolean - True if this lock type is available in this environment.
*/
public function is_available() {
return $this->db->get_dbfamily() === 'mysql';
}
/**
* Return information about the blocking behaviour of the lock type on this platform.
* @return boolean - Defer to the DB driver.
*/
public function supports_timeout() {
return true;
}
/**
* Will this lock type will be automatically released when a process ends.
*
* @return boolean - Via shutdown handler.
*/
public function supports_auto_release() {
return true;
}
/**
* @deprecated since Moodle 3.10.
*/
public function supports_recursion() {
throw new coding_exception('The function supports_recursion() has been removed, please do not use it anymore.');
}
/**
* Create and get a lock
* @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
* @param int $timeout - The number of seconds to wait for a lock before giving up.
* @param int $maxlifetime - Unused by this lock type.
* @return boolean - true if a lock was obtained.
*/
public function get_lock($resource, $timeout, $maxlifetime = 86400) {
// We sha1 to avoid long key names hitting the mysql str limit.
$resourcekey = sha1($this->dbprefix . $resource);
// Even though some versions of MySQL and MariaDB can support stacked locks
// just never stack them and always fail fast.
if (isset($this->openlocks[$resourcekey])) {
return false;
}
$params = [
'resourcekey' => $resourcekey,
'timeout' => $timeout
];
$result = $this->db->get_record_sql('SELECT GET_LOCK(:resourcekey, :timeout) AS locked', $params);
$locked = $result->locked == 1;
if ($locked) {
$this->openlocks[$resourcekey] = 1;
return new lock($resourcekey, $this);
}
return false;
}
/**
* Release a lock that was previously obtained with @lock.
* @param lock $lock - a lock obtained from this factory.
* @return boolean - true if the lock is no longer held (including if it was never held).
*/
public function release_lock(lock $lock) {
$params = [
'resourcekey' => $lock->get_key()
];
$result = $this->db->get_record_sql('SELECT RELEASE_LOCK(:resourcekey) AS unlocked', $params);
$result = $result->unlocked == 1;
if ($result) {
unset($this->openlocks[$lock->get_key()]);
}
return $result;
}
/**
* @deprecated since Moodle 3.10.
*/
public function extend_lock() {
throw new coding_exception('The function extend_lock() has been removed, please do not use it anymore.');
}
/**
* Auto release any open locks on shutdown.
* This is required, because we may be using persistent DB connections.
*/
public function auto_release() {
// Called from the shutdown handler. Must release all open locks.
foreach ($this->openlocks as $key => $unused) {
$lock = new lock($key, $this);
$lock->release();
}
}
}
+246
View File
@@ -0,0 +1,246 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\lock;
use coding_exception;
/**
* Postgres advisory locking factory.
*
* Postgres locking implementation using advisory locks. Some important points. Postgres has
* 2 different forms of lock functions, some accepting a single int, and some accepting 2 ints. This implementation
* uses the 2 int version so that it uses a separate namespace from the session locking. The second note,
* is because postgres uses integer keys for locks, we first need to map strings to a unique integer. This is done
* using a prefix of a sha1 hash converted to an integer. There is a realistic chance of collisions by using this
* prefix when locking multiple resources at the same time (multiple resource identifiers leading to the
* same token/prefix). We need to deal with that.
*
* @package core
* @category lock
* @copyright Damyon Wiese 2013
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class postgres_lock_factory implements lock_factory {
/** @var int $dblockid - used as a namespace for these types of locks (separate from session locks) */
protected $dblockid = -1;
/** @var array $lockidcache - static cache for string -> int conversions required for pg advisory locks. */
protected static $lockidcache = array();
/** @var \moodle_database $db Hold a reference to the global $DB */
protected $db;
/** @var string $type Used to prefix lock keys */
protected $type;
/** @var int[] $resourcetokens Mapping of held locks (resource identifier => internal token) */
protected $resourcetokens = [];
/** @var int[] $locksbytoken Mapping of held locks (db connection => internal token => number of locks held) */
static protected $locksbytoken = [];
/**
* Calculate a unique instance id based on the database name and prefix.
* @return int.
*/
protected function get_unique_db_instance_id() {
global $CFG;
$strkey = $CFG->dbname . ':' . $CFG->prefix;
$intkey = crc32($strkey);
// Normalize between 64 bit unsigned int and 32 bit signed ints. Php could return either from crc32.
if (PHP_INT_SIZE == 8) {
if ($intkey > 0x7FFFFFFF) {
$intkey -= 0x100000000;
}
}
return $intkey;
}
/**
* Almighty constructor.
* @param string $type - Used to prefix lock keys.
*/
public function __construct($type) {
global $DB;
$this->type = $type;
$this->dblockid = $this->get_unique_db_instance_id();
// Save a reference to the global $DB so it will not be released while we still have open locks.
$this->db = $DB;
\core_shutdown_manager::register_function(array($this, 'auto_release'));
}
/**
* Is available.
* @return boolean - True if this lock type is available in this environment.
*/
public function is_available() {
return $this->db->get_dbfamily() === 'postgres';
}
/**
* Return information about the blocking behaviour of the lock type on this platform.
* @return boolean - Defer to the DB driver.
*/
public function supports_timeout() {
return true;
}
/**
* Will this lock type will be automatically released when a process ends.
*
* @return boolean - Via shutdown handler.
*/
public function supports_auto_release() {
return true;
}
/**
* @deprecated since Moodle 3.10.
*/
public function supports_recursion() {
throw new coding_exception('The function supports_recursion() has been removed, please do not use it anymore.');
}
/**
* This function generates the unique index for a specific lock key using
* a sha1 prefix converted to decimal.
*
* @param string $key
* @return int
* @throws \moodle_exception
*/
protected function get_index_from_key($key) {
// A prefix of 7 hex chars is chosen as fffffff is the largest hex code
// which when converted to decimal (268435455) fits inside a 4 byte int
// which is the second param to pg_try_advisory_lock().
$hash = substr(sha1($key), 0, 7);
$index = hexdec($hash);
return $index;
}
/**
* Create and get a lock
*
* @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
* @param int $timeout - The number of seconds to wait for a lock before giving up.
* @param int $maxlifetime - Unused by this lock type.
* @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) {
$dbid = spl_object_id($this->db);
$giveuptime = time() + $timeout;
$resourcekey = $this->type . '_' . $resource;
$token = $this->get_index_from_key($resourcekey);
if (isset($this->resourcetokens[$resourcekey])) {
return false;
}
if (isset(self::$locksbytoken[$dbid][$token])) {
// There is a hash collision, another resource identifier leads to the same token.
// As we already hold an advisory lock for this token, we just increase the counter.
self::$locksbytoken[$dbid][$token]++;
$this->resourcetokens[$resourcekey] = $token;
return new lock($resourcekey, $this);
}
$params = [
'locktype' => $this->dblockid,
'token' => $token
];
$locked = false;
do {
$result = $this->db->get_record_sql('SELECT pg_try_advisory_lock(:locktype, :token) AS locked', $params);
$locked = $result->locked === 't';
if (!$locked && $timeout > 0) {
usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
}
// Try until the giveup time.
} while (!$locked && time() < $giveuptime);
if ($locked) {
self::$locksbytoken[$dbid][$token] = 1;
$this->resourcetokens[$resourcekey] = $token;
return new lock($resourcekey, $this);
}
return false;
}
/**
* Release a lock that was previously obtained with @lock.
* @param lock $lock - a lock obtained from this factory.
* @return boolean - true if the lock is no longer held (including if it was never held).
*/
public function release_lock(lock $lock) {
$dbid = spl_object_id($this->db);
$resourcekey = $lock->get_key();
if (isset($this->resourcetokens[$resourcekey])) {
$token = $this->resourcetokens[$resourcekey];
} else {
return true;
}
if (self::$locksbytoken[$dbid][$token] > 1) {
// There are multiple resource identifiers that lead to the same token.
// We will unlock the token later when the last resource is released.
unset($this->resourcetokens[$resourcekey]);
self::$locksbytoken[$dbid][$token]--;
return true;
}
$params = [
'locktype' => $this->dblockid,
'token' => $token,
];
$result = $this->db->get_record_sql('SELECT pg_advisory_unlock(:locktype, :token) AS unlocked', $params);
$result = $result->unlocked === 't';
if ($result) {
unset($this->resourcetokens[$resourcekey]);
unset(self::$locksbytoken[$dbid][$token]);
}
return $result;
}
/**
* @deprecated since Moodle 3.10.
*/
public function extend_lock() {
throw new coding_exception('The function extend_lock() has been removed, please do not use it anymore.');
}
/**
* Auto release any open locks on shutdown.
* This is required, because we may be using persistent DB connections.
*/
public function auto_release() {
// Called from the shutdown handler. Must release all open locks.
foreach ($this->resourcetokens as $resourcekey => $unused) {
$lock = new lock($resourcekey, $this);
$lock->release();
}
}
}
@@ -0,0 +1,191 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\lock;
use coding_exception;
/**
* Timing wrapper around a lock factory.
*
* This passes all calls through to the underlying lock factory, but adds timing information on how
* long it takes to get a lock and how long the lock is held for.
*
* @package core
* @category lock
* @copyright 2022 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class timing_wrapper_lock_factory implements lock_factory {
/** @var lock_factory Real lock factory */
protected $factory;
/** @var string Type (Frankenstyle) used for these locks */
protected $type;
/**
* Constructor required by interface.
*
* @param string $type Type (should be same as passed to real lock factory)
* @param lock_factory $factory Real lock factory
*/
public function __construct($type, lock_factory $factory = null) {
$this->type = $type;
if (!$factory) {
// This parameter has to be optional because of the interface, but it is actually
// required.
throw new \coding_exception('The $factory parameter must be specified');
}
$this->factory = $factory;
}
/**
* Gets the real lock factory that this is wrapping.
*
* @return lock_factory ReaL lock factory
*/
public function get_real_factory(): lock_factory {
return $this->factory;
}
/**
* Implementation of lock_factory::get_lock that defers to function inner_get_lock and keeps
* track of how long it took.
*
* @param string $resource Identifier for the lock
* @param int $timeout Number of seconds to wait for a lock before giving up
* @param int $maxlifetime Number of seconds to wait before reclaiming a stale lock
* @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) {
$before = microtime(true);
$result = $this->factory->get_lock($resource, $timeout, $maxlifetime);
$after = microtime(true);
self::record_lock_data($after, $before, $this->type, $resource, (bool)$result, $result);
if ($result) {
$result->init_factory($this);
}
return $result;
}
/**
* 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
// the last one.
for ($index = count($PERF->locks) - 1; $index >= 0; $index--) {
$lockdata = $PERF->locks[$index];
if (!empty($lockdata->lock) && $lockdata->lock === $lock) {
// Update the time held.
unset($lockdata->lock);
$lockdata->held = microtime(true) - $lockdata->timestart;
break;
}
}
}
/**
* Calls parent factory to check if it supports timeout.
*
* @return boolean False if attempting to get a lock will block indefinitely.
*/
public function supports_timeout() {
return $this->factory->supports_timeout();
}
/**
* Calls parent factory to check if it auto-releases locks.
*
* @return boolean True if this lock type will be automatically released when the current process ends.
*/
public function supports_auto_release() {
return $this->factory->supports_auto_release();
}
/**
* @deprecated since Moodle 3.10.
*/
public function supports_recursion() {
throw new coding_exception('The function supports_recursion() has been removed, please do not use it anymore.');
}
/**
* Calls parent factory to check if it is available.
*
* @return boolean True if this lock type is available in this environment.
*/
public function is_available() {
return $this->factory->is_available();
}
/**
* @deprecated since Moodle 3.10.
*/
public function extend_lock() {
throw new coding_exception('The function extend_lock() has been removed, please do not use it anymore.');
}
}