3 namespace Drupal\Core\Lock;
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Core\Database\Connection;
7 use Drupal\Core\Database\IntegrityConstraintViolationException;
8 use Drupal\Core\Database\SchemaObjectExistsException;
11 * Defines the database lock backend. This is the default backend in Drupal.
15 class DatabaseLockBackend extends LockBackendAbstract {
18 * The database table name.
20 const TABLE_NAME = 'semaphore';
23 * The database connection.
25 * @var \Drupal\Core\Database\Connection
30 * Constructs a new DatabaseLockBackend.
32 * @param \Drupal\Core\Database\Connection $database
33 * The database connection.
35 public function __construct(Connection $database) {
36 // __destruct() is causing problems with garbage collections, register a
37 // shutdown function instead.
38 drupal_register_shutdown_function([$this, 'releaseAll']);
39 $this->database = $database;
45 public function acquire($name, $timeout = 30.0) {
46 $name = $this->normalizeName($name);
48 // Insure that the timeout is at least 1 ms.
49 $timeout = max($timeout, 0.001);
50 $expire = microtime(TRUE) + $timeout;
51 if (isset($this->locks[$name])) {
52 // Try to extend the expiration of a lock we already acquired.
53 $success = (bool) $this->database->update('semaphore')
54 ->fields(['expire' => $expire])
55 ->condition('name', $name)
56 ->condition('value', $this->getLockId())
59 // The lock was broken.
60 unset($this->locks[$name]);
65 // Optimistically try to acquire the lock, then retry once if it fails.
66 // The first time through the loop cannot be a retry.
68 // We always want to do this code at least once.
71 $this->database->insert('semaphore')
74 'value' => $this->getLockId(),
78 // We track all acquired locks in the global variable.
79 $this->locks[$name] = TRUE;
80 // We never need to try again.
83 catch (IntegrityConstraintViolationException $e) {
84 // Suppress the error. If this is our first pass through the loop,
85 // then $retry is FALSE. In this case, the insert failed because some
86 // other request acquired the lock but did not release it. We decide
87 // whether to retry by checking lockMayBeAvailable(). This will clear
88 // the offending row from the database table in case it has expired.
89 $retry = $retry ? FALSE : $this->lockMayBeAvailable($name);
91 catch (\Exception $e) {
92 // Create the semaphore table if it does not exist and retry.
93 if ($this->ensureTableExists()) {
101 // We only retry in case the first attempt failed, but we then broke
105 return isset($this->locks[$name]);
111 public function lockMayBeAvailable($name) {
112 $name = $this->normalizeName($name);
115 $lock = $this->database->query('SELECT expire, value FROM {semaphore} WHERE name = :name', [':name' => $name])->fetchAssoc();
117 catch (\Exception $e) {
118 $this->catchException($e);
119 // If the table does not exist yet then the lock may be available.
125 $expire = (float) $lock['expire'];
126 $now = microtime(TRUE);
127 if ($now > $expire) {
128 // We check two conditions to prevent a race condition where another
129 // request acquired the lock and set a new expire time. We add a small
130 // number to $expire to avoid errors with float to string conversion.
131 return (bool) $this->database->delete('semaphore')
132 ->condition('name', $name)
133 ->condition('value', $lock['value'])
134 ->condition('expire', 0.0001 + $expire, '<=')
143 public function release($name) {
144 $name = $this->normalizeName($name);
146 unset($this->locks[$name]);
148 $this->database->delete('semaphore')
149 ->condition('name', $name)
150 ->condition('value', $this->getLockId())
153 catch (\Exception $e) {
154 $this->catchException($e);
161 public function releaseAll($lock_id = NULL) {
162 // Only attempt to release locks if any were acquired.
163 if (!empty($this->locks)) {
165 if (empty($lock_id)) {
166 $lock_id = $this->getLockId();
168 $this->database->delete('semaphore')
169 ->condition('value', $lock_id)
175 * Check if the semaphore table exists and create it if not.
177 protected function ensureTableExists() {
179 $database_schema = $this->database->schema();
180 if (!$database_schema->tableExists(static::TABLE_NAME)) {
181 $schema_definition = $this->schemaDefinition();
182 $database_schema->createTable(static::TABLE_NAME, $schema_definition);
186 // If another process has already created the semaphore table, attempting to
187 // recreate it will throw an exception. In this case just catch the
188 // exception and do nothing.
189 catch (SchemaObjectExistsException $e) {
196 * Act on an exception when semaphore might be stale.
198 * If the table does not yet exist, that's fine, but if the table exists and
199 * yet the query failed, then the semaphore is stale and the exception needs
207 protected function catchException(\Exception $e) {
208 if ($this->database->schema()->tableExists(static::TABLE_NAME)) {
214 * Normalizes a lock name in order to comply with database limitations.
216 * @param string $name
217 * The passed in lock name.
220 * An ASCII-encoded lock name that is at most 255 characters long.
222 protected function normalizeName($name) {
223 // Nothing to do if the name is a US ASCII string of 255 characters or less.
224 $name_is_ascii = mb_check_encoding($name, 'ASCII');
226 if (strlen($name) <= 255 && $name_is_ascii) {
229 // Return a string that uses as much as possible of the original name with
230 // the hash appended.
231 $hash = Crypt::hashBase64($name);
233 if (!$name_is_ascii) {
237 return substr($name, 0, 255 - strlen($hash)) . $hash;
241 * Defines the schema for the semaphore table.
245 public function schemaDefinition() {
247 'description' => 'Table for holding semaphores, locks, flags, etc. that cannot be stored as state since they must not be cached.',
250 'description' => 'Primary Key: Unique name.',
251 'type' => 'varchar_ascii',
257 'description' => 'A value for the semaphore.',
258 'type' => 'varchar_ascii',
264 'description' => 'A Unix timestamp with microseconds indicating when the semaphore should expire.',
271 'value' => ['value'],
272 'expire' => ['expire'],
274 'primary key' => ['name'],