diff --git a/core/core.services.yml b/core/core.services.yml index b015208b669cb75601c558b9c6775b8f31c8b520..72bf10e04312d2764e474b7cda3f8e595c97687f 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -7,7 +7,7 @@ parameters: services: cache_factory: class: Drupal\Core\Cache\CacheFactory - arguments: ['@settings'] + arguments: ['@settings', '%cache_default_bin_backends%'] calls: - [setContainer, ['@service_container']] cache_contexts: @@ -47,14 +47,14 @@ services: cache.bootstrap: class: Drupal\Core\Cache\CacheBackendInterface tags: - - { name: cache.bin } + - { name: cache.bin, default_backend: cache.backend.chainedfast } factory_method: get factory_service: cache_factory arguments: [bootstrap] cache.config: class: Drupal\Core\Cache\CacheBackendInterface tags: - - { name: cache.bin } + - { name: cache.bin, default_backend: cache.backend.chainedfast } factory_method: get factory_service: cache_factory arguments: [config] @@ -96,7 +96,7 @@ services: cache.discovery: class: Drupal\Core\Cache\CacheBackendInterface tags: - - { name: cache.bin } + - { name: cache.bin, default_backend: cache.backend.chainedfast } factory_method: get factory_service: cache_factory arguments: [discovery] @@ -113,7 +113,8 @@ services: class: Drupal\Core\Config\ConfigInstaller arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher'] config.storage: - alias: config.storage.active + class: Drupal\Core\Config\CachedStorage + arguments: ['@config.storage.active', '@cache.config'] config.storage.active: class: Drupal\Core\Config\DatabaseStorage arguments: ['@database', 'config'] diff --git a/core/lib/Drupal/Core/Cache/CacheFactory.php b/core/lib/Drupal/Core/Cache/CacheFactory.php index a3b5a01270fb57d3d91d2d4f41abd8e92e4159b1..0a0a977738f4e7099eee1616ee597a1e05abec7a 100644 --- a/core/lib/Drupal/Core/Cache/CacheFactory.php +++ b/core/lib/Drupal/Core/Cache/CacheFactory.php @@ -26,14 +26,31 @@ class CacheFactory implements CacheFactoryInterface, ContainerAwareInterface { */ protected $settings; + /** + * A map of cache bin to default cache backend service name. + * + * All mappings in $settings takes precedence over this, but this can be used + * to optimize cache storage for a Drupal installation without cache + * customizations in settings.php. For example, this can be used to map the + * 'bootstrap' bin to 'cache.backend.chainedfast', while allowing other bins + * to fall back to the global default of 'cache.backend.database'. + * + * @var array + */ + protected $defaultBinBackends; + /** * Constructs CacheFactory object. * * @param \Drupal\Core\Site\Settings $settings * The settings array. + * @param array $default_bin_backends + * (optional) A mapping of bin to backend service name. Mappings in + * $settings take precedence over this. */ - function __construct(Settings $settings) { + public function __construct(Settings $settings, array $default_bin_backends = array()) { $this->settings = $settings; + $this->defaultBinBackends = $default_bin_backends; } /** @@ -59,6 +76,9 @@ public function get($bin) { elseif (isset($cache_settings['default'])) { $service_name = $cache_settings['default']; } + elseif (isset($this->defaultBinBackends[$bin])) { + $service_name = $this->defaultBinBackends[$bin]; + } else { $service_name = 'cache.backend.database'; } diff --git a/core/lib/Drupal/Core/Cache/ChainedFastBackend.php b/core/lib/Drupal/Core/Cache/ChainedFastBackend.php index 6a825af35a0b7f7039c27a4dff2df81ac10bd20b..5a9ede4b343dfb7a7091e94dc1c71649031363a7 100644 --- a/core/lib/Drupal/Core/Cache/ChainedFastBackend.php +++ b/core/lib/Drupal/Core/Cache/ChainedFastBackend.php @@ -19,9 +19,11 @@ * Mecached or Redis, and will be used by all web nodes, thus making it * consistent, but also require a network round trip for each cache get. * - * It is expected this backend will be used primarily on sites running on - * multiple web nodes, as single-node configurations can just use the fast - * cache backend directly. + * In addition to being useful for sites running on multiple web nodes, this + * backend can also be useful for sites running on a single web node where the + * fast backend (e.g., APCu) isn't shareable between the web and CLI processes. + * Single-node configurations that don't have that limitation can just use the + * fast cache backend directly. * * We always use the fast backend when reading (get()) entries from cache, but * check whether they were created before the last write (set()) to this @@ -68,7 +70,7 @@ class ChainedFastBackend implements CacheBackendInterface { /** * The time at which the last write to this cache bin happened. * - * @var int + * @var float */ protected $lastWriteTimestamp; @@ -102,14 +104,47 @@ public function get($cid, $allow_invalid = FALSE) { * {@inheritdoc} */ public function getMultiple(&$cids, $allow_invalid = FALSE) { - // Retrieve as many cache items as possible from the fast backend. (Some - // cache entries may have been created before the last write to this cache - // bin and therefore be stale/wrong/inconsistent.) $cids_copy = $cids; $cache = array(); + + // If we can determine the time at which the last write to the consistent + // backend occurred (we might not be able to if it has been recently + // flushed/restarted), then we can use that to validate items from the fast + // backend, so try to get those first. Otherwise, we can't assume that + // anything in the fast backend is valid, so don't even bother fetching + // from there. $last_write_timestamp = $this->getLastWriteTimestamp(); if ($last_write_timestamp) { - foreach ($this->fastBackend->getMultiple($cids, $allow_invalid) as $item) { + // Items in the fast backend might be invalid based on their timestamp, + // but we can't check the timestamp prior to getting the item, which + // includes unserializing it. However, unserializing an invalid item can + // throw an exception. For example, a __wakeup() implementation that + // receives object properties containing references to code or data that + // no longer exists in the application's current state. + // + // Unserializing invalid data, whether it throws an exception or not, is + // a waste of time, but we only incur it while a cache invalidation has + // not yet finished propagating to all the fast backend instances. + // + // Most cache backend implementations should not wrap their internal + // get() implementations with a try/catch, because they have no reason to + // assume that their data is invalid, and doing so would mask + // unserialization errors of valid data. We do so here, only because the + // fast backend is non-authoritative, and after discarding its + // exceptions, we proceed to check the consistent (authoritative) backend + // and allow exceptions from that to bubble up. + try { + $items = $this->fastBackend->getMultiple($cids, $allow_invalid); + } + catch (\Exception $e) { + $cids = $cids_copy; + $items = array(); + } + + // Even if items were successfully fetched from the fast backend, they + // are potentially invalid if older than the last time the bin was + // written to in the consistent backend, so only keep ones that aren't. + foreach ($items as $item) { if ($item->created < $last_write_timestamp) { $cids[array_search($item->cid, $cids_copy)] = $item->cid; } @@ -229,6 +264,13 @@ public function removeBin() { $this->fastBackend->removeBin(); } + /** + * @todo Document in https://www.drupal.org/node/2311945. + */ + public function reset() { + $this->lastWriteTimestamp = NULL; + } + /** * Gets the last write timestamp. */ diff --git a/core/lib/Drupal/Core/Cache/ChainedFastBackendFactory.php b/core/lib/Drupal/Core/Cache/ChainedFastBackendFactory.php index 8d8fcf43d4b29a3fb0d5f0303f44ec93bdba7a1d..c261b84b0bbf37d3fa90ea392013480721f03d33 100644 --- a/core/lib/Drupal/Core/Cache/ChainedFastBackendFactory.php +++ b/core/lib/Drupal/Core/Cache/ChainedFastBackendFactory.php @@ -6,11 +6,60 @@ */ namespace Drupal\Core\Cache; +use Drupal\Core\Site\Settings; +use Symfony\Component\DependencyInjection\ContainerAwareTrait; /** * Defines the chained fast cache backend factory. */ -class ChainedFastBackendFactory extends CacheFactory { +class ChainedFastBackendFactory implements CacheFactoryInterface { + + use ContainerAwareTrait; + + /** + * The service name of the consistent backend factory. + * + * @var string + */ + protected $consistentServiceName; + + /** + * The service name of the fast backend factory. + * + * @var string + */ + protected $fastServiceName; + + /** + * Constructs ChainedFastBackendFactory object. + * + * @param \Drupal\Core\Site\Settings|NULL $settings + * (optional) The settings object. + * @param string|NULL $consistent_service_name + * (optional) The service name of the consistent backend factory. Defaults + * to: + * - $settings->get('cache')['default'] (if specified) + * - 'cache.backend.database' (if the above isn't specified) + * @param string|NULL $fast_service_name + * (optional) The service name of the fast backend factory. Defaults to: + * - 'cache.backend.apcu' (if the PHP process has APCu enabled) + * - NULL (if the PHP process doesn't have APCu enabled) + */ + public function __construct(Settings $settings = NULL, $consistent_service_name = NULL, $fast_service_name = NULL) { + // Default the consistent backend to the site's default backend. + if (!isset($consistent_service_name)) { + $cache_settings = isset($settings) ? $settings->get('cache') : array(); + $consistent_service_name = isset($cache_settings['default']) ? $cache_settings['default'] : 'cache.backend.database'; + } + + // Default the fast backend to APCu if it's available. + if (!isset($fast_service_name) && function_exists('apc_fetch')) { + $fast_service_name = 'cache.backend.apcu'; + } + + $this->consistentServiceName = $consistent_service_name; + $this->fastServiceName = $fast_service_name; + } /** * Instantiates a chained, fast cache backend class for a given cache bin. @@ -22,24 +71,18 @@ class ChainedFastBackendFactory extends CacheFactory { * The cache backend object associated with the specified bin. */ public function get($bin) { - $consistent_service = 'cache.backend.database'; - $fast_service = 'cache.backend.apcu'; - - $cache_settings = $this->settings->get('cache'); - if (isset($cache_settings['chained_fast_cache']) && is_array($cache_settings['chained_fast_cache'])) { - if (!empty($cache_settings['chained_fast_cache']['consistent'])) { - $consistent_service = $cache_settings['chained_fast_cache']['consistent']; - } - if (!empty($cache_settings['chained_fast_cache']['fast'])) { - $fast_service = $cache_settings['chained_fast_cache']['fast']; - } + // Use the chained backend only if there is a fast backend available; + // otherwise, just return the consistent backend directly. + if (isset($this->fastServiceName)) { + return new ChainedFastBackend( + $this->container->get($this->consistentServiceName)->get($bin), + $this->container->get($this->fastServiceName)->get($bin), + $bin + ); + } + else { + return $this->container->get($this->consistentServiceName)->get($bin); } - - return new ChainedFastBackend( - $this->container->get($consistent_service)->get($bin), - $this->container->get($fast_service)->get($bin), - $bin - ); } } diff --git a/core/lib/Drupal/Core/Cache/ListCacheBinsPass.php b/core/lib/Drupal/Core/Cache/ListCacheBinsPass.php index 5381186a15e5d832fe2f02a0ff5adf41fc5f688b..e9e715845f466ca954d26c3009ac266ce1ff23d8 100644 --- a/core/lib/Drupal/Core/Cache/ListCacheBinsPass.php +++ b/core/lib/Drupal/Core/Cache/ListCacheBinsPass.php @@ -22,9 +22,15 @@ class ListCacheBinsPass implements CompilerPassInterface { */ public function process(ContainerBuilder $container) { $cache_bins = array(); + $cache_default_bin_backends = array(); foreach ($container->findTaggedServiceIds('cache.bin') as $id => $attributes) { - $cache_bins[$id] = substr($id, strpos($id, '.') + 1); + $bin = substr($id, strpos($id, '.') + 1); + $cache_bins[$id] = $bin; + if (isset($attributes[0]['default_backend'])) { + $cache_default_bin_backends[$bin] = $attributes[0]['default_backend']; + } } $container->setParameter('cache_bins', $cache_bins); + $container->setParameter('cache_default_bin_backends', $cache_default_bin_backends); } } diff --git a/core/lib/Drupal/Core/Config/CachedStorage.php b/core/lib/Drupal/Core/Config/CachedStorage.php index db5d598877f33c021c9a8faac432adb53c00eb6c..0b089ca4f139025c4729e3b0d4bd2d18ad5a4fa2 100644 --- a/core/lib/Drupal/Core/Config/CachedStorage.php +++ b/core/lib/Drupal/Core/Config/CachedStorage.php @@ -9,6 +9,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\DependencyInjection\DependencySerializationTrait; /** * Defines the cached storage. @@ -18,6 +19,7 @@ * handles cache invalidation. */ class CachedStorage implements StorageInterface, StorageCacheInterface { + use DependencySerializationTrait; /** * The configuration storage to be cached. diff --git a/core/modules/config/src/Tests/ConfigOtherModuleTest.php b/core/modules/config/src/Tests/ConfigOtherModuleTest.php index 1cfdc1ae373761054421bcd7c755806a5d5503c0..ba497ab53d7614a36d27f14386120031d7886237 100644 --- a/core/modules/config/src/Tests/ConfigOtherModuleTest.php +++ b/core/modules/config/src/Tests/ConfigOtherModuleTest.php @@ -16,24 +16,11 @@ */ class ConfigOtherModuleTest extends WebTestBase { - /** - * @var \Drupal\Core\Extension\ModuleHandler - */ - protected $moduleHandler; - - /** - * Sets up the module handler for enabling and disabling modules. - */ - protected function setUp() { - parent::setUp(); - $this->moduleHandler = $this->container->get('module_handler'); - } - /** * Tests enabling the provider of the default configuration first. */ public function testInstallOtherModuleFirst() { - $this->moduleHandler->install(array('config_other_module_config_test')); + $this->installModule('config_other_module_config_test'); // Check that the config entity doesn't exist before the config_test module // is enabled. We cannot use the entity system because the config_test @@ -43,18 +30,18 @@ public function testInstallOtherModuleFirst() { // Install the module that provides the entity type. This installs the // default configuration. - $this->moduleHandler->install(array('config_test')); + $this->installModule('config_test'); $this->assertTrue(entity_load('config_test', 'other_module_test', TRUE), 'Default configuration has been installed.'); // Uninstall the module that provides the entity type. This will remove the // default configuration. - $this->moduleHandler->uninstall(array('config_test')); + $this->uninstallModule('config_test'); $config = $this->container->get('config.factory')->get('config_test.dynamic.other_module_test'); $this->assertTrue($config->isNew(), 'Default configuration for other modules is removed when the config entity provider is disabled.'); // Install the module that provides the entity type again. This installs the // default configuration. - $this->moduleHandler->install(array('config_test')); + $this->installModule('config_test'); $other_module_config_entity = entity_load('config_test', 'other_module_test', TRUE); $this->assertTrue($other_module_config_entity, "Default configuration has been recreated."); @@ -64,14 +51,14 @@ public function testInstallOtherModuleFirst() { $other_module_config_entity->save(); // Uninstall the module that provides the default configuration. - $this->moduleHandler->uninstall(array('config_other_module_config_test')); + $this->uninstallModule('config_other_module_config_test'); $this->assertTrue(entity_load('config_test', 'other_module_test', TRUE), 'Default configuration for other modules is not removed when the module that provides it is uninstalled.'); // Default configuration provided by config_test should still exist. $this->assertTrue(entity_load('config_test', 'dotted.default', TRUE), 'The configuration is not deleted.'); // Re-enable module to test that default config is unchanged. - $this->moduleHandler->install(array('config_other_module_config_test')); + $this->installModule('config_other_module_config_test'); $config_entity = entity_load('config_test', 'other_module_test', TRUE); $this->assertEqual($config_entity->get('style'), "The piano ain't got no wrong notes.", 'Re-enabling the module does not install default config over the existing config entity.'); } @@ -80,10 +67,10 @@ public function testInstallOtherModuleFirst() { * Tests enabling the provider of the config entity type first. */ public function testInstallConfigEnityModuleFirst() { - $this->moduleHandler->install(array('config_test')); + $this->installModule('config_test'); $this->assertFalse(entity_load('config_test', 'other_module_test', TRUE), 'Default configuration provided by config_other_module_config_test does not exist.'); - $this->moduleHandler->install(array('config_other_module_config_test')); + $this->installModule('config_other_module_config_test'); $this->assertTrue(entity_load('config_test', 'other_module_test', TRUE), 'Default configuration provided by config_other_module_config_test has been installed.'); } @@ -91,12 +78,34 @@ public function testInstallConfigEnityModuleFirst() { * Tests uninstalling Node module removes views which are dependent on it. */ public function testUninstall() { - $this->moduleHandler->install(array('views')); + $this->installModule('views'); $this->assertTrue(entity_load('view', 'frontpage', TRUE) === NULL, 'After installing Views, frontpage view which is dependant on the Node and Views modules does not exist.'); - $this->moduleHandler->install(array('node')); + $this->installModule('node'); $this->assertTrue(entity_load('view', 'frontpage', TRUE) !== NULL, 'After installing Node, frontpage view which is dependant on the Node and Views modules exists.'); - $this->moduleHandler->uninstall(array('node')); + $this->uninstallModule('node'); $this->assertTrue(entity_load('view', 'frontpage', TRUE) === NULL, 'After uninstalling Node, frontpage view which is dependant on the Node and Views modules does not exist.'); } + /** + * Installs a module. + * + * @param string $module + * The module name. + */ + protected function installModule($module) { + $this->container->get('module_handler')->install(array($module)); + $this->container = \Drupal::getContainer(); + } + + /** + * Uninstalls a module. + * + * @param string $module + * The module name. + */ + protected function uninstallModule($module) { + $this->container->get('module_handler')->uninstall(array($module)); + $this->container = \Drupal::getContainer(); + } + } diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index e3ab65791035187e6132d238b3e85fb4adaa350d..ea15a3201cf2e22891135e5b0c83b972f9d97b2a 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -257,7 +257,7 @@ public function containerBuild(ContainerBuilder $container) { $container->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory'); $container - ->register('config.storage.active', 'Drupal\Core\Config\DatabaseStorage') + ->register('config.storage', 'Drupal\Core\Config\DatabaseStorage') ->addArgument(Database::getConnection()) ->addArgument('config'); diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 5dd62c2bfe3b9974f18e8585bf0e04e55045e3f2..c1fe6f70a4f8e6aeab297b553099e9c524c7b442 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -12,6 +12,7 @@ use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\String; +use Drupal\Core\Cache\Cache; use Drupal\Core\DependencyInjection\YamlFileLoader; use Drupal\Core\DrupalKernel; use Drupal\Core\Database\Database; @@ -1171,9 +1172,17 @@ protected function resetAll() { */ protected function refreshVariables() { // Clear the tag cache. + // @todo Replace drupal_static() usage within classes and provide a + // proper interface for invoking reset() on a cache backend: + // https://www.drupal.org/node/2311945. drupal_static_reset('Drupal\Core\Cache\CacheBackendInterface::tagCache'); drupal_static_reset('Drupal\Core\Cache\DatabaseBackend::deletedTags'); drupal_static_reset('Drupal\Core\Cache\DatabaseBackend::invalidatedTags'); + foreach (Cache::getBins() as $backend) { + if (is_callable(array($backend, 'reset'))) { + $backend->reset(); + } + } $this->container->get('config.factory')->reset(); $this->container->get('state')->resetCache();