diff --git a/core/core.services.yml b/core/core.services.yml index c1f36142b3..cd57452b70 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -569,7 +569,7 @@ services: class: Drupal\Core\Extension\ThemeInstaller arguments: ['@theme_handler', '@config.factory', '@config.installer', '@module_handler', '@config.manager', '@asset.css.collection_optimizer', '@router.builder', '@logger.channel.default', '@state', '@extension.list.module'] entity.memory_cache: - class: Drupal\Core\Cache\MemoryCache\MemoryCache + class: Drupal\Core\Cache\MemoryCache\WeakReferenceMemoryCache entity_type.manager: class: Drupal\Core\Entity\EntityTypeManager arguments: ['@container.namespaces', '@module_handler', '@cache.discovery', '@string_translation', '@class_resolver', '@entity.last_installed_schema.repository'] diff --git a/core/lib/Drupal/Core/Cache/MemoryCache/WeakReferenceMemoryCache.php b/core/lib/Drupal/Core/Cache/MemoryCache/WeakReferenceMemoryCache.php new file mode 100644 index 0000000000..c8c99bb8a6 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/MemoryCache/WeakReferenceMemoryCache.php @@ -0,0 +1,92 @@ +data)) { + return FALSE; + } + // Check expire time. + $cache->valid = $cache->expire == static::CACHE_PERMANENT || $cache->expire >= $this->getRequestTime(); + + if (!$allow_invalid && !$cache->valid) { + return FALSE; + } + + // If the cache data is a WeakReference, then reconstruct the $cache + // object so that it contains the original data, when available. + if ($cache->data instanceof \WeakReference) { + $data = $cache->data->get(); + if (!isset($data)) { + return FALSE; + } + return (object) [ + 'cid' => $cache->cid, + 'data' => $data, + 'created' => $cache->created, + 'expire' => $cache->expire, + 'tags' => $cache->tags, + ]; + } + + return $cache; + } + + /** + * {@inheritdoc} + */ + public function set($cid, $data, $expire = MemoryCacheInterface::CACHE_PERMANENT, array $tags = []) { + assert(Inspector::assertAllStrings($tags), 'Cache tags must be strings.'); + $tags = array_unique($tags); + + if (is_object($data)) { + $this->cache[$cid] = (object) [ + 'cid' => $cid, + 'data' => \WeakReference::create($data), + 'created' => $this->getRequestTime(), + 'expire' => $expire, + 'tags' => $tags, + ]; + } + else { + $this->cache[$cid] = (object) [ + 'cid' => $cid, + 'data' => $data, + 'created' => $this->getRequestTime(), + 'expire' => $expire, + 'tags' => $tags, + ]; + } + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Cache/GenericCacheBackendUnitTestBase.php b/core/tests/Drupal/KernelTests/Core/Cache/GenericCacheBackendUnitTestBase.php index c19bc88c8c..dfeb9fea47 100644 --- a/core/tests/Drupal/KernelTests/Core/Cache/GenericCacheBackendUnitTestBase.php +++ b/core/tests/Drupal/KernelTests/Core/Cache/GenericCacheBackendUnitTestBase.php @@ -4,6 +4,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface; use Drupal\KernelTests\KernelTestBase; /** @@ -97,6 +98,9 @@ protected function getCacheBackend($bin = NULL) { $this->cachebackends[$bin] = $this->createCacheBackend($bin); // Ensure the backend is empty. $this->cachebackends[$bin]->deleteAll(); + if ($this->cachebackends[$bin] instanceof MemoryCacheInterface) { + \Drupal::service('cache_tags.invalidator')->addInvalidator($this->cachebackends[$bin]); + } } return $this->cachebackends[$bin]; } @@ -189,25 +193,6 @@ public function testSetGet() { $this->assertIsObject($cached); $this->assertSame($with_variable, $cached->data); - // Make sure that a cached object is not affected by changing the original. - $data = new \stdClass(); - $data->value = 1; - $data->obj = new \stdClass(); - $data->obj->value = 2; - $backend->set('test7', $data); - $expected_data = clone $data; - // Add a property to the original. It should not appear in the cached data. - $data->this_should_not_be_in_the_cache = TRUE; - $cached = $backend->get('test7'); - $this->assertIsObject($cached); - $this->assertEqual($expected_data, $cached->data); - $this->assertFalse(isset($cached->data->this_should_not_be_in_the_cache)); - // Add a property to the cache data. It should not appear when we fetch - // the data from cache again. - $cached->data->this_should_not_be_in_the_cache = TRUE; - $fresh_cached = $backend->get('test7'); - $this->assertFalse(isset($fresh_cached->data->this_should_not_be_in_the_cache)); - // Check with a long key. $cid = str_repeat('a', 300); $backend->set($cid, 'test'); @@ -228,6 +213,31 @@ public function testSetGet() { } } + /** + * Tests that cache objects are not stored by reference. + */ + public function testSetGetNoReference() { + $backend = $this->getCacheBackend(); + // Make sure that a cached object is not affected by changing the original. + $data = new \stdClass(); + $data->value = 1; + $data->obj = new \stdClass(); + $data->obj->value = 2; + $backend->set('test7', $data); + $expected_data = clone $data; + // Add a property to the original. It should not appear in the cached data. + $data->this_should_not_be_in_the_cache = TRUE; + $cached = $backend->get('test7'); + $this->assertIsObject($cached); + $this->assertEqual($expected_data, $cached->data); + $this->assertFalse(isset($cached->data->this_should_not_be_in_the_cache)); + // Add a property to the cache data. It should not appear when we fetch + // the data from cache again. + $cached->data->this_should_not_be_in_the_cache = TRUE; + $fresh_cached = $backend->get('test7'); + $this->assertFalse(isset($fresh_cached->data->this_should_not_be_in_the_cache)); + } + /** * Tests Drupal\Core\Cache\CacheBackendInterface::delete(). */ diff --git a/core/tests/Drupal/KernelTests/Core/Cache/WeakReferenceMemoryCacheTest.php b/core/tests/Drupal/KernelTests/Core/Cache/WeakReferenceMemoryCacheTest.php new file mode 100644 index 0000000000..f57b7a4ae4 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Cache/WeakReferenceMemoryCacheTest.php @@ -0,0 +1,83 @@ +differentScope($memory_cache, $key); + $cached = $memory_cache->get($key); + $this->assertSame($cached->data->{$key}, $key); + unset($cached); + $this->assertFalse($memory_cache->get($key)); + } + + /** + * Helper function to set the cache item in a different scope. + * + * WeakReference references should persist across scope, but be removed + * if an object is explicitly unset. + */ + protected function differentScope($memory_cache, $key) { + $item = (object) [$key => $key]; + $memory_cache->set($key, $item); + $cached = $memory_cache->get($key); + $this->assertSame($item, $cached->data); + } + + + /** + * Tests Drupal\Core\Cache\CacheBackendInterface::invalidateTags(). + */ + public function testInvalidateTags() { + $backend = $this->getCacheBackend(); + + // Create two cache entries with the same tag and tag value. + $backend->set('test_cid_invalidate1', $this->defaultValue, $backend::CACHE_PERMANENT, ['test_tag:2']); + $backend->set('test_cid_invalidate2', $this->defaultValue, $backend::CACHE_PERMANENT, ['test_tag:2']); + $this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate1')->data); + $this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate2')->data); + + // Invalidate test_tag of value 1. This should invalidate both entries. + $backend->invalidateTags(['test_tag:2']); + $this->assertFalse($backend->get('test_cid_invalidate1') || $backend->get('test_cid_invalidate2'), 'Two cache items invalidated after invalidating a cache tag.'); + // Verify that cache items have not been deleted after invalidation. + $this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate1', TRUE)->data); + $this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate2', TRUE)->data); + } + + /** + * Don't test reference/resource behaviour. + * + * MemoryCacheInterface requires that objects are the same instance when + * retrieved from cache. + * + * @see Drupal\Core\Cache\MemoryCache\MemoryCacheInterface + */ + public function testSetGetNoReference() { + $this->markTestSkipped(); + } + + /** + * {@inheritdoc} + */ + protected function createCacheBackend($bin) { + return new WeakReferenceMemoryCache(); + } + +}