Problem/Motivation

Quoting @effulgentsia in #2444231-23: Fix CacheableInterface so it makes sense (was: "Make Config objects & Entities implement CacheableInterface + add BubbleableMetadata::createFromCacheableObject()"):

Thinking more about #22, I wonder if we should ditch CacheableInterface entirely, and replace it with CacheAffectInterface? It's a BC break, but, a grep of HEAD shows CacheableInterface used by only a couple of classes, and we can preserve the BC of those classes if we want to. And I wonder if CacheableInterface is simply a misleading concept that we'd be best served to move contrib away from anyway. Because #15 is true in general, not just for config and entities: for an object to implement an interface named CacheableInterface implies that that object is cacheable and that the corresponding methods are to enable caching that object itself. Which makes no sense, since you can't ask an instantiated object how to get itself from cache: you need to get it from cache in order to have the instance to begin with. Instead, instantiated objects can only tell you about how they affect the cache of something else (access result objects affect the cache of things rendered based on that access, block plugin instances affect the cache of what they return in their build() method, and config and entity objects affect the cache of rendered things (and maybe also non-rendered things) that depend on that config/entity data).

Proposed resolution

BlockPluginInterface should implement CacheableDependencyInterface rather than CacheableInterface.

Remaining tasks

None.

User interface changes

None.

API changes

BlockPluginInterface now implements CacheableDependencyInterface rather than CacheableInterface. This means that ::getCacheKeys() and ::isCacheable() are no longer necessary/used; the other 3 methods on CacheableInterface continue to exist on CacheableDependencyInterface.

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug because #2444231: Fix CacheableInterface so it makes sense (was: "Make Config objects & Entities implement CacheableInterface + add BubbleableMetadata::createFromCacheableObject()") concluded that CacheableInterface was nonsensical; but changing BlockPluginInterface accordingly was deferred to this follow-up issue.
Issue priority Major because without this, we have a nonsensical cache-related interface in core, and blocks continue to not make sense.
Prioritized changes The main goal of this issue is performance, through better cacheability; and DX.
Disruption Disruptive for core/contributed and custom modules/themes that have either implemented blocks by directly implementing BlockPluginInterface (likely zero) or by subclassing BlockBase and then overriding ::getCacheKeys() or ::isCacheable().
Most blocks subclass BlockBase and don't override either over those methods. Hence actual disruption is likely zero.
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers’s picture

Title: Remove CacheableInterface (and no longer let block plugins implement it) » [PP-1] Remove CacheableInterface (and no longer let block plugins implement it)
Status: Needs review » Postponed

Postponed on the parent.

Wim Leers’s picture

Issue summary: View changes

IS fix.

Wim Leers’s picture

This is rolled on top of #2444231-63: Fix CacheableInterface so it makes sense (was: "Make Config objects & Entities implement CacheableInterface + add BubbleableMetadata::createFromCacheableObject()").


This removes CacheableInterface, and hence also removes it from BlockPluginInterface and BlockBase. For blocks, this means that:

  1. the ::isCacheable() method is removed, instead we rely on ::getCacheMaxAge() not being zero to determine if a block is cacheable or not
  2. the ::getCacheKeys() method is removed (see the IS for why blocks having that doesn't even make sense). The only user of that was ForumBlockBase, and there it didn't even actually make sense. It was used to make the used DB query part of the cache ID, i.e. to capture both node_access query alterations (for node view grants) *and* the configured number of forum topics to show. But the number of forum topics to show is guaranteed to be correct thanks to the block config entity's cache tag. And the node view grants aspect can be captured by associating the user.node_grants:view cache tag.

This allowed BlockViewBuilder to be greatly simplified.

Fabianx’s picture

FWIW, The interdiff looks great!

rteijeiro’s picture

Re-rolled and fixed a couple of nitpicks.

Wim Leers’s picture

Title: [PP-1] Remove CacheableInterface (and no longer let block plugins implement it) » Remove CacheableInterface (and no longer let block plugins implement it)
FileSize
9.96 KB
Fabianx’s picture

One use case that I came up with for cacheability of a block is the system_powered_by block.

Caching does not make sense as a cache_get is potentially more costly then executing the block.

max-age = 0 also does not make any sense.

What might make sense is to omit the 'keys' in that case and just pass any lower level information through to upper levels.

Another possibility is to use downstream-ttl for that, e.g. system_powered_by_block would set:

max-age = 0
downstream-ttl = CACHE_PERMANENT

indicating that while this block should not be cached, its fine to cache the data in upper levels.

If we remove isCacheable() we will need to / should find a solution for such blocks. (Concerns also by berdir)

Wim Leers’s picture

At that point, we might as well continue to find inspiration in HTTP's Cache-Control header rather than looking at an Akamai-invented header.

no-cache and no-store are the relevant properties in HTTP's Cache-Control header.

effulgentsia’s picture

What about not doing that via an interface method or cache property at all, and just implement hook_block_view_BASE_BLOCK_ID_alter() to unset #cache['keys']?

Fabianx’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/block/src/BlockViewBuilder.php
    @@ -72,25 +73,14 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
    -      if ($plugin->isCacheable()) {
    -        $build[$entity_id]['#pre_render'][] = array($this, 'buildBlock');
    -        // Generic cache keys, with the block plugin's custom keys appended.
    ...
    -        $build[$entity_id]['#cache']['keys'] = array_merge($default_cache_keys, $plugin->getCacheKeys());
    

    which is exactly what this did ...

  2. +++ b/core/modules/node/src/Plugin/Block/SyndicateBlock.php
    @@ -65,10 +65,10 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
    -  public function isCacheable() {
    +  public function getCacheMaxAge() {
         // The 'Syndicate' block is never cacheable, because it is cheaper to just
         // render it rather than to cache it and incur I/O.
    -    return FALSE;
    +    return 0;
       }
    

    This is also wrong as it will bubble ...

  3. +++ b/core/modules/system/src/Plugin/Block/SystemMainBlock.php
    @@ -59,9 +59,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
    -  public function isCacheable() {
    +  public function getCacheMaxAge() {
         // The main content block is never cacheable, because it may be dynamic.
    -    return FALSE;
    +    return 0;
    

    This is wrong as that will bubble, as effulgentsia said, do not set the cache keys in that case.

I am not sure the hook_*_alter() is the solution here however.

Maybe leaving isCacheable() on the block plugins is simpler (they do exactly that) and just remove the getCacheKeys()?

And again we should avoid dealing with #cache tags, contexts and max-age directly.

Even if that is out of scope for this issue, we need to use high level functions on that based on interfaces now that we can.

effulgentsia’s picture

Maybe leaving isCacheable() on the block plugins is simpler (they do exactly that)

The current API docs of CacheableInterface explicitly say otherwise:

+++ /dev/null
@@ -1,43 +0,0 @@
- * ... And if a child object is not
- * cacheable, then neither is any parent object.

And, most implementations are based on that interpretation, both for blocks, such as SystemMainBlock, and for non-blocks, such as menu links and Views plugins. So I don't think we should keep the name isCacheable() while changing its meaning to be different for blocks than for everything else. If we really want "too cheap to bother caching" as a method, I think it's fine to add a new method for blocks, such as isCheap(). But I'm not convinced that it's worth it: most blocks aren't cheap, so seems like a lot of bother just to save on a cache_get() of a few silly blocks, especially if we can have some other issue to do all block cache gets as a multiple operation, and when sites that really want to squeeze out those extra cache gets can do so via the alter hook.

Fabianx’s picture

I am fine with that, so what about the main content block?

Currently that one would screw up the cache headers ...

Wim Leers’s picture

FileSize
11.6 KB
2.36 KB

So, to summarize the problem:

  • Some blocks are cacheable, but we don't want them to be cached because the cost of retrieving from cache is greater than the cost of rendering them. We don't want to set max-age = 0 because that will bubble up to the response level, making the entire response max-age = 0, thus uncacheable.
  • For most blocks, we could decide to say that complicating things is not worth it, and just do those cache gets instead of rendering them.
  • But, for the main content block, that is not an option. Because the main content block's cacheability is impossible to know: it depends on the main content it contains, which depends on the route.

I think the simplest solution is to attack the truly special case here: the main content block. That block is very special, because it's the only block that is the only block that is solely a placeholder, a placeholder that has no idea what content it will contain.

So special case that, and make BlockPageVariant unset #cache[keys].

Wim Leers’s picture

Status: Needs work » Needs review
Wim Leers’s picture

Related to the above, but feel free to ignore. This was an alternative solution I explored.

For a moment, I considered this alternative solution:

  1. we know the main content depends on the route
  2. the main content must be rendered early, before rendering blocks, because the main content may provide the title (see HtmlRenderer::prepare())
  3. the main content block is given the rendered main content, so that it knows what to render, and given point 2, this already contains the bubbled cacheability metadata
  4. so let the main content block return the route cache context and just return the #cache[max-age] = Cache::PERMANENT; the max-age will be bubbled and thus adjusted downward automatically, depending on the cacheability of the main content
  5. combined with the main content's bubbled cache contexts, this would then be correctly varied, and thanks to the main content's bubbled cache tags, it'd also be invalidated correctly.

In other words: I thought I'd finally made the main content block cacheable! This would then be similar to #2429617: Make D8 2x as fast: Dynamic Page Cache: context-dependent page caching (for *all* users!), but at a more granular level (and thus with less of a performance boost, but nonetheless, a great improvement). And it actually works!

Unfortunately, there is one problem with this: blocks are display variants, and display variant selection happens in HtmlRenderer, which is called by MainContentViewSubscriber if the controller's return value is a render array and the negotiated format is HTML.
In other words: this is mostly pointless because we only know that we'll be rendering blocks after we've already calculated the controller's return value, i.e. after we've already built the main content render array. We do save the rendering work though.

The SmartCache patch (#2429617: Make D8 2x as fast: Dynamic Page Cache: context-dependent page caching (for *all* users!)) doesn't suffer from this problem, because it runs during the KernelEvents::REQUEST event, i.e. much sooner.

Feel free to try the PoC patch. (Tested against 443ef756172fa9ca9e47106f0b360c8b758d03b1.)

Fabianx’s picture

#14:

+++ b/core/modules/node/src/Plugin/Block/SyndicateBlock.php
@@ -65,10 +65,10 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
-  public function isCacheable() {
+  public function getCacheMaxAge() {
     // The 'Syndicate' block is never cacheable, because it is cheaper to just
     // render it rather than to cache it and incur I/O.
-    return FALSE;
+    return 0;

This should be removed too, then ...

Fabianx’s picture

I am okay to cache the main content block in that fashion. I ran into the exact same problem in render_cache for Drupal 7 :-D.

I think setting the route context and max-age is set automatically anyway, does not hurt and is actually the correct thing to do and we can still leave the unsetting of keys for now as in #15 for the BlockDisplayVariant with a @todo to remove when SmartCache lands.

And for controllers returning a #pre_render(_cache) pattern themselves, this will also work with maximum effect for caching.

So +1 to the POC, but another +1 to having that code path not yet active.

Not yet sure I want the block display variant to do it though.

I think I even would prefer an alter hook (wow!) in system module for now.

Wim Leers’s picture

FileSize
11.95 KB
1.09 KB

Right.

fgm’s picture

Just writing this down before I forget it : an idea came from reading this discussion (and others) about caching. It seems we are discussing something a bit different from "cacheability = ability to be cached", which would be "worthy of caching" instead. To take the "power by" block example, it is indeed cacheable, but not cache-worthy. This will probably happen a lot on contrib and site-specific blocks bringing content from static markup (ads, stats, various embeds) : the markup generation will be cheaper than the associated cache_get, and will not need to bubble up to higher-level components.

Fabianx’s picture

#20:

the markup generation will be cheaper than the associated cache_get, and will not need to bubble up to higher-level components.

Use #post_render_cache (or #cache placeholders when we implement those) if this really should always be generated newly.

However most of those blocks can be easily cached in upper levels (i.e. as part of the whole page).

For now the solution is to unset #keys to avoid caching at the blocks level - maybe we add an AlterableBlockPluginInterface to be able to not have to add a .module and a hook just to remove the #keys or set the visibility of the block (more to that later).

Wim Leers’s picture

Use #post_render_cache (or #cache placeholders when we implement those) if this really should always be generated newly.

However most of those blocks can be easily cached in upper levels (i.e. as part of the whole page).

I disagree. Don't use #post_render_cache for something like this! First, it shouldn't be necessary to use that except in the most dynamic, most complex situations (i.e. worse DX). Second, if the concern is to not cache a block because it's more costly to retrieve it from cache (because I/O) than to generate it dynamically, then that is likely offset by using #post_render_cache, which has a bit of overhead of its own (to generate the placeholder and to then find it in the final HTML and replace it). Imagine doing that for the "Powered by Drupal" block. That'd just be silly.

I think that it'd then still be better to just add a non-bubbled #cache[no-cache] (or similar) property instead, rather than recommending #post_render_cache.

Fabianx’s picture

... if this really should always be generated newly.

Only in cases where the content is so dynamic that it _must_ be generated newly.

I agree that unsetting #keys is the better solution for all generic cases.

effulgentsia’s picture

Status: Needs review » Needs work
Issue tags: +Needs issue summary update, +Needs change record updates
+++ b/core/modules/system/src/Plugin/Block/SystemMainBlock.php
@@ -42,26 +41,4 @@ public function build() {
-  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
-    $form = parent::buildConfigurationForm($form, $form_state);
-
-    // The main content block is never cacheable, because it may be dynamic.
-    $form['cache']['#disabled'] = TRUE;
-    $form['cache']['#description'] = $this->t('This block is never cacheable, it is not configurable.');
-    $form['cache']['max_age']['#value'] = 0;
-
-    return $form;
-  }

Although this UI will be removed entirely in #2458763: Remove the ability to configure a block's cache max-age, it still exists for now, so let's have this patch not introduce a regression here (i.e., being able to configure this block's max-age, even though that makes no sense). Perhaps we should just change the #value to permanent and the text to something better like "This block's maximum age cannot be configured, because it depends on the contents."

This also needs an issue summary update for API changes and beta evaluation, and possibly some CRs need to be updated.

Other than that, I think this is RTBC.

Wim Leers’s picture

Issue summary: View changes
Status: Needs work » Needs review
Issue tags: -Needs issue summary update, -Needs change record updates
FileSize
12.05 KB
1.14 KB

Everything in #24 makes a ton of sense. Done.

IS updated, also includes a beta evaluation now. CRs updated, only one affected (https://www.drupal.org/node/2222293/revisions/view/8331085/8331087).

Fabianx’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -142,6 +142,13 @@ public function build() {
    +        if ($block_plugin instanceof MainContentBlockPluginInterface) {
    +          unset($build[$region][$key]['#cache']['keys']);
    +        }
    

    I still feel uncomfortable about that.

    Can we add an alterSomething() method to the blocks itself that is called by BlockViewBuilder?

    Or lets say it like that to keep this in scope:

    Can we discuss adding an AlterableBlockPluginInterface in a follow-up?

    Implementing a hook to change my own final rendered data feels like a hack and hardcoding such things does not help - even if this is a really special case.

  2. +++ b/core/modules/node/src/Plugin/Block/SyndicateBlock.php
    @@ -48,27 +48,4 @@ public function build() {
    -    // @see ::isCacheable()
    -    $form['cache']['#disabled'] = TRUE;
    -    $form['cache']['#description'] = $this->t('This block is never cacheable, it is not configurable.');
    -    $form['cache']['max_age']['#value'] = 0;
    

    I believe we need the same here as for the main content block.

CNW, for 2.

Wim Leers’s picture

Status: Needs work » Needs review
  1. This really is a really special case. That's why it's justified IMO. I'm fine with a follow-up to discuss it more though.
  2. No, we don't. The main content block is not the same case as the syndicate block. The main content block's cacheability is impossible to define. The syndicate block ideally would be generated dynamically instead of render cached, but since we don't have some kind of "no-cache" flag, we choose to keep things simple and consistent instead, and render cache them anyway. Quoting #14:

    For most blocks, we could decide to say that complicating things is not worth it, and just do those cache gets instead of rendering them.

(Actually, everything in #26 is answered by #14.)

effulgentsia’s picture

I opened #2467097: Decide if and how block plugins should alter the BlockViewBuilder generated render array for #26.1. In case that's not sufficient, leaving to Fabianx to RTBC.

I agree with #27.1 that even independent of that issue, the code in this patch is the right place for that unsetting, because the necessity to do it is not due to the plugin per se, but due to a particular strategy of how/when to aggregate it onto the page (see #16), and BlockPageVariant seems like a fine home for the consequence of that strategy.

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Yep, lets open a follow-up for that.

So we make the max-age now again configurable for the syndicate block? I don't care, because we remove it anyway ...

RTBC - if that is not an issue.

effulgentsia’s picture

Status: Reviewed & tested by the community » Needs work

Actually, we should make SyndicateBlock consistent with SystemPoweredBy and SystemMessages, so nonconfigurable as permanent. True about the UI being temporary anyway, but good to have consistency in the interim. NW for that, but otherwise, RTBC+1.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
13.9 KB
3.35 KB

Did #30, back to RTBC.

Also updated SystemPoweredBy + SystemMessages to have exactly the same patterns.

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Yep, RTBC it is. #30 is what I meant in #26.2 :).

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This issue addresses a major bug and is allowed per https://www.drupal.org/core/beta-changes. Committed 3e275d6 and pushed to 8.0.x. Thanks!

  • alexpott committed 3e275d6 on 8.0.x
    Issue #2459819 by Wim Leers, rteijeiro: Remove CacheableInterface (and...

Status: Fixed » Closed (fixed)

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