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 }