annotate core/lib/Drupal/Core/Lock/DatabaseLockBackend.php @ 19:fa3358dc1485 tip

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