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:
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']
......
......@@ -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';
}
......
......@@ -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.
*/
......
......@@ -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
);
}
}
......@@ -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);
}
}
......@@ -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.
......
......@@ -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();
}
}
......@@ -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');
......
......@@ -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();
......
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