Commit c1f444b1 authored by catch's avatar catch

Issue #2248767 by effulgentsia, beejeebus, alexpott: Use fast, local cache...

Issue #2248767 by effulgentsia, beejeebus, alexpott: Use fast, local cache back-end (APCu, if available) for low-write caches (bootstrap, discovery, and config).
parent 30cad25c
...@@ -7,7 +7,7 @@ parameters: ...@@ -7,7 +7,7 @@ parameters:
services: services:
cache_factory: cache_factory:
class: Drupal\Core\Cache\CacheFactory class: Drupal\Core\Cache\CacheFactory
arguments: ['@settings'] arguments: ['@settings', '%cache_default_bin_backends%']
calls: calls:
- [setContainer, ['@service_container']] - [setContainer, ['@service_container']]
cache_contexts: cache_contexts:
...@@ -47,14 +47,14 @@ services: ...@@ -47,14 +47,14 @@ services:
cache.bootstrap: cache.bootstrap:
class: Drupal\Core\Cache\CacheBackendInterface class: Drupal\Core\Cache\CacheBackendInterface
tags: tags:
- { name: cache.bin } - { name: cache.bin, default_backend: cache.backend.chainedfast }
factory_method: get factory_method: get
factory_service: cache_factory factory_service: cache_factory
arguments: [bootstrap] arguments: [bootstrap]
cache.config: cache.config:
class: Drupal\Core\Cache\CacheBackendInterface class: Drupal\Core\Cache\CacheBackendInterface
tags: tags:
- { name: cache.bin } - { name: cache.bin, default_backend: cache.backend.chainedfast }
factory_method: get factory_method: get
factory_service: cache_factory factory_service: cache_factory
arguments: [config] arguments: [config]
...@@ -96,7 +96,7 @@ services: ...@@ -96,7 +96,7 @@ services:
cache.discovery: cache.discovery:
class: Drupal\Core\Cache\CacheBackendInterface class: Drupal\Core\Cache\CacheBackendInterface
tags: tags:
- { name: cache.bin } - { name: cache.bin, default_backend: cache.backend.chainedfast }
factory_method: get factory_method: get
factory_service: cache_factory factory_service: cache_factory
arguments: [discovery] arguments: [discovery]
...@@ -113,7 +113,8 @@ services: ...@@ -113,7 +113,8 @@ services:
class: Drupal\Core\Config\ConfigInstaller class: Drupal\Core\Config\ConfigInstaller
arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher'] arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher']
config.storage: config.storage:
alias: config.storage.active class: Drupal\Core\Config\CachedStorage
arguments: ['@config.storage.active', '@cache.config']
config.storage.active: config.storage.active:
class: Drupal\Core\Config\DatabaseStorage class: Drupal\Core\Config\DatabaseStorage
arguments: ['@database', 'config'] arguments: ['@database', 'config']
......
...@@ -26,14 +26,31 @@ class CacheFactory implements CacheFactoryInterface, ContainerAwareInterface { ...@@ -26,14 +26,31 @@ class CacheFactory implements CacheFactoryInterface, ContainerAwareInterface {
*/ */
protected $settings; 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. * Constructs CacheFactory object.
* *
* @param \Drupal\Core\Site\Settings $settings * @param \Drupal\Core\Site\Settings $settings
* The settings array. * 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->settings = $settings;
$this->defaultBinBackends = $default_bin_backends;
} }
/** /**
...@@ -59,6 +76,9 @@ public function get($bin) { ...@@ -59,6 +76,9 @@ public function get($bin) {
elseif (isset($cache_settings['default'])) { elseif (isset($cache_settings['default'])) {
$service_name = $cache_settings['default']; $service_name = $cache_settings['default'];
} }
elseif (isset($this->defaultBinBackends[$bin])) {
$service_name = $this->defaultBinBackends[$bin];
}
else { else {
$service_name = 'cache.backend.database'; $service_name = 'cache.backend.database';
} }
......
...@@ -19,9 +19,11 @@ ...@@ -19,9 +19,11 @@
* Mecached or Redis, and will be used by all web nodes, thus making it * 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. * 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 * In addition to being useful for sites running on multiple web nodes, this
* multiple web nodes, as single-node configurations can just use the fast * backend can also be useful for sites running on a single web node where the
* cache backend directly. * 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 * 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 * check whether they were created before the last write (set()) to this
...@@ -68,7 +70,7 @@ class ChainedFastBackend implements CacheBackendInterface { ...@@ -68,7 +70,7 @@ class ChainedFastBackend implements CacheBackendInterface {
/** /**
* The time at which the last write to this cache bin happened. * The time at which the last write to this cache bin happened.
* *
* @var int * @var float
*/ */
protected $lastWriteTimestamp; protected $lastWriteTimestamp;
...@@ -102,14 +104,47 @@ public function get($cid, $allow_invalid = FALSE) { ...@@ -102,14 +104,47 @@ public function get($cid, $allow_invalid = FALSE) {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getMultiple(&$cids, $allow_invalid = FALSE) { 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; $cids_copy = $cids;
$cache = array(); $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(); $last_write_timestamp = $this->getLastWriteTimestamp();
if ($last_write_timestamp) { 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) { if ($item->created < $last_write_timestamp) {
$cids[array_search($item->cid, $cids_copy)] = $item->cid; $cids[array_search($item->cid, $cids_copy)] = $item->cid;
} }
...@@ -229,6 +264,13 @@ public function removeBin() { ...@@ -229,6 +264,13 @@ public function removeBin() {
$this->fastBackend->removeBin(); $this->fastBackend->removeBin();
} }
/**
* @todo Document in https://www.drupal.org/node/2311945.
*/
public function reset() {
$this->lastWriteTimestamp = NULL;
}
/** /**
* Gets the last write timestamp. * Gets the last write timestamp.
*/ */
......
...@@ -6,11 +6,60 @@ ...@@ -6,11 +6,60 @@
*/ */
namespace Drupal\Core\Cache; namespace Drupal\Core\Cache;
use Drupal\Core\Site\Settings;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
/** /**
* Defines the chained fast cache backend factory. * 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. * Instantiates a chained, fast cache backend class for a given cache bin.
...@@ -22,24 +71,18 @@ class ChainedFastBackendFactory extends CacheFactory { ...@@ -22,24 +71,18 @@ class ChainedFastBackendFactory extends CacheFactory {
* The cache backend object associated with the specified bin. * The cache backend object associated with the specified bin.
*/ */
public function get($bin) { public function get($bin) {
$consistent_service = 'cache.backend.database'; // Use the chained backend only if there is a fast backend available;
$fast_service = 'cache.backend.apcu'; // otherwise, just return the consistent backend directly.
if (isset($this->fastServiceName)) {
$cache_settings = $this->settings->get('cache'); return new ChainedFastBackend(
if (isset($cache_settings['chained_fast_cache']) && is_array($cache_settings['chained_fast_cache'])) { $this->container->get($this->consistentServiceName)->get($bin),
if (!empty($cache_settings['chained_fast_cache']['consistent'])) { $this->container->get($this->fastServiceName)->get($bin),
$consistent_service = $cache_settings['chained_fast_cache']['consistent']; $bin
} );
if (!empty($cache_settings['chained_fast_cache']['fast'])) { }
$fast_service = $cache_settings['chained_fast_cache']['fast']; 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
);
} }
} }
...@@ -22,9 +22,15 @@ class ListCacheBinsPass implements CompilerPassInterface { ...@@ -22,9 +22,15 @@ class ListCacheBinsPass implements CompilerPassInterface {
*/ */
public function process(ContainerBuilder $container) { public function process(ContainerBuilder $container) {
$cache_bins = array(); $cache_bins = array();
$cache_default_bin_backends = array();
foreach ($container->findTaggedServiceIds('cache.bin') as $id => $attributes) { 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_bins', $cache_bins);
$container->setParameter('cache_default_bin_backends', $cache_default_bin_backends);
} }
} }
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
/** /**
* Defines the cached storage. * Defines the cached storage.
...@@ -18,6 +19,7 @@ ...@@ -18,6 +19,7 @@
* handles cache invalidation. * handles cache invalidation.
*/ */
class CachedStorage implements StorageInterface, StorageCacheInterface { class CachedStorage implements StorageInterface, StorageCacheInterface {
use DependencySerializationTrait;
/** /**
* The configuration storage to be cached. * The configuration storage to be cached.
......
...@@ -16,24 +16,11 @@ ...@@ -16,24 +16,11 @@
*/ */
class ConfigOtherModuleTest extends WebTestBase { 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. * Tests enabling the provider of the default configuration first.
*/ */
public function testInstallOtherModuleFirst() { 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 // 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 // is enabled. We cannot use the entity system because the config_test
...@@ -43,18 +30,18 @@ public function testInstallOtherModuleFirst() { ...@@ -43,18 +30,18 @@ public function testInstallOtherModuleFirst() {
// Install the module that provides the entity type. This installs the // Install the module that provides the entity type. This installs the
// default configuration. // 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.'); $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 // Uninstall the module that provides the entity type. This will remove the
// default configuration. // 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'); $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.'); $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 // Install the module that provides the entity type again. This installs the
// default configuration. // default configuration.
$this->moduleHandler->install(array('config_test')); $this->installModule('config_test');
$other_module_config_entity = entity_load('config_test', 'other_module_test', TRUE); $other_module_config_entity = entity_load('config_test', 'other_module_test', TRUE);
$this->assertTrue($other_module_config_entity, "Default configuration has been recreated."); $this->assertTrue($other_module_config_entity, "Default configuration has been recreated.");
...@@ -64,14 +51,14 @@ public function testInstallOtherModuleFirst() { ...@@ -64,14 +51,14 @@ public function testInstallOtherModuleFirst() {
$other_module_config_entity->save(); $other_module_config_entity->save();
// Uninstall the module that provides the default configuration. // 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.'); $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. // Default configuration provided by config_test should still exist.
$this->assertTrue(entity_load('config_test', 'dotted.default', TRUE), 'The configuration is not deleted.'); $this->assertTrue(entity_load('config_test', 'dotted.default', TRUE), 'The configuration is not deleted.');
// Re-enable module to test that default config is unchanged. // 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); $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.'); $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() { ...@@ -80,10 +67,10 @@ public function testInstallOtherModuleFirst() {
* Tests enabling the provider of the config entity type first. * Tests enabling the provider of the config entity type first.
*/ */
public function testInstallConfigEnityModuleFirst() { 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->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.'); $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() { ...@@ -91,12 +78,34 @@ public function testInstallConfigEnityModuleFirst() {
* Tests uninstalling Node module removes views which are dependent on it. * Tests uninstalling Node module removes views which are dependent on it.
*/ */
public function testUninstall() { 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->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->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.'); $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();
}
} }
...@@ -257,7 +257,7 @@ public function containerBuild(ContainerBuilder $container) { ...@@ -257,7 +257,7 @@ public function containerBuild(ContainerBuilder $container) {
$container->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory'); $container->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory');
$container $container
->register('config.storage.active', 'Drupal\Core\Config\DatabaseStorage') ->register('config.storage', 'Drupal\Core\Config\DatabaseStorage')
->addArgument(Database::getConnection()) ->addArgument(Database::getConnection())
->addArgument('config'); ->addArgument('config');
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\String; use Drupal\Component\Utility\String;
use Drupal\Core\Cache\Cache;
use Drupal\Core\DependencyInjection\YamlFileLoader; use Drupal\Core\DependencyInjection\YamlFileLoader;
use Drupal\Core\DrupalKernel; use Drupal\Core\DrupalKernel;
use Drupal\Core\Database\Database; use Drupal\Core\Database\Database;
...@@ -1171,9 +1172,17 @@ protected function resetAll() { ...@@ -1171,9 +1172,17 @@ protected function resetAll() {
*/ */
protected function refreshVariables() { protected function refreshVariables() {
// Clear the tag cache. // 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\CacheBackendInterface::tagCache');
drupal_static_reset('Drupal\Core\Cache\DatabaseBackend::deletedTags'); drupal_static_reset('Drupal\Core\Cache\DatabaseBackend::deletedTags');
drupal_static_reset('Drupal\Core\Cache\DatabaseBackend::invalidatedTags'); 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('config.factory')->reset();
$this->container->get('state')->resetCache(); $this->container->get('state')->resetCache();
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment