Problem/Motivation

Breadcrumb builders are called in a specific order, the first one that returns a valid Breadcrumb object from its build() method is the one that will be returned from the manager. Before a builder is called, we first check its applies() method and only if that returns TRUE do we call build().

The problem lies within the fact that the applies check might introduce variability that needs to be represented by a cache context, but is not unless it returns TRUE. At that point does the build() method add the right cacheable metadata to the breadcrumb result. This is wrong, though, because a negative outcome also needs to have said cacheable metadata.

You can see how this breaks in #3452181-7: VariationCache needs to be more defensive about cache context manipulation to avoid broken redirects, comments 7 through 9.

Proposed resolution

Pass cacheable metadata to the applies() method so that it can properly set its variability (cache contexts) and dependencies.

Remaining tasks

Review MR and commit

User interface changes

N/A

API changes

BreadcrumbBuilderInterface::applies() now takes a second argument CacheableMetadata $cacheable_metadata

Data model changes

N/A

Issue fork drupal-2719721

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

jhodgdon created an issue. See original summary.

wim leers’s picture

Status: Active » Postponed (maintainer needs more info)

That issue only optimized the path-based breadcrumb builder, so I highly doubt that that broke anything.

which has a custom breadcrumb class/service

I bet this service is not specifying the correct cache context :)

jhodgdon’s picture

Title: Breadcrumbs are too non-specific » Document caching on breadcrumb interface
Component: base system » documentation
Priority: Major » Normal
Status: Postponed (maintainer needs more info) » Needs review
StatusFileSize
new775 bytes

Yeah, talked to catch in IRC and it seems I didn't realize I had to add cache stuff to my breadcrumb class. We think the change in the other issue just exposed a bug that was in my module all along but was just exposed by this change.

I am repurposing this issue to add documentation to the interface to remind developers to add cache information.

Here's a patch.

wim leers’s picture

Issue tags: +D8 cacheability

+1 for better documentation!

+++ b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php
@@ -28,7 +28,9 @@ public function applies(RouteMatchInterface $route_match);
+   *   breadcrumb, which needs to take into account what information is needed
+   *   to see if this breadcrumb builder applies.

Not "to see if this bread crumb builder applies", but instead what the cacheability of the built breadcrumbs is.

jhodgdon’s picture

Well, the problem I ran into here was that without this being added to my returned breadcrumb, Drupal in its wisdom decided to apply it to all pages, even though applies() would have returned FALSE. It wasn't even being checked. ?!?

I still think this is a bug in Core, but the workaround in my class was to add the line

    $breadcrumb->addCacheContexts(['route.name']);

to my breadcrumb that the build() returned.

The applies() already said:

  public function applies(RouteMatchInterface $route_match) {
    return $route_match->getRouteName() == 'entity.help_topic.canonical';
  }

but that was not enough -- after checking it once, it then got applied to every admin page. Totally wrong... but adding that cache thing fixed it, so I think it needs to be documented if that is the way to fix it.

berdir’s picture

What core decided is that the breadcrumb block cache was sufficient for the new patch. So it never tried to apply the builders because never rebuilds the block.

However, you do have a valid point. The problem is that only the builder that was chosen for the page is capable of adding cacheability metadata. Not those that decided that they don't apply.

As always, we also need cacheability metadata for negative results. So we need a way a way for a breadcrumb builder to say under which conditions he might or might not apply.

catch’s picture

Title: Document caching on breadcrumb interface » BreadcrumbBuilder::applies() mismatch with cacheability metadata
Version: 8.2.x-dev » 8.1.x-dev
Component: documentation » base system
Priority: Normal » Major

Changing the issue title to what I think the bug is here. applies() affects when the builder will run, but it doesn't stop the results of build() getting cached in such a way that it might apply to another page. That's not how you'd normally think a method called 'applies' would work.

jhodgdon’s picture

Status: Needs review » Active

No kidding:

That's not how you'd normally think a method called 'applies' would work.

:)
Status back to Active as the docs patch does not fix the bug as it is now defined.

berdir’s picture

Yes. The only reason we haven't this yet and why the mentioned fix for @jhodgdon's problem is fixing it for that use cases is because the chosen builder's cacheablity metadata sufficiently covers the previous builders.

But that's not always the case. #2699627: url.path cache context for breadcrumbs is unnecessarily granular is triggering the first problems I guess, I have a feeling I will have to fix some of my custom breadcrumb builders as well.

Here is a scenario where I think will fail right now with book module:

1. Create a node that could be in a book but isn't ( guess you need some other node that is a book parent to actually see something later on)
2. You should see the standard path based breadcrumb, because book applies() returns false.
3. Then edit the node and put it in a book with some hierarchy
4. Save, you should still get the old breadcrumb because that is cached and the path didn't change.

And book.module can do nothing to fix it.

What we could do to fix this is \Drupal\Core\Cache\CacheableDependencyInterface on builders and if they implement it, collect cacheability metadata of all builders we asked and add that to the breadcrumb returned by the one we actually chose.

One problem that the example above also shows is that book needs information in those methods that is not directly available. Specifically, it needs the $node object from the route match as it needs the cache tag from it (probably that and the path?). As a quick fix, we could introduce some state into those services and have getCacheTags() only work when called after applies. The only other way that I see is that we introduce a new applies() method on a new interfac that can return something like AccessResult. A binary result + cacheablity metadata.

catch’s picture

The only other way that I see is that we introduce a new applies() method on a new interfac that can return something like AccessResult. A binary result + cacheablity metadata.

This was my first thought here - new interface, extend the old interface. We could also pass in an optional CacheableMetadata object into the applies method so the return doesn't need to change.

dawehner’s picture

As always, we also need cacheability metadata for negative results. So we need a way a way for a breadcrumb builder to say under which conditions he might or might not apply.

Does this mean we need to run through all breadcrumb builders?

jhodgdon’s picture

Any thoughts on the approach we should use here? I think we still need to fix this...

acbramley’s picture

I believe we are seeing a similar issue with the breadcrumb block but it's reproducible by clicking around the site.

The problem (probably) stems from having url aliases generated by menu structure. As I understand it #2699627: url.path cache context for breadcrumbs is unnecessarily granular changed the block to cache by url parent, wouldn't this mean that siblings (when url aliases are by menu structure) would get the same cached version?

That's basically what we are seeing, we can click through the top level navigation of our site and eventually it'll continue to show the same breadcrumbs across the same level pages.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

benjy’s picture

I ran into a similar issue today where we didn't have a breadcrumb on the homepage, it was using the path based breadcrumb builder which adds the 'url.path.parent' but that isn't sufficient. The breadcrumb gets cached with no links and the cache context is empty since there isn't a parent page, then, on all other top level pages, they also no longer get breadcrumbs because the cache context is the same as the homepage.

The only other way that I see is that we introduce a new applies() method on a new interface that can return something like AccessResult. A binary result + cacheablity metadata.

I agree with this approach, it would solve the issue for me. FilterProcessResult is another example of this pattern in core.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

dpi’s picture

Version: 8.6.x-dev » 8.7.x-dev

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

pameeela credited cilefen.

pameeela credited rothlive.

pameeela’s picture

Added #2932341: Incorrect node title in breadcrumb as related and transferred credit because I've closed that as a duplicate.

pameeela’s picture

Issue tags: +Bug Smash Initiative

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

kristiaanvandeneynde’s picture

This surfaced as part of some extra hardening I'm trying to introduce into VariationCache: #3452181: VariationCache needs to be more defensive about cache context manipulation to avoid broken redirects

Re #11:

Does this mean we need to run through all breadcrumb builders?

No, because they always run in the same order. So as long as we clear the cache when a new builder is introduced into the list, we're fine.

Let's say you have 3 builders, the 1st applies() check varies by route, the 2nd by domain and the 3rd by user roles (for whatever reason). Then, as long as the first in the list applies, all we need to care about is the 'route' cache context. Only when the first one ever fails and we fall back to the second one, do add the url.site cache context. At this point, VariationCache will write a CacheRedirect at the current value for 'route', setting the 'url.site' cache contexts. Then, if both the 1st and 2nd applies() check fail, will another CacheRedirect be created to allow VC to store items from the 3rd builder.

kristiaanvandeneynde’s picture

Assigned: Unassigned » kristiaanvandeneynde
The only other way that I see is that we introduce a new applies() method on a new interface that can return something like AccessResult. A binary result + cacheablity metadata.

This was my first thought here - new interface, extend the old interface. We could also pass in an optional CacheableMetadata object into the applies method so the return doesn't need to change.

Why note use the deprecation policy guideline on introducing a new argument to an existing signature? https://www.drupal.org/about/core/policies/core-change-policies/drupal-d...

I'll try that approach to allow a 2nd argument to be passed to applies().

kristiaanvandeneynde’s picture

Assigned: kristiaanvandeneynde » Unassigned
Status: Active » Needs review

needs-review-queue-bot’s picture

Status: Needs review » Needs work

The Needs Review Queue Bot tested this issue.

While you are making the above changes, we recommend that you convert this patch to a merge request. Merge requests are preferred over patches. Be sure to hide the old patch files as well. (Converting an issue to a merge request without other contributions to the issue will not receive credit.)

kristiaanvandeneynde’s picture

Status: Needs work » Needs review

Should go green now. From the other issue:

This caused StandardPerformanceTest to fail as there are now 2 more cache gets, which makes sense because we added a cache context.

smustgrave’s picture

Issue summary: View changes
Status: Needs review » Needs work

The issue summary mentions "Revert or fix #2699627: url.path cache context for breadcrumbs is unnecessarily granular" can I ask which one it is? Maybe this can be flushed out more. Don't think this is an API or model change so put those as N/A

kristiaanvandeneynde’s picture

The MR is the correct approach, it's just that the issue summary needs severe updating.
The new approach is #10, #36 and #37

In essence: Allow the applies() method to set cacheability. Because the breadcrumb builders run in the same order every time and because only one can build the actual breadcrumb, we don't need to care about ALL builder's cacheable metadata, just the data up until the builder that actually applies.

I'll try to update the IS tomorrow.

michael.acampora’s picture

Attached a patch that can be used for Drupal 10.2. Thanks for fixing the issue.

kristiaanvandeneynde’s picture

Please don't convert MRs into patches as it creates a lot of unnecessary noise. Especially when the MR is still up for review and might change.

kristiaanvandeneynde’s picture

Issue summary: View changes
kristiaanvandeneynde’s picture

Status: Needs work » Needs review

@smustgrave Updated the IS to reflect the actual bug and how it should be fixed. The initial IS was just one case of running into it, but #3452181: VariationCache needs to be more defensive about cache context manipulation to avoid broken redirects exposes the problem in more detail as it also causes incorrect cache redirects to be written.

bramdriesen’s picture

Status: Needs review » Needs work

Maybe because I'm tired, but I had a really hard time reading the doc block to try and understand what it was trying to say. Grammatically strange sentences and missing capital letters spotted.

Code wise not much to add to the review. Looks clean and the other comments are easy to read.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review

Hopefully this is a bit easier on the eyes.

bramdriesen’s picture

Already better 🙂

The cacheable metadata to add to if your check varies by or depends on something

Can't the "to" be removed? Or specify to what we're adding metadata?

Anything you specified here does not have to be repeated in the build() method as it will be merged in automatically.

Anything you have? ... merged in automatically?

I'm no native english speaker, but with above changes I suggested it would read easier to me. Te addition of "have" is probably not needed.

kristiaanvandeneynde’s picture

Can't the "to" be removed?

That would make it broken English. You have the infinite "to add" and then "to" as you're adding to something (the CacheableMetadata).

Or specify to what we're adding metadata?

This documentation is for the $cacheable_metadata parameter, so I think it should be clear that that's the thing you're adding to?

Anything you specified here does not have to be repeated in the build() method as it will be merged in automatically.

You can use "Anything you specify here" or "Anything you specified here", depending on whether you want it to read like a forward-looking suggestion or vice versa. E.g.: Anything you say will be used against you (warning, you haven't said anything yet). Anything you said will be used against you (informative, you already said those things).

I wouldn't use "have", though. Present perfect doesn't seem right here. If you want we can change it to the simple present tense.

As for "merged in", it's because you merge whatever was specified into the final result. Because that sentence has no mention of "the final result", the "to" part of "into" is dropped. Example: John was about to walk into the river, as he walked in, he felt something sharp beneath his feet.

bramdriesen’s picture

Status: Needs review » Reviewed & tested by the community

Nah, it’s fine then 😇 again, not a native speaker. 😉

neclimdul’s picture

I think I understand the reason of the change and it makes sense. Previously you'd have to return true from applies if you _might_ apply and then builder would have to do the actual check and apply the cache-ability. Yeah, that's a pretty big gotcha when building these builders.

Looking at the merge, there's a lot of duplicate adding of the route to the cache-ability. I guess maybe this is a dumb question and probably well understood in the building of BreadcrumbManager but isn't varying by route kinda a baseline implied functionality for breadcrumb builders? That's why we pass the route_manager in right? Should the manager just include that by default and this change would only be used for more exotic cases?

If it helps the discussion, and example where it doesn't cache by route in core is PathBasedBreadcrumbBuilder. I found that's broken on our site and actually decorate the core version with one that adds the route to the cache-ability of the path breadcrumb builder.

kristiaanvandeneynde’s picture

Status: Reviewed & tested by the community » Needs review

No worries :) I'm not a native speaker either but I tend to want to get the grammar right, so I tend to look these things up when in doubt. You did make me think, though, and I believe the simple present is preferred here, as the implementation is arguably still to be written at the time of reading the documentation.

Will change that little detail and leave at RTBC.

neclimdul’s picture

Status: Needs review » Reviewed & tested by the community

left rtbc because this isn't a change this merge makes just something it highlights so could be a follow up.

kristiaanvandeneynde’s picture

Should the manager just include that by default and this change would only be used for more exotic cases?

It could, but just like you I'd argue that that is out of scope. For now I would focus on fixing the bugs caused by not correctly setting any cacheable metadata when the applies() check returns FALSE.

bramdriesen’s picture

RE #54 now it reads better to my mind 😅

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

I have some BC concerns added to the MR.

alexpott’s picture

Issue tags: +Needs change record

This issue also requires a CR as implementations in contrib and custom code will need to change.

kristiaanvandeneynde’s picture

Will try to fix these minor issues tomorrow. In the meantime, I answered the question regarding cache operation counts.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
Issue tags: -Needs change record

Only needs a follow-up to change the signatures in D12.

kristiaanvandeneynde’s picture

mxr576’s picture

Status: Needs review » Reviewed & tested by the community

Reviewed the CR and both the MR. I could only nitpicking on consistency regarding whether we have , see [link]. or . See [link], but since PHPCS is happy, I can be happy with the current comments too, they are still readable and understandable.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed a75df83 and pushed to 11.x. Thanks!

  • alexpott committed bb5bc5db on 11.0.x
    Issue #2719721 by kristiaanvandeneynde, jhodgdon, BramDriesen, pameeela...

  • alexpott committed 7f752fe0 on 10.4.x
    Issue #2719721 by kristiaanvandeneynde, jhodgdon, BramDriesen, pameeela...
alexpott’s picture

Version: 11.x-dev » 10.4.x-dev

Discussed with @catch and we agreed to backport this to 11.0.x and 10.4.x.

  • alexpott committed a75df83c on 11.x
    Issue #2719721 by kristiaanvandeneynde, jhodgdon, BramDriesen, pameeela...

Status: Fixed » Closed (fixed)

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

james.williams’s picture

Thanks for this improvement! I have though found a bug for a follow-up: the cacheability is ignored if no breadcrumb builders apply, which can cause breadcrumbs to disappear across a whole site. Reported as #3482691: BreadcrumbManager ignores cacheability when no builders apply.

kristiaanvandeneynde’s picture

They're tagged services, which I think means they fall under this rule of not being covered by BC.
https://www.drupal.org/about/core/policies/core-change-policies/bc-polic...