PageRenderTime 68ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/classes/lock/db_record_lock_factory.php

https://github.com/rwijaya/moodle
PHP | 254 lines | 118 code | 38 blank | 98 comment | 9 complexity | fd6da68147a8bcce5d6d0305e48241d4 MD5 | raw file
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * This is a db record locking factory.
  18. *
  19. * @package core
  20. * @category lock
  21. * @copyright Damyon Wiese 2013
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. namespace core\lock;
  25. defined('MOODLE_INTERNAL') || die();
  26. /**
  27. * This is a db record locking factory.
  28. *
  29. * This lock factory uses record locks relying on sql of the form "SET XXX where YYY" and checking if the
  30. * value was set. It supports timeouts, autorelease and can work on any DB. The downside - is this
  31. * will always be slower than some shared memory type locking function.
  32. *
  33. * @package core
  34. * @category lock
  35. * @copyright Damyon Wiese 2013
  36. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37. */
  38. class db_record_lock_factory implements lock_factory {
  39. /** @var \moodle_database $db Hold a reference to the global $DB */
  40. protected $db;
  41. /** @var string $type Used to prefix lock keys */
  42. protected $type;
  43. /** @var array $openlocks - List of held locks - used by auto-release */
  44. protected $openlocks = array();
  45. /**
  46. * Is available.
  47. * @return boolean - True if this lock type is available in this environment.
  48. */
  49. public function is_available() {
  50. return true;
  51. }
  52. /**
  53. * Almighty constructor.
  54. * @param string $type - Used to prefix lock keys.
  55. */
  56. public function __construct($type) {
  57. global $DB;
  58. $this->type = $type;
  59. // Save a reference to the global $DB so it will not be released while we still have open locks.
  60. $this->db = $DB;
  61. \core_shutdown_manager::register_function(array($this, 'auto_release'));
  62. }
  63. /**
  64. * Return information about the blocking behaviour of the lock type on this platform.
  65. * @return boolean - True
  66. */
  67. public function supports_timeout() {
  68. return true;
  69. }
  70. /**
  71. * Will this lock type will be automatically released when a process ends.
  72. *
  73. * @return boolean - True (shutdown handler)
  74. */
  75. public function supports_auto_release() {
  76. return true;
  77. }
  78. /**
  79. * Multiple locks for the same resource can be held by a single process.
  80. * @return boolean - False - not process specific.
  81. */
  82. public function supports_recursion() {
  83. return false;
  84. }
  85. /**
  86. * This function generates a unique token for the lock to use.
  87. * It is important that this token is not solely based on time as this could lead
  88. * to duplicates in a clustered environment (especially on VMs due to poor time precision).
  89. */
  90. protected function generate_unique_token() {
  91. $uuid = '';
  92. if (function_exists("uuid_create")) {
  93. $context = null;
  94. uuid_create($context);
  95. uuid_make($context, UUID_MAKE_V4);
  96. uuid_export($context, UUID_FMT_STR, $uuid);
  97. } else {
  98. // Fallback uuid generation based on:
  99. // "http://www.php.net/manual/en/function.uniqid.php#94959".
  100. $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
  101. // 32 bits for "time_low".
  102. mt_rand(0, 0xffff), mt_rand(0, 0xffff),
  103. // 16 bits for "time_mid".
  104. mt_rand(0, 0xffff),
  105. // 16 bits for "time_hi_and_version",
  106. // four most significant bits holds version number 4.
  107. mt_rand(0, 0x0fff) | 0x4000,
  108. // 16 bits, 8 bits for "clk_seq_hi_res",
  109. // 8 bits for "clk_seq_low",
  110. // two most significant bits holds zero and one for variant DCE1.1.
  111. mt_rand(0, 0x3fff) | 0x8000,
  112. // 48 bits for "node".
  113. mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff));
  114. }
  115. return trim($uuid);
  116. }
  117. /**
  118. * Create and get a lock
  119. * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
  120. * @param int $timeout - The number of seconds to wait for a lock before giving up.
  121. * @param int $maxlifetime - Unused by this lock type.
  122. * @return boolean - true if a lock was obtained.
  123. */
  124. public function get_lock($resource, $timeout, $maxlifetime = 86400) {
  125. $token = $this->generate_unique_token();
  126. $now = time();
  127. $giveuptime = $now + $timeout;
  128. $expires = $now + $maxlifetime;
  129. if (!$this->db->record_exists('lock_db', array('resourcekey' => $resource))) {
  130. $record = new \stdClass();
  131. $record->resourcekey = $resource;
  132. $result = $this->db->insert_record('lock_db', $record);
  133. }
  134. $params = array('expires' => $expires,
  135. 'token' => $token,
  136. 'resourcekey' => $resource,
  137. 'now' => $now);
  138. $sql = 'UPDATE {lock_db}
  139. SET
  140. expires = :expires,
  141. owner = :token
  142. WHERE
  143. resourcekey = :resourcekey AND
  144. (owner IS NULL OR expires < :now)';
  145. do {
  146. $now = time();
  147. $params['now'] = $now;
  148. $this->db->execute($sql, $params);
  149. $countparams = array('owner' => $token, 'resourcekey' => $resource);
  150. $result = $this->db->count_records('lock_db', $countparams);
  151. $locked = $result === 1;
  152. if (!$locked) {
  153. usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
  154. }
  155. // Try until the giveup time.
  156. } while (!$locked && $now < $giveuptime);
  157. if ($locked) {
  158. $this->openlocks[$token] = 1;
  159. return new lock($token, $this);
  160. }
  161. return false;
  162. }
  163. /**
  164. * Release a lock that was previously obtained with @lock.
  165. * @param lock $lock - a lock obtained from this factory.
  166. * @return boolean - true if the lock is no longer held (including if it was never held).
  167. */
  168. public function release_lock(lock $lock) {
  169. $params = array('noexpires' => null,
  170. 'token' => $lock->get_key(),
  171. 'noowner' => null);
  172. $sql = 'UPDATE {lock_db}
  173. SET
  174. expires = :noexpires,
  175. owner = :noowner
  176. WHERE
  177. owner = :token';
  178. $result = $this->db->execute($sql, $params);
  179. if ($result) {
  180. unset($this->openlocks[$lock->get_key()]);
  181. }
  182. return $result;
  183. }
  184. /**
  185. * Extend a lock that was previously obtained with @lock.
  186. * @param lock $lock - a lock obtained from this factory.
  187. * @param int $maxlifetime - the new lifetime for the lock (in seconds).
  188. * @return boolean - true if the lock was extended.
  189. */
  190. public function extend_lock(lock $lock, $maxlifetime = 86400) {
  191. $now = time();
  192. $expires = $now + $maxlifetime;
  193. $params = array('expires' => $expires,
  194. 'token' => $lock->get_key());
  195. $sql = 'UPDATE {lock_db}
  196. SET
  197. expires = :expires,
  198. WHERE
  199. owner = :token';
  200. $this->db->execute($sql, $params);
  201. $countparams = array('owner' => $lock->get_key());
  202. $result = $this->count_records('lock_db', $countparams);
  203. return $result === 0;
  204. }
  205. /**
  206. * Auto release any open locks on shutdown.
  207. * This is required, because we may be using persistent DB connections.
  208. */
  209. public function auto_release() {
  210. // Called from the shutdown handler. Must release all open locks.
  211. foreach ($this->openlocks as $key => $unused) {
  212. $lock = new lock($key, $this);
  213. $this->release_lock($lock);
  214. }
  215. }
  216. }