Commit 21812ed3 authored by catch's avatar catch

Issue #1858616 by Berdir: Extract generic CacheCollector implementation and...

Issue #1858616 by Berdir: Extract generic CacheCollector implementation and interface from CacheArray.
parent 5f67fd1d
......@@ -131,7 +131,7 @@ services:
class: Drupal\Core\Path\AliasWhitelist
tags:
- { name: needs_destruction }
arguments: [path_alias_whitelist, cache, '@keyvalue', '@database']
arguments: [path_alias_whitelist, '@cache.cache', '@lock', '@state', '@database']
path.alias_manager:
class: Drupal\Core\Path\AliasManager
arguments: ['@database', '@path.alias_whitelist', '@language_manager']
......
<?php
/**
* @file
* Contains \Drupal\Core\Cache\CacheCollector.
*/
namespace Drupal\Core\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\DestructableInterface;
use Drupal\Core\Lock\LockBackendInterface;
/**
* Default implementation for CacheCollectorInterface.
*
* By default, the class accounts for caches where calling functions might
* request keys that won't exist even after a cache rebuild. This prevents
* situations where a cache rebuild would be triggered over and over due to a
* 'missing' item. These cases are stored internally as a value of NULL. This
* means that the CacheCollector::get() method must be overridden if caching
* data where the values can legitimately be NULL, and where
* CacheCollector->has() needs to correctly return (equivalent to
* array_key_exists() vs. isset()). This should not be necessary in the majority
* of cases.
*/
abstract class CacheCollector implements CacheCollectorInterface, DestructableInterface {
/**
* The cache id that is used for the cache entry.
*
* @var string
*/
protected $cid;
/**
* A list of tags that are used for the cache entry.
*
* @var array
*/
protected $tags;
/**
* The cache backend that should be used.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The lock backend that should be used.
*
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected $lock;
/**
* An array of keys to add to the cache on service termination.
*
* @var array
*/
protected $keysToPersist = array();
/**
* An array of keys to remove from the cache on service termination.
*
* @var array
*/
protected $keysToRemove = array();
/**
* Storage for the data itself.
*
* @var array
*/
protected $storage = array();
/**
* Stores the cache creation time.
*
* This is used to check if an invalidated cache item has been overwritten in
* the meantime.
*
* @var int
*/
protected $cacheCreated;
/**
* Flag that indicates of the cache has been invalidated.
*
* @var bool
*/
protected $cacheInvalidated = FALSE;
/**
* Indicates if the collected cache was already loaded.
*
* The collected cache is lazy loaded when an entry is set, get or deleted.
*
* @var bool
*/
protected $cacheLoaded = FALSE;
/**
* Constructs a CacheCollector object.
*
* @param string $cid
* The cid for the array being cached.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param array $tags
* (optional) The tags to specify for the cache item.
*/
public function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $lock, $tags = array()) {
$this->cid = $cid;
$this->cache = $cache;
$this->tags = $tags;
$this->lock = $lock;
}
/**
* {@inheritdoc}
*/
public function has($key) {
// Make sure the value is loaded.
$this->get($key);
return isset($this->storage[$key]) || array_key_exists($key, $this->storage);
}
/**
* {@inheritdoc}
*/
public function get($key) {
$this->lazyLoadCache();
if (isset($this->storage[$key]) || array_key_exists($key, $this->storage)) {
return $this->storage[$key];
}
else {
return $this->resolveCacheMiss($key);
}
}
/**
* Implements \Drupal\Core\Cache\CacheCollectorInterface::set().
*
* This is not persisted by default. In practice this means that setting a
* value will only apply while the object is in scope and will not be written
* back to the persistent cache. This follows a similar pattern to static vs.
* persistent caching in procedural code. Extending classes may wish to alter
* this behavior, for example by adding a call to persist().
*/
public function set($key, $value) {
$this->lazyLoadCache();
$this->storage[$key] = $value;
// The key might have been marked for deletion.
unset($this->keysToRemove[$key]);
$this->invalidateCache();
}
/**
* {@inheritdoc}
*/
public function delete($key) {
$this->lazyLoadCache();
unset($this->storage[$key]);
$this->keysToRemove[$key] = $key;
// The key might have been marked for persisting.
unset($this->keysToPersist[$key]);
$this->invalidateCache();
}
/**
* Flags an offset value to be written to the persistent cache.
*
* @param string $key
* The key that was request.
* @param bool $persist
* (optional) Whether the offset should be persisted or not, defaults to
* TRUE. When called with $persist = FALSE the offset will be unflagged so
* that it will not written at the end of the request.
*/
protected function persist($key, $persist = TRUE) {
$this->keysToPersist[$key] = $persist;
}
/**
* Resolves a cache miss.
*
* When an offset is not found in the object, this is treated as a cache
* miss. This method allows classes using this implementatio to look up the
* actual value and allow it to be cached.
*
* @param sring $key
* The offset that was requested.
*
* @return mixed
* The value of the offset, or NULL if no value was found.
*/
abstract protected function resolveCacheMiss($key);
/**
* Writes a value to the persistent cache immediately.
*
* @param bool $lock
* (optional) Whether to acquire a lock before writing to cache. Defaults to
* TRUE.
*/
protected function updateCache($lock = TRUE) {
$data = array();
foreach ($this->keysToPersist as $offset => $persist) {
if ($persist) {
$data[$offset] = $this->storage[$offset];
}
}
if (empty($data) && empty($this->keysToRemove)) {
return;
}
// Lock cache writes to help avoid stampedes.
$lock_name = $this->cid . ':' . __CLASS__;
if (!$lock || $this->lock->acquire($lock_name)) {
// Set and delete operations invalidate the cache item. Try to also load
// an eventually invalidated cache entry, only update an invalidated cache
// entry if the creation date did not change as this could result in an
// inconsistent cache.
if ($cache = $this->cache->get($this->cid, $this->cacheInvalidated)) {
if ($this->cacheInvalidated && $cache->created != $this->cacheCreated) {
// We have invalidated the cache in this request and got a different
// cache entry. Do not attempt to overwrite data that might have been
// changed in a different request. We'll let the cache rebuild in
// later requests.
$this->cache->delete($this->cid);
$this->lock->release($lock_name);
return;
}
$data = array_merge($cache->data, $data);
}
// Remove keys marked for deletion.
foreach ($this->keysToRemove as $delete_key) {
unset($data[$delete_key]);
}
$this->cache->set($this->cid, $data, CacheBackendInterface::CACHE_PERMANENT, $this->tags);
if ($lock) {
$this->lock->release($lock_name);
}
}
$this->keysToPersist = array();
$this->keysToRemove = array();
}
/**
* {@inheritdoc}
*/
public function reset() {
$this->storage = array();
$this->keysToPersist = array();
$this->keysToRemove = array();
$this->cacheLoaded = FALSE;
}
/**
* {@inheritdoc}
*/
public function clear() {
$this->reset();
if ($this->tags) {
$this->cache->deleteTags($this->tags);
}
else {
$this->cache->delete($this->cid);
}
}
/**
* {@inheritdoc}
*/
public function destruct() {
$this->updateCache();
}
/**
* Loads the cache if not already done.
*/
protected function lazyLoadCache() {
if ($this->cacheLoaded) {
return;
}
// The cache was not yet loaded, set flag to TRUE.
$this->cacheLoaded = TRUE;
if ($cache = $this->cache->get($this->cid)) {
$this->cacheCreated = $cache->created;
$this->storage = $cache->data;
}
}
/**
* Invalidate the cache.
*/
protected function invalidateCache() {
// Invalidate the cache to make sure that other requests immediately see the
// deletion before this request is terminated.
$this->cache->invalidate($this->cid);
$this->cacheInvalidated = TRUE;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Cache\CacheCollectorInterface.
*/
namespace Drupal\Core\Cache;
/**
* Provides a caching wrapper to be used in place of large structures.
*
* This should be extended by systems that need to cache large amounts of data
* to calling functions. These structures can become very large, so this
* class is used to allow different strategies to be used for caching internally
* (lazy loading, building caches over time etc.). This can dramatically reduce
* the amount of data that needs to be loaded from cache backends on each
* request, and memory usage from static caches of that same data.
*
* The default implementation is \Drupal\Core\Cache\CacheCollector.
*/
interface CacheCollectorInterface {
/**
* Gets value from the cache.
*
* @param string $key
* Key that identifies the data.
*
* @return mixed
* The corresponding cache data.
*/
public function get($key);
/**
* Sets cache data.
*
* It depends on the specific case and implementation whether this has a
* permanent effect or if it just affects the current request.
*
* @param string $key
* Key that identifies the data.
* @param mixed $value
* The data to be set.
*/
public function set($key, $value);
/**
* Deletes the element.
*
* It depends on the specific case and implementation whether this has a
* permanent effect or if it just affects the current request.
*
* @param string $key
* Key that identifies the data.
*/
public function delete($key);
/**
* Returns whether data exists for this key.
*
* @param string $key
* Key that identifies the data.
*/
public function has($key);
/**
* Resets the local cache.
*
* Does not clear the persistent cache.
*/
public function reset();
/**
* Clears the collected cache entry.
*/
public function clear();
}
......@@ -142,7 +142,9 @@ public function deleteAll() {
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidate().
*/
public function invalidate($cid) {
$this->cache[$cid]->expire = REQUEST_TIME - 1;
if (isset($this->cache[$cid])) {
$this->cache[$cid]->expire = REQUEST_TIME - 1;
}
}
/**
......
......@@ -217,7 +217,7 @@ protected function lookupPathAlias($path, $langcode) {
// Check the path whitelist, if the top-level part before the first /
// is not in the list, then there is no need to do anything further,
// it is not in the database.
elseif (!isset($this->whitelist[strtok($path, '/')])) {
elseif (!$this->whitelist->get(strtok($path, '/'))) {
return FALSE;
}
// For system paths which were not cached, query aliases individually.
......@@ -307,7 +307,7 @@ protected function pathAliasWhitelistRebuild($source = NULL) {
// When paths are inserted, only rebuild the whitelist if the system path
// has a top level component which is not already in the whitelist.
if (!empty($source)) {
if (isset($this->whitelist[strtok($source, '/')])) {
if ($this->whitelist->get(strtok($source, '/'))) {
return;
}
}
......
......@@ -7,15 +7,19 @@
namespace Drupal\Core\Path;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheCollector;
use Drupal\Core\Database\Connection;
use Drupal\Core\DestructableInterface;
use Drupal\Core\KeyValueStore\KeyValueFactory;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Utility\CacheArray;
/**
* Extends CacheArray to build the path alias whitelist over time.
*/
class AliasWhitelist extends CacheArray implements DestructableInterface {
class AliasWhitelist extends CacheCollector {
/**
* The Key/Value Store to use for state.
......@@ -36,16 +40,18 @@ class AliasWhitelist extends CacheArray implements DestructableInterface {
*
* @param string $cid
* The cache id to use.
* @param string $bin
* The cache bin that should be used.
* @param \Drupal\Core\KeyValueStore\KeyValueFactory $keyvalue
* The keyvalue factory to get the state cache from.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $state
* The state keyvalue store.
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
*/
public function __construct($cid, $bin, KeyValueFactory $keyvalue, Connection $connection) {
parent::__construct($cid, $bin);
$this->state = $keyvalue->get('state');
public function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $lock, KeyValueStoreInterface $state, Connection $connection) {
parent::__construct($cid, $cache, $lock);
$this->state = $state;
$this->connection = $connection;
// On a cold start $this->storage will be empty and the whitelist will
......@@ -74,9 +80,10 @@ protected function loadMenuPathRoots() {
}
/**
* Overrides \ArrayAccess::offsetGet().
* {@inheritdoc}
*/
public function offsetGet($offset) {
public function get($offset) {
$this->lazyLoadCache();
// url() may be called with paths that are not represented by menu router
// items such as paths that will be rewritten by hook_url_outbound_alter().
// Therefore internally TRUE is used to indicate whitelisted paths. FALSE is
......@@ -93,7 +100,7 @@ public function offsetGet($offset) {
}
/**
* Overrides \Drupal\Core\Utility\CacheArray::resolveCacheMiss().
* {@inheritdoc}
*/
public function resolveCacheMiss($root) {
$query = $this->connection->select('url_alias', 'u');
......@@ -111,43 +118,11 @@ public function resolveCacheMiss($root) {
}
/**
* Overrides \Drupal\Core\Utility\CacheArray::set().
*/
public function set($data, $lock = TRUE) {
$lock_name = $this->cid . ':' . $this->bin;
if (!$lock || lock()->acquire($lock_name)) {
if ($cached = cache($this->bin)->get($this->cid)) {
// Use array merge instead of union so that filled in values in $data
// overwrite empty values in the current cache.
$data = array_merge($cached->data, $data);
}
cache($this->bin)->set($this->cid, $data);
if ($lock) {
lock()->release($lock_name);
}
}
}
/**
* Overrides \Drupal\Core\Utility\CacheArray::clear().
* {@inheritdoc}
*/
public function clear() {
parent::clear();
$this->loadMenuPathRoots();
}
/**
* Implements Drupal\Core\DestructableInterface::destruct().
*/
public function destruct() {
parent::__destruct();
}
/**
* Overrides \Drupal\Core\Utility\CacheArray::clear().
*/
public function __destruct() {
// Do nothing to avoid segmentation faults. This can go away after the
// cache collector from http://drupal.org/node/1786490 is used.
}
}
......@@ -7,15 +7,15 @@
namespace Drupal\locale;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheCollector;
use Drupal\Core\DestructableInterface;
use Drupal\Core\Utility\CacheArray;
use Drupal\locale\SourceString;
use Drupal\locale\TranslationString;
use Drupal\Core\Lock\LockBackendInterface;
/**
* Extends CacheArray to allow for dynamic building of the locale cache.
* A cache collector to allow for dynamic building of the locale cache.
*/
class LocaleLookup extends CacheArray implements DestructableInterface {
class LocaleLookup extends CacheCollector {
/**
* A language code.
......@@ -39,9 +39,34 @@ class LocaleLookup extends CacheArray implements DestructableInterface {
protected $stringStorage;
/**
* Constructs a LocaleCache object.
* The cache backend that should be used.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The lock backend that should be used.
*
* @var \Drupal\Core\Lock\LockBackendInterface
*/
public function __construct($langcode, $context, $string_storage) {
protected $lock;
/**
* Constructs a LocaleLookup object.
*
* @param string $langcode
* The language code.
* @param string $context
* The string context.
* @param \Drupal\locale\StringStorageInterface $string_storage
* The string storage.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
*/
public function __construct($langcode, $context, StringStorageInterface $string_storage, CacheBackendInterface $cache, LockBackendInterface $lock) {
$this->langcode = $langcode;
$this->context = (string) $context;
$this->stringStorage = $string_storage;
......@@ -50,7 +75,7 @@ public function __construct($langcode, $context, $string_storage) {
// example, strings for admin menu items and settings forms are not cached
// for anonymous users.
$rids = isset($GLOBALS['user']) ? implode(':', array_keys($GLOBALS['user']->roles)) : '0';
parent::__construct("locale:$langcode:$context:$rids", 'cache', array('locale' => TRUE));
parent::__construct("locale:$langcode:$context:$rids", $cache, $lock, array('locale' => TRUE));
}
/**
......@@ -87,19 +112,4 @@ protected function resolveCacheMiss($offset) {
return $value;
}
/**
* {@inheritdoc}
*/
public function destruct() {
parent::__destruct();
}
/**
* {@inheritdoc}
*/
public function __destruct() {
// Do nothing to avoid segmentation faults. This can be restored after the
// cache collector from http://drupal.org/node/1786490 is used.
}
}
......@@ -7,8 +7,11 @@
namespace Drupal\locale;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\DestructableInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Lock\LockBackendAbstract;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\StringTranslation\Translator\TranslatorInterface;
use Drupal\locale\StringStorageInterface;
use Drupal\locale\LocaleLookup;
......@@ -37,14 +40,34 @@ class LocaleTranslation implements TranslatorInterface, DestructableInterface {
*/
protected $translations = array();
/**
* The cache backend that should be used.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The lock backend that should be used.
*
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected $lock;
/**
* Constructs a translator using a string storage.
*
* @param \Drupal\locale\StringStorageInterface $storage
* Storage to use when looking for new translations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
*/
public function __construct(StringStorageInterface $storage) {
public function __construct(StringStorageInterface $storage, CacheBackendInterface $cache, LockBackendInterface $lock) {
$this->storage = $storage;
$this->cache = $cache;
$this->lock = $lock;
}
/**
......@@ -58,9 +81,9 @@ public function getStringTranslation($langcode, $string, $context) {
// Strings are cached by langcode, context and roles, using instances of the
// LocaleLookup class to handle string lookup and caching.
if (!isset($this->translations[$langcode][$context])) {
$this->translations[$langcode][$context] = new LocaleLookup($langcode, $context, $this->storage);
$this->translations[$langcode][$context] = new LocaleLookup($langcode, $context, $this->storage, $this->cache, $this->lock);
}
$translation = $this->translations[$langcode][$context][$string];
$translation = $this->translations[$langcode][$context]->get($string);
return $translation === TRUE ? FALSE : $translation;
}
......@@ -75,8 +98,6 @@ public function reset() {
* {@inheritdoc}
*/
public function destruct() {
// @see \Drupal\locale\Locale\Lookup::__destruct().
// @todo Remove once http://drupal.org/node/1786490 is in.
foreach ($this->translations as $context) {
foreach ($context as $lookup) {
if ($lookup instanceof DestructableInterface) {
......
......@@ -12,7 +12,7 @@ services:
arguments: ['@database']
string_translator.locale.lookup:
class: Drupal\locale\LocaleTranslation
arguments: ['@locale.storage']
arguments: ['@locale.storage', '@cache.cache', '@lock']
tags:
- { name: string_translator }
- { name: needs_destruction }
......@@ -38,13 +38,15 @@ public static function getInfo() {
*/
protected function setUp() {
$this->storage = $this->getMock('Drupal\locale\StringStorageInterface');
$this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
$this->lock = $this->getMock('Drupal\Core\Lock\LockBackendInterface');
}
/**
* Tests for \Drupal\locale\LocaleTranslation::destruct()
*/
public function testDestruct() {
$translation = new LocaleTranslation($this->storage);
$translation = new LocaleTranslation($this->storage, $this->cache, $this->lock);
// Prove that destruction works without errors when translations are empty.
$this->assertAttributeEmpty('translations', $translation);
$translation->destruct();
......