Pull merge.
[yaffs-website] / web / core / lib / Drupal / Core / Lock / DatabaseLockBackend.php
1 <?php
2
3 namespace Drupal\Core\Lock;
4
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Core\Database\Connection;
7 use Drupal\Core\Database\IntegrityConstraintViolationException;
8 use Drupal\Core\Database\SchemaObjectExistsException;
9
10 /**
11  * Defines the database lock backend. This is the default backend in Drupal.
12  *
13  * @ingroup lock
14  */
15 class DatabaseLockBackend extends LockBackendAbstract {
16
17   /**
18    * The database table name.
19    */
20   const TABLE_NAME = 'semaphore';
21
22   /**
23    * The database connection.
24    *
25    * @var \Drupal\Core\Database\Connection
26    */
27   protected $database;
28
29   /**
30    * Constructs a new DatabaseLockBackend.
31    *
32    * @param \Drupal\Core\Database\Connection $database
33    *   The database connection.
34    */
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;
40   }
41
42   /**
43    * {@inheritdoc}
44    */
45   public function acquire($name, $timeout = 30.0) {
46     $name = $this->normalizeName($name);
47
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())
57         ->execute();
58       if (!$success) {
59         // The lock was broken.
60         unset($this->locks[$name]);
61       }
62       return $success;
63     }
64     else {
65       // Optimistically try to acquire the lock, then retry once if it fails.
66       // The first time through the loop cannot be a retry.
67       $retry = FALSE;
68       // We always want to do this code at least once.
69       do {
70         try {
71           $this->database->insert('semaphore')
72             ->fields([
73               'name' => $name,
74               'value' => $this->getLockId(),
75               'expire' => $expire,
76             ])
77             ->execute();
78           // We track all acquired locks in the global variable.
79           $this->locks[$name] = TRUE;
80           // We never need to try again.
81           $retry = FALSE;
82         }
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);
90         }
91         catch (\Exception $e) {
92           // Create the semaphore table if it does not exist and retry.
93           if ($this->ensureTableExists()) {
94             // Retry only once.
95             $retry = !$retry;
96           }
97           else {
98             throw $e;
99           }
100         }
101         // We only retry in case the first attempt failed, but we then broke
102         // an expired lock.
103       } while ($retry);
104     }
105     return isset($this->locks[$name]);
106   }
107
108   /**
109    * {@inheritdoc}
110    */
111   public function lockMayBeAvailable($name) {
112     $name = $this->normalizeName($name);
113
114     try {
115       $lock = $this->database->query('SELECT expire, value FROM {semaphore} WHERE name = :name', [':name' => $name])->fetchAssoc();
116     }
117     catch (\Exception $e) {
118       $this->catchException($e);
119       // If the table does not exist yet then the lock may be available.
120       $lock = FALSE;
121     }
122     if (!$lock) {
123       return TRUE;
124     }
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, '<=')
135         ->execute();
136     }
137     return FALSE;
138   }
139
140   /**
141    * {@inheritdoc}
142    */
143   public function release($name) {
144     $name = $this->normalizeName($name);
145
146     unset($this->locks[$name]);
147     try {
148       $this->database->delete('semaphore')
149         ->condition('name', $name)
150         ->condition('value', $this->getLockId())
151         ->execute();
152     }
153     catch (\Exception $e) {
154       $this->catchException($e);
155     }
156   }
157
158   /**
159    * {@inheritdoc}
160    */
161   public function releaseAll($lock_id = NULL) {
162     // Only attempt to release locks if any were acquired.
163     if (!empty($this->locks)) {
164       $this->locks = [];
165       if (empty($lock_id)) {
166         $lock_id = $this->getLockId();
167       }
168       $this->database->delete('semaphore')
169         ->condition('value', $lock_id)
170         ->execute();
171     }
172   }
173
174   /**
175    * Check if the semaphore table exists and create it if not.
176    */
177   protected function ensureTableExists() {
178     try {
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);
183         return TRUE;
184       }
185     }
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) {
190       return TRUE;
191     }
192     return FALSE;
193   }
194
195   /**
196    * Act on an exception when semaphore might be stale.
197    *
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
200    * to propagate.
201    *
202    * @param $e
203    *   The exception.
204    *
205    * @throws \Exception
206    */
207   protected function catchException(\Exception $e) {
208     if ($this->database->schema()->tableExists(static::TABLE_NAME)) {
209       throw $e;
210     }
211   }
212
213   /**
214    * Normalizes a lock name in order to comply with database limitations.
215    *
216    * @param string $name
217    *   The passed in lock name.
218    *
219    * @return string
220    *   An ASCII-encoded lock name that is at most 255 characters long.
221    */
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');
225
226     if (strlen($name) <= 255 && $name_is_ascii) {
227       return $name;
228     }
229     // Return a string that uses as much as possible of the original name with
230     // the hash appended.
231     $hash = Crypt::hashBase64($name);
232
233     if (!$name_is_ascii) {
234       return $hash;
235     }
236
237     return substr($name, 0, 255 - strlen($hash)) . $hash;
238   }
239
240   /**
241    * Defines the schema for the semaphore table.
242    *
243    * @internal
244    */
245   public function schemaDefinition() {
246     return [
247       'description' => 'Table for holding semaphores, locks, flags, etc. that cannot be stored as state since they must not be cached.',
248       'fields' => [
249         'name' => [
250           'description' => 'Primary Key: Unique name.',
251           'type' => 'varchar_ascii',
252           'length' => 255,
253           'not null' => TRUE,
254           'default' => '',
255         ],
256         'value' => [
257           'description' => 'A value for the semaphore.',
258           'type' => 'varchar_ascii',
259           'length' => 255,
260           'not null' => TRUE,
261           'default' => '',
262         ],
263         'expire' => [
264           'description' => 'A Unix timestamp with microseconds indicating when the semaphore should expire.',
265           'type' => 'float',
266           'size' => 'big',
267           'not null' => TRUE,
268         ],
269       ],
270       'indexes' => [
271         'value' => ['value'],
272         'expire' => ['expire'],
273       ],
274       'primary key' => ['name'],
275     ];
276   }
277
278 }