Problem/Motivation
CacheableInterface was always intended to be the sole generic interface to implement for cacheability metadata. Entities (EntityInterface) and Config objects (ConfigBase) don't do this yet only because of historical reasons: doing that immediately would've been a daunting task. But now that we have comprehensive test coverage for the cache tags & cache tag invalidations for all of those, it's now a mere matter of deciding to do it.
Doing this would also allow us to solve this concern by @amateescu at #2407765-46: Wherever simple config is used to render output to end user, associate their cache tags.1 elegantly:
+++ b/core/modules/book/src/BookManager.php @@ -352,6 +353,7 @@ protected function addParentSelectFormElements(array $book_link) { + $form['#cache']['tags'] = Cache::mergeTags(isset($form['#cache']['tags']) ? $form['#cache']['tags'] : [], $config->getCacheTags());This
mergeTags()with ternary logic as an argument is used a lot and it's quite cumbersome to write, if we had a helper method on the</code>how about adding a <code>Cache::mergeRenderArrayTags()(or similar) that receives a render array and does all the isset logic for you?
However, in the process of working on this, we again encountered the previously seen-but-dismissed-because-not-important-enough-at-the-time problem: CacheableInterface doesn't make sense. (See #22 & #23 in particular.)
Proposed resolution
- Start to fix
CacheableInterface: introduceCacheableDependencyInterface, which only has the methods for getting the cache contexts, cache tags and cache max-age (i.e. the bubbleable cache metadata). We settled on that name after many long conversations. It signifiesI'm an object that may be a dependency for calculating some other data/output, and my cacheability should affect the cacheability of that other data/output
. - Keep
CacheableInterfacefor now, so this issue doesn't get delayed on bikesheds, and remove it in #2459819: Remove CacheableInterface (and no longer let block plugins implement it). That issue already has a green patch, so the risk of not fixing this properly is very low. - This then allows us make
EntityInterfaceandConfigBaseimplementCacheableDependencyInterfacewithout WTFs. - And that finally brings us to the original goal of the issue: this allows us to add a
RendererInterface::addDependency(array &$build, CacheableDependencyInterface $dependency)method, which finally provides a great DX when building render arrays that depend on entities, config, access results (all of which implementCacheableDependencyInterface).
Before vs. after:
+++ b/core/modules/forum/src/Controller/ForumController.php
@@ -191,7 +203,7 @@ protected function build($forums, TermInterface $term, $topics = array(), $paren
- $build['#cache']['tags'] = Cache::mergeTags(isset($build['#cache']['tags']) ? $build['#cache']['tags'] : [], $config->getCacheTags());
+ $this->renderer->addDependency($build, $config);
Note: this also mean when more cacheability metadata is added to render arrays, that'd work automatically, no code changes necessary. That's very, very important.
Remaining tasks
Review.
User interface changes
None.
API changes
- API addition:
CacheableDependencyInterface - API addition:
RendererInterface::addDependency() - API change:
AccessResultnow implementsCacheableDependencyInterfaceinstead ofCacheableInterface. Effective consequences:setCacheable(FALSE)->setCacheMaxAge(0)setCacheable(TRUE)->setCacheMaxAge(Cache::PERMANENT or N, e.g. 600)isCacheable()->getCacheMaxAge() !== 0
Beta phase evaluation
| Issue category | Task because not technically broken, but vastly improving the DX. |
|---|---|
| Issue priority | Major because not release-blocking, but absolutely crucial to not have a super frustrating DX. |
| Prioritized changes | The main goal of this issue is performance. The direct goal is DX, to indirectly improve cacheability and thus performance of the D8 module ecosystem. |
| Disruption | Disruptive for core/contributed that have access that call ::setCacheable(). Also affects modules calling ::isCacheable(), but that's likely zero modules in the wild right now.
This is a necessary BC break because |
| Comment | File | Size | Author |
|---|---|---|---|
| #95 | cacheableinterface-2444231-95.patch | 64.42 KB | wim leers |
Comments
Comment #1
wim leersComment #2
wim leersComment #3
berdiri remember this being discussed before, do we already have an issue for this, at least for entities?
Comment #4
wim leersI don't think there's already an issue, I can only find #2351847-14: Rename getCacheTag() to getCacheTags() and comments 16, 27 and 28 there. It was discussed there, but no follow-up was actually filed.
Comment #5
wim leersComment #6
wim leers#2429257: Bubble cache contexts landed, rough initial patch.
Comment #7
wim leersComment #8
wim leersComment #9
dawehnerIs there a reason why we don't call to the wrapped object?
Comment #10
wim leersBecause I thought a
ViewUIobject is intended to never ever be cached, but only be a runtime representation? If that's wrong, happy to change :)Comment #11
berdirViewUI implements ConfigEntityInterface to wrap the actual view config entity, any call to it should work as if called on the view config entity. So yes, I think this should pass through, just like id() and so on.
Comment #12
wim leersOh, ok :)
Comment #13
wim leersComment #14
wim leersThis should've been part of #13, but I forgot.
Comment #15
berdirShould we use some of those methods in CachedStorage instead of hardcoding it there?
Note that the keys actually don't match with what we have right now, we don't include a config prefix there but we have a collection prefix that the object doesn't know about.
Also, we can't actually access this method on cache get, so no, we can't... thinking about it, this the usefulness of this method is a bit questionable.. :)
same here, the cache key that we use for entities in SqlContentEntityStorage for example doesn't match this.
Comment #16
wim leers#2407765: Wherever simple config is used to render output to end user, associate their cache tags is in, now we should include the fix for #2407765-46: Wherever simple config is used to render output to end user, associate their cache tags.
Comment #17
wim leersIn the original proposal, I presented
as sufficient. But I was mistaken. What is necessary is this:
Because
applyTo()doesn't automatically merge what's already in the render array. Know that this was originally introduced only to be used insideRendereritself, where the merging already happened in an earlier step.I think there's something to be said to make
applyTo()perform merging with the render array's bubbleable metadata automatically. If others agree, I'll makeapplyTo()do$merged = $this->merge(static::createFromRenderArray($build));internally.Here's what the code looks like without doing that.
Comment #18
wim leers#15:
ConfigBaseis unaware of the collection it's in. Because onlyStorableConfigBase(a subclass) does know that, and the other subclass (ThemeSettings) cannot be stored (it's a runtime-only object).but more importantly, these are the cache tags used for caching entities:
Ideally, we'd return those as the cache tags for the entity object also. And I see no reason not to. The "_values"-specific cache tag can go away, since it serves no more purpose. This will cause many test failures though. Plus, in theory, we should no longer need the bit of code in
SqlContentEntityStorage::resetCache()that invalidates an entity type's entire entity cache. That should only be necessary when field info is modified, and by associating that cache tag with all entities, that will simply always be true.But this:
… I have no words for this, I can't believe nobody even thought of that since we introduced this almost a year ago in #2158003: Remove Block Cache API in favor of blocks returning #cache with cache tags.
You're right. Which I guess means
CacheableInterfaceactually is specifically designed for render cacheable objects.This again makes the case for the
CacheabilityAffectorInterfaceI wanted to add in #2287071: Add cacheability metadata to access checks (see the first patch there, and the discussion following it): an interface that only allows you to define affecting cacheability metadata (contexts + tags + max-age), i.e. the metadata for an object (A) that affects the cacheability of another object that contains it (P contains A).Further indication that
getCacheKeys()is useless: there's only one implementation of it in core that doesn't return the empty array:ForumBlockBase. And that one should probably be converted to use a cache context instead.So I guess I'm saying: we should ditch
getCacheKeys()because as Berdir so simply pointed out:There goes the dream of having a single interface used to indicate the cacheability of anything and everything.
Comment #19
wim leers#18 should fail majestically and be slower than most test runs, because I'm stupid:
… so just ignore that patch.
Comment #21
effulgentsia commented+1. I think we should remove
CacheableInterface::getCacheKeys()for the reasons mentioned. I think there might be good reasons to leave it on blocks (BC and also potentially moving some of the keys currently defined in BlockViewBuilder to BlockBase), but that can be done by adding aBlockPluginInterface::getCacheKeys()method.I don't get why. I think CacheableInterface without getCacheKeys() is that interface and satisfies everything we need a generic interface for. Getting the (explicitly non-bubblable) keys is only ever needed by the code that manages the cache get and set of that specific item, and that code can always know something specific about the kind of item it's dealing with and therefore how to best construct its keys, which may include delegating to the object (in the case of rendered blocks) or not (in the case of stored entities).
Comment #22
effulgentsia commentedThinking more about this, perhaps this is now the issue in which to do exactly that. Part of the problem when we discussed this in the earlier issue is what does "contains" mean? Because we were both trying to figure out the case of render elements that contain their child render elements and also render element cacheability being affected by an access check object or an entity. And those two relationships seem different. Now that we have render element bubbling solved, I think revisiting the concept of $entity or $config affecting the caching of some other object (or pseudo-object, like a render array), makes a lot of sense.
So how about this:
Comment #23
effulgentsia commentedThinking 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).
Comment #24
yched commentedA bit over my head, but I must say "Make Config objects & Entities implement CacheableInterface" did strike me as a bit weird. Those are not cacheable in themselves (well, not in the render sense anyway), they "merely" have cacheability info to provide for code building render arrays, right ?
Comment #25
fabianx commented#23 #24 Totally correct. This issue will be updated soon by Wim with the outcome of a chat discussion, which will make what is proposed here more clear.
Sorry for the confusion.
Comment #26
wim leersThe summary from the chat discussion that Fabianx mentioned, we had this chat last Friday (March 6, 2015).
(Sorry for the delay in posting it — working on too many issues at once at the moment.)
For the sake of simplicity and transparency, I'm just posting the entire chat log, I only omitted messages that were unrelated. I did split it up according to trains of thought, so that it's easier to follow.
CacheableInterfaceCacheableInterface: what makes sense and what doesn't?Comment #27
wim leersSo, to tie the discussion in this issue together with the one on IRC (transcript in previous comment): we mostly came to to the same conclusions.
And I'd like to +100 this from #23:
Patch proposal
From most important and seemingly-most-agreed-upon to least.
CacheableInterfaceintoCacheableDependencyInterface, letCacheableInterfaceextend that interface.EntityInterfaceandConfigBaseimplementCacheableDependencyInterface.Renderer::vary(array $render_array), which then allows us to doRenderer::vary($build)->by($access)->by($config). OrRenderer::depends($build)->on($access)->on($config)?BubbleableMetadata::createFromCacheableDependency(CacheableDependencyInterface $dep), which then allows us to doCacheableInterface, but keep its methods onBlockPluginInterfaceto avoid BC break. Or perhaps just onBlockBase?Comment #28
wim leersComment #29
wim leersAnd the lullapad link cited in #26, attached for posterity.
Comment #30
wim leersAnother idea.
Keeping all patch proposal points in #27 except point 3, replace that with:
Cacheabilityclass, which is a value object containing all cacheability metadata (contexts/tags/max-age)$build['#cache'] = new Cacheability()$build['#cache']->merge($access_result)->merge($config)… i.e. if there is a value object in the render array itself, it removes the unavoidable requirement to take the data in the render array, merge some other object's cacheability with it and then re-apply it onto the render array. We can then reduce that to a single step, that doesn't require knowing some static method, and the merging happens right in the context of where you expect it to happen: right where
#cachelives.Comment #31
fabianx commentedWould need to implement Array access then.
There is a performance hit for that and not taking into account existing values (which is a proper use case). Until proven that value objects in arrays really have more advantage than plain arrays, I am -1 on #30.
Comment #32
wim leersThat's fine, #30 was just another idea. For now, we'll go with #27. Looking forward to @effulgentsia's feedback in particular.
Comment #33
fabianx commentedAssigning to effulgentsia for feedback.
Comment #34
effulgentsia commented+1 to #27, and I like the name CacheableDependencyInterface. I'm wondering however what the semantics of CacheableDependencyInterface::getCacheTags() should be in terms of whether the object should return the cache tags of its dependencies (especially its constructor dependencies). For example, if we at some point decide that formatters should implement CacheableDependencyInterface, should their implementation of getCacheTags() return only what the formatter class cares about or also include the cache tags of the $field_definition passed to the formatter's constructor? I think what makes that question tricky to answer is that getCacheTags() is called both for adding the tags to a cached item and also for invalidating the tag when the object changes, and I think those two cases require a different answer, which might mean we need two different getCacheTags() methods?
Comment #35
wim leers::getCacheTags()has always been about this object, the object that it's called on. Bubbling is done during rendering, where nested dependencies are reflected by nested render arrays, with each level of nesting have its own level in a render array, and its own cache tags/contexts/max-age.But I see what you're saying: there may be use cases where an object receives a whole bunch of other objects and we want to know the aggregated cache tags/contexts/max-age. For that (currently still hypothetical) use case, we could introduce e.g. a
NestedCacheableDependencyInterfacein the future, with::getAggregateCache(Tags|Keys|MaxAge)(), to address the use case you describe. (Where that interface also conveys that the object in question can depend itself on objects implementing(Nested)CacheableDependencyInterface.) But I think that's the least of our problems right now.I'll get started on a patch for #27 tomorrow then!
Comment #36
effulgentsia commentedI'm a bit confused about what this object means for certain relationships. For example, BookNavigationBlock::getRequiredCacheContexts() returns ['user.roles', 'book.navigation'], even though the former is a cache context dependency for the code in $this->nodeStorage and the latter is a cache context dependency for the code in $this->bookManager; neither is a dependency for code within the lexical scope of the BookNavigationBlock class itself. So it seems to me we both have situations where objects are responsible for only returning their own information and situations where objects return information due to their dependence on other objects.
At some point, docs on CacheableDependencyInterface that clarify what a class is responsible for including and what it should not include within the return values of these methods would be handy, whether we can figure that out in this issue or follow-ups.
Yay. Changing the "assigned" attribute accordingly.
Comment #37
fabianx commented#36: That was before cache context bubbling though, so the block had no other chance, than to take this responsibility.
Comment #38
effulgentsia commentedRe #37, good point. Thanks for the clarification.
While I think that's kind of cute, I don't see a benefit that the intermediary objects would offer over
$renderer->addCacheDependencies($build, [$access, $config]).Comment #39
wim leersCompletely new patch. Implemented #27, points 1 and 2.
Comment #40
wim leersImplemented #27.3, but based on #38, I went with:
To demonstrate its use, I converted all the calls criticized in #2407765-46: Wherever simple config is used to render output to end user, associate their cache tags (i.e. the very comment that triggered this issue originally). This now leads to calls like
Quite elegant, I think. I'll let the interdiff speak for itself.
Note: Alex Pott originally said in #2407765-47: Wherever simple config is used to render output to end user, associate their cache tags that he'd prefer to not see it work by reference. I agree with that in principle, but I think it's hard to argue with the excellent DX above.
Comment #43
fabianx commentedJust $config; should dependsOn not have an assertion or why did this work?
Or do we not have tests for this part?
DX is great, I liked ::vary or ::addDependencies() better, but I am not attached to either.
Comment #44
wim leersSpent a lot of time debugging the failures in #39. Started with what seemed the easiest:
PathLanguageTest. Turns out that's a bug in language negotiation/language manager, which then naturally causes the cache context to be wrong too, and hence causes these failures. The good news: #2424171 actually fixes this — see #2424171-24: Language module vs. content translation module interaction exposes content translation bug.The next step was to look at the other test failures. They all have one thing in common: the test failures are triggered by the change below. Basically, this gets rid of the hardcoded key in favor of using the entity's cache contexts.
This works great in 99% of use cases. It breaks down in one case: where the code determining which entity gets rendered overrides the langcode to use, and thus which variation of an entity is used. In that case, the language cache context doesn't matter. And this is exactly what the failing tests do: they are for Views that are configured to e.g. list Spanish and French translations of nodes. I.e. it doesn't matter what the negotiated content language is; the View ignores that and lists those translations regardless of the content language!
This is of course a perfectly valid use case. So, updated accordingly.
This should thus fix 26 of the 27 failures in #39.
#43: yep, that's wrong. We do have failure because of that: two contact test suites have failures, see #40's test results :)
Comment #45
wim leersThe problem #43 pointed out was responsible for 52 of the 52 additional failures (additional compared to #39).
This should bring the number of failures down to 1 — that last failure is fixed by #2424171: Language module vs. content translation module interaction exposes content translation bug. It's not in any way caused by this issue; this issue merely surfaces that same problem.
Comment #46
wim leersComment #49
wim leers#45 has only 1 failure, as predicted :)
Comment #50
wim leersPer #23/#24/#25/#27, removing
CacheableInterfacealtogether. First step: updatingAccessResultto useCacheableDependencyInterfaceinstead.While doing so, I noticed that
AccessResultstill contained::getCacheBin(), despite that being removed fromCacheableInterfacein #2339373: Remove getCacheBin() from CacheableInterface. We forgot to remove it there (almost 6 months ago). Removing that as well as part of this patch.Comment #51
fabianx commentedNot reviewed in depth, but:
->setCacheMaxAge(0) I wondered if it would not be nice to be able to do:
->setCacheMaxAge(Cache::NEVER)
compared to Cache::PERSISTENT
Comment #53
wim leersLooks like this single line that I forgot to update caused 2315 fails :)
Should be back down to 1 fail!
Comment #55
wim leersThe only remaining user of
CacheableInterfaceleft now isBlockPluginInterface. Doing that would become significantly simpler once we've done #2428805: Remove the ability to configure a block's cache contexts. So, doing that first. Just posted an initial patch.That being said, the current patch here can already be reviewed.
Postponed on #2424171: Language module vs. content translation module interaction exposes content translation bug and #2428805: Remove the ability to configure a block's cache contexts.
Comment #56
wim leers#2424171: Language module vs. content translation module interaction exposes content translation bug landed. Re-testing. Should come back green. That allows me to mark this issue as NR. Updating block plugins' use of
CacheableInterfacestill needs to happen, but that doesn't mean this can't be reviewed already.Comment #58
wim leers#2428805: Remove the ability to configure a block's cache contexts landed, now unblocked.
Comment #59
wim leersStraight reroll.
Next: quoting #55:
Comment #61
wim leersFixed the todos that were blocked on #2432837: Make cache contexts hierarchical (e.g. 'user' is more specific than 'user.roles'), which has since landed. This should fix at least some of the test failures.
Comment #63
wim leersAll fails occur when language.module's
ConfigurableLanguageManageris in use instead of the defaultLanguageManager. Turns out the language cache context's code had a small bug due to the misleadingLanguageManagerInterfaceAPI.Comment #64
wim leersActually, I propose that we do this in a follow-up issue:
That keeps this patch small & focused. Removing
CacheableInterfacewill require relatively many & tricky changes, unlike the patch here.To show that it is very doable however, I've already opened an issue that includes a patch to do just that: #2459819-3: Remove CacheableInterface (and no longer let block plugins implement it). Turns out that it's actually easier than expected: only a 10 KB patch is necessary. But I still think it's better to do it in a follow-up issue instead of here, because I suspect there will be quite a bit of discussion/bikeshedding on that, whereas we have agreement on the approach for this patch, so we can continue with the bulk of the work here IMO.
Comment #65
wim leersBubbleableMetadata::createFromObject()andRenderer::dependsOn().)BubbleableMetadataimplementCacheableDependencyInterface.Comment #66
fabianx commentedNeeds an issue summary update
Comment #67
wim leersThis is a remnant that should be removed.
Comment #68
effulgentsia commentedDiscussed with Wim, and here's a summary of that discussion:
Let's move this and all other language-related changes out of this issue. Not really in scope and could use dedicated discussion from D8MI folks.
The renderer doesn't depend on anything. Rather, we're asking it to add a dependency to the first argument / informing it about a dependency that the first argument has. So perhaps a better name would be
addDependency()?Comment #69
wim leersMy conclusion from my discussion with @effulgentsia, with a few more details than #68:
::dependsOn()-> non-static::addDependency()(#68.2)Renderer(Interface)On it, tomorrow :)
Comment #70
fabianx commentedFor the record I agree with #68
Comment #71
wim leersNext: #68.2/#69.2.
Comment #72
wim leersFixed #68.2/#69.2.
Updated IS.
Comment #73
wim leersFurther improved IS.
Comment #75
wim leersComment #76
wim leersTestbot fail:
General error: 13 database or disk is fullsigh
Comment #79
wim leersTestbot-- :(
Now waiting for this testbot to be killed.
Comment #81
wim leersAfter I was able to access the log of the latest failed run, there was actually useful information in the log, unlike after the first failure: this time it had so many failing tests that it somehow was filling up the DB/disk completely.
Turns out I made a typo… and copy/pasted it several times. Interesting consequences :)
Comment #82
fabianx commentedIS looks great now, patch too, this needs a change record.
This is just a re-factor, user.roles => user.permissions is - if we want it at all - a follow up change.
This is so much nicer!
This looks great now.
RTBC!
Comment #83
wim leersI think we actually want to:
CacheableInterfaceintroduction)RendererInterface::addDependency()AccessResult, which explicitly mentionsCacheableInterface)Already did point 2: https://www.drupal.org/node/2460819. I'll do points 1 and 3 as soon as this lands (I already associated them with this issue).
Comment #85
nlisgo commentedReroll
Comment #86
fabianx commentedBack to RTBC
Comment #87
wim leersThanks!
Comment #88
alexpottNeeds a reroll.
Comment #89
alexpottComment #90
nlisgo commentedComment #91
fabianx commentedAnd back :)
Comment #93
nlisgo commentedThe following issue is now fixed #2453059: Set default render cache contexts: 'theme' + 'languages:' . LanguageInterface::TYPE_INTERFACE.
Maybe this is causing some or all of the fails? I'll take a look.
Comment #94
nlisgo commented@WimLeers is taking a look.
Comment #95
wim leersThe test failures stemmed from #2458349: Route's access result's cacheability not applied to the response's cacheability, but now that #2453059: Set default render cache contexts: 'theme' + 'languages:' . LanguageInterface::TYPE_INTERFACE has landed, we can indeed simplify the @todo mentioned in #93.
Comment #96
fabianx commentedRTBC + 1
Comment #97
alexpottCommitted 2b90b08 and pushed to 8.0.x. Thanks!
Thanks for adding the beta evaluation to the issue summary.
Removed this unused uses on commit.
Comment #99
wim leersOMG YAY!!!!
This unblocked #2459819: Remove CacheableInterface (and no longer let block plugins implement it). And did a small bit of the work necessary in #2099137: Entity/field access and node grants not taken into account with core cache contexts, hence forcing the need for a reroll of that one.
Quoting #83:
Now did points 1 & 3 and published the new CR in point 2.
Comment #100
wim leersNow also updated the handbook page at https://www.drupal.org/developing/api/8/render/arrays/cacheability