diff -u b/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php --- b/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -544,7 +544,7 @@ $data = $this->getCacheableRenderArray($elements); - // Cache tags are cached, but we also want to assocaite the "rendered" cache + // Cache tags are cached, but we also want to associate the "rendered" cache // tag. This allows us to invalidate the entire render cache, regardless of // the cache bin. $data['#cache']['tags'][] = 'rendered'; @@ -558,6 +558,106 @@ // @see ::doRender() // @see ::cacheGet() if (isset($pre_bubbling_cid) && $pre_bubbling_cid !== $cid) { + // The cache redirection strategy we're implementing here is pretty + // simple in concept. Suppose we have the following render structure: + // - A (pre-bubbling, specifies #cache['keys'] = ['foo']) + // -- B (specifies #cache['contexts'] = ['b']) + // + // At the time that we're evaluating whether A's rendering can be + // retrieved from cache, we won't know the contexts required by its + // children (the children might not even be built yet), so cacheGet() + // will only be able to get what is cached for a $cid of 'foo'. But at + // the time we're writing to that cache, we do know all the contexts that + // were specified by all children, so what we need is a way to + // persist that information between the cache write and the next cache + // read. So, what we can do is store the following into 'foo': + // [ + // '#cache_redirect' => TRUE, + // '#cache' => [ + // ... + // 'contexts' => ['b'], + // ], + // ] + // + // This efficiently lets cacheGet() redirect to a $cid that includes all + // of the required contexts. The strategy is on-demand: in the case where + // there aren't any additional contexts required by children that aren't + // already included in the parent's pre-bubbled #cache information, no + // cache redirection is needed. + // + // When implementing this redirection strategy, special care is needed to + // resolve potential cache ping-pong problems. For example, consider the + // following render structure: + // - A (pre-bubbling, specifies #cache['keys'] = ['foo']) + // -- B (pre-bubbling, specifies #cache['contexts'] = ['b']) + // --- C (pre-bubbling, specifies #cache['contexts'] = ['c']) + // --- D (pre-bubbling, specifies #cache['contexts'] = ['d']) + // + // Additionally, suppose that: + // - C only exists for a 'b' context value of 'b1' + // - D only exists for a 'b' context value of 'b2' + // This is an acceptable variation, since B specifies that its contents + // vary on context 'b'. + // + // A naive implementation of cache redirection would result in the + // following: + // - When a request is processed where context 'b' = 'b1', what would be + // cached for a $pre_bubbling_cid of 'foo' is: + // [ + // '#cache_redirect' => TRUE, + // '#cache' => [ + // ... + // 'contexts' => ['b', 'c'], + // ], + // ] + // - When a request is processed where context 'b' = 'b2', we would + // retrieve the above from cache, but when following that redirection, + // get a cache miss, since we're processing a 'b' context value that + // has not yet been cached. Given the cache miss, we would continue + // with rendering the structure, perform the required context bubbling + // and then overwrite the above item with: + // [ + // '#cache_redirect' => TRUE, + // '#cache' => [ + // ... + // 'contexts' => ['b', 'd'], + // ], + // ] + // - Now, if a request comes in where context 'b' = 'b1' again, the above + // would redirect to a cache key that doesn't exist, since we have not + // yet cached an item that includes 'b'='b1' and something for 'd'. So + // we would process this request as a cache miss, at the end of which, + // we would overwrite the above item back to: + // [ + // '#cache_redirect' => TRUE, + // '#cache' => [ + // ... + // 'contexts' => ['b', 'c'], + // ], + // ] + // - The above would always result in accurate renderings, but would + // result in poor performance as we keep processing requests as cache + // misses even though the target of the redirection is cached, and + // it's only the redirection element itself that is creating the + // ping-pong problem. + // + // A way to resolve the ping-pong problem is to eventually reach a cache + // state where the redirection element includes all of the contexts used + // throughout all requests: + // [ + // '#cache_redirect' => TRUE, + // '#cache' => [ + // ... + // 'contexts' => ['b', 'c', 'd'], + // ], + // ] + // + // We can't reach that state right away, since we don't know what the + // result of future requests will be, but we can incrementally move + // towards that state by progressively merging the 'contexts' value + // 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'];