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
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. |
Comment | File | Size | Author |
---|---|---|---|
#31 | cacheableinterface_blocks-2459819-31.patch | 13.9 KB | Wim Leers |
Comments
Comment #1
Wim LeersPostponed on the parent.
Comment #2
Wim LeersIS fix.
Comment #3
Wim LeersThis 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 fromBlockPluginInterface
andBlockBase
. For blocks, this means that:::isCacheable()
method is removed, instead we rely on::getCacheMaxAge()
not being zero to determine if a block is cacheable or not::getCacheKeys()
method is removed (see the IS for why blocks having that doesn't even make sense). The only user of that wasForumBlockBase
, 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 theuser.node_grants:view
cache tag.This allowed
BlockViewBuilder
to be greatly simplified.Comment #4
Fabianx CreditAttribution: Fabianx for Drupal Association commentedFWIW, The interdiff looks great!
Comment #5
rteijeiro CreditAttribution: rteijeiro commentedRe-rolled and fixed a couple of nitpicks.
Comment #7
Wim Leers#2444231: Fix CacheableInterface so it makes sense (was: "Make Config objects & Entities implement CacheableInterface + add BubbleableMetadata::createFromCacheableObject()") landed, this is now unblocked!
Reuploading a rebased #3, without #2444231 included.
Comment #8
Fabianx CreditAttribution: Fabianx for Drupal Association commentedOne 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)
Comment #9
Wim LeersAt 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
andno-store
are the relevant properties in HTTP'sCache-Control
header.Comment #10
effulgentsia CreditAttribution: effulgentsia commentedWhat 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']
?Comment #11
Fabianx CreditAttribution: Fabianx for Drupal Association commentedwhich is exactly what this did ...
This is also wrong as it will bubble ...
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.
Comment #12
effulgentsia CreditAttribution: effulgentsia commentedThe current API docs of CacheableInterface explicitly say otherwise:
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.Comment #13
Fabianx CreditAttribution: Fabianx for Drupal Association commentedI am fine with that, so what about the main content block?
Currently that one would screw up the cache headers ...
Comment #14
Wim LeersSo, to summarize the problem:
max-age = 0
because that will bubble up to the response level, making the entire responsemax-age = 0
, thus uncacheable.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]
.Comment #15
Wim LeersComment #16
Wim LeersRelated to the above, but feel free to ignore. This was an alternative solution I explored.
For a moment, I considered this alternative solution:
HtmlRenderer::prepare()
)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 contentIn 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 byMainContentViewSubscriber
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.)
Comment #17
Fabianx CreditAttribution: Fabianx for Drupal Association commented#14:
This should be removed too, then ...
Comment #18
Fabianx CreditAttribution: Fabianx for Drupal Association commentedI 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.
Comment #19
Wim LeersRight.
Comment #20
fgmJust 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.
Comment #21
Fabianx CreditAttribution: Fabianx for Drupal Association commented#20:
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).
Comment #22
Wim LeersI 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
.Comment #23
Fabianx CreditAttribution: Fabianx for Drupal Association commentedOnly 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.
Comment #24
effulgentsia CreditAttribution: effulgentsia at Acquia commentedAlthough 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.
Comment #25
Wim LeersEverything 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).
Comment #26
Fabianx CreditAttribution: Fabianx for Drupal Association commentedI 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.
I believe we need the same here as for the main content block.
CNW, for 2.
Comment #27
Wim Leers(Actually, everything in #26 is answered by #14.)
Comment #28
effulgentsia CreditAttribution: effulgentsia at Acquia commentedI 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.
Comment #29
Fabianx CreditAttribution: Fabianx commentedYep, 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.
Comment #30
effulgentsia CreditAttribution: effulgentsia at Acquia commentedActually, 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.
Comment #31
Wim LeersDid #30, back to RTBC.
Also updated SystemPoweredBy + SystemMessages to have exactly the same patterns.
Comment #32
Fabianx CreditAttribution: Fabianx commentedYep, RTBC it is. #30 is what I meant in #26.2 :).
Comment #33
alexpottThis 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!