DatabaseLockBackend.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. <?php
  2. namespace Drupal\Core\Lock;
  3. use Drupal\Component\Utility\Crypt;
  4. use Drupal\Core\Database\Connection;
  5. use Drupal\Core\Database\DatabaseException;
  6. use Drupal\Core\Database\IntegrityConstraintViolationException;
  7. /**
  8. * Defines the database lock backend. This is the default backend in Drupal.
  9. *
  10. * @ingroup lock
  11. */
  12. class DatabaseLockBackend extends LockBackendAbstract {
  13. /**
  14. * The database table name.
  15. */
  16. const TABLE_NAME = 'semaphore';
  17. /**
  18. * The database connection.
  19. *
  20. * @var \Drupal\Core\Database\Connection
  21. */
  22. protected $database;
  23. /**
  24. * Constructs a new DatabaseLockBackend.
  25. *
  26. * @param \Drupal\Core\Database\Connection $database
  27. * The database connection.
  28. */
  29. public function __construct(Connection $database) {
  30. // __destruct() is causing problems with garbage collections, register a
  31. // shutdown function instead.
  32. drupal_register_shutdown_function([$this, 'releaseAll']);
  33. $this->database = $database;
  34. }
  35. /**
  36. * {@inheritdoc}
  37. */
  38. public function acquire($name, $timeout = 30.0) {
  39. $name = $this->normalizeName($name);
  40. // Insure that the timeout is at least 1 ms.
  41. $timeout = max($timeout, 0.001);
  42. $expire = microtime(TRUE) + $timeout;
  43. if (isset($this->locks[$name])) {
  44. // Try to extend the expiration of a lock we already acquired.
  45. $success = (bool) $this->database->update('semaphore')
  46. ->fields(['expire' => $expire])
  47. ->condition('name', $name)
  48. ->condition('value', $this->getLockId())
  49. ->execute();
  50. if (!$success) {
  51. // The lock was broken.
  52. unset($this->locks[$name]);
  53. }
  54. return $success;
  55. }
  56. else {
  57. // Optimistically try to acquire the lock, then retry once if it fails.
  58. // The first time through the loop cannot be a retry.
  59. $retry = FALSE;
  60. // We always want to do this code at least once.
  61. do {
  62. try {
  63. $this->database->insert('semaphore')
  64. ->fields([
  65. 'name' => $name,
  66. 'value' => $this->getLockId(),
  67. 'expire' => $expire,
  68. ])
  69. ->execute();
  70. // We track all acquired locks in the global variable.
  71. $this->locks[$name] = TRUE;
  72. // We never need to try again.
  73. $retry = FALSE;
  74. }
  75. catch (IntegrityConstraintViolationException $e) {
  76. // Suppress the error. If this is our first pass through the loop,
  77. // then $retry is FALSE. In this case, the insert failed because some
  78. // other request acquired the lock but did not release it. We decide
  79. // whether to retry by checking lockMayBeAvailable(). This will clear
  80. // the offending row from the database table in case it has expired.
  81. $retry = $retry ? FALSE : $this->lockMayBeAvailable($name);
  82. }
  83. catch (\Exception $e) {
  84. // Create the semaphore table if it does not exist and retry.
  85. if ($this->ensureTableExists()) {
  86. // Retry only once.
  87. $retry = !$retry;
  88. }
  89. else {
  90. throw $e;
  91. }
  92. }
  93. // We only retry in case the first attempt failed, but we then broke
  94. // an expired lock.
  95. } while ($retry);
  96. }
  97. return isset($this->locks[$name]);
  98. }
  99. /**
  100. * {@inheritdoc}
  101. */
  102. public function lockMayBeAvailable($name) {
  103. $name = $this->normalizeName($name);
  104. try {
  105. $lock = $this->database->query('SELECT expire, value FROM {semaphore} WHERE name = :name', [':name' => $name])->fetchAssoc();
  106. }
  107. catch (\Exception $e) {
  108. $this->catchException($e);
  109. // If the table does not exist yet then the lock may be available.
  110. $lock = FALSE;
  111. }
  112. if (!$lock) {
  113. return TRUE;
  114. }
  115. $expire = (float) $lock['expire'];
  116. $now = microtime(TRUE);
  117. if ($now > $expire) {
  118. // We check two conditions to prevent a race condition where another
  119. // request acquired the lock and set a new expire time. We add a small
  120. // number to $expire to avoid errors with float to string conversion.
  121. return (bool) $this->database->delete('semaphore')
  122. ->condition('name', $name)
  123. ->condition('value', $lock['value'])
  124. ->condition('expire', 0.0001 + $expire, '<=')
  125. ->execute();
  126. }
  127. return FALSE;
  128. }
  129. /**
  130. * {@inheritdoc}
  131. */
  132. public function release($name) {
  133. $name = $this->normalizeName($name);
  134. unset($this->locks[$name]);
  135. try {
  136. $this->database->delete('semaphore')
  137. ->condition('name', $name)
  138. ->condition('value', $this->getLockId())
  139. ->execute();
  140. }
  141. catch (\Exception $e) {
  142. $this->catchException($e);
  143. }
  144. }
  145. /**
  146. * {@inheritdoc}
  147. */
  148. public function releaseAll($lock_id = NULL) {
  149. // Only attempt to release locks if any were acquired.
  150. if (!empty($this->locks)) {
  151. $this->locks = [];
  152. if (empty($lock_id)) {
  153. $lock_id = $this->getLockId();
  154. }
  155. $this->database->delete('semaphore')
  156. ->condition('value', $lock_id)
  157. ->execute();
  158. }
  159. }
  160. /**
  161. * Check if the semaphore table exists and create it if not.
  162. */
  163. protected function ensureTableExists() {
  164. try {
  165. $database_schema = $this->database->schema();
  166. if (!$database_schema->tableExists(static::TABLE_NAME)) {
  167. $schema_definition = $this->schemaDefinition();
  168. $database_schema->createTable(static::TABLE_NAME, $schema_definition);
  169. return TRUE;
  170. }
  171. }
  172. // If another process has already created the semaphore table, attempting to
  173. // recreate it will throw an exception. In this case just catch the
  174. // exception and do nothing.
  175. catch (DatabaseException $e) {
  176. return TRUE;
  177. }
  178. return FALSE;
  179. }
  180. /**
  181. * Act on an exception when semaphore might be stale.
  182. *
  183. * If the table does not yet exist, that's fine, but if the table exists and
  184. * yet the query failed, then the semaphore is stale and the exception needs
  185. * to propagate.
  186. *
  187. * @param $e
  188. * The exception.
  189. *
  190. * @throws \Exception
  191. */
  192. protected function catchException(\Exception $e) {
  193. if ($this->database->schema()->tableExists(static::TABLE_NAME)) {
  194. throw $e;
  195. }
  196. }
  197. /**
  198. * Normalizes a lock name in order to comply with database limitations.
  199. *
  200. * @param string $name
  201. * The passed in lock name.
  202. *
  203. * @return string
  204. * An ASCII-encoded lock name that is at most 255 characters long.
  205. */
  206. protected function normalizeName($name) {
  207. // Nothing to do if the name is a US ASCII string of 255 characters or less.
  208. $name_is_ascii = mb_check_encoding($name, 'ASCII');
  209. if (strlen($name) <= 255 && $name_is_ascii) {
  210. return $name;
  211. }
  212. // Return a string that uses as much as possible of the original name with
  213. // the hash appended.
  214. $hash = Crypt::hashBase64($name);
  215. if (!$name_is_ascii) {
  216. return $hash;
  217. }
  218. return substr($name, 0, 255 - strlen($hash)) . $hash;
  219. }
  220. /**
  221. * Defines the schema for the semaphore table.
  222. *
  223. * @internal
  224. */
  225. public function schemaDefinition() {
  226. return [
  227. 'description' => 'Table for holding semaphores, locks, flags, etc. that cannot be stored as state since they must not be cached.',
  228. 'fields' => [
  229. 'name' => [
  230. 'description' => 'Primary Key: Unique name.',
  231. 'type' => 'varchar_ascii',
  232. 'length' => 255,
  233. 'not null' => TRUE,
  234. 'default' => '',
  235. ],
  236. 'value' => [
  237. 'description' => 'A value for the semaphore.',
  238. 'type' => 'varchar_ascii',
  239. 'length' => 255,
  240. 'not null' => TRUE,
  241. 'default' => '',
  242. ],
  243. 'expire' => [
  244. 'description' => 'A Unix timestamp with microseconds indicating when the semaphore should expire.',
  245. 'type' => 'float',
  246. 'size' => 'big',
  247. 'not null' => TRUE,
  248. ],
  249. ],
  250. 'indexes' => [
  251. 'value' => ['value'],
  252. 'expire' => ['expire'],
  253. ],
  254. 'primary key' => ['name'],
  255. ];
  256. }
  257. }