Problem/Motivation

This blocks #2466585: Decouple cache implementation from the renderer and expose as renderCache service
and #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts

It is possible to corrupt the redirect cache by not varying cache contexts, but cache keys and trying to bubble that.

Proposed resolution

- Fix it by throwing a LogicException as it is a hard requirement that keys are not changed afterwards.
- Instead we could also opt to use the keys of the pre_bubbling_elements always.
- This refactors from pre_bubbling_cid to pre_bubbling_elements so that #keys are available for comparison and its a needed pre-step, too.

It also fixes cases where keys are suddenly available, but never before, so leading to unreachable cache entries.

Remaining tasks

- Review
- Commit

User interface changes

* None

API changes

* None

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug because functionality is broken
Issue priority Normal bugfix
Prioritized changes The main goal of this issue is fixing a side effect of the cache bubbling system that could be gotten wrong. Therefore: Correctness and DX.
Disruption Only internal changes, not disruptive at all.
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Fabianx’s picture

This is both a bugfix as well as a requirement for placeholders / BigPipe (split out to get early reviews on it).

Fabianx’s picture

Status: Active » Needs review
FileSize
2.56 KB
7.2 KB

- Fixed with a test
- Added unrelated @todo that I found while looking at the code (could also be removed)
- Exception message might need work

Fabianx’s picture

The last submitted patch, 2: changing_cache_keys-2469277-2--test-only.patch, failed testing.

Fabianx’s picture

Priority: Normal » Major
Issue tags: +blocker

This blocks #2466585: Decouple cache implementation from the renderer and expose as renderCache service as that issue will be simpler once $pre_bubbling_elements is used for cacheSet() as second argument and we don't want to change API for that.

It also blocks #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts, but that is just in proof-of-concept stage.

Hence increasing priority.

Wim Leers’s picture

Test review:

  1. +++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
    @@ -502,6 +502,45 @@ public function providerTestBubblingWithPrerender() {
    +   * Tests that overwriting #cache keys does not corrupt the shared bubble cache.
    

    s/does not/cannot/

  2. +++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
    @@ -502,6 +502,45 @@ public function providerTestBubblingWithPrerender() {
    +    // Setup a shared cache redirect at llama::bar pointing to llama:bar:r.1
    

    Nit: s/llama::bar/llama:bar/

  3. +++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
    @@ -502,6 +502,45 @@ public function providerTestBubblingWithPrerender() {
    +    // Ensure the cached redirect is not overwritten when non-matching keys
    

    80 cols.

  4. +++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
    @@ -583,4 +622,27 @@ public static function bubblingPostRenderCache(array $element, array $context) {
    +   * #pre_render callback for testOverWriteCacheKeys().
    ...
    +      'keys' => ['llama', 'foo'],
    +      'contexts' => ['overwritefoo'],
    

    I expected this to only be overriding keys, but it's also overriding contexts?


Code review:

  1. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -201,6 +200,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +          // @todo This does not work in all cases as recursive #post_render_cache
    +          // is not supported.
    

    That's not actually true, we support that. Since #2382667: #post_render_callback's that result from other #post_render_calback are not processed . Test coverage at \Drupal\Tests\Core\Render\RendererPostRenderCacheTest::testRenderRecursivePostRenderCache().

  2. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -212,16 +213,11 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    // Two-tier caching: set pre-bubbling elements for later comparison.
    +    // @see ::cacheGet()
    +    // @see ::cacheSet()
    +    $pre_bubbling_elements = $elements;
    
    @@ -381,10 +377,13 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    // Cache the processed element if both $pre_bubbling_elements and $elements
    +    // have the metadata necessary to generate a cache ID.
    +    if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) {
    

    I'd think this also costs us extra memory. Is this really necessary?

    Can't we instead: 1) keep $pre_bubbling_cid, 2) add $pre_bubbling_cache_keys (or even just $cache_keys)?

    i.e. keep the same logic, but track the original cache keys and continue to use that, thus ignoring any cache keys set by #pre_render callbacks?

Wim Leers’s picture

Status: Needs review » Needs work
Fabianx’s picture

Test review notes:

1. Will fix
2. Will fix
3. Will fix
4. Will ...

Hold on ;)

I expected this to only be overriding keys, but it's also overriding contexts?

Yes, if nothing else is bubbled the smart code in cacheSet won't overwrite the shared cache entry ;).

I think I will add a comment there, why overwrite foo is needed.


Code reviews:

1. The cache hit case is not covered (I have a test proving that), I will open a separate issue for that and apply the test-only patch.
2. Discussed in IRC

We came to the conclusion we only need the original $elements['#cache'] for now.

Fabianx’s picture

Status: Needs work » Needs review
FileSize
2.96 KB
6.79 KB
YesCT’s picture

YesCT’s picture

Status: Needs review » Needs work
+++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
@@ -502,6 +502,45 @@ public function providerTestBubblingWithPrerender() {
+    // Setup a shared cache redirect at llama:bar pointing to llama:bar:r.1

Oh, r.1 stands for ..
role 1

----

I didn't understand everything, but looks like most of the items from @Wim Leers 's #6 were addressed, except @Fabianx mentioned "I think I will add a comment there, why overwrite foo is needed." and I do not see that change.

---

+++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
@@ -583,4 +622,27 @@ public static function bubblingPostRenderCache(array $element, array $context) {
+   * #pre_render callback for testOverWriteCacheKeys().
+   */
+  public static function bubblingCacheNoOverwritePrerender($elements) {
+    // Overwrite the #cache entry with new data.
+    $elements['#cache']['contexts'] = ['user.roles'];
...
+  public static function bubblingCacheOverwritePrerender($elements) {

Is "Overwrite the #cache entry with new data." a copy and paste error? Because this does is called "NoOverwrite" and it is the same comment as in bubblingCacheOverwritePrerender()

YesCT’s picture

Fabianx’s picture

Thanks for the review, YesCT! And yes I indeed missed implementing my comment!

Fabianx’s picture

Status: Needs work » Needs review
FileSize
1.18 KB
6.9 KB

And fixed the issues from #12.

Fabianx’s picture

Issue tags: +D8 Accelerate Dev Days

Tagging for dev days

Wim Leers’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -212,16 +211,12 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +    // Two-tier caching: set pre-bubbling elements for later comparison.
    

    s/set/track/
    s/elements/elements' #cache/

  2. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -381,10 +376,13 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +      if ($pre_bubbling_elements['#cache']['keys'] != $elements['#cache']['keys']) {
    

    Let's use strict comparison.

  3. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -381,10 +376,13 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
    +        throw new \LogicException('Cache keys cannot be changed after initial setup. Use the contexts property instead to bubble added metadata.');
    

    s/cannot/may not/
    s/added/additional/

  4. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -551,15 +549,15 @@ protected function cacheGet(array $elements) {
    +   *   The pre-bubbling render cache array.
    

    The pre-bubbling cacheability metadata.

  5. +++ b/core/lib/Drupal/Core/Render/Renderer.php
    @@ -574,11 +572,14 @@ protected function cacheSet(array &$elements, $pre_bubbling_cid) {
    +    // Calculate pre_bubbling_cid.
    

    Calculate the pre-bubbling CID.

  6. +++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
    @@ -502,6 +502,45 @@ public function providerTestBubblingWithPrerender() {
    +    // Setup a shared cache redirect at llama:bar pointing to llama:bar:r.1
    

    s/Setup/Set up/

  7. +++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
    @@ -502,6 +502,45 @@ public function providerTestBubblingWithPrerender() {
    +    $redirect_after = $this->memoryCache->get('llama:bar');
    +
    +    $this->assertEquals($redirect, $redirect_after, 'Invalid bubbled keys cause redirect to be the same.');
    

    These are never executed because the exception is triggered by the ::render() call. Let's remove them.

marcvangend’s picture

Seems like a novice re-roll, I'll do it.

marcvangend’s picture

Status: Needs work » Needs review
FileSize
6.76 KB

Here's a patch with all changes proposed in #16.

Wim Leers’s picture

@marcvangend: can you post an interdiff? :) Thanks!

Fabianx’s picture

FileSize
3.93 KB
6.76 KB

Thanks, addressed all feedback. (should be the same as marc's)

Fabianx’s picture

FileSize
622 bytes
6.77 KB

Thanks to marc's patch (which will fail tests due to missing update to the Exception) I found a coding style, line too long error in mine ...

The last submitted patch, 18: changing_cache_keys-2469277-18.patch, failed testing.

Wim Leers’s picture

Assigned: Fabianx » Unassigned
Status: Needs review » Reviewed & tested by the community

Thanks for #21, I was going to mark it NW for that :)

effulgentsia’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
6.12 KB
3.12 KB

Patch looks great, but the test seems to run a bunch of lines of code without asserting anything related to them. Since the fix that we ended up with here is to simply disallow changing #cache['keys'] mid-rendering (completely independently of anything having to do with contexts), how about simplifying the test to only testing that? See patch.

I also expanded the docs of the $pre_bubbling_elements parameter in cacheSet().

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

The interdiff looks great to me, therefore back to RTBC.

Yes, indeed we only need to test wanted behavior. Not the reasons why.

Thanks so much!

  • effulgentsia committed 8de7842 on 8.0.x
    Issue #2469277 by Fabianx, effulgentsia, marcvangend, Wim Leers, YesCT:...
effulgentsia’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed to 8.0.x. Thanks all! (Yay, my first commit)

I don't think this needs a change record, because if a D7 or D8 module was changing #cache['keys'] after the initial cache get, it was broken anyway. This issue just changes it from being broken silently to having an explicit exception.

Fabianx’s picture

Congratulations on the first commit!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

vincenzodb’s picture

Maybe the problem of this bug report (https://www.drupal.org/node/2589703) is related with changes in this issue.

Fabianx’s picture

#30: This issue revealed the bug that was present in #2589703: Override number of items to display in contextual filter doesn't work.

So this issue is just the bearer of the bad news / Exception. It is not the cause of it :).

vincenzodb’s picture

Ok, I've started my period with 'maybe' :)