Updated: Comment #83
Problem/Motivation
Blocks having their own caching system is obsolete as of #918808: Standardize block cache as a drupal_render() #cache: they effectively are using render caching (#cache
).
Proposed resolution
Therefore:
- We should remove the Block Cache system, since it's just a shorthand/abstraction for render caching.
- We should make blocks set the appropriate cache tags, so that they can use the same render cache invalidation mechanism as everything else.
- (#6) We should not force block implementers to deal with the implementation details of leveraging the render cache maximally: setting
#cache
is easy, putting the expensive computations in#pre_render
is not. In other words: keep the Block API simple, and make it use render cache automatically (block plugins implement abuild()
method to build a render array of what they represent, and that's basically it). - Ideally (in the spirit of "Fast by default"), we will eventually be able to make as many blocks as possible in Drupal core cacheable forevery by default. Thanks to cache tags, that's feasible: cache a rendered block forever (
Cache::PERMANENT
), until one of its cache tags is invalidated. That's it!
Note that in case of complex needs for sites with e.g. highly dynamic (e.g. time-dependent)hook_entity_access()
implementations, it's trivial to disable caching: just change the default "Cache: Maximum age" setting on a block from "Forever" to "zero seconds"! I.e.: opt-out as a default, therefore fast by default.
To be able to do that for a given block plugin, it must have test coverage to ensure that its cache tags are indeed invalidated in all necessary cases. The first examples for which we are able to do this, are theSystemMenuBlock
(#62) (thanks to the test coverage added in #2179083: Rendered menus (e.g. menu blocks) should set cache tags to inform the page cache),CustomBlockBlock
(#106) blocks (thanks to #2188565: Test coverage for Comment, Custom Block, Node, Taxonomy Term and User entity cache tags), andSystemHelpBlock
(#114). - (#85) Furthermore, admin users should be able to specify the cache contexts that a cached block should be varied by. This empowers them to cache most blocks.
- While the above ("make blocks cacheable forever") may seem like it's out of scope, it's really part of the necessary API validation.
For more screenshots, see #96. This is what it looks like overall:
For profiling, see #124. The short version:
Remaining tasks
None!
Optional follow-ups:
- #2020387: Convert "Active forum topics" block to a View
- #2158419: BookNavigationBlock should have better cache granularity and cache tags
- Make as many blocks as possible cached forever by default.
User interface changes
- Blocks now have a "Maximum age" setting, which is a
<select>
with the same options as the "Page cache maximum age" setting. - Blocks now have a "Vary by context" setting, which is a set of checkboxes of available contexts.
- Block caching is removed from the Views UI. Views keeps its time and tag based caching.
API changes
- Blocks no longer declare a cache strategy. Instead, they implement
CacheableInterface
, for whichBlockBase
sets sane defaults, but which each block plugin may override. E.g. by default, there is no cache granularity, but a menu block may declare itself to be cacheable per page and per role. - There is a new
@cache_contexts
service that looks for tagged "cache context" services and makes those cache contexts available to the entire Drupal caching system. Drupal core will ship with:cache_context.url
(UrlCacheContext)
, cfr.DRUPAL_CACHE_PER_PAGE
)cache_context.language
(LanguageCacheContext)
, cfr. the old implicit per-language-if-multiple-available caching of blocks)cache_context.theme
(ThemeCacheContext)
, cfr. the old implicit per-theme caching of blocks)cache_context.user
(UserCacheContext)
, cfr.DRUPAL_CACHE_PER_USER
)cache_context.user.roles
(UserRolesCacheContext)
, cfr.DRUPAL_CACHE_PER_ROLE
)
- Query-dependent cache keys can now be generated using
Cache::keyFromQuery()
. - Consequently, the old block cache API and related functions are now removed: the old constants, and the
drupal_render_cache_by_query()
anddrupal_render_cid_parts()
functions. - A block's "cache" property is no longer set to to "enabled" (1) or "disabled" (-1), but has a
max_age
property now, that defaults to0
(as in "this block may be cached for zero seconds"). It also has acontexts
property, which allows the user to specify cache contexts the block should be varied by. - Significantly simplified block rendering logic.
- (#58)
Cache::PERMANENT === -1
instead ofCache::PERMANENT === 0
Original report by @moshe weitzman
Problem/Motivation
As of #918808: Standardize block cache as a drupal_render() #cache, The Block Cache system is a shorthand that blocks can optionally use to get #cache set on their render arrays. That #cache array gets the proper keys on it for each block constant (e.g. DRUPAL_CACHE_PER_ROLE). The #cache array can't be further customized though, so it has no custom expire time, no custom bin, and now no custom cache tags. Especially this last limitation is the final straw.The value of the shorthand is no longer there.
Proposed resolution
Blocks should simply return render arrays, with #cache on them if they want to be cached. Only one block in all of core uses the block cache constants (book module). That block is converted to the new system without logic changes
Comment | File | Size | Author |
---|---|---|---|
#132 | blockcache-2158003-132.patch | 126.35 KB | Wim Leers |
#124 | d8_blockcache_diff.png | 34.49 KB | msonnabaum |
#80 | interdiff.txt | 745 bytes | Wim Leers |
#80 | blockcache-2158003-80.patch | 77.83 KB | Wim Leers |
#67 | blockcache-2158003-67.patch | 81.11 KB | Wim Leers |
Comments
Comment #1
moshe weitzman CreditAttribution: moshe weitzman commentedComment #2
moshe weitzman CreditAttribution: moshe weitzman commentedComment #3
moshe weitzman CreditAttribution: moshe weitzman commentedComment #4
moshe weitzman CreditAttribution: moshe weitzman commentedComment #5
benjy CreditAttribution: benjy commentedI tested the patch and every seems to work OK. The block caching stuff has gone from the UI and overall it's a nice clean-up.
Few things,
Should we remove this as well? Or maybe that serves a purpose for the migration people.
Is the book module the only one that provides a block that's cached in core?
+1 for RTBC from me but I think it would be worth having another set of eyes look at this as well.
Comment #6
msonnabaum CreditAttribution: msonnabaum commentedI like the general idea here of not having block cache be its own thing, but I think this approach has some downsides. Primarily, it exposes the guts of render caching to the block implementor. While the #cache part is rather simple, putting the expensive bits in #pre_render is not. I think this will result in some very long, complex Block::build methods, as well as confusion around how #pre_render works. Some might argue that this is knowledge that most Drupal developers should have, but considering that it was introduced in D7 and there are still relatively few that are familiar with it, I'd say that's unrealistic.
Instead of throwing out all of block cache, I think we can just fix what's not working, and expose what render cache needs at a higher level.
This patch builds on #1, but adds a "CacheableInterface," which contains methods for each part of #cache. This allows us to assemble a render array for a block using the cache data from the CacheableInterface methods, and assigning it's "build" method as a #pre_render callback. This is actually similar to how block cache works in core now, we just dont expose anything other than "granularity" in the block configuration.
Some notes about the patch:
- Adds support for passing "cache contexts" as keys. This is essentially what we do now with granularity, but I think it makes sense to just treat them as keys and not their own special thing. I'm using the existing constants in the patch, but I could see us adding something like Cache::CURRENT_USER, or "%current_user". There is also a clear overlap between CacheHelper::addCacheContextsToKeys and drupal_render_cid_parts. Ideally we'd be deprecate granularity and drupal_render_cid_parts would go away.
- To demonstrate how this could work in practice, I added a "Cache: max age" field to the default block form. This seems to work fine, even for views blocks (views could actually remove their blockcache support if we did this). Although this implementation is a bit naive, we should have *some* basic cache config on the block form.
- CacheHelper is obviously not a great name, but I just used it as a temporarily place to throw new things. Stuff like keyFromQuery should probably be in a Cacheable trait, whereas addCacheContextsToKeys should be in some type of CacheContext service (it appears that we still dont have a consistent method of accessing global contexts).
- I reworked the forum blocks a bit since they use drupal_render_cache_by_query, which does quite a bit on its own. I extracted the logic for getting a key from the query, so it's just returning that for it's cacheKeys() method, and the base class handles the execution of the query rather than having to stash it in #query for a function to execute later on. This feels like a way cleaner separation of responsibilities to me.
Another thing to note here, is that most of this isnt specific to block cache or render arrays, so we reuse this interface elsewhere, regardless of the caching mechanism.
Comment #8
moshe weitzman CreditAttribution: moshe weitzman commentedThe code and the justification look good to me. Thanks.
Should be boolean based on method name
Why is #pre_render nested under #content (and #cache is not)? Thats not how current code does it.
Comment #9
msonnabaum CreditAttribution: msonnabaum commented@moshe -
1. That method does return a bool.
2. That was the only way I could get it to work. I believe the difference is the result of handling it in the view builder as opposed to _block_get_renderable_region. Moving this wasn't strictly necessary, but it just seemed wrong to me to have the knowledge of how to construct a render array from a block spread across two different places. Who is responsible for this knowledge is debatable, but it should at least be in one place.
This patch adds the block cache tests back and adds more docs.
Comment #12
msonnabaum CreditAttribution: msonnabaum commentedUpdated the blockcache tests to use the new interface instead of the constants.
Comment #14
dawehnerAwesome work!
Let's pull that from the request object instead.
WTF to this line-wrapping.
Should we really expose this in a UI? If we do we should explain this option better.
Do we need expire as well as max_age?
It is really great that this this information can be provided by blocks now! I could imagine that even a view could implement that interface and used both on pages and blocks.
Comment #15
Wim LeersSince #2167039-85: Regression: page cache tags broken, also: some routes incorrectly use _controller -> No page object in template variables -> Fatal in seven_preprocess_page(), the
CacheableInterface
has been committed, so this patch no longer needs to introduce it. Note that methods have been prefixed with "get", though.Comment #16
msonnabaum CreditAttribution: msonnabaum commentedHere's a re-roll to use the cache interface that's in core now.
Comment #17
msonnabaum CreditAttribution: msonnabaum commented@dawehner
1. Agreed. I was just hesitant to do much with this helper since the whole thing feels kinda temporary, it should be changed in this patch either way.
2. I do that style of line wrapping instead of letting it go over 80 chars because I think it's a lot more readable and looks ok when you add more. I'm sure I'm breaking some doc I've never read by doing it, so happy to change it if it avoids a bike shed.
3. Yes, we definitely need to have this in the UI. Any block can disable it easily by returning FALSE for isCacheable. I think what's there is the bare minimum, but there's certainly room for improvement. This patch should improve the docs as you suggest, but I'd like to leave anything more advanced there for a separate issue.
4. Expire is just in render api. I think we should only ever expose max age, since it better describes what is wanted. Max age is already used for page cache, so just doing the same thing here.
Comment #19
sunNice work!
Would it make sense to add the block's ID as a tag in this base implementation?
So if a particular block is updated/deleted, we'd be able to invalidate all the caches that contain it?
The new method name is not really clear to me.
Also, the name makes it sound as if the query would be executed, but the method actually just builds a Select query object only.
Can we rename this into
buildForumQuery()
?Shouldn't all of these block-specific implementations call into parent::getCacheKeys() + add their own, instead of replacing the base tags?
Comment #20
Wim Leers#19.1: YES! Blocks lack cache tags. The only question is whether it's appropriate to do it in this issue or not. However, it turns out to be rather trivial:
Therefor I propose to add it here.
In this reroll
CacheableInterface
to the get-prefixed ones. Fixed._block_get_renderable_region()
and_block_get_renderable_block()
.build()
is supposed to return just a render array, but Views used the fact that this ran beforegetConfiguration()
to dynamically change the label configuration. This is wrong. And it breaks now that the calling order has changed. Fixed.CacheableInterface
doesn't belong onBlockBase
, but onBlockPluginInterface
.BlockViewBuilder
, not byBlockBase
. Block plugins can *add* more cache keys though (only "cache context" keys are necessary: per role, per page, query). The default cache keys now are analogous to those used for other entities.hook_block_view_alter()
is NOT being invoked when building blocks that will be cached in D8 HEAD. Fixed.#pre_render
callback).TODO
BlockTest
, one assertion fails, and I can't seem to fix it. I hope somebody else can.CacheableHelper
missing.Marking critical because this blocks #2124957: Replace 'content' cache tag with 'rendered' and use it sparingly.
Comment #22
msonnabaum CreditAttribution: msonnabaum commentedHere's a re-roll of #20 that hopefully applies.
Comment #23
msonnabaum CreditAttribution: msonnabaum commentedComment #25
Wim LeersAt least *less* fails now :)
Comment #26
Wim LeersNow there should be only one fail.
Comment #28
moshe weitzman CreditAttribution: moshe weitzman commentedI'll take a look at the test fail now.
Comment #29
Wim Leersmenu_tree()
is used for rendering menus and its docs say:Comment #30
Wim LeersI just found a massive bug introduced by
CacheableHelper
: becauseDRUPAL_CACHE_PER_(PAGE|ROLE|USER)
are integers, that means that any cache key that is integer 1, 2 or 3 will also be upcasted to the corresponding cache key thatCacheableHelper
generates.So, the render cache entry for node/term/user/… 1, 2 or 3 will have a wrong cache key like
entity_view:taxonomy_term:r.anonymous:full:bartik:r.anonymous
instead of
entity_view:taxonomy_term:1:full:bartik:r.anonymous
The simplest solution would be to change
DRUPAL_CACHE_PER_(PAGE|ROLE|USER)
to strings. But even in that case, unwanted replacements are possible (though improbable).Keeping cache granularities, having them affect cache keys & cache tags
Overall, it looks like it might be better to not merge "cache granularity" with "cache keys"? When caching, "cache granularity" should indeed be converted to "cache keys". But there's an important distinction:
CACHE_PER_ROLE
, then changing anything about the role might cause this cache entry to be stale. The mechanism for that is cache tags.So, in short:
Finally, at the very least some cache granularities are missing, such as
DRUPAL_CACHE_PER_LANGUAGE
(currently, entity render caching is language-agnostic… which means that it will unfortunately serve the same cache entry regardless of current language — easily fixable, but what if UI translations change? Since there's no cache tags for languages, you'll have to clear all the caches. Keeping cache granularities and have them affect cache keys & tags would solve that.) But more likely, we're missing the ability to *add* cache granularities, such asDRUPAL_CACHE_PER_COUNTRY
.Comment #32
catchThis isn't necessarily true, CACHE_PER_PAGE/per language etc. are only about ensuring the right version of content is served, not necessarily at all about invalidation. If invalidation is also needed (for example a role name being rendered), then tags should be added separately.
It's also not feasible to bubble up cache granularity. There's #2099137: Entity/field access and node grants not taken into account with core cache contexts for allowing modules to affect cache granularity higher than the level they actually get invoked. Then there's #post_render_cache for allowing content to avoid adding additional granularity at all, that's the best we can do I think.
Comment #33
Wim LeersI agree that currently it's not at all about invalidation. But I'm questioning whether that's correct. If it needs to be cached on a per-X basis… doesn't that always imply that if X changes, the cache entries should be invalidated too?
Can you come up with an example of where my statement doesn't hold?
Why not?
It'd be just the same as bubbling up cache tags:
Comment #34
catchCache tags only need to be calculated on a cache miss, the cache ID needs to be calculated on cache hits as well.
Per-page caching doesn't relate to invalidation at all - it just means you want something recalculated on every path uniquely.
Per-language caching - possibly you might want to update the HTML cache of some user interface text if a translation gets updated, but I don't think you'd want an entity to be invalidated. Updating a translation of the entity means the entity itself gets updated anyway.
Comment #35
Wim LeersBut many (most?) rendered entities contain at least some labels, and labels are translated using UI translation, not entity translation.
Comment #36
catchThis is true. In that case I'd expect the field system to add that tag though - it knows it's adding interface translated labels that might need invalidation per-language, and it could do so only when one instance has the label set to not hidden even.
Comment #37
Wim LeersIf the field system is responsible for that, then the menu system will have to add it for every menu block, the search block will have to add it for itself, and so on. Does that really make sense?
The current UI language affects everything with a UI string, which is… a lot, if not almost everything. That's why I argue it should be a cache granularity.
Comment #38
catchSince block titles are translatable/translated, wouldn't the block system just add it for all blocks?
Comment #39
Wim LeersReroll; patch didn't apply anymore.
Additional changes (see interdiff):
Cache::Permanent
.Detailed test coverage for block's cache tags should wait until #2188565: Test coverage for Comment, Custom Block, Node, Taxonomy Term and User entity cache tags gets in, which adds helpful base classes to do exactly that.
Comment #41
dawehnerAdding a reference to an issue which touches A LOT of similar code.
Comment #42
dawehnerIf we use it like that we have to document why this cannot be a service
Is there any reason we don't name it #label instead or even just #title?
See below
We do have alternatives for both drupal_map_assoc and format_interval.
Is it just me that the cacheable_helper is not used here?
Should we use CacheBackendInterface::CACHE_PERMANENT instead?
We should maybe also explain why the block is not cacheable.
Note: this method uses assertIdentical which caused problems in the linked issue above. I guess you had to manually rearrange all those tags?
I really like that we have support for it now!
Are you sure this change is needed? This will potentially change the behavior before saving, while the old code did just ran during building() so the stored data never changes.
Given that the configuration of the block might change the output of the block (like change the total displayed items) we should rely on the actual configured block ID instead.
Comment #43
dawehnerGreat work btw.!
Comment #44
dawehnerGreat work btw.!
Comment #45
effulgentsia CreditAttribution: effulgentsia commentedJust a reroll. Does not address any feedback. CNR for bot.
Comment #47
Wim Leers45: blockcache-2158003-45.patch queued for re-testing.
Comment #49
Wim LeersStraight reroll from #39, hopefully this one will allow Drupal to install again…
Comment #51
Wim LeersFixed test failures, except the one we've been trying to debug for weeks. Debugging that one is up next.
#42:
#title
.Cache::Permanent
means it can be cached infinitely.However, that's a great point, because
Cache::Permanent === 0
! That's a pretty big problem right there… @msonnabaum, any suggestions on how we fix that?getConfiguration()
MUST return the effective configuration. Otherwise you break assumptions in the block render layer.Finally, Views already was doing the exact same thing: it was modifying
$this->configuration
before the patch, that hasn't changed. It'd be better if$configuration['views_label']
didn't exist at all, but that was probably done for good reason (i.e. for some reason you cannot use$configuration['label']
?).BlockViewBuilder
:In there,
$entity->id()
ensures the configured block ID ends up in the cache key. The funny thing is that this is *identical* to the code you were commenting on… so it's actually completely useless. Removed it. Well-spotted :)Comment #53
Wim LeersFixed one test failure in this reroll. The problem was this hunk that I added for usability reasons:
The problem is that by default, every block is not cacheable. So you wouldn't be able to configure them. So very silly. The solution is to not do this automatically, but let blocks with hardcoded cacheability settings slightly alter the form to communicate their hard-coded defaults to the end user. We can worry about an abstraction to do that later, for now this is sufficiently clean.
Now this should be down to 3 test failures again.
Comment #55
Wim LeersSome unexpected failures. Fixed and locally verified that this should now be down to 3 failures again.
Comment #58
Wim Leers@dawehner/#42.6: Regarding
Cache::PERMANENT === 0
: I discussed this with msonnabaum. The short version is: "Cache::PERMANENT === 0
doesn't make sense".The longer version:
Cache::PERMANENT
is intended to be used in a cache bin'sexpire
column. That indicates a UNIX timestamp at which a given cache entry should expire.expire
column is really a materialized/absolute TTL. A TTL is relative, and with only a TTL, you don't know when to expire a cache entry. Therefor, we convert a TTL into a UNIX timestamp.CACHE_PERMANENT === 0
andCACHE_TEMPORARY === -1
. Now we only haveCache::PERMANENT
anymore.0
is a valid UNIX timestamp. Probably because-1
already had been assigned a different meaning in Drupal 7, we opted for0
as the value representing "cache permanently".Cache::PERMANENT === -1
.Cache::PERMANENT
both for TTL (i.e. when indicating how long something should be cacheable, e.g. a block's max age) and actual cache entry expiration (i.e. how long something is cacheable, e.g. a cached block).Comment #60
Wim LeersI finally found the root cause for the two
BookTest
failures since #51:#block_label
to#title
.testNavigationBlockOnAccessModuleEnabled()
, which specifically sets'block_mode' => 'book pages'
. This causesBookNavigationBlock::build()
to execute the code in theelseif ($current_bid) {}
scope. That scope contains this code:Yep, that's
#title
right there.#title
is. And apparently, that's empty.$book_title_link
is broken. But what *should* it generate? Taking a look at where it comes from… it comes from the massive blocks-as-plugins commit/patch.theme_book_title_link()
also didn't exist. And there was no book title or title link visible in the output. So this has been broken for well over a year. Furthermore, the test coverage is unchanged:testNavigationBlockOnAccessModuleEnabled()
is functionally equivalent with what we had back then.Most interesting: it explicitly tests that the block label is the randomly generated one, not the book title!
#title
thing is just dead code and can be removed!Now this should be down to 1 test failure again.
Comment #62
Wim LeersClean-up:
defaultConfiguration()
should call the parent method.block.schema.yml
was outdated.SystemMenuBlock
now defaults to caching forever. We can do this now (and enjoy the performance improvement that this brings), because #2179083: Rendered menus (e.g. menu blocks) should set cache tags to inform the page cache ensures menu cache tags are cleared correctly in all cases. In the future, we should be able to make most blocks cached forever by default. Note that for those who have advanced/very dynamic menu blocks: it's trivial to disable caching. We just want Drupal 8 to be fast by default.Comment #63
tim.plunkettThis could have just been
return parent::defaultConfiguration() + array(
... Oh well.Also, the + of two arrays won't work if its nested, but that's probably overkill.
Comment #65
Wim LeersAddressed #63.
Comment #67
Wim LeersAnd this should fix the last test failure! Finally figured it out :)
Comment #68
BerdirWe should type hint this as a SelectQueryInterface... ?
Nitpick, multi-line @todo's should be indented two spaces.
Comment #69
msonnabaum CreditAttribution: msonnabaum commentedWasn't happy with how block defaults were being handled. Here's a different approach that simplifies it a bit. The block subclasses no longer have to call parent:: and merging of defaults is now enforced whenever it is set (this seems like an existing bug).
Also took care of Berdir's feedback.
Comment #71
tim.plunkettThis used to be $configuration + default + base defaults, you've switched the order.
Shouldn't this now be in setConfiguration?
Comment #72
msonnabaum CreditAttribution: msonnabaum commentedTakes care of the first part of #71. Good call.
Comment #73
msonnabaum CreditAttribution: msonnabaum commentedComment #75
msonnabaum CreditAttribution: msonnabaum commentedTry that again.
Comment #77
tim.plunkettWe have traits now
Comment #78
Wim LeersMuch better! :)
Making patch green again.
Comment #80
Wim LeersThe exceptions are because we removed one
parent::defaultConfiguration()
too much:ViewsBlock
was explicitly getting the defaults fromViewsBlockBase
, so we want to keep that.Now it should be green again.
Comment #81
effulgentsia CreditAttribution: effulgentsia commentedI tried reading through the patch, but it was a lot to digest, so I haven't grokked a lot of it yet. An updated issue summary explaining everything the patch changes and why, along with what is still being debated (if anything), would be extremely helpful.
I do want to chime in though with a +1 to CacheableInterface (already in HEAD), and to this issue's decision to make BlockBase implement it. I think doing the same to FormatterBase will be the key to the way we solve #2099137: Entity/field access and node grants not taken into account with core cache contexts. That's not this issue's concern, but just adding that for context.
I agree with catch in #32 that it's not feasible to have render api auto bubble up cache granularity, since that would require you to build out the entire render array tree prior to calculating a cache hit/miss, so on cache hits, you'd lose most of the benefit of the cache. In fact, I think this is the fundamental problem with #cache as a public portion of render api, because if it can be set on any arbitrary element, then descendant elements get no chance to affect the granularity, and instead the module responsible for the descendant element must also know every possible cacheable ancestor and implement an alter for it. However, where D8 gives us a way out of this conundrum is that we now have a manageable set of render cacheable objects: blocks, views, menus, entity displays, and formatters. So, because we know the architectural relationships between these objects, we can implement optimized approaches for bubbling up what's needed via these objects, and for that, having these objects implement CacheableInterface is very handy.
Therefore, I think we can remove 'granularity' entirely from #cache, since with CacheHelper, granularities can be set in 'keys'. That could be a follow up though.
Comment #82
msonnabaum CreditAttribution: msonnabaum commentedAgreed. I'm working on refactoring CacheHelper into a CacheContext service that uses it's own constants, which I should hopefully have posted tomorrow. My plan was to introduce that now, then in a followup we can move stuff like drupal_render_cid_parts, drupal_render_cid_create, etc to classes, and then get rid of granularity.
Comment #83
Wim LeersIssue summary updated. I think it should now be possible to only read the issue summary now (+ linked select comments) and get a good sense of what this issue is doing.
Comment #84
Wim LeersComment #85
msonnabaum CreditAttribution: msonnabaum commentedAt first I started creating new constants so we weren't abusing the old ones, like Cache::CURRENT_USER. That works, but it is no more capable or flexible than how we used granularity in the past. I feel like if we're going to do this, we should improve it, so that's what this patch is attempting.
It introduces a new CacheContexts service, that holds all the available contexts, and takes over the resolving responsibilities from CacheableHelper. The available contexts are now extendible via tagged services. The existing ones (per user, per role, per page) are converted in this patch and split between core and user module.
To provide a cache context, you simply need a class that declares the contexts you provide and which method to call for each.
I also added a form element to the block configuration form for picking contexts on a per-block basis. This is the feature I've always wanted for blocks/views/panes, but couldn't easily be implemented because granularity was fixed. Here's what it looks like:
CacheableHelper is now gone. I was going to make a trait, but once I moved all the context stuff out, there was only one method, so I moved keyFromQuery to the main Cache class.
I tried to make declaring cache contexts as simple as possible for contrib, and this is the best I could come up with. I'm not a fan of tagged services, but these should be rare enough that they don't warrant an additional yaml file. It's pretty nuts how complex it is now to just map a string to a callable. I'm open to ideas to simplify it.
Comment #87
msonnabaum CreditAttribution: msonnabaum commentedHoping this fixes many of those failures…
Comment #89
Wim LeersOnly making the patch green, only essential changes, no review of #85.
Comment #90
Wim Leers#85:
I like the changes you made a lot.
Superficial clean-up changes in this reroll
Almost all changes can be considered "superficial clean-up".
Cache-Control: max-age=<X seconds>
header. I think that similarly, the "cache contexts" setting can be considered the same concept as HTTP'sVary: <some request header>
.#type = fieldset
, but#type = details
.#states
to ensure it's disabled otherwise.CacheContexts::addContextsToKeys()
was poorly named IMHO: it implies cache contexts are being added to cache keys, whereas cache contexts already *are* in the cache keys, but they're placeholders, and this method replaces the placeholders. Therefore, renamed toCacheContexts::convertTokensToKeys()
DRUPAL_CACHE_PER_PAGE
and friends) had not yet been updated to use the system introduced in #85. Fixed.BlockBase::isCacheable()
was returning FALSE when the max age was set to "Forever" (Cache::PERMANENT
).With the above changes, a block using
BlockBase
now looks like this by default:Once a max age is configured, the user can configure contexts:
And some blocks come with required contexts, which then looks like this:
Bigger changes in this reroll
Request
object. Please verify that what I did is okay.CacheContextInterface
that such services should implement.Comment #92
Wim LeersThis reroll is about removing dead code. And updating the state of the overall issue (updated issue summary + tags). I didn't fix the 2 simple test failures, but I hope it stays at 2 :)
As of #85, we're removing the "cache granularity" concept entirely as a stand-alone concept: cache keys can now be either static strings or special token/placeholder strings, that are replaced by the corresponding Cache Context service.
In this reroll
DRUPAL_NO_CACHE
,DRUPAL_CACHE_CUSTOM
,DRUPAL_CACHE_PER_ROLE
,DRUPAL_CACHE_PER_USER
,DRUPAL_CACHE_PER_PAGE
,DRUPAL_CACHE_GLOBAL
.drupal_render_cache_by_query()
— its successor was added in #85:Cache::keyFromQuery()
.drupal_render_cid_parts()
LanguageCacheContext
: functionally identical to the default granularity thatdrupal_render_cid_parts()
used to addThemeCacheContext
: functionally identical to the default granularity thatdrupal_render_cid_parts()
used to addEntityViewBuilder
was the only code besides block caching that was using cache granularity — now it's setting thecache_context.user.role
cache key insteadBlockViewBuilder
used to depend ondrupal_render_cid_parts()
to always set the "per theme" cache granularity, now it's settingcache_context.theme
for every block. Note that it's not setting the "per language" cache granularity, since that should be a deliberate choice by the admin user or the block plugin developer.Comment #93
msonnabaum CreditAttribution: msonnabaum commentedAwesome work. I agree with nearly all those changes, and the form stuff you changed is exactly what I wanted, but I didn't know how to do with form api.
That's interesting, I'd never considered that that would be interpreted as the user editing the form. I'm not 100% convinced, but I don't feel strongly either way, I'm fine with the change if others agree.
Comment #95
Wim LeersI found the root cause for the two test failures since #90.
I enabled "cache forever" for
SystemMenuBlock
since #62. But due to a bug, it was not actually being cached yet, and I fixed that in #90:With caching of menu blocks actually working, the blocks were now being built in a
#pre_render
callback. And apparently, cache tags set by#pre_render
callbacks are actually broken. Patch to fix that at #2215719: Cache tags set by #pre_render callbacks are lost in page cache. Once that's in, this patch will become green again.Comment #96
Wim LeersI went over the patch once more, and this is the final clean-up I found. Notable:
block.schema.yml
again, for cache contexts.<select multiple></select>
form element to select cache contexts doesn't work: many users don't know how to select multiple things and it's too easy to not notice you can scroll to list more things once there are several. So I replaced them with checkboxes.So, redid the screenshots from #90:
There should still be 2 failures, see the previous comment.
Comment #98
Wim LeersComment #99
Wim LeersComment #100
Gábor HojtsyI think this is a great generalization and akin to what I imagine EclipseGC envisioned way back to provide contexts to feed into blocks, so they know what they are dependent on. Did not find major issues, found the following though:
Without further explanation why this looks a bit puzzling...
I know language was not here but entity views would depend on language a great deal also.
Based on the screenshot it was not evident if this details element is collapsed by default. I think the settings here looks pretty technical and scary for an innocent eye. At least I would move it to the bottom of the form but also probably just collapse it by default. I think we are confident in our defaults?
Comment #101
Wim Leers#100: thanks for the review!
As you can see, it's only added if the entity exists in >1 languages.
<details>
element, precisely for the reasons you cite. Of course, showing a screenshot of a closed<details>
element isn't very helpful, so I opened them just for the screenshots :) We are indeed confident in our defaults.This reroll also fixes the 146 exceptions introduced by the previous patch, which apparently were caused by
#type = checkboxes
… WTF!Comment #103
Wim LeersChasing HEAD. #2134857: PHPUnit test the entity base classes broke several hunks of this patch.
Comment #105
Wim Leers#2188565: Test coverage for Comment, Custom Block, Node, Taxonomy Term and User entity cache tags landed, so in the next reroll of this patch (which is blocked on #2215719: Cache tags set by #pre_render callbacks are lost in page cache) can also enable "cache forever" for custom blocks :)
Comment #106
Wim Leers#2215719: Cache tags set by #pre_render callbacks are lost in page cache landed — now we can continue here! :)
In the mean time, there have been quite significant changes to HEAD that require changes to this patch:
Comment #107
Wim LeersComment #109
catchEnabling block caching for Views blocks in 7.x allows for cache hits to avoid initialising the View object at all - this can save anything up to 100ms compared to using a views cache plugin on the same block. Feels like we'd still want a way for views to set #cache on its block plugins so they only have to initialize the view object itself on misses, otherwise that's quite an easy optimization on views-block-heavy sites gone.
Comment #110
Wim Leers#109: I'm not sure what you're saying exactly. Block caching is now available for every block by default, including Views' blocks. That means we're just able to remove Views' specific support for it, since Views' blocks — like all others — get it for free :)
So: yes, absolutely, we definitely still want a way for Views to set
#cache
. It's still configurable, and still in the same place, just with a UI that's consistent with all other blocks, and with more refined options.Comment #111
Wim LeersGreenification.
I've jotted down a bunch of additional test cases I want to add. I'll do that tomorrow. In the mean time, this should be good to review :)
Comment #112
msonnabaum CreditAttribution: msonnabaum commented@catch - What wim said. You can now set block cache settings on the block config for the view, now that those view blocks show up in the block UI.
Comment #113
catchI completely missed that the Views blocks now have cache settings in the blocks UI :( That makes complete sense though!
Comment #114
Wim LeersFive different block plugins are enabled in a default install of Drupal:
SystemHelpBlock
SystemMainContent
— this is impossible to cache (at least in the foreseeable future)SystemMenuBlock
— already cached forever since #62SearchBlockForm
UserLoginBlock
Three of those are very feasible to cache forever by default: the two form blocks (search + user login) are easily cacheable (they already are cached for anonymous users when page caching is enabled, which demonstrates they work just fine) and the help block (which is cacheable per URL).
So now all four block plugins for which caching is feasible ship with caching enabled by default.
This helps validating what we're doing in this issue, and should help simplify profiling.
Tests coming next.
Comment #115
catchThis is only true if we disable $form['#cache'] and $form['#token'], and additionally assume that no custom module is going to AJAX-enable those forms per #1694574: drupal_process_form() deletes cached form + form_state despite still needed for later POSTs with enabled page caching. That might be fine to do for those blocks just pointing out. Also I doubt we have tests for those forms where one user visits the form then another submits which is where the CSRF protection will kick in/break if the HTML is cached.
Comment #116
Wim Leers#115: I considered that too, but fact is that those forms already work with page caching enabled today. It's trivial for other modules that AJAX-enable those forms to disable render caching. In fact, that's one of the test cases I'm working on :)
Comment #117
catchThat's because the form token is based on the session ID. Cached pages are only built for/served to users with no session, so the token is always the same.
As soon as you get a session ID, the form token will be different, but you'll also not see anything from the page cache so it continues to work (mostly by accident).
Comment #118
Wim LeersI see! So I'll remove the "cache by default" for
SearchBlockForm
.UserLoginBlock
is fine because it only allows itself to be rendered for the anonymous user.Comment #120
Wim LeersFrom IRC:
So I'll have to remove the "cache by default" for
UserLoginBlock
also.Follow-up created: #2221391: Cacheable form blocks: Remove #token from the user login form.
By now I've undone almost everything in #114 :( Attached is also
interdiff-111.txt
, which shows the interdiff relative to #111, to make things easier to track.Comment #123
Wim LeersGreenification. Also: I'm stupid.
Comment #124
msonnabaum CreditAttribution: msonnabaum commentedHere's some quick profiling on this patch, using the front page as an authenticated user, with a few blocks added to the sidebar. The following blocks are either cached or uncached in each run:
block:bartik_footer
block:bartik_tools
block:views_block__comments_recent_block_1
block:views_block__content_recent_block_1
Granted, this isn't that surprising, and not an entirely realistic scenario, but a useful reminder that block cache is still the most important place we can add caching.
And although the improvement is significant, I was slightly disappointed that it wasn't more. I realized that much of this is due to the fact that the main content of the page is a view, and since it's SystemMainBlock, we can't use block caching there. That seems like something we should try to tackle after this since it's pretty limiting.
Comment #125
Wim LeersAnd here's the promised extra test coverage!
This reroll introduces
BlockViewBuilderTest
— the block-specific equivalent ofEntityViewBuilderTest
. It comes with extensive tests for all possible kinds of alters, which unveiled problems in the previous patches (that I was aware of, but also more subtle problems with the solution I had in mind).Note:
BlockStorageUnitTest
supposedly did CRUD tests, but it also did render tests. So I moved those over toBlockViewBuilderTest
.Comment #126
Wim LeersThis is now ready for final reviews — all remaining tasks have been addressed.
Comment #127
Anonymous (not verified) CreditAttribution: Anonymous commentedspent some time looking through this patch - i like it! looks ready to go to me.
Comment #128
jessebeach CreditAttribution: jessebeach commentedDraft change notice: https://drupal.org/node/2222293
Comment #129
moshe weitzman CreditAttribution: moshe weitzman commentedThis solves the problem at hand regarding cache tags, and also embraces our improved render cache API by implementing CacheableInterface
Comment #130
webchickZoooom!
Comment #132
Wim LeersChasing HEAD! #2080823: Create API to discover config entities' soft dependencies and use this to present a confirm form on module uninstall broke a few hunks.
Comment #134
BerdirThis has been committed by @catch. Published change record.
Comment #135
superspring CreditAttribution: superspring commentedThis patch causes an issue when adding fields to a content type.
Drupal now throws an error when rendering the successfully created page. The field is still created.
The error thrown is:
Twig_Error_Runtime: An exception has been thrown during the rendering of a template ("SQLSTATE[22001]: String data, right truncated: 1406 Data too long for column 'cid' at row 1: INSERT INTO {cache_block} (cid, serialized, created, expire, tags, checksum_invalidations, checksum_deletions, data) VALUES [...] in "core/themes/bartik/templates/page.html.twig" at line 173. in Twig_Template->displayWithErrorHandling() (line 291 of core/vendor/twig/twig/lib/Twig/Template.php).
Comment #136
BerdirWhat is the cid that it is trying to insert? The field is limited to 255 characters, sounds like you end up with a very long cid. Not sure why tests don't catch this?
The only thing that I can think of is to check the length and hash it when it's over the limit. Probably in a new issue, as it's not really the fault of this one but a more generic problem in the render cache API?
Comment #137
tim.plunkettRegardless of the bug, this is fixed. If you can reproduce reliably, please open a new issue and link it here.
Comment #138
Wim LeersTo address #135: #2224861: Cache SystemMenuBlock and BookNavigationBlock per active trail (currently cached per URL, this breaks on very long URLs).
Comment #140
znerol CreditAttribution: znerol commentedFollow-up: #2236167: Use request stack in cache contexts