diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index e1c15fe..3044774 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -2549,7 +2549,7 @@ function module_hook($module, $hook) { * @return Drupal\Core\KeyValueStore\KeyValueStoreInterface */ function state() { - return drupal_container()->get('keyvalue')->get('state'); + return drupal_container()->get('state'); } /** diff --git a/core/lib/Drupal/Core/Cache/CacheCollector.php b/core/lib/Drupal/Core/Cache/CacheCollector.php new file mode 100644 index 0000000..76444ec --- /dev/null +++ b/core/lib/Drupal/Core/Cache/CacheCollector.php @@ -0,0 +1,229 @@ +has() needs to correctly return (equivalent to + * array_key_exists() vs. isset()). This should not be necessary in the majority + * of cases. + * + * Classes extending this class must override at least the + * CacheCollector::resolveCacheMiss() method to have a working implementation. + */ +abstract class CacheCollector implements CacheCollectorInterface { + + /** + * A cid to pass to cache()->set() and cache()->get(). + * + * @var string + */ + protected $cid; + + /** + * A tags array to pass to cache()->set(). + * + * @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 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(); + + /** + * Constructs a CacheArray 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; + + if ($cached = $this->cache->get($this->cid)) { + $this->storage = $cached->data; + } + } + + /** + * Implements CacheCollectorInterface::has(). + */ + public function has($key) { + return $this->get($key) !== NULL; + } + + /** + * Implements CacheCollectorInterface::get(). + */ + public function get($key) { + if (isset($this->storage[$key]) || array_key_exists($key, $this->storage)) { + return $this->storage[$key]; + } + else { + return $this->resolveCacheMiss($key); + } + } + + /** + * Implements 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->storage[$key] = $value; + // The key might have been marked for deletion. + unset($this->keysToRemove[$key]); + + } + + /** + * Implements CacheCollectorInterface::delete(). + */ + public function delete($key) { + unset($this->storage[$key]); + $this->keysToRemove[$key] = $key; + // The key might have been marked for persisting. + unset($this->keysToPersist[$key]); + } + + /** + * Flags an offset value to be written to the persistent cache. + * + * @param $key + * The key that was request. + * @param $persist + * Optional boolean to specify 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 $key + * The offset that was requested. + * + * @return + * 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 $data + * The data to write to the persistent cache. + * @param $lock + * Whether to acquire a lock before writing to cache. + */ + protected function updateCache($lock = TRUE) { + $data = array(); + foreach ($this->keysToPersist as $offset => $persist) { + if ($persist) { + $data[$offset] = $this->storage[$offset]; + } + } + if (empty($data)) { + return; + } + + // Lock cache writes to help avoid stampedes. + // To implement locking for cache misses, override __construct(). + $lock_name = $this->cid . ':' . __CLASS__; + if (!$lock || $this->lock->acquire($lock_name)) { + if ($cached = $this->cache->get($this->cid)) { + $data = array_merge($cached->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); + } + } + } + + /** + * Implements KernelServiceDestruction::destruct(). + */ + public function destruct() { + $this->updateCache(); + } + + /** + * Implements CacheCollectorInterface::reset(). + */ + public function reset() { + $this->storage = array(); + $this->keysToPersist = array(); + $this->keysToRemove = array(); + } +} diff --git a/core/lib/Drupal/Core/Cache/CacheCollectorInterface.php b/core/lib/Drupal/Core/Cache/CacheCollectorInterface.php new file mode 100644 index 0000000..cf5fe51 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/CacheCollectorInterface.php @@ -0,0 +1,72 @@ +setFactoryMethod('getSingleton'); // Register the State k/v store as a service. - $container->register('state', 'Drupal\Core\KeyValueStore\KeyValueStoreInterface') - ->setFactoryService(new Reference('keyvalue')) - ->setFactoryMethod('get') - ->addArgument('state'); + $container->register('state', 'Drupal\Core\KeyValueStore\KeyValueCacheDecorator') + ->addArgument(new Reference('cache.cache')) + ->addArgument(new Reference('lock')) + ->addArgument(new Reference('keyvalue')) + ->addArgument('state') + ->addTag('needs_destruction'); // Register the Queue factory. $container diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueCacheDecorator.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueCacheDecorator.php new file mode 100644 index 0000000..c177bed --- /dev/null +++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueCacheDecorator.php @@ -0,0 +1,152 @@ +keyValueStore = $keyValueFactory->get($collection); + } + + /** + * Implements \Drupal\Core\Cache\CacheCollector::resolveCacheMiss(). + */ + protected function resolveCacheMiss($offset) { + $this->storage[$offset] = $this->keyValueStore->get($offset); + $this->persist($offset); + return $this->storage[$offset]; + } + + /** + * Overrides \Drupal\Core\Cache\CacheCollector::delete(). + */ + public function delete($key) { + parent::delete($key); + $this->keyValueStore->delete($key); + // Delete the cache to make sure that other requests immediately see the new + // value before this request is terminated. + $this->cache->delete($this->cid); + } + + /** + * Overrides \Drupal\Core\Cache\CacheCollector::deleteMultiple(). + */ + public function deleteMultiple(array $keys) { + foreach ($keys as $key) { + $this->delete($key); + } + $this->keyValueStore->deleteMultiple($keys); + // Delete the cache to make sure that other requests immediately see the new + // value before this request is terminated. + $this->cache->delete($this->cid); + } + + /** + * Implements KeyValueStoreInterface::getAll(). + */ + public function getAll() { + // Don't cache this. + return $this->keyValueStore->getAll(); + } + + /** + * Implements KeyValueStoreInterface::getCollectionName(). + */ + public function getCollectionName() { + return $this->keyValueStore->getCollectionName(); + } + + /** + * Implements KeyValueStoreInterface::getAll(). + */ + public function getMultiple(array $keys) { + $values = array(); + foreach ($keys as $key) { + $value = $this->get($key); + // Only return keys with a value. + if ($value !== NULL) { + $values[$key] = $value; + } + } + return $values; + } + + /** + * Implements KeyValueStoreInterface::setIfNotExists(). + */ + public function setIfNotExists($key, $value) { + if ($this->keyValueStore->setIfNotExists($key, $value)) { + $this->set($key, $value); + return TRUE; + } + return FALSE; + } + + /** + * Implements KeyValueStoreInterface::setMultiple(). + */ + public function setMultiple(array $data) { + $this->keyValueStore->setMultiple($data); + foreach ($data as $key => $value) { + parent::set($key, $value); + $this->keysToPersist[$key] = $value; + } + // Delete the cache to make sure that other requests immediately see the new + // value before this request is terminated. + $this->cache->delete($this->cid); + } + + /** + * Implements KeyValueStoreInterface::set(). + */ + public function set($key, $value) { + $this->keyValueStore->set($key, $value); + parent::set($key, $value); + $this->keysToPersist[$key] = $value; + // Delete the cache to make sure that other requests immediately see the new + // value before this request is terminated. + $this->cache->delete($this->cid); + } + + /** + * Implements KeyValueStoreInterface::set(). + */ + public function deleteAll() { + $this->keyValueStore->deleteAll(); + $this->cache->delete($this->cid); + } + +} diff --git a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php index e5f33d4..3cfda5f 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php @@ -168,6 +168,18 @@ public function containerBuild(ContainerBuilder $container) { $container ->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueFactory') ->addArgument(new Reference('service_container')); + + $container + ->register('cache.cache', 'Drupal\Core\Cache\CacheBackendInterface') + ->setFactoryClass('Drupal\Core\Cache\CacheFactory') + ->setFactoryMethod('get') + ->addArgument('cache'); + + $container->register('state', 'Drupal\Core\KeyValueStore\KeyValueCacheDecorator') + ->addArgument(new Reference('cache.cache')) + ->addArgument(new Reference('lock')) + ->addArgument(new Reference('keyvalue')) + ->addArgument('state'); } } diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index 5cca38a..2a98cd2 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -890,7 +890,8 @@ protected function refreshVariables() { global $conf; cache('bootstrap')->delete('variables'); $conf = variable_initialize(); - drupal_container()->get('config.factory')->reset(); + \Drupal::service('config.factory')->reset(); + \Drupal::service('state')->reset(); } /** diff --git a/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/CacheDecoratorTest.php b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/CacheDecoratorTest.php new file mode 100644 index 0000000..fa56f24 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/CacheDecoratorTest.php @@ -0,0 +1,87 @@ + 'Key value cache decorator', + 'description' => 'Tests the key value cache decorator.', + 'group' => 'Key-value store', + ); + } + + protected function setUp() { + parent::setUp(); + $this->container + ->register('keyvalue.memory', 'Drupal\Core\KeyValueStore\KeyValueMemoryFactory'); + global $conf; + $conf['keyvalue_default'] = 'keyvalue.memory'; + $this->cache = $cache = new MemoryBackend('bin'); + } + + /** + * Tests that values are cached. + */ + public function testCache() { + $stores = $this->createStorage(); + $values = array(); + // Set the value and test that it is correctly returned. + foreach ($this->collections as $i => $collection) { + $stores[$i]->set('key', $this->objects[$i]); + $this->assertEqual($stores[$i]->get('key'), $this->objects[$i]); + // Destruct the class to have it write the cache. + $stores[$i]->destruct(); + + // Delete the value from the key value storage. + $this->container->get($this->factory)->get($collection)->delete('key'); + } + + // Create new objects. + $stores = $this->createStorage(); + + // Verify that we get the cached state as we have not notified the decorator + // about the deletion. + foreach ($this->collections as $i => $collection) { + $this->assertEqual($stores[$i]->get('key'), $this->objects[$i]); + + // Reset the cache and make sure the value was updated. + $stores[$i]->reset(); + $this->assertNull($stores[$i]->get('key')); + } + } + + /** + * Overrides StorageTestBase::createStorage() + */ + protected function createStorage() { + $stores = array(); + // Prepare the memory key value storages and decorated ones. + foreach ($this->collections as $i => $collection) { + $stores[$i] = new KeyValueCacheDecorator($this->cache, new NullLockBackend(), $this->container->get($this->factory), $collection); + } + + return $stores; + } + +}