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 }
|