comparison core/lib/Drupal/Core/Cache/DatabaseBackend.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 1fec387a4317
comparison
equal deleted inserted replaced
-1:000000000000 0:4c8ae668cc8c
1 <?php
2
3 namespace Drupal\Core\Cache;
4
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Core\Database\Connection;
7 use Drupal\Core\Database\SchemaObjectExistsException;
8
9 /**
10 * Defines a default cache implementation.
11 *
12 * This is Drupal's default cache implementation. It uses the database to store
13 * cached data. Each cache bin corresponds to a database table by the same name.
14 *
15 * @ingroup cache
16 */
17 class DatabaseBackend implements CacheBackendInterface {
18
19 /**
20 * The default maximum number of rows that this cache bin table can store.
21 *
22 * This maximum is introduced to ensure that the database is not filled with
23 * hundred of thousand of cache entries with gigabytes in size.
24 *
25 * Read about how to change it in the @link cache Cache API topic. @endlink
26 */
27 const DEFAULT_MAX_ROWS = 5000;
28
29 /**
30 * -1 means infinite allows numbers of rows for the cache backend.
31 */
32 const MAXIMUM_NONE = -1;
33
34 /**
35 * The maximum number of rows that this cache bin table is allowed to store.
36 *
37 * @see ::MAXIMUM_NONE
38 *
39 * @var int
40 */
41 protected $maxRows;
42
43 /**
44 * @var string
45 */
46 protected $bin;
47
48
49 /**
50 * The database connection.
51 *
52 * @var \Drupal\Core\Database\Connection
53 */
54 protected $connection;
55
56 /**
57 * The cache tags checksum provider.
58 *
59 * @var \Drupal\Core\Cache\CacheTagsChecksumInterface
60 */
61 protected $checksumProvider;
62
63 /**
64 * Constructs a DatabaseBackend object.
65 *
66 * @param \Drupal\Core\Database\Connection $connection
67 * The database connection.
68 * @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider
69 * The cache tags checksum provider.
70 * @param string $bin
71 * The cache bin for which the object is created.
72 * @param int $max_rows
73 * (optional) The maximum number of rows that are allowed in this cache bin
74 * table.
75 */
76 public function __construct(Connection $connection, CacheTagsChecksumInterface $checksum_provider, $bin, $max_rows = NULL) {
77 // All cache tables should be prefixed with 'cache_'.
78 $bin = 'cache_' . $bin;
79
80 $this->bin = $bin;
81 $this->connection = $connection;
82 $this->checksumProvider = $checksum_provider;
83 $this->maxRows = $max_rows === NULL ? static::DEFAULT_MAX_ROWS : $max_rows;
84 }
85
86 /**
87 * {@inheritdoc}
88 */
89 public function get($cid, $allow_invalid = FALSE) {
90 $cids = [$cid];
91 $cache = $this->getMultiple($cids, $allow_invalid);
92 return reset($cache);
93 }
94
95 /**
96 * {@inheritdoc}
97 */
98 public function getMultiple(&$cids, $allow_invalid = FALSE) {
99 $cid_mapping = [];
100 foreach ($cids as $cid) {
101 $cid_mapping[$this->normalizeCid($cid)] = $cid;
102 }
103 // When serving cached pages, the overhead of using ::select() was found
104 // to add around 30% overhead to the request. Since $this->bin is a
105 // variable, this means the call to ::query() here uses a concatenated
106 // string. This is highly discouraged under any other circumstances, and
107 // is used here only due to the performance overhead we would incur
108 // otherwise. When serving an uncached page, the overhead of using
109 // ::select() is a much smaller proportion of the request.
110 $result = [];
111 try {
112 $result = $this->connection->query('SELECT cid, data, created, expire, serialized, tags, checksum FROM {' . $this->connection->escapeTable($this->bin) . '} WHERE cid IN ( :cids[] ) ORDER BY cid', [':cids[]' => array_keys($cid_mapping)]);
113 }
114 catch (\Exception $e) {
115 // Nothing to do.
116 }
117 $cache = [];
118 foreach ($result as $item) {
119 // Map the cache ID back to the original.
120 $item->cid = $cid_mapping[$item->cid];
121 $item = $this->prepareItem($item, $allow_invalid);
122 if ($item) {
123 $cache[$item->cid] = $item;
124 }
125 }
126 $cids = array_diff($cids, array_keys($cache));
127 return $cache;
128 }
129
130 /**
131 * Prepares a cached item.
132 *
133 * Checks that items are either permanent or did not expire, and unserializes
134 * data as appropriate.
135 *
136 * @param object $cache
137 * An item loaded from cache_get() or cache_get_multiple().
138 * @param bool $allow_invalid
139 * If FALSE, the method returns FALSE if the cache item is not valid.
140 *
141 * @return mixed|false
142 * The item with data unserialized as appropriate and a property indicating
143 * whether the item is valid, or FALSE if there is no valid item to load.
144 */
145 protected function prepareItem($cache, $allow_invalid) {
146 if (!isset($cache->data)) {
147 return FALSE;
148 }
149
150 $cache->tags = $cache->tags ? explode(' ', $cache->tags) : [];
151
152 // Check expire time.
153 $cache->valid = $cache->expire == Cache::PERMANENT || $cache->expire >= REQUEST_TIME;
154
155 // Check if invalidateTags() has been called with any of the items's tags.
156 if (!$this->checksumProvider->isValid($cache->checksum, $cache->tags)) {
157 $cache->valid = FALSE;
158 }
159
160 if (!$allow_invalid && !$cache->valid) {
161 return FALSE;
162 }
163
164 // Unserialize and return the cached data.
165 if ($cache->serialized) {
166 $cache->data = unserialize($cache->data);
167 }
168
169 return $cache;
170 }
171
172 /**
173 * {@inheritdoc}
174 */
175 public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) {
176 $this->setMultiple([
177 $cid => [
178 'data' => $data,
179 'expire' => $expire,
180 'tags' => $tags,
181 ],
182 ]);
183 }
184
185 /**
186 * {@inheritdoc}
187 */
188 public function setMultiple(array $items) {
189 $try_again = FALSE;
190 try {
191 // The bin might not yet exist.
192 $this->doSetMultiple($items);
193 }
194 catch (\Exception $e) {
195 // If there was an exception, try to create the bins.
196 if (!$try_again = $this->ensureBinExists()) {
197 // If the exception happened for other reason than the missing bin
198 // table, propagate the exception.
199 throw $e;
200 }
201 }
202 // Now that the bin has been created, try again if necessary.
203 if ($try_again) {
204 $this->doSetMultiple($items);
205 }
206 }
207
208 /**
209 * Stores multiple items in the persistent cache.
210 *
211 * @param array $items
212 * An array of cache items, keyed by cid.
213 *
214 * @see \Drupal\Core\Cache\CacheBackendInterface::setMultiple()
215 */
216 protected function doSetMultiple(array $items) {
217 $values = [];
218
219 foreach ($items as $cid => $item) {
220 $item += [
221 'expire' => CacheBackendInterface::CACHE_PERMANENT,
222 'tags' => [],
223 ];
224
225 assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($item[\'tags\'])', 'Cache Tags must be strings.');
226 $item['tags'] = array_unique($item['tags']);
227 // Sort the cache tags so that they are stored consistently in the DB.
228 sort($item['tags']);
229
230 $fields = [
231 'cid' => $this->normalizeCid($cid),
232 'expire' => $item['expire'],
233 'created' => round(microtime(TRUE), 3),
234 'tags' => implode(' ', $item['tags']),
235 'checksum' => $this->checksumProvider->getCurrentChecksum($item['tags']),
236 ];
237
238 if (!is_string($item['data'])) {
239 $fields['data'] = serialize($item['data']);
240 $fields['serialized'] = 1;
241 }
242 else {
243 $fields['data'] = $item['data'];
244 $fields['serialized'] = 0;
245 }
246 $values[] = $fields;
247 }
248
249 // Use an upsert query which is atomic and optimized for multiple-row
250 // merges.
251 $query = $this->connection
252 ->upsert($this->bin)
253 ->key('cid')
254 ->fields(['cid', 'expire', 'created', 'tags', 'checksum', 'data', 'serialized']);
255 foreach ($values as $fields) {
256 // Only pass the values since the order of $fields matches the order of
257 // the insert fields. This is a performance optimization to avoid
258 // unnecessary loops within the method.
259 $query->values(array_values($fields));
260 }
261
262 $query->execute();
263 }
264
265 /**
266 * {@inheritdoc}
267 */
268 public function delete($cid) {
269 $this->deleteMultiple([$cid]);
270 }
271
272 /**
273 * {@inheritdoc}
274 */
275 public function deleteMultiple(array $cids) {
276 $cids = array_values(array_map([$this, 'normalizeCid'], $cids));
277 try {
278 // Delete in chunks when a large array is passed.
279 foreach (array_chunk($cids, 1000) as $cids_chunk) {
280 $this->connection->delete($this->bin)
281 ->condition('cid', $cids_chunk, 'IN')
282 ->execute();
283 }
284 }
285 catch (\Exception $e) {
286 // Create the cache table, which will be empty. This fixes cases during
287 // core install where a cache table is cleared before it is set
288 // with {cache_render} and {cache_data}.
289 if (!$this->ensureBinExists()) {
290 $this->catchException($e);
291 }
292 }
293 }
294
295 /**
296 * {@inheritdoc}
297 */
298 public function deleteAll() {
299 try {
300 $this->connection->truncate($this->bin)->execute();
301 }
302 catch (\Exception $e) {
303 // Create the cache table, which will be empty. This fixes cases during
304 // core install where a cache table is cleared before it is set
305 // with {cache_render} and {cache_data}.
306 if (!$this->ensureBinExists()) {
307 $this->catchException($e);
308 }
309 }
310 }
311
312 /**
313 * {@inheritdoc}
314 */
315 public function invalidate($cid) {
316 $this->invalidateMultiple([$cid]);
317 }
318
319 /**
320 * {@inheritdoc}
321 */
322 public function invalidateMultiple(array $cids) {
323 $cids = array_values(array_map([$this, 'normalizeCid'], $cids));
324 try {
325 // Update in chunks when a large array is passed.
326 foreach (array_chunk($cids, 1000) as $cids_chunk) {
327 $this->connection->update($this->bin)
328 ->fields(['expire' => REQUEST_TIME - 1])
329 ->condition('cid', $cids_chunk, 'IN')
330 ->execute();
331 }
332 }
333 catch (\Exception $e) {
334 $this->catchException($e);
335 }
336 }
337
338 /**
339 * {@inheritdoc}
340 */
341 public function invalidateAll() {
342 try {
343 $this->connection->update($this->bin)
344 ->fields(['expire' => REQUEST_TIME - 1])
345 ->execute();
346 }
347 catch (\Exception $e) {
348 $this->catchException($e);
349 }
350 }
351
352 /**
353 * {@inheritdoc}
354 */
355 public function garbageCollection() {
356 try {
357 // Bounded size cache bin, using FIFO.
358 if ($this->maxRows !== static::MAXIMUM_NONE) {
359 $first_invalid_create_time = $this->connection->select($this->bin)
360 ->fields($this->bin, ['created'])
361 ->orderBy("{$this->bin}.created", 'DESC')
362 ->range($this->maxRows, $this->maxRows + 1)
363 ->execute()
364 ->fetchField();
365
366 if ($first_invalid_create_time) {
367 $this->connection->delete($this->bin)
368 ->condition('created', $first_invalid_create_time, '<=')
369 ->execute();
370 }
371 }
372
373 $this->connection->delete($this->bin)
374 ->condition('expire', Cache::PERMANENT, '<>')
375 ->condition('expire', REQUEST_TIME, '<')
376 ->execute();
377 }
378 catch (\Exception $e) {
379 // If the table does not exist, it surely does not have garbage in it.
380 // If the table exists, the next garbage collection will clean up.
381 // There is nothing to do.
382 }
383 }
384
385 /**
386 * {@inheritdoc}
387 */
388 public function removeBin() {
389 try {
390 $this->connection->schema()->dropTable($this->bin);
391 }
392 catch (\Exception $e) {
393 $this->catchException($e);
394 }
395 }
396
397 /**
398 * Check if the cache bin exists and create it if not.
399 */
400 protected function ensureBinExists() {
401 try {
402 $database_schema = $this->connection->schema();
403 if (!$database_schema->tableExists($this->bin)) {
404 $schema_definition = $this->schemaDefinition();
405 $database_schema->createTable($this->bin, $schema_definition);
406 return TRUE;
407 }
408 }
409 // If another process has already created the cache table, attempting to
410 // recreate it will throw an exception. In this case just catch the
411 // exception and do nothing.
412 catch (SchemaObjectExistsException $e) {
413 return TRUE;
414 }
415 return FALSE;
416 }
417
418 /**
419 * Act on an exception when cache might be stale.
420 *
421 * If the table does not yet exist, that's fine, but if the table exists and
422 * yet the query failed, then the cache is stale and the exception needs to
423 * propagate.
424 *
425 * @param $e
426 * The exception.
427 * @param string|null $table_name
428 * The table name. Defaults to $this->bin.
429 *
430 * @throws \Exception
431 */
432 protected function catchException(\Exception $e, $table_name = NULL) {
433 if ($this->connection->schema()->tableExists($table_name ?: $this->bin)) {
434 throw $e;
435 }
436 }
437
438 /**
439 * Normalizes a cache ID in order to comply with database limitations.
440 *
441 * @param string $cid
442 * The passed in cache ID.
443 *
444 * @return string
445 * An ASCII-encoded cache ID that is at most 255 characters long.
446 */
447 protected function normalizeCid($cid) {
448 // Nothing to do if the ID is a US ASCII string of 255 characters or less.
449 $cid_is_ascii = mb_check_encoding($cid, 'ASCII');
450 if (strlen($cid) <= 255 && $cid_is_ascii) {
451 return $cid;
452 }
453 // Return a string that uses as much as possible of the original cache ID
454 // with the hash appended.
455 $hash = Crypt::hashBase64($cid);
456 if (!$cid_is_ascii) {
457 return $hash;
458 }
459 return substr($cid, 0, 255 - strlen($hash)) . $hash;
460 }
461
462 /**
463 * Defines the schema for the {cache_*} bin tables.
464 *
465 * @internal
466 */
467 public function schemaDefinition() {
468 $schema = [
469 'description' => 'Storage for the cache API.',
470 'fields' => [
471 'cid' => [
472 'description' => 'Primary Key: Unique cache ID.',
473 'type' => 'varchar_ascii',
474 'length' => 255,
475 'not null' => TRUE,
476 'default' => '',
477 'binary' => TRUE,
478 ],
479 'data' => [
480 'description' => 'A collection of data to cache.',
481 'type' => 'blob',
482 'not null' => FALSE,
483 'size' => 'big',
484 ],
485 'expire' => [
486 'description' => 'A Unix timestamp indicating when the cache entry should expire, or ' . Cache::PERMANENT . ' for never.',
487 'type' => 'int',
488 'not null' => TRUE,
489 'default' => 0,
490 ],
491 'created' => [
492 'description' => 'A timestamp with millisecond precision indicating when the cache entry was created.',
493 'type' => 'numeric',
494 'precision' => 14,
495 'scale' => 3,
496 'not null' => TRUE,
497 'default' => 0,
498 ],
499 'serialized' => [
500 'description' => 'A flag to indicate whether content is serialized (1) or not (0).',
501 'type' => 'int',
502 'size' => 'small',
503 'not null' => TRUE,
504 'default' => 0,
505 ],
506 'tags' => [
507 'description' => 'Space-separated list of cache tags for this entry.',
508 'type' => 'text',
509 'size' => 'big',
510 'not null' => FALSE,
511 ],
512 'checksum' => [
513 'description' => 'The tag invalidation checksum when this entry was saved.',
514 'type' => 'varchar_ascii',
515 'length' => 255,
516 'not null' => TRUE,
517 ],
518 ],
519 'indexes' => [
520 'expire' => ['expire'],
521 'created' => ['created'],
522 ],
523 'primary key' => ['cid'],
524 ];
525 return $schema;
526 }
527
528 /**
529 * The maximum number of rows that this cache bin table is allowed to store.
530 *
531 * @return int
532 */
533 public function getMaxRows() {
534 return $this->maxRows;
535 }
536
537 }