ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç lock.php000064400000010501152267211140006203 0ustar00. 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; // Track where the lock was raised, so we can report un-released locks in a helpful way. $this->caller = format_backtrace(debug_backtrace(), true); } /** * 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; } /** * 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; } /** * Release a lock if it has not already been released. * * @param bool $withexception If true, throw an exception if the lock has not been released. * @throws \coding_exception */ public function release_if_not_released(bool $withexception = false): void { if (!$this->released) { $key = $this->key; $this->release(); if ($withexception) { throw new \core\exception\coding_exception(<<caller} Code should look like: \$factory = \core\lock\lock_config::get_lock_factory('type'); \$lock = \$factory->get_lock($key); \$lock->release(); // Locks must ALWAYS be released like this. EOF); } } } /** * Print debugging if this lock falls out of scope before being released. */ public function __destruct() { global $CFG; $this->release_if_not_released(defined('BEHAT_SITE_RUNNING') || PHPUNIT_TEST || $CFG->debugdeveloper); } } installation_lock_factory.php000064400000005650152267211140012524 0ustar00. namespace core\lock; use coding_exception; /** * Lock factory for use during installation. * * @package core * @category lock * @copyright Andrew Nicols * @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(); } /** * 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; } } db_record_lock_factory.php000064400000014041152267211140011740 0ustar00. 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; } /** * 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; } /** * 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(); } } } file_lock_factory.php000064400000013102152267211140010731 0ustar00. 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; } /** * 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; } } lock_config.php000064400000007361152267211140007542 0ustar00. /** * 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; } } lock_factory.php000064400000006370152267211140007743 0ustar00. /** * 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); } mysql_lock_factory.php000064400000011671152267211140011170 0ustar00. namespace core\lock; use coding_exception; /** * MySQL / MariaDB locking factory. * * @package core * @category lock * @copyright Brendan Heywood * @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; } /** * 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; } /** * 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(); } } } postgres_lock_factory.php000064400000020375152267211140011672 0ustar00. 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; } /** * 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; } /** * 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(); } } } timing_wrapper_lock_factory.php000064400000013735152267211140013055 0ustar00. 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(); } /** * 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(); } } lock_utils.php000064400000006153152267211140007433 0ustar00. 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 * @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; } } timing_wrapper_lock_factory_test.php000064400000004722152267221320014111 0ustar00. namespace core\lock; /** * Unit tests for the lock factory. * * @covers \core\lock\timing_wrapper_lock_factory * @package core * @copyright 2022 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class timing_wrapper_lock_factory_test extends \advanced_testcase { /** * Tests lock timing wrapper class. */ public function test_lock_timing(): void { global $PERF; $this->resetAfterTest(); // Reset the storage in case previous tests have added anything. unset($PERF->locks); // Set up a lock factory using the db record lock type which is always available. $lockfactory = new timing_wrapper_lock_factory('phpunit', new db_record_lock_factory('phpunit')); // Get 2 locks and release them. $before = microtime(true); $lock1 = $lockfactory->get_lock('frog', 2); $lock2 = $lockfactory->get_lock('toad', 2); $lock2->release(); $lock1->release(); $duration = microtime(true) - $before; // Confirm that perf info is now logged and appears plausible. $this->assertObjectHasProperty('locks', $PERF); $this->assertEquals('phpunit', $PERF->locks[0]->type); $this->assertEquals('frog', $PERF->locks[0]->resource); $this->assertTrue($PERF->locks[0]->wait <= $duration); $this->assertTrue($PERF->locks[0]->success); $this->assertTrue($PERF->locks[0]->held <= $duration); $this->assertEquals('phpunit', $PERF->locks[1]->type); $this->assertEquals('toad', $PERF->locks[1]->resource); $this->assertTrue($PERF->locks[1]->wait <= $duration); $this->assertTrue($PERF->locks[1]->success); $this->assertTrue($PERF->locks[1]->held <= $duration); } } postgres_lock_factory_test.php000064400000006016152267221320012726 0ustar00. namespace core\lock; /** * Unit tests for the postgres lock factory. * * @covers \core\lock\postgres_lock_factory * @package core * @copyright 2024 Martin Gauk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class postgres_lock_factory_test extends \advanced_testcase { /** * Set up. */ public function setUp(): void { global $DB; parent::setUp(); // Skip tests if not using Postgres. if (!($DB instanceof \pgsql_native_moodle_database)) { $this->markTestSkipped('Postgres-only test'); } } /** * Test locking resources that lead to the same token computed by postgres_lock_factory::get_index_from_key. * * It is known that get_index_from_key computes the same token for the strings 'cron_core_cron' and * 'cron_adhoc_82795762'. */ public function test_resources_with_same_hash(): void { $cronlockfactory = new postgres_lock_factory('cron'); $lock1 = $cronlockfactory->get_lock('core_cron', 0); $this->assertNotFalse($lock1); $lock2 = $cronlockfactory->get_lock('adhoc_82795762', 0); $this->assertNotFalse($lock2); $lock2->release(); $lock1->release(); } /** * Test auto_release() method. * * Check that all locks were released after calling auto_release(). */ public function test_auto_release(): void { $cronlockfactory = new postgres_lock_factory('test'); $lock1 = $cronlockfactory->get_lock('1', 0); $this->assertNotFalse($lock1); $lock2 = $cronlockfactory->get_lock('2', 0); $this->assertNotFalse($lock2); // Trying to get the lock again should fail as the lock is already held. $this->assertFalse($cronlockfactory->get_lock('1', 0)); $cronlockfactory->auto_release(); // We can now lock the resources again. $lock1again = $cronlockfactory->get_lock('1', 0); $this->assertNotFalse($lock1again); $lock2again = $cronlockfactory->get_lock('2', 0); $this->assertNotFalse($lock2again); $lock1again->release(); $lock2again->release(); // Need to explicitly release the locks (although they were already released) to avoid the debug message. $lock1->release(); $lock2->release(); } }