Issue #2463581 by Wim Leers, swentel: #cache_redirect cache items should have an 'expire' timestamp that matches the merged max-age

8.0.x
Nathaniel Catchpole 2015-07-01 11:19:02 +01:00
parent 7f420bd908
commit 6b371cc5d5
2 changed files with 68 additions and 27 deletions

View File

@ -8,6 +8,7 @@
namespace Drupal\Core\Render;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\Core\Cache\CacheFactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
@ -96,7 +97,6 @@ class RenderCache implements RenderCacheInterface {
$data = $this->getCacheableRenderArray($elements);
$bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
$expire = ($elements['#cache']['max-age'] === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $elements['#cache']['max-age'];
$cache = $this->cacheFactory->get($bin);
// Calculate the pre-bubbling CID.
@ -207,26 +207,28 @@ class RenderCache implements RenderCacheInterface {
// across requests. That's the strategy employed below and tested in
// \Drupal\Tests\Core\Render\RendererBubblingTest::testConditionalCacheContextBubblingSelfHealing().
// The set of cache contexts for this element, including the bubbled ones,
// for which we are handling a cache miss.
$cache_contexts = $data['#cache']['contexts'];
// Get the contexts by which this element should be varied according to
// the current redirecting cache item, if any.
$stored_cache_contexts = [];
$stored_cache_tags = [];
// Get the cacheability of this element according to the current (stored)
// redirecting cache item, if any.
$redirect_cacheability = new CacheableMetadata();
if ($stored_cache_redirect = $cache->get($pre_bubbling_cid)) {
$stored_cache_contexts = $stored_cache_redirect->data['#cache']['contexts'];
$stored_cache_tags = $stored_cache_redirect->data['#cache']['tags'];
$redirect_cacheability = CacheableMetadata::createFromRenderArray($stored_cache_redirect->data);
}
// Calculate the union of the cache contexts for this request and the
// stored cache contexts.
$merged_cache_contexts = Cache::mergeContexts($stored_cache_contexts, $cache_contexts);
// Calculate the union of the cacheability for this request and the
// current (stored) redirecting cache item. We need:
// - the union of cache contexts, because that is how we know which cache
// item to redirect to;
// - the union of cache tags, because that is how we know when the cache
// redirect cache item itself is invalidated;
// - the union of max ages, because that is how we know when the cache
// redirect cache item itself becomes stale. (Without this, we might end
// up toggling between a permanently and a briefly cacheable cache
// redirect, because the last update's max-age would always "win".)
$redirect_cacheability_updated = CacheableMetadata::createFromRenderArray($data)->merge($redirect_cacheability);
// Stored cache contexts incomplete: this request causes cache contexts to
// be added to the redirecting cache item.
if (array_diff($merged_cache_contexts, $stored_cache_contexts)) {
if (array_diff($redirect_cacheability_updated->getCacheContexts(), $redirect_cacheability->getCacheContexts())) {
$redirect_data = [
'#cache_redirect' => TRUE,
'#cache' => [
@ -234,14 +236,16 @@ class RenderCache implements RenderCacheInterface {
// across requests.
'keys' => $elements['#cache']['keys'],
// The union of the current element's and stored cache contexts.
'contexts' => $merged_cache_contexts,
'contexts' => $redirect_cacheability_updated->getCacheContexts(),
// The union of the current element's and stored cache tags.
'tags' => Cache::mergeTags($stored_cache_tags, $data['#cache']['tags']),
'tags' => $redirect_cacheability_updated->getCacheTags(),
// The union of the current element's and stored cache max-ages.
'max-age' => $redirect_cacheability_updated->getCacheMaxAge(),
// The same cache bin as the one for the actual render cache items.
'bin' => $bin,
],
];
$cache->set($pre_bubbling_cid, $redirect_data, $expire, Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered']));
$cache->set($pre_bubbling_cid, $redirect_data, $this->maxAgeToExpire($redirect_cacheability_updated->getCacheMaxAge()), Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered']));
}
// Current cache contexts incomplete: this request only uses a subset of
@ -249,20 +253,35 @@ class RenderCache implements RenderCacheInterface {
// additional (conditional) cache contexts as well, otherwise the
// redirecting cache item would be pointing to a cache item that can never
// exist.
if (array_diff($merged_cache_contexts, $cache_contexts)) {
if (array_diff($redirect_cacheability_updated->getCacheContexts(), $data['#cache']['contexts'])) {
// Recalculate the cache ID.
$recalculated_cid_pseudo_element = [
'#cache' => [
'keys' => $elements['#cache']['keys'],
'contexts' => $merged_cache_contexts,
'contexts' => $redirect_cacheability_updated->getCacheContexts(),
]
];
$cid = $this->createCacheID($recalculated_cid_pseudo_element);
// Ensure the about-to-be-cached data uses the merged cache contexts.
$data['#cache']['contexts'] = $merged_cache_contexts;
$data['#cache']['contexts'] = $redirect_cacheability_updated->getCacheContexts();
}
}
$cache->set($cid, $data, $expire, Cache::mergeTags($data['#cache']['tags'], ['rendered']));
$cache->set($cid, $data, $this->maxAgeToExpire($elements['#cache']['max-age']), Cache::mergeTags($data['#cache']['tags'], ['rendered']));
}
/**
* Maps a #cache[max-age] value to an "expire" value for the Cache API.
*
* @param int $max_age
* A #cache[max-age] value.
*
* @return int
* A corresponding "expire" value.
*
* @see \Drupal\Core\Cache\CacheBackendInterface::set()
*/
protected function maxAgeToExpire($max_age) {
return ($max_age === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $max_age;
}
/**

View File

@ -125,6 +125,7 @@ class RendererBubblingTest extends RendererTestBase {
'contexts' => ['bar', 'foo'],
'tags' => [],
'bin' => $bin,
'max-age' => 3600,
],
], $bin);
}
@ -279,6 +280,7 @@ class RendererBubblingTest extends RendererTestBase {
'contexts' => ['bar', 'foo'],
'tags' => ['dee', 'fiddle', 'har', 'yar'],
'bin' => 'render',
'max-age' => Cache::PERMANENT,
],
],
'parent:bar:foo' => [
@ -325,6 +327,8 @@ class RendererBubblingTest extends RendererTestBase {
'#cache' => [
'contexts' => ['foo'],
'tags' => ['c'],
// A lower max-age; the redirecting cache item should be updated.
'max-age' => 1800,
],
'grandgrandchild' => [
'#access_callback' => function () {
@ -335,6 +339,8 @@ class RendererBubblingTest extends RendererTestBase {
'#cache' => [
'contexts' => ['bar'],
'tags' => ['d'],
// A lower max-age; the redirecting cache item should be updated.
'max-age' => 300,
],
],
],
@ -353,6 +359,7 @@ class RendererBubblingTest extends RendererTestBase {
'contexts' => ['user.roles'],
'tags' => ['a', 'b'],
'bin' => 'render',
'max-age' => Cache::PERMANENT,
],
]);
$this->assertRenderCacheItem('parent:r.A', [
@ -366,7 +373,7 @@ class RendererBubblingTest extends RendererTestBase {
]);
// Request 2: role B, the grandchild is accessible => bubbled cache
// contexts: foo, user.roles.
// contexts: foo, user.roles + merged max-age: 1800.
$element = $test_element;
$current_user_role = 'B';
$this->renderer->renderRoot($element);
@ -377,6 +384,7 @@ class RendererBubblingTest extends RendererTestBase {
'contexts' => ['foo', 'user.roles'],
'tags' => ['a', 'b', 'c'],
'bin' => 'render',
'max-age' => 1800,
],
]);
$this->assertRenderCacheItem('parent:foo:r.B', [
@ -384,7 +392,7 @@ class RendererBubblingTest extends RendererTestBase {
'#cache' => [
'contexts' => ['foo', 'user.roles'],
'tags' => ['a', 'b', 'c'],
'max-age' => Cache::PERMANENT,
'max-age' => 1800,
],
'#markup' => 'parent',
]);
@ -409,6 +417,7 @@ class RendererBubblingTest extends RendererTestBase {
'contexts' => ['foo', 'user.roles'],
'tags' => ['a', 'b', 'c'],
'bin' => 'render',
'max-age' => 1800,
],
]);
$this->assertRenderCacheItem('parent:foo:r.A', [
@ -416,13 +425,17 @@ class RendererBubblingTest extends RendererTestBase {
'#cache' => [
'contexts' => ['foo', 'user.roles'],
'tags' => ['a', 'b'],
// Note that the max-age here is unaffected. When role A, the grandchild
// is never rendered, so neither is its max-age of 1800 present here,
// despite 1800 being the max-age of the redirecting cache item.
'max-age' => Cache::PERMANENT,
],
'#markup' => 'parent',
]);
// Request 4: role C, both the grandchild and the grandgrandchild are
// accessible => bubbled cache contexts: foo, bar, user.roles.
// accessible => bubbled cache contexts: foo, bar, user.roles + merged
// max-age: 300.
$element = $test_element;
$current_user_role = 'C';
$this->renderer->renderRoot($element);
@ -433,6 +446,7 @@ class RendererBubblingTest extends RendererTestBase {
'contexts' => ['bar', 'foo', 'user.roles'],
'tags' => ['a', 'b', 'c', 'd'],
'bin' => 'render',
'max-age' => 300,
],
];
$this->assertRenderCacheItem('parent', $final_parent_cache_item);
@ -441,7 +455,7 @@ class RendererBubblingTest extends RendererTestBase {
'#cache' => [
'contexts' => ['bar', 'foo', 'user.roles'],
'tags' => ['a', 'b', 'c', 'd'],
'max-age' => Cache::PERMANENT,
'max-age' => 300,
],
'#markup' => 'parent',
]);
@ -456,6 +470,10 @@ class RendererBubblingTest extends RendererTestBase {
'#cache' => [
'contexts' => ['bar', 'foo', 'user.roles'],
'tags' => ['a', 'b'],
// Note that the max-age here is unaffected. When role A, the grandchild
// is never rendered, so neither is its max-age of 1800 present here,
// nor the grandgrandchild's max-age of 300, despite 300 being the
// max-age of the redirecting cache item.
'max-age' => Cache::PERMANENT,
],
'#markup' => 'parent',
@ -471,7 +489,11 @@ class RendererBubblingTest extends RendererTestBase {
'#cache' => [
'contexts' => ['bar', 'foo', 'user.roles'],
'tags' => ['a', 'b', 'c'],
'max-age' => Cache::PERMANENT,
// Note that the max-age here is unaffected. When role B, the
// grandgrandchild is never rendered, so neither is its max-age of 300
// present here, despite 300 being the max-age of the redirecting cache
// item.
'max-age' => 1800,
],
'#markup' => 'parent',
]);