-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Fix PageCache: async rendering of blocks can corrupt layout cache #8554 #9050 #9560
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix PageCache: async rendering of blocks can corrupt layout cache #8554 #9050 #9560
Conversation
Good catch. A couple of questions:
|
For sure, @adragus-inviqa, I'll try to detail my answer as much as I can. About your first question: How is that additional_page_cache_handle handle fixing the issue? Can you expand on it a bit? Just for my knowledge. For the example, we are using page custom layout update to insert a cms block into content container:
First of all, layout cache does not only contain merged xml files; it saves separately:
When layout data is saved into cache, it needs to generate a cache id. As it's considered that a unique set of handles should produce the same result, layout handles are used to generate an unique id for that set of handles. This can be checked here: \Magento\Framework\View\Model\Layout\Merge::getCacheId
This method is used by the three layout cache types explained above. Let's start erasing layout and full page cache. When you are requesting a cms page, and full page cache is not hit, \Magento\Cms\Helper\Page::prepareResultPage method is called. This method is responsible of applying custom page layout handle and custom page layout updates for the current request. At the end of that method we find this: This forces to build the layout in three steps, as found in \Magento\Framework\View\Layout\Builder::build method:
The last two save data in layout cache; generateLayoutXml() method saves merged xml (including custom layout updates!) and page layout info, and generateLayoutBlocks() save the page structure. Page gets rendered ok, cached data is ok, and output result is cached in full page cache. So far so good. At this point, with layout and full page content cached, let's say your layout cache gets invalidated, because of its expiry date, or another reason, but your full page cache for that page is still valid. If you have a private content block in that cms page, an ajax call to /page_cache/block/render/ url is performed, containing the same layout handles that were used in normal load of the page (see example in PR description). Let's see what happens from here. Since this ajax call uses the layout in the standard way to get blocks and render them, layout is rebuilt with specified handles. Previous steps building the layout are reproduced again, but without initializing the involved cms page, so its custom layout updates are not loaded. But as the layout handles are the same, the same cache id is generated, and merged xml and page structure get cached without the page custom updates. Now we have a cache version, with the same key, and diferent content than expected. Now, let's clean only full page cache. You may think this is a little bit tricky scenario, so let's suppose full page cache expires at a different point in time that layout cache. Let's reload the page. Full page cache misses, so, as said before, \Magento\Cms\Helper\Page::prepareResultPage method is called, adding proper page custom layout updates. Surprisingly, even with wrong merged xml files in layout cache (not including page custom layout), the resulting merged xml for the current request is ok, but block added via layout update is not showing up. What's happening? The answer is in the step 3 of layout build, when layout tries to generate the blocks, \Magento\Framework\View\Layout::generateElements is called, and we find this piece of code here:
We can see this: $this->getUpdate()->getCacheId(), that again is based on current layout handles... Unfortunately, this cache id now exists in cache: it was generated by the ajax request with the same handles, and when that cache was saved, didn't include the correct page structure, since it did not load the cms page to load it's custom layout updates! So an invalid page structure is loaded from cache, without our added block, and it's used to render the elements. I've put this example with a cms block included via custom layout update, but it's a potential issue for the rest of layout updates coming from db, that may get overriden eventually by this ajax request. That's why, by adding a 'dummy' handle for that ajax request, handle that is not intended to be used by anyone else, cache id gets unique for all ajax requests of that type, so it never conflicts with regular requests, that will be forced (if needed) to generate its own data. Last paragraph contains the answer to question 2. It's not a constant because it is not intended to be used anywhere else (excluding Test classes) nor referenced nowhere, it's a private property of that class for private use. I hope this explanation helps ;) |
Many thanks for the write-up. I get it now. One small suggestion: if you think that dummy handle name is unique enough that it won't conflict with modules etc., that's fine. But if you think there's a chance of future conflict, rename it to In its current form, it sounds useful enough to be actually used for other purposes. |
@adragus-inviqa I like your suggestion, I have updated the pull request: 1bb25ed |
@adrian-martinez-interactiv4 thanks a lot for the submission! I'd like to merge this, I'll try to get somee xtra feedback from the issues you mentioned and will get back to you in case I have more questions. |
Hi @miguelbalparda, it seems that related issues #8554 and #9050 have been resolved in develop branch in another PR, as explained in #8554 (comment), so that issues are not related anymore with this one. This issue explained isn't solved yet in develop branch, I have updated this PR to include support for layout cache keys and now it's already fixed. If you need more feedback about this one don't hesitate to ask for it ;) |
Where can we get magento patch for this solution ? what is the patch id or URL? |
* @param array|string $cacheKey | ||
* @return ProcessorInterface | ||
*/ | ||
public function addCacheKey($cacheKey); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To maximize backwards compatibility with the existing customizations and extensions we can not introduce a new public method in the interface. More information is available in the Backwards Compatible Development Guide
} | ||
$this->cacheKeys = array_merge($this->cacheKeys, $cacheKeys); | ||
|
||
return $this; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allowing method chaining in the classes which are not designed for it may introduce Temporal Coupling in the client code, therefore it is not encouraged.
* | ||
* @var array | ||
*/ | ||
protected $cacheKeys = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently Magento Technical Guidelines do not encourage designing classes for inheritance. Would be great to make this property private.
HI @adrian-martinez-interactiv4 |
Hi @adrian-martinez-interactiv4 |
According to contributor guide, tickets without response for two weeks should be closed. |
Related to support ticket #MDVA-5525 |
…ento#8554 magento#9050 magento#9560 Create cache key object to be injected separately with its own interface
/** | ||
* Interface CacheKeyInterface | ||
*/ | ||
interface CacheKeyInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please consider more specific naming.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'll try to find a more specific name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before refactoring, I'd like to ask, LayoutCacheKeyInterface sounds like an appropiate name?
* Add cache key for generating different cache id for same handles | ||
* | ||
* @param array|string $cacheKey | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return type is missing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method returns nothing, should it be noted as @return void?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, docBlock should be clearly informing on that
/** | ||
* Add cache key for generating different cache id for same handles | ||
* | ||
* @param array|string $cacheKey |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method may accept array, to be consistent with getCacheKeys
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method already accepts array as parameters as specified in @param notation, do you mean methods names are not consistent between them? (addCacheKey vs. getCacheKeys)
Or maybe addCacheKey should be called addCacheKeys if it accepts array?
@@ -0,0 +1,26 @@ | |||
<?php |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use PHP 7.0 type hinting syntax when possible
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one I just don't know what is it about, can you be a little more specific please? Thanks!
…ento#8554 magento#9050 magento#9560 Renamed interface, LayoutCacheKeyInterface made optional in constructor, injected via di.xml, some other little fixes
…ento#8554 magento#9050 magento#9560 Adapted PageCache and Framework tests
…ento#8554 magento#9050 magento#9560 Adapted PageCache and Framework tests
…ento#8554 magento#9050 magento#9560 Adapted PageCache and Framework tests
…ento#8554 magento#9050 magento#9560 Adapted PageCache and Framework tests
…ento#8554 magento#9050 magento#9560 Adapted PageCache and Framework tests
3f60168
to
205a408
Compare
Description
This pull request expects to fix layout cache corruption due to async pagecache requests, when the async request generates a layout cacheId based on same handles as a cms page, but without loading the cms page associated, so cms page related layout updates get lost. Further explanation on issues #8554 and #9050. The testing scenario may sound weird, but it actually happens due to layout cache expiration, so layout cache expires but full page cache is still valid.
Fixed Issues
Manual testing scenarios
Adapted Tests
\Magento\PageCache\Test\Unit\Controller\Block\RenderTest::testExecute
\Magento\PageCache\Test\Unit\Controller\Block\EsiTest::testExecute
Expected Result
Page rendered correctly, as the first time.
Actual Result
Depending on included content, full blank page may be returned, but at least, it fails to apply page layout update handles, so block added via layout update is not shown.