Mercurial > hg > isophonics-drupal-site
comparison core/lib/Drupal/Core/Lock/DatabaseLockBackend.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 129ea1e6d783 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
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 } |