Updated: Comment #0

Problem/Motivation

If we want to improve the page cache by removing the "content" cache tag (see #2124957: Replace 'content' cache tag with 'rendered' and use it sparingly), then we must ensure that cache tags exist for all user-modifiable pieces of content on the page. Including menus. And menus don't (yet) set cache tags in their render arrays.

Proposed resolution

Set cache tags.

Remaining tasks

User interface changes

None.

API changes

None.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers’s picture

Fun fact: it is apparently useless to set cache tags on the main and secondary menu (configurable in menu.settings.yml using the main_links and secondary_links keys).

Why? Because they never end up in "the page array". They're defined in template_preprocess_page() ($variables['main_menu'] = array(… render array …)), and therefor are rendered directly by Twig templates. I think that changing how all that works is a whole new can of worms and therefor probably out of scope for this issue.

Wim Leers’s picture

Issue summary: View changes
FileSize
11.5 KB

There are two aspects:

Ensuring cache tags are set

There are two cases:

The regular case
This applies to menus that are rendered through menu_tree()/menu_tree_output() (which includes all of the menu blocks, i.e. \Drupal\system\Plugin\Block\SystemMenuBlock)
The irregular case
The "main" and "secondary" menus are never part of the page-level render array and therefor their cache tags will never bubble up into the page cache, even though they should be. This happens because they're rendered directly by the theme system. For these two highly exceptional cases, I've had to modify drupal_prepare_page().
It's the cleanest solution I could think of. If somebody else sees a cleaner solution, I'm all ears. This is the most questionable part of the patch.

Ensuring cache tags are marked as invalid when menus change

There are again two cases:

Editing Menu entities
The \Drupal\system\Entity\Menu config entity has received postSave() and postDelete() methods that invalidate the relevant cache tags.
This is completely analogous to what the \Drupal\user\Entity\Role config entity does.
Editing MenuLink entities
Menu links were calling menu_cache_clear(), which ensured cache entries tagged with a certain menu were deleted … but only in the cache_menu cache bin!
Instead of having a procedural function for this, I replaced the callers of that function with a Cache::invalidateTags() call. One less procedural function to be called.

I've added test coverage for both cases.


Unfortunately, this is still blocked on #2167039: Regression: page cache tags broken, also: some routes incorrectly use _controller -> No page object in template variables -> Fatal in seven_preprocess_page(), and this in turn blocks #2124957: Replace 'content' cache tag with 'rendered' and use it sparingly. Let us please get #2167039 fixed ASAP, so we can continue here!

Wim Leers’s picture

FileSize
11.49 KB

Straight reroll to track HEAD.

Wim Leers’s picture

moshe weitzman’s picture

Only nitpick I founf is that PageCacheIntegrationTest should have the word 'Tags' in it since it isn't a thorough test of whole page cache system.

Status: Needs review » Needs work

The last submitted patch, 3: menu_cache_tags-2179083-3.patch, failed testing.

vijaycs85’s picture

+++ b/core/includes/common.inc
@@ -3562,6 +3562,27 @@ function drupal_prepare_page($page) {
+  if (theme_get_setting('features.main_menu') && count(menu_main_menu())) {
...
+  }

Looks like menu_main_menu() and menu_main_menu() does exactly same calculation what we do inside this if statements. Would be great, if we can avoid and make use of main. Something like this - https://gist.github.com/vijaycs85/8690309

So this patch will become below

+++ b/core/includes/common.inc
@@ -3562,6 +3562,27 @@ function drupal_prepare_page($page) {
+  if (theme_get_setting('features.main_menu') && count(menu_main_menu())) {
+    $page['page_top']['#cache']['tags']['menu'][$main_links_source] = menu_get_Links_source('main_links', 'main');
+  }
+  if (theme_get_setting('features.secondary_menu') && count(menu_secondary_menu())) {
+    $page['page_top']['#cache']['tags']['menu'][$secondary_links_source] = menu_get_Links_source('secondary_links', 'account');
+  }
vijaycs85’s picture

Status: Needs work » Needs review
FileSize
12.52 KB
11.04 KB
3.44 KB

Updates:
2179083-menu-cache-tag-8-reroll.patch is just reroll
2179083-menu-cache-tag-8-with-changes.patch has changes mentioned in #7

vijaycs85’s picture

Issue summary: View changes

The last submitted patch, 8: 2179083-menu-cache-tag-8-with-changes.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 8: 2179083-menu-cache-tag-8-reroll.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
13.03 KB
5.5 KB

#8 introduced a new public API function, which is completely unrelated to the problem at hand. If such off-topic refactoring is done, let's at least only introduce a private API function so that there is no API change. The function name also was highly peculiarly capitalized, fixed that (s/menu_get_Links_source/_menu_get_links_source/ :)


Renamed the new test to address #5.

Fixed the test failures (dumb copy/paste mistake that I made).

Status: Needs review » Needs work

The last submitted patch, 12: menu_cache_tags-2179083-12.patch, failed testing.

dawehner’s picture

  1. +++ b/core/includes/menu.inc
    @@ -1786,6 +1783,23 @@ function menu_secondary_menu() {
    +function menu_get_Links_source($name, $default) {
    

    WOW, you even can use "Links" without breaking something. Let's better name it to 'links' to stay sane.

  2. +++ b/core/modules/system/lib/Drupal/system/Tests/Cache/PageCacheIntegrationTest.php
    @@ -0,0 +1,105 @@
    +class PageCacheIntegrationTest extends WebTestBase {
    

    Is there any reason we create a new test even we do have PageCacheTest already?

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
13.26 KB
3.24 KB

#14:

  1. Already did that in #12 :)
  2. Yes: PageCacheTest solely tests whether the page cache itself works. This new test (since renamed to PageCacheTagsIntegrationTest, per Moshe's request) tests whether the integration of cache tags with the page cache is working correctly. It should eventually become a pretty large test, that covers many scenarios, to guarantee our page-level cache tags are reliable.

The tests fail because they assumed the presence of a core bug that was fixed in #2180375: menu_link_rebuild_defaults() is creating duplicates of a few menu links. Fixed. They now no longer rely on forms, but just modify entities directly, which means less code, faster tests, same coverage.

nielsvm’s picture

TEST PREPARATION:

  • Page cache maximum age ==> 86400/1 day
  • Enabled the internal page cache.
  • Tested basic page cache and cache busting, all good.

Warning for co-testers: because of the cache-control policy set on the outgoing responses your browser will try to fetch it from local cache till expiry reached, therefore always refresh using "control+shift+R" to assure a forced re-fetch.

WITHOUT menu_cache_tags-2179083-15.patch APPLIED:

  1. Generated cached front page, created page+link (pri.links). After saving the node and hard-refresh the front-page showed tabbed link: CORRECT.
  2. Frontpage refreshed in cache, renamed menu link: INCORRECT

.. stopped tests as main original problem was confirmed to me, which emphasizes why this patch is so important.

WITH PATCH APPLIED AND AFTER CLEARING CACHES:

  1. Generated cached front page, created page+link (pri.links) . After saving the node and hard-refresh the front-page showed the tabbed link: CORRECT.
  2. Frontpage refreshed in cache, renamed menu link: CORRECT.
  3. With warmed frontpage I created a second page+link (pri.links). After saving it is expected that both the frontpage and the older page with menu link is expired: CORRECT
  4. When a menu link gets disabled, all pages showing the link in the main menu bar should see it disappear from the page cache: CORRECT.
  5. More direct menu link manipulation through the admin (/admin/structure/menu/item/$MLID/edit) should expire all pages showing the link: CORRECT
  6. Created a new page+link where the link has another link as parent, making it not show up in the primary links naturally. Expected behavior is that this does NOT expire the parent page as long as the rendered link is not shown on it (e.g. menu block): INCORRECT
    I had "node/2" that had "Test #1" as title and the link was as tab in primary links. I created "node/4" with a link that had the tabbed link from "Test #1" as parent, which means that - as long as the menu tree isn't rendered on any of the pages - the parent page should NOT expire, as the child was not rendered on it.

FOOTER MENU + BLOCK WITH LINKS RENDERED IN THEM:

  1. With three warmed pages in cache and all showing the footer menu in the footer region rendered, we expect that a change to any of the links in them, wipe all three warmed pages: CORRECT
  2. With warmed pages in cache we expect a new link in the footer menu to wipe all pages: CORRECT
  3. With warmed pages we expect a deleted link to wipe all pages: CORRECT

NEXT: I will take a stab at writing test cases for all documented test steps above, however, I'll timebox my effort and might decide to stop it, lets hope it'll works out.

nielsvm’s picture

Status: Needs review » Reviewed & tested by the community

There is one important thing that I didn't mention in my last comment:
bullet #6 causes more paths to be expired than theoretically necessary but the costs of runtime-level detection whether a sibling menu link is rendered somewhere in the HTML is barely doable. Therefore having this patch behave the way it currently does far outweighs the cost of currently common scenarios where sites run with low TTLs and expiry of many pages is still MUCH cheaper than constant full-site expiry through a way too low TTL.

In other words, its a theoretic thing I noticed but this patch is totally RTBC in my opinion.

Wim Leers’s picture

Priority: Major » Critical

Thanks, Niels!

Marking critical since the parent issue is critical and this blocks the parent.

dawehner’s picture

+++ b/core/includes/common.inc
@@ -3562,6 +3562,19 @@ function drupal_prepare_page($page) {
+  // directly by the theme system.
...
 
+  // The "main" and "secondary" menus are never part of the page-level render
+  // array and therefor their cache tags will never bubble up into the page
+  // cache, even though they should be. This happens because they're rendered

+++ b/core/includes/common.inc
@@ -3562,6 +3562,19 @@ function drupal_prepare_page($page) {

Is there any way to not couple the very specific details of having specific menus on basically every page onto the drupal_prepare_page() method which feels like part of an API or at least architecture.

Wim Leers’s picture

#19: there's not any way that I know of. I don't like it either. Note that I'm not *creating* this problem, it's a problem that already exists. I'm merely fixing the implementation to at least set the appropriate cache tags. The real problem is that the theme system is rendering menus directly, which should not happen at all. At least now we'll have proper cache tags for those menus, and test coverage, which will help when that problem gets fixed.

Moving the special-case rendering of those menus out of the theme system in this issue is completely out of scope.

catch’s picture

Yeah that issue is #1869476: Convert global menus (primary links, secondary links) into blocks, while it's ugly hard coding those menus, it's not the fault of this patch. (not committing this yet because I haven't reviewed it).

catch’s picture

  1. +++ b/core/includes/common.inc
    @@ -3562,6 +3562,19 @@ function drupal_prepare_page($page) {
    +  // array and therefor their cache tags will never bubble up into the page
    

    therefore.

    Could use a @todo to remove this when they're blocks.

  2. +++ b/core/includes/menu.inc
    @@ -1786,6 +1783,23 @@ function menu_secondary_menu() {
    +  // When menu module is not enabled, we need a hardcoded default value.
    

    The default is a parameter to the function, so it's not really hardcoded?

  3. +++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
    @@ -457,6 +457,66 @@ public function testBlockContextualLinks() {
    +  public function testMenuBlockPageCacheTags() {
    

    Looks like this is checking block cache tags, but not those from the secondary/main menus?

catch’s picture

Status: Reviewed & tested by the community » Needs work
Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
13.25 KB
1.47 KB

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 24: menu_cache_tags-2179083-24.patch, failed testing.

Wim Leers’s picture

The last submitted patch, 24: menu_cache_tags-2179083-24.patch, failed testing.

Wim Leers’s picture

The last submitted patch, 24: menu_cache_tags-2179083-24.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
13.28 KB

Straight reroll.

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC per #17. Everything since was just to keep up with HEAD and fix docs.

To answer #22.3:

Looks like this is checking block cache tags, but not those from the secondary/main menus?

Correct, because I cannot actually check those in there, since it's not the standard profile. There's a separate test for doing the necessary integration testing in the Standard profile, which includes the secondary/main menus: PageCacheTagsIntegrationTest.

catch’s picture

Status: Reviewed & tested by the community » Fixed

Thanks for docs fixes/re-rolls. Committed/pushed to 8.x, thanks!

Wim Leers’s picture

Issue tags: -sprint

Hurray! :)

Status: Fixed » Closed (fixed)

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