3 namespace Drupal\Core\Lock;
5 use Drupal\Core\Database\Connection;
6 use Drupal\Core\Database\IntegrityConstraintViolationException;
7 use Drupal\Core\Database\SchemaObjectExistsException;
10 * Defines the database lock backend. This is the default backend in Drupal.
14 class DatabaseLockBackend extends LockBackendAbstract {
17 * The database table name.
19 const TABLE_NAME = 'semaphore';
22 * The database connection.
24 * @var \Drupal\Core\Database\Connection
29 * Constructs a new DatabaseLockBackend.
31 * @param \Drupal\Core\Database\Connection $database
32 * The database connection.
34 public function __construct(Connection $database) {
35 // __destruct() is causing problems with garbage collections, register a
36 // shutdown function instead.
37 drupal_register_shutdown_function([$this, 'releaseAll']);
38 $this->database = $database;
44 public function acquire($name, $timeout = 30.0) {
45 // Insure that the timeout is at least 1 ms.
46 $timeout = max($timeout, 0.001);
47 $expire = microtime(TRUE) + $timeout;
48 if (isset($this->locks[$name])) {
49 // Try to extend the expiration of a lock we already acquired.
50 $success = (bool) $this->database->update('semaphore')
51 ->fields(['expire' => $expire])
52 ->condition('name', $name)
53 ->condition('value', $this->getLockId())
56 // The lock was broken.
57 unset($this->locks[$name]);
62 // Optimistically try to acquire the lock, then retry once if it fails.
63 // The first time through the loop cannot be a retry.
65 // We always want to do this code at least once.
68 $this->database->insert('semaphore')
71 'value' => $this->getLockId(),
75 // We track all acquired locks in the global variable.
76 $this->locks[$name] = TRUE;
77 // We never need to try again.
80 catch (IntegrityConstraintViolationException $e) {
81 // Suppress the error. If this is our first pass through the loop,
82 // then $retry is FALSE. In this case, the insert failed because some
83 // other request acquired the lock but did not release it. We decide
84 // whether to retry by checking lockMayBeAvailable(). This will clear
85 // the offending row from the database table in case it has expired.
86 $retry = $retry ? FALSE : $this->lockMayBeAvailable($name);
88 catch (\Exception $e) {
89 // Create the semaphore table if it does not exist and retry.
90 if ($this->ensureTableExists()) {
98 // We only retry in case the first attempt failed, but we then broke
102 return isset($this->locks[$name]);
108 public function lockMayBeAvailable($name) {
110 $lock = $this->database->query('SELECT expire, value FROM {semaphore} WHERE name = :name', [':name' => $name])->fetchAssoc();
112 catch (\Exception $e) {
113 $this->catchException($e);
114 // If the table does not exist yet then the lock may be available.
120 $expire = (float) $lock['expire'];
121 $now = microtime(TRUE);
122 if ($now > $expire) {
123 // We check two conditions to prevent a race condition where another
124 // request acquired the lock and set a new expire time. We add a small
125 // number to $expire to avoid errors with float to string conversion.
126 return (bool) $this->database->delete('semaphore')
127 ->condition('name', $name)
128 ->condition('value', $lock['value'])
129 ->condition('expire', 0.0001 + $expire, '<=')
138 public function release($name) {
139 unset($this->locks[$name]);
141 $this->database->delete('semaphore')
142 ->condition('name', $name)
143 ->condition('value', $this->getLockId())
146 catch (\Exception $e) {
147 $this->catchException($e);
154 public function releaseAll($lock_id = NULL) {
155 // Only attempt to release locks if any were acquired.
156 if (!empty($this->locks)) {
158 if (empty($lock_id)) {
159 $lock_id = $this->getLockId();
161 $this->database->delete('semaphore')
162 ->condition('value', $lock_id)
168 * Check if the semaphore table exists and create it if not.
170 protected function ensureTableExists() {
172 $database_schema = $this->database->schema();
173 if (!$database_schema->tableExists(static::TABLE_NAME)) {
174 $schema_definition = $this->schemaDefinition();
175 $database_schema->createTable(static::TABLE_NAME, $schema_definition);
179 // If another process has already created the semaphore table, attempting to
180 // recreate it will throw an exception. In this case just catch the
181 // exception and do nothing.
182 catch (SchemaObjectExistsException $e) {
189 * Act on an exception when semaphore might be stale.
191 * If the table does not yet exist, that's fine, but if the table exists and
192 * yet the query failed, then the semaphore is stale and the exception needs
200 protected function catchException(\Exception $e) {
201 if ($this->database->schema()->tableExists(static::TABLE_NAME)) {
207 * Defines the schema for the semaphore table.
209 public function schemaDefinition() {
211 'description' => 'Table for holding semaphores, locks, flags, etc. that cannot be stored as state since they must not be cached.',
214 'description' => 'Primary Key: Unique name.',
215 'type' => 'varchar_ascii',
221 'description' => 'A value for the semaphore.',
222 'type' => 'varchar_ascii',
228 'description' => 'A Unix timestamp with microseconds indicating when the semaphore should expire.',
235 'value' => ['value'],
236 'expire' => ['expire'],
238 'primary key' => ['name'],