diff --git a/core/lib/Drupal/Core/Cache/MemoryBackend.php b/core/lib/Drupal/Core/Cache/MemoryBackend.php index d402d70b543a9c7083c45233602059799dd46758..a0269f5301031ec4c120a0bb6b5e0e8817664a25 100644 --- a/core/lib/Drupal/Core/Cache/MemoryBackend.php +++ b/core/lib/Drupal/Core/Cache/MemoryBackend.php @@ -64,26 +64,21 @@ public function getMultiple(&$cids, $allow_invalid = FALSE) { * Checks that items are either permanent or did not expire, and returns data * as appropriate. * - * @param object $cache + * @param array $cache * An item loaded from self::get() or self::getMultiple(). * @param bool $allow_invalid * (optional) If TRUE, cache items may be returned even if they have expired * or been invalidated. * - * @return mixed + * @return object|false * The item with data as appropriate or FALSE if there is no * valid item to load. */ - protected function prepareItem($cache, $allow_invalid) { - if (!isset($cache->data)) { + protected function prepareItem(array $cache, $allow_invalid) { + if (!isset($cache['data'])) { return FALSE; } - // The object passed into this function is the one stored in $this->cache. - // We must clone it as part of the preparation step so that the actual - // cache object is not affected by the unserialize() call or other - // manipulations of the returned object. - - $prepared = clone $cache; + $prepared = (object) $cache; $prepared->data = unserialize($prepared->data); // Check expire time. @@ -104,7 +99,10 @@ public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) { $tags = array_unique($tags); // Sort the cache tags so that they are stored consistently in the database. sort($tags); - $this->cache[$cid] = (object) [ + + // Do not create an object at this point to minimize the number of objects + // garbage collection has to keep a track off. + $this->cache[$cid] = [ 'cid' => $cid, 'data' => serialize($data), 'created' => $this->getRequestTime(), @@ -148,7 +146,7 @@ public function deleteAll() { */ public function invalidate($cid) { if (isset($this->cache[$cid])) { - $this->cache[$cid]->expire = $this->getRequestTime() - 1; + $this->cache[$cid]['expire'] = $this->getRequestTime() - 1; } } @@ -158,7 +156,7 @@ public function invalidate($cid) { public function invalidateMultiple(array $cids) { $items = array_intersect_key($this->cache, array_flip($cids)); foreach ($items as $cid => $item) { - $this->cache[$cid]->expire = $this->getRequestTime() - 1; + $this->cache[$cid]['expire'] = $this->getRequestTime() - 1; } } @@ -167,8 +165,8 @@ public function invalidateMultiple(array $cids) { */ public function invalidateTags(array $tags) { foreach ($this->cache as $cid => $item) { - if (array_intersect($tags, $item->tags)) { - $this->cache[$cid]->expire = $this->getRequestTime() - 1; + if (array_intersect($tags, $item['tags'])) { + $this->cache[$cid]['expire'] = $this->getRequestTime() - 1; } } } @@ -178,7 +176,7 @@ public function invalidateTags(array $tags) { */ public function invalidateAll() { foreach ($this->cache as $cid => $item) { - $this->cache[$cid]->expire = $this->getRequestTime() - 1; + $this->cache[$cid]['expire'] = $this->getRequestTime() - 1; } } diff --git a/core/lib/Drupal/Core/Cache/MemoryCache/MemoryCache.php b/core/lib/Drupal/Core/Cache/MemoryCache/MemoryCache.php index 327110821159b6523c022dfe75505eebb7e96a49..841e4568a07270ecb284eb05087887e86bb4d822 100644 --- a/core/lib/Drupal/Core/Cache/MemoryCache/MemoryCache.php +++ b/core/lib/Drupal/Core/Cache/MemoryCache/MemoryCache.php @@ -8,40 +8,29 @@ /** * Defines a memory cache implementation. * - * Stores cache items in memory using a PHP array. + * Stores cache items in memory using a PHP array. Cache data is not serialized + * thereby returning the same object as was cached. * * @ingroup cache */ class MemoryCache extends MemoryBackend implements MemoryCacheInterface { /** - * Prepares a cached item. - * - * Checks that items are either permanent or did not expire, and returns data - * as appropriate. - * - * @param object $cache - * An item loaded from self::get() or self::getMultiple(). - * @param bool $allow_invalid - * (optional) If TRUE, cache items may be returned even if they have expired - * or been invalidated. Defaults to FALSE. - * - * @return mixed - * The item with data as appropriate or FALSE if there is no - * valid item to load. + * {@inheritdoc} */ - protected function prepareItem($cache, $allow_invalid = FALSE) { - if (!isset($cache->data)) { + protected function prepareItem(array $cache, $allow_invalid = FALSE) { + if (!isset($cache['data'])) { return FALSE; } + $prepared = (object) $cache; // Check expire time. - $cache->valid = $cache->expire == static::CACHE_PERMANENT || $cache->expire >= $this->getRequestTime(); + $prepared->valid = $prepared->expire == static::CACHE_PERMANENT || $prepared->expire >= $this->getRequestTime(); - if (!$allow_invalid && !$cache->valid) { + if (!$allow_invalid && !$prepared->valid) { return FALSE; } - return $cache; + return $prepared; } /** @@ -51,8 +40,11 @@ public function set($cid, $data, $expire = MemoryCacheInterface::CACHE_PERMANENT assert(Inspector::assertAllStrings($tags), 'Cache tags must be strings.'); $tags = array_unique($tags); - $this->cache[$cid] = (object) [ + // Do not create an object at this point to minimize the number of objects + // garbage collection has to keep a track off. + $this->cache[$cid] = [ 'cid' => $cid, + // Note that $data is not serialized. 'data' => $data, 'created' => $this->getRequestTime(), 'expire' => $expire, diff --git a/core/lib/Drupal/Core/Update/UpdateBackend.php b/core/lib/Drupal/Core/Update/UpdateBackend.php new file mode 100644 index 0000000000000000000000000000000000000000..08e4cfd1f6e7fd9e22ac73ed2c6c6cd822dd58a6 --- /dev/null +++ b/core/lib/Drupal/Core/Update/UpdateBackend.php @@ -0,0 +1,57 @@ +<?php + +namespace Drupal\Core\Update; + +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Cache\MemoryBackend; + +/** + * Defines a cache backend for use during updating Drupal. + * + * Passes on deletes to another backend while using a memory backend to avoid + * using anything cached prior to running updates. + */ +class UpdateBackend extends MemoryBackend { + + /** + * The regular runtime cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $backend; + + /** + * UpdateBackend constructor. + * + * @param \Drupal\Core\Cache\CacheBackendInterface $backend + * The regular runtime cache backend. + */ + public function __construct(CacheBackendInterface $backend) { + $this->backend = $backend; + } + + /** + * {@inheritdoc} + */ + public function delete($cid) { + parent::delete($cid); + $this->backend->delete($cid); + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple(array $cids) { + parent::deleteMultiple($cids); + $this->backend->deleteMultiple($cids); + } + + /** + * {@inheritdoc} + */ + public function deleteAll() { + parent::deleteAll(); + $this->backend->deleteAll(); + } + +} diff --git a/core/lib/Drupal/Core/Update/UpdateCacheBackendFactory.php b/core/lib/Drupal/Core/Update/UpdateCacheBackendFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..e3f7dd12f16526c4191b886802e51c026f003ce2 --- /dev/null +++ b/core/lib/Drupal/Core/Update/UpdateCacheBackendFactory.php @@ -0,0 +1,51 @@ +<?php + +namespace Drupal\Core\Update; + +use Drupal\Core\Cache\CacheFactoryInterface; + +/** + * Cache factory implementation for updating Drupal. + * + * Decorates the regular runtime cache_factory service so that caches use + * \Drupal\Core\Update\UpdateBackend. + * + * @see \Drupal\Core\Update\UpdateServiceProvider::register() + */ +class UpdateCacheBackendFactory implements CacheFactoryInterface { + + /** + * The regular runtime cache_factory service. + * + * @var \Drupal\Core\Cache\CacheFactoryInterface + */ + protected $cacheFactory; + + /** + * Instantiated update cache bins. + * + * @var \Drupal\Core\Update\UpdateBackend[] + */ + protected $bins = []; + + /** + * UpdateCacheBackendFactory constructor. + * + * @param \Drupal\Core\Cache\CacheFactoryInterface $cache_factory + * The regular runtime cache_factory service. + */ + public function __construct(CacheFactoryInterface $cache_factory) { + $this->cacheFactory = $cache_factory; + } + + /** + * {@inheritdoc} + */ + public function get($bin) { + if (!isset($this->bins[$bin])) { + $this->bins[$bin] = new UpdateBackend($this->cacheFactory->get($bin)); + } + return $this->bins[$bin]; + } + +} diff --git a/core/lib/Drupal/Core/Update/UpdateServiceProvider.php b/core/lib/Drupal/Core/Update/UpdateServiceProvider.php index 22c9131eb381ca935dd2fa33ffa809cffab36526..7ac68e6b18e4aab9d5f475af94002d49a04d0098 100644 --- a/core/lib/Drupal/Core/Update/UpdateServiceProvider.php +++ b/core/lib/Drupal/Core/Update/UpdateServiceProvider.php @@ -19,8 +19,16 @@ class UpdateServiceProvider implements ServiceProviderInterface, ServiceModifier */ public function register(ContainerBuilder $container) { $definition = new Definition('Drupal\Core\Cache\NullBackend', ['null']); + $definition->setDeprecated(TRUE, 'The "%service_id%\" service is deprecated. While updating Drupal all caches use \Drupal\Core\Update\UpdateBackend. See https://www.drupal.org/node/3066407'); $container->setDefinition('cache.null', $definition); + // Decorate the cache factory in order to use + // \Drupal\Core\Update\UpdateBackend while running updates. + $container + ->register('update.cache_factory', UpdateCacheBackendFactory::class) + ->setDecoratedService('cache_factory') + ->addArgument(new Reference('update.cache_factory.inner')); + $container->addCompilerPass(new UpdateCompilerPass(), PassConfig::TYPE_REMOVE, 128); } @@ -28,25 +36,6 @@ public function register(ContainerBuilder $container) { * {@inheritdoc} */ public function alter(ContainerBuilder $container) { - // Ensures for some services that they don't cache. - $null_cache_service = new Reference('cache.null'); - - $definition = $container->getDefinition('asset.resolver'); - $definition->replaceArgument(5, $null_cache_service); - - $definition = $container->getDefinition('library.discovery.collector'); - $definition->replaceArgument(0, $null_cache_service); - - $definition = $container->getDefinition('theme.registry'); - $definition->replaceArgument(1, $null_cache_service); - $definition->replaceArgument(7, $null_cache_service); - - $definition = $container->getDefinition('theme.initialization'); - $definition->replaceArgument(2, $null_cache_service); - - $definition = $container->getDefinition('plugin.manager.element_info'); - $definition->replaceArgument(1, $null_cache_service); - // Prevent the alias-based path processor, which requires a path_alias db // table, from being registered to the path processor manager. We do this by // removing the tags that the compiler pass looks for. This means the url diff --git a/core/modules/system/tests/src/Functional/Update/UpdateCacheTest.php b/core/modules/system/tests/src/Functional/Update/UpdateCacheTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1d9765da4ddf17dca44dab0e2be4e50bb7d66262 --- /dev/null +++ b/core/modules/system/tests/src/Functional/Update/UpdateCacheTest.php @@ -0,0 +1,44 @@ +<?php + +namespace Drupal\Tests\system\Functional\Update; + +use Drupal\Core\Url; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\RequirementsPageTrait; + +/** + * Tests caches during updates. + * + * @group Update + */ +class UpdateCacheTest extends BrowserTestBase { + use RequirementsPageTrait; + + /** + * Tests that caches are cleared during updates. + * + * @see \Drupal\Core\Update\UpdateServiceProvider + * @see \Drupal\Core\Update\UpdateBackend + */ + public function testCaches() { + \Drupal::cache()->set('will_not_exist_after_update', TRUE); + // The site might be broken at the time so logging in using the UI might + // not work, so we use the API itself. + $this->writeSettings([ + 'settings' => [ + 'update_free_access' => (object) [ + 'value' => TRUE, + 'required' => TRUE, + ], + ], + ]); + + // Clicking continue should clear the caches. + $this->drupalGet(Url::fromRoute('system.db_update', [], ['path_processing' => FALSE])); + $this->updateRequirementsProblem(); + $this->clickLink(t('Continue')); + + $this->assertFalse(\Drupal::cache()->get('will_not_exist_after_update', FALSE)); + } + +}