This is split off from #2512718: EntityManager::getTranslationFromContext() should add the content language cache context to the entity to keep the scope manageable, please read the IS of that issue too!
Problem/Motivation
While discussing #2512718: EntityManager::getTranslationFromContext() should add the content language cache context to the entity we realised the configuration overrides do not provide cache contexts.
This does not currently manifest itself as a bug in core, but that is only because configuration overrides depend on the interface language, and interface language is a required cache context.
However as soon as you add domain module, organic groups or any other type of configuration override, then you have cache poisoning.
Proposed resolution
Have ConfigFactoryOverrideInterface
extend CacheableDependencyInterface
.
Then ConfigFactory.php can collect the cache contexts from the overrides and apply them to the config object. This is an API change for all config overrides.
If we decide that it's valid to have config overrides that don't require any cacheability metadata at all, then we could possibly add an instanceof check in ConfigFactory.php. That would remove the API change, but support for it would still be a contributed project blocker (and while interface language is a required cache context, IMO language config overrides should set a good example here).
Alex Pott also noticed that we have a bug with getCacheSuffix() - but I'll let him update the issue with that or open a spin-off (I think it was that the order, and hence cache key, could vary inconsistently).
While contributed project blockers are not usually critical, in this case the contrib module can port in every other aspect, but would have a security vulnerability until we fix this issue in core and it's updated to use it - and we have no way to stop people doing cache poisoning things in config overrides because really that's the entire point of them (arbitrarily changing config values based on whatever).
Remaining tasks
- Yep.
- Unpostpone #2526326: Update CacheableMetadata & AccessResult to use RefinableCacheableDependency(Interface|Trait).
User interface changes
Nope.
API changes
ConfigFactoryOverrideInterface
now extendsCacheableDependencyInterface
.ConfigBase
now implementsRefinableCacheableDependencyInterface
Data model changes
Nope.
Beta phase evaluation
Issue category | Bug |
---|---|
Issue priority | Critical because it allows cache poisoning through config overrides. |
Prioritized changes | The main goals of this issue are to enhance security and performance. |
Comment | File | Size | Author |
---|---|---|---|
#158 | interdiff.txt | 2.36 KB | pfrenssen |
#158 | 2524082-157.patch | 47.1 KB | pfrenssen |
#119 | 2524082-119.patch | 46.65 KB | Gábor Hojtsy |
| |||
#116 | 2524082-116.patch | 41.93 KB | Gábor Hojtsy |
#106 | interdiff.txt | 20.48 KB | pfrenssen |
Comments
Comment #1
pfrenssenComment #2
Wim LeersI think the fundamental idea here is solid.
-1, because it allows implementations to not think about cacheability. And given config overrides can have huge impact on a site, and therefore also huge cache poisoning, I don't think that's acceptable. IOW: we must force all config overrides to implement this, and to therefore think about this.
The total number of config overrides is very low anyway, so disruption will still be minimal.
Now, let's talk about the sole config override in core:
\Drupal\language\Config\LanguageConfigFactoryOverride
. I first wanted to point out its inability to figure out how it was actually negotiated, but then I realized this always uses the interface language. Just to be one hundred percent certain: it is impossible to introduce a new "config language" language type, right? (Which would then be used instead of the interface language.)Comment #3
Wim Leerscatch pointed out for my question at the bottom of #2 that the module introducing such a new "config language" language type could also just override the
LanguageConfigFactoryOverride
service and change that. Plus it's an extremely esoteric need.So: a definitive +1 to this issue, as long as every config override is required to implement
CacheableDependencyInterface
.Comment #4
dawehnerWell on the other hand there is a good reason that you should write software in a way that not everything couples to everything in general.
For this config overrides are special already given their limited usecase.
Comment #5
Wim LeersIn talking to pfrenssen, I realized something:
(Emphasis mine.)
This is not something you can just do, because:
That just gives you getters, not setters. At #2512718-29: EntityManager::getTranslationFromContext() should add the content language cache context to the entity (and in IRC), Fabian said:
And that's what the quote in the IS is based on. But it's wrong/oversimplifying.
CacheableMetadata
has setters that do not yet have an interface. So we'd need to add such an interface (no idea what it'd be named), and haveCacheableMetadata
andConfigBase
implement it.Comment #6
Wim LeersFor software targeting the web, it's fine to couple everything to the cacheability metadata subsystem. Because only then you can do proper HTTP caching.
(Only the things that DO NOT depend on any request context don't need cacheability metadata. If you don't like cacheability metadata in config overrides, then the only solution is to disallow config overrides to depend on request context.)
Comment #7
plachI bet it takes way less to summon Satan himself...
Comment #8
pfrenssenStarted working on this. I like the pirate day idea from @catch :)
Comment #9
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedSuggested re-org:
-----
Looks like a GREAT start!
Comment #10
jibranComment #12
plachThis is the final form we agreed upon in #2512718: EntityManager::getTranslationFromContext() should add the content language cache context to the entity:
Btw, I wrote a draft change record mentioning this:
https://www.drupal.org/node/2525764
Comment #13
pfrenssenWIP, implemented the actual adding of cache contexts to the config object. This will still fail since we are missing a PirateCacheContext.
I'm wondering since
ConfigFactoryOverrideInterface
now implementsCacheableDependencyInterface
we are not only adding the requiredgetCacheContexts()
method to all config overrides, but alsogetCacheTags()
andgetCacheMaxAge()
which are both unused in the scope of this ticket. What shall we do with these?Comment #14
pfrenssenCross-posted with @plach, will address that and implement the cache context.
Comment #15
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedSo is there really no use-case for config entities needing cache tags.
e.g. domain module overrides config.site_name per domain.
So it adds the cache context, but it also would like that when config.domains changes that config.site_name is invalidated and likely other overrides.
Yes, it could do so itself, but is that such an esoteric need?
Similar when I want to expire data that is using config.site_name after 10 min, because I change site_name every 10 min dynamically.
Yes, I could use a cache context, but I don't want 1000 variations, I want to invalidate as the old site name, never comes back ...
I am still to go full interface and full override and have RuntimeCacheableDependencyInterface. That gives the most flexibility.
And yes if the config entity is saved I want config.domains to be invalidated as well, that is okay and a trade off I make.
Edit:
Thought about it some more and we _need_ to go full interface.
Use case:
A dynamic config override, yes it can probably be expressed as a cache context, but:
- It fills up the DB with 1000s of Cache::Permanent variations
- If it is so dynamic that it can't be cached, it cannot say: max-age=0, which is a MUST in some situations
And an always changing cache contexts => infinite variations
There is a reason we have this CacheableDependencyInterface and the same we need the RuntimeCacheableDependencyInterface.
Comment #16
pfrenssenThanks for providing some good examples of overrides setting cache tags. It's not critical though. Shall we make a separate issue for this and refer to it in the @todo messages?
Comment #18
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commented#16: Why would we introduce an Interface that is incomplete to only extend it later?
I think I would rather do it all together. Already now the config overrides have CacheableDependencyInterface, so why would we only go half-way?
Not being able to set max-age=0 could be critical under certain circumstances (e.g. sensitive data).
Or rather: Setting max-age=0 on the ConfigOverride (as you can now) and it not being taken into account would be critical.
But introducing an Interface that only has getCacheContexts() is fragile IMHO and misses the point of that this should provide cacheability metadata.
Comment #19
pfrenssenSo then we can best extend the interface with two additional methods
addRuntimeCacheTags()
andsetRuntimeCacheMaxAge()
, and change the interface name toRuntimeCacheableDependencyInterface
.Now that we make it complete, would there be any need to remove existing cache contexts / tags, or change existing ones?
Comment #20
Wim Leers#15 runs counter to what catch said in #2512718-45: EntityManager::getTranslationFromContext() should add the content language cache context to the entity. Let's get his feedback.
EDIT: Note that an especially gnarly consequence of that is that it then becomes possible for entities, upon saving, to invalidate the run-time cache tags as well. (Config objects do not have this consequence, because config overrides are only applied to immutable config, not to editable config, and calling
ImmutableConfig::save()
results in an exception, hence no run-time cache tags are invalidated).Comment #21
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commented#19: I was not able to find a use-case for that, if there really is we would need to add that later, but it would make things more complex.
Comment #22
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commented#20: Lets go full interface for config and just cache contexts for entities for now - that should resolve the concern.
As a follow-up I suggest that route derived entities are made / wrapped immutable, too.
Comment #23
pfrenssenOK, I already added the cache tags support but I'll remove it again. This will have the strange consequence for now that the config override objects will need to implement an unused
getCacheTags()
method.Posting work in progress.
Comment #24
Wim LeersFabianx replied to #20 at #2512718-72: EntityManager::getTranslationFromContext() should add the content language cache context to the entity. I'd still like catch's POV too.
#23: you can keep it in for now if that's easier (less work to remove it later than add it back later?).
Comment #25
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedThis needs to be fixed, too and cache tags too obivously.
--
#23: Not sure what you want to remove, but for now lets go full interface with cache tags and cache contexts and max-age here.
Comment #26
pfrenssenImplemented the remaining changes.
Comment #27
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedI think this should return 'pirate-day-tag' and 24 hours ...
--
Looks great overall.
Comment #29
pfrenssenGreat points @FabianX, as great as it would be, Pirate Day does not last forever. Also fixed the test failure.
Comment #30
pfrenssenThis needs work:
Comment #31
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commented#30.1: Why are our tests passing then?
What is a config entity and how as a user would I use them / get them?
Comment #32
pfrenssenGoing to work on this again.
@FabianX, will make sure we have tests for the missing functionality. Will start with that. Config entities are one of the two entity types we have, the counterweight of content entities. I'm sure you're familiar with them :)
Comment #33
pfrenssenBackported the relevant changes to the interface, as well as the trait that was added from #2512718: EntityManager::getTranslationFromContext() should add the content language cache context to the entity. I've also uploaded a patch that contains the code from both issues, since this is now depending on the other issue.
The points from #30 and #31 still need to be addressed.
Comment #34
pfrenssenThis is a version of the patch that is a diff between this issue and the parent issue #2512718: EntityManager::getTranslationFromContext() should add the content language cache context to the entity. In case the parent issue gets committed this is the one to continue work on.
Comment #35
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedROTFL :-D
---
Looks all great, I have no idea what is missing, but as berdir knows and we still need a failing test for the other config entities, this should be able to make progress.
Comment #36
Berdirthis should be updated to call $this->getCacheTagsForInvalidation() instead, so that the custom override of that method are considered.
What's missing are the add* calls in ConfigEntityStorage to pass the config cacheability metadata to the config entity and some tests for that.
Comment #37
plachlol, @pfressen++
Comment #38
Wim Leerss/Runtime c/C/
Can be simplified to
Missing docblock.
s/cacheable/cacheability/
LOL
LOLLLLLLLLLLLL
Isn't it the other way around?It's fine, but it's missing a dash between "context" and "providing". Right now, it reads as if it's a cache context providing a config override, which makes no sense :P
s/array()/[]/
Comment #39
pfrenssenQuoting @berdir from #36:
getCacheTagsForInvalidation()
is not part ofConfigBase
but ofConfigEntityBase
. @berdir confirmed in IRC that this does not need to be changed.Comment #40
pfrenssenThis is testing more than only cache contexts now. The class name and documentation should be updated.
Comment #41
Gábor HojtsyComment #42
Gábor HojtsyRe the issue summary:
It is also true that config overrides may be used in whatever language needed. See LanguageManagerInterface::setConfigOverrideLanguage(). This is not used very widely in Drupal core. User module uses it to send the right language emails to people (which depending on how its rendered may or may not depend on cached things I guess). And DateFormatter::dateFormat() uses it to let people format dates in whatever language needed. So assuming that across the same request overrides from only one language are used is not entirely true.
Comment #43
pfrenssenThis addresses the missing test coverage for config entities. 3 versions of the patch:
Next up is to address the remaining remarks from #38 and #40.
@Gabor do you think we are missing test coverage for the existing use cases?
Comment #44
pfrenssenAnd the interdiff.
Comment #45
pfrenssenFixed remaining remarks. This is ready for review.
The big patch includes the code from the parent issue #2512718: EntityManager::getTranslationFromContext() should add the content language cache context to the entity so it can be tested on the bot. The smaller patch only includes the code from this issue and is the best to review.
Would we need negative tests to see if the overrides are not present when it is not Pirate Day? And do we need to test any bubbling up of the cache contexts?
Comment #48
catchI know this was discussed a lot, but I'm not sure if we need the IfLower. When you set max age, it is always the maximum age, and other places we merge max ages we always take min($max_ages) - so it seems implied pretty well already. When I first saw the method name, I thought 'how do you know if it's lower when you set it?'.
This could probably be dynamically set to 'seconds until pirate day', unless it's pirate day, in which case it could be 'seconds until midnight'. But completely unnecessary to do that of course.
Comment #49
Wim Leers#48
setCacheMaxAge()
means "set the given value, i.e. overwrite whatever the current value is" (that's also whatCacheableMetadata::setCacheMaxAge()
does), whereassetCacheMaxAgeIfLower()
does not have that problem. An alternative would beaddCacheMaxAge()
.Comment #50
Wim LeersAnother complete review. Mostly nits.
We can remove
CacheableDependencyInterface
here becauseMutableCacheableDependencyInterface
extends that.So this test is first testing using the theme configuration (a "plain" config object) and then testing the placed block config entity (a config entity).
I think it should be clarified that it's testing those two distinct things; at first sight it seems you're testing the same thing twice.
Perhaps split this up in
testPlainConfigOverride()
andtestConfigEntityOverride()
? (Better names TBD.)Nit: s/properties are/metadata is/
Nit: s/ensures/verifies/
LOLLLLL :D
Nit: s/properties are/is/
Nit: "override" vs "overrider". The interface is "override", so let's use that everywhere.
Comment #52
pfrenssenAddressed the remarks and the failures. There are two patches: the large one includes the parent issue #2512718: EntityManager::getTranslationFromContext() should add the content language cache context to the entity and is intended for the testbot. The 'do-not-test' patch is the one to review, it only contains the code of this issue, but is non-functional on its own since it depends on the parent issue.
@berdir suggested IRL to also provide an integration test that checks the correct bubbling of the cache context. Will work on that now.
Comment #53
Wim LeersDiscussed the proposed solution in this issue with @catch, @alexpott, @effulgentsia, @Gábor Hojtsy and @Berdir. Both @catch and @alexpott approve of this solution, so we can work on finishing this patch, notably the additional test coverage that we want.
This is split off from #2512718: EntityManager::getTranslationFromContext() should add the content language cache context to the entity, to make the scope of both more manageable/easier to review. The IS of that issue has been rewritten completely to capture the agreed upon solution.
I updated the IS here to point to the other IS.
Comment #54
pfrenssenComment #55
Wim LeersI can't find any more complaints. IMO this is RTBC. But the other patch needs to go in first. I updated the IS. The only thing is missing, is a CR.
These have all been renamed in the other issue.
Yay, better/clearer!
Spam in core! <3
Comment #56
pfrenssenThanks Wim! Backported upstream changes.
In addition to the CR this still needs an integration test on @berdir's request.
Comment #57
Wim LeersAdding an issue tag for that integration test then :)
Comment #58
Wim Leers#2512718: EntityManager::getTranslationFromContext() should add the content language cache context to the entity has just landed! :)
To do:
ConfigOverrideInterface
now extendsCacheableDependencyInterface
etc. etc.Comment #59
pfrenssenThe integration test took some time, 3 individual bugs were discovered during while writing it. I wouldn't have been able to bring this to conclusion without the great help of @berdir, he should get commit credit.
BlockViewBuilder::viewMultiple()
the cache contexts from the entity were not included in the#cache
property of the render array. This was critical to fix in this issue, because it prevented the cache contexts from being invoked.AccessResult::cacheUntilEntityChanges()
when the cacheability metadata is taken from the entity and applied to theAccessResult
only the cache tags are considered:It seems like we should also copy the cache contexts, since the entity might vary on a parameter that affects accessibility, for example the block visibility might be toggled in a config override. This seems to be out of scope for this issue though, but let's discuss this and create a followup if needed.
Comment #60
pfrenssenComment #62
BerdirThe way this API is defined right now makes it impossible to have different cacheablity metadata for different config objects.
Is that really what we want?
If not, then we should either pass the config name to those methods (which mean they can't implement CacheableDependencyInterface anymore, but that's fine IMHO. nobody will call them in a generic way).
Or we use the pattern that we've used in other places and pass in a CacheableMetadata object to loadOverrides().
I'm not sure if this approach is correct. I think we should call the methods the return value of mapFromStorageRecords instead of relaying on implementation details here (how the property is named + that properties are just assigned like that).
This is the crucial fix that we discovered in the web/integration test.
Comment #63
larowlanConfig overrides aren't my thing, so this is a general review, feel free to ignore if not relevant.
Could there ever be an instance where array_search doesn't find this item? Should we allow for that?
Any reason not to use a boolean here?
nice!
needs more yarr
<3
Comment #64
pfrenssenFrom #62:
To me it looks wrong. The current implementation is applying the cacheability metadata of all config overrides to all config objects. That throws out the granularity we depend on. This is not currently tested.
@Wim Leers, what is your opinion of this?
Very good suggestion.
mapFromStorageRecords()
returns entity objects so we can set the values using the proper API instead.It is, thanks a lot for the help!
From #63:
There shouldn't be since
ConfigBase::getCacheTags()
always returns the cache tag in this format. Technically nothing is stopping a contrib module from implementing its own variation of this method and returning another cache tag format. I don't think this would be recommended but you never know the exotic use cases contrib can dream up.Ideally we should have a method like
getOwnCacheTag()
but we could also simply throw an exception, forcing contrib do deal with it themselves if they decide to steer away from the recommended course.I originally had a boolean but changed it to a string at some point, will revert to a boolean again, it makes more sense.
PirateDayCacheContext::getContext()
.Comment #65
Wim Leers#59: thank you very much for your thoroughness!
Also doing a code review, will be in my next comment.
#62
ConfigFactoryOverrideInterface
was designed with this in mind though. It seems like it's designed to override all config objects in the same, consistent way. The single, parameterlessgetCacheSuffix()
method seems to further indicate that?But I think it'd indeed work, because
ConfigFactory
only does static caching.EntityViewBuilder
, that also applies the cache contexts of the entity. Now that a block (i.e. a placed block config entity) can also have cache contexts and max-age and not only cache tags, we need this. Which gets me to my point: this should also merge the max-age of the block entity!#63: :)
Comment #66
pfrenssenImplemented first suggestion of #62-1.
Comment #67
pfrenssenKeelhauled the remaining remarks. I had to rush this because I have little time left before I embark on my return voyage, so please excuse any typos or other problems.
Comment #69
Wim LeersThis is a review of the #66 + #67 interdiffs. Still working on what I promised in #65.
#66:
Let's then just add
getCacheableMetadata($name)
instead. We're doing it for analogous reasons as we did that in #2512866: CacheContextsManager::optimizeTokens() optimizes ['user', 'user.permissions'] to ['user'] without adding cache tags to invalidate that when the user's roles are modified (i.e. parameter-dependent cacheability metadata), so we should end up with an analogous solution.#67:
Why do that here; why not add
ConfigEntityStorage::mapFromStorageRecords()
, have that call the parent (i.e. the default implementation) and then just add the cache tags to it?Then
ConfigEntityStorage::doLoadMultiple()
doesn't need to change at all.Comment #70
Berdir1. No 100% sure. The difference is that we then don't merge it, we assign it again using addCacheContexts(). So we'd create an object just to get the value out again. We definitely need to profile this becuase this is all in the critical path, we're loading lots and lots of config on pages.
2. And add what cache tags exactly? ;) We need the config objects. We tried to pass it to the map method, but the problem is that it would be another override-parent-method-with-optional-argument hack.
Comment #71
catchWondering a bit if we should add cacheByEntityCacheability() (better names welcome) and use that instead of this?
For BC cacheUntilEntityChanges() could be deprecated and call through to that method.
Nit: is it worth rewording to 'They're converted before storing or retrieving an object in cache' or similar?
Is it OK to remove this?
I can see code applying cache metadata from config objects, then expecting the config's own cache tag to be included there - so when they invalidate it the rendered stuff gets invalidated.
Sounds like a bug yes.
Per #48/49 this could use a comment to explain why 86400 wouldn't work for a real implementation, same as the one for $GLOBALS['is_pirate_day']
This is a good change.
Comment #72
pfrenssenAddressing test failures, will now look at the remarks.
Comment #73
pfrenssen#69 has been fully answered by #70, leaving that as is, unless @Wim Leers disagrees.
#71:
cacheUntilEntityChanges()
, I like the idea of deprecating it in favor of a better named one, what aboutaddCacheabilityMetadata()
? Or doesn't that convey the action well enough? Would it be OK to defer this to a followup issue?ConfigBase::getCacheTags()
).Comment #75
Wim LeersQuoting myself in #65:
#59.2 + #65.2 + #71.4: Had to wrap up today before I could get to this. Will do tomorrow.
#59.3 + #65.3: addressed at the bottom, in my reply to #71.1
#70.1: Good point. But I think API similarity is important here, if it doesn't cost us perf-wise.
#70.2: Ahhh! I didn't realize that. That makes a lot of sense, thanks! :)
#71.1 + #73.1: So the rationale for
cacheUntilEntityChanges()
dates back to the days whereEntityInterface
only had cache tags (it predates the existence ofCacheableDependencyInterface
). It's type-hinted toEntityInterface
:Similarly, we have:
What about deprecating and redirecting both to a new
addCacheableDependency()
method, like we have on several interfaces now?Comment #76
pfrenssenTest failures were due to #2512866: CacheContextsManager::optimizeTokens() optimizes ['user', 'user.permissions'] to ['user'] without adding cache tags to invalidate that when the user's roles are modified getting in, fixed those, still have to look at the other remarks.
Comment #77
pfrenssenComment #78
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commented#75: +1 to use addCacheableDependency() instead.
Comment #81
catchMakes sense to me.
Comment #82
pfrenssenAddressed all remarks from comment #71. Still need to do #75+#78+#81.
Comment #83
pfrenssenStarted implementing
addCacheableDependency()
. I thought it would be nice to use theRefinableCacheableDependencyTrait
for this, and pass in aCacheableMetaData
object.I ran out of time before I was able to test it. This will probably fail horribly.
Comment #84
Gábor HojtsyAlthough our stable testbot did not yet return with a result (after 4h), the CI testbot has numerous
Undefined property: Drupal\Core\Access\AccessResultAllowed::$getCacheMaxAge
.Here is a quick fix so we get better test results.
Comment #87
pfrenssenStarted addressing the failures in the unit tests. Covered
ContentTranslationManageAccessCheckTest
andAccessResultTest
.Comment #88
pfrenssenComment #89
BerdiraddCacheableDependency() should work with CacheableDependencyInterface, not CacheableMetadata. See the last few comments in #2525910: Ensure token replacements have cacheability + attachments metadata and that it is bubbled in any case, which is doing the same now.
Not sure it is a good idea to already add that method to the interface here and implement it. In that other issue and also in #2512718: EntityManager::getTranslationFromContext() should add the content language cache context to the entity, we explicitly moved that part to a follow-up to only have to do the minimal changes as part of fixing the criticals. If we implement that method separately but in a consistent way then it's easy to move the method to the common interface/trait and use it for AccessResult and CacheableMetadata.
Comment #91
pfrenssen@Wim Leers already created an issue for letting AccessResult implement RefinableCacheableDependencyInterface and use the trait. That means we can limit the scope of this and defer all those changes to that issue: #2526326: Update CacheableMetadata & AccessResult to use RefinableCacheableDependency(Interface|Trait).
Comment #92
pfrenssenComment #93
Gábor HojtsyTrying to help @pfrennsen, because he indicated he would not have time for this today anymore. Restarting from #82 then as per #89 and adding a standalone
addCacheableDependency(CacheableDependencyInterface $other_object)
to AccessResult with the same implementation that #2525910: Ensure token replacements have cacheability + attachments metadata and that it is bubbled in any case uses (I guess it can also be argued that access results may be numerous on a page, so the same merge optimizations may be in order, but we can obviously simplify it). That allows us to have a common trait / interface for them in #2526326: Update CacheableMetadata & AccessResult to use RefinableCacheableDependency(Interface|Trait). Not sure what kind of testing do we want on this method, its just bolted on at the moment.Comment #94
BerdirWe still want this part, except that it's now a one line call to pass $entity to addCacheableDependency().
Same here.
Comment #95
Gábor HojtsyUpdated with those, which ensures the new method is at least indirectly tested. Also edited the deprecation note with more info on the scope of depreciation (in D9).
Comment #97
Gábor HojtsyIt helps to actually pass on the variable we got passed :D
Comment #99
BerdirCopying the current approach from the other issue, more or less. without the optimizations for cache tags/contexts since AccessResult dos not use Cache::mergeContexts() right now and adding that makes those tests fail.
Also fixed and updated the test a bit. Didn't make much sense anymore what we tested there, so I just dropped some of the old tests for the now deprecated methods and instead add some new tests for addCacheableDependency().
Comment #100
pfrenssenLatest changes from @Gábor Hojtsy and @Berdir look great, just fixed a few nits. I think this is ready for final review.
Comment #101
pfrenssenComment #102
Gábor HojtsyAdded two change record drafts: https://www.drupal.org/node/2532870 and https://www.drupal.org/node/2532882 please review.
Comment #103
pfrenssenProfiling results, tested 200 requests with a concurrency of 1 as authenticated user.
node/1
HEAD:
Patch:
node/1/edit
HEAD:
Patch:
admin/config
HEAD:
Patch:
Comment #104
Wim LeersI mostly have nitpicks and relatively-nitpicky things. But those relatively-nitpicky things I feel are actually quite important because they affect future code quite a bit, since after this critical issue/patch, it won't be changed again.
Same concerns here as at #2525910-108: Ensure token replacements have cacheability + attachments metadata and that it is bubbled in any case.
The
this::
in there looks super bizarre. Let's s/this::/::/, no?The comments are inconsistent.
Actually, this is a bad method name too.
We're not propagating cacheable dependencies; overridden config is a cacheable dependency, and a config overrider is NOT a cacheable dependency (it can't be a cacheable dependency because it's not a value object; it's logic). What happens here is we're propagating the config-specific cacheability metadata from the config overrider onto the overridden config.
(A simpler example: a render array that only shows a child if a certain access result indicates it's allowed; then that access result is a cacheable dependency of the render array.)
So, I think in this case it is actually
propagateConfigOverriderCacheabilityOntoConfig()
or something like that.I'd like to repeat #69.1. Also see Berdir's reply in #70.1.
But it still feels like it'd easier to understand.
Given the recent changes,
it'd mean the cited code could then actually be changed to:
Also: it'd mean slightly fewer function calls.
But I think the similarity of interfaces for similarity of reasons is the strongest argument for #69.1.
These comments feel like overkill/unnecessary repetition.
Nit: Can be chained. Not sure if that's actually better here though.
Normally, things that depend on
State
need to specifymax-age = 0
(because it can change at any time).If this should not specify
max-age = 0
, we should document why not.Nit: needs
\n
in between.Comment #105
Anonymous (not verified) CreditAttribution: Anonymous commentedahoy me hearties, here be a drive-by review :-)
trying to figure out if i have any clue how caching and config work these days, so these comments may not be worth much.
this seemed suspicious to me - why wouldn't merging contexts be fast when the the merge has an empty array? so i looked at Cache::mergeContexts() and i see this:
does it matter that we don't call that method in the 'fast path'? i have no idea, but validateTokens seems important?
this doesn't make sense to me. we remove a tag because we know the entity system will just add it back? how does that help performance? do we not de-dupe before doing expensive stuff with cache tags?
Comment #106
pfrenssen#104:
this::
is weird indeed, more common is to useself::
or indeed just the shorthand::
.propagateConfigOverriderCacheability()
.RefinableCacheableDependencyInterface::addCacheableMetadata()
andConfigOverriderInterface::getCacheableMetadata()
. Wim suggested to add theaddCacheableMetadata()
method in addition to the existing ones (addCacheContexts()
etc). In the case ofgetCacheableMetadata()
I removed the original methods since they are now redundant.In converting the patch it does seem like using a
CacheableMetadata
object actually results in fewer function calls, and it is less code in total, the patch is smaller now.I had to rush a bit, didn't have time to run all relevant tests, and still have to address remarks from #105. Will do that tomorrow!
Comment #107
Wim LeersThanks! Looking great :)
This should be named
addCacheableDependency()
, mirrored after\Drupal\Core\Cache\CacheableResponseInterface::addCacheableDependency()
.Also, its parameter should not be typehinted to
CacheableMetadata
, but to nothing, just like that other example. In case the passed in parameter implementsCacheableDependencyInterface
, we have cacheability metadata, otherwise, we assumemax-age = 0
. Just like that other example.The method is renamed, but the docblock isn't updated accordingly.
This is now missing docs for the
$name
parameter. Let's bring that back :)This can now be simplified to:
Except we still have this, which is why the previously commented code looks like it does.
Can't we simplify this too? Can't we move this logic to
ConfigEntity::addCacheTags()
, so that that simply filters out cache tags that are returned byConfigEntity::getCacheTagsToInvalidate()
?Or… even simpler, and more generically, can't we change
RefinableCacheableDependencyTrait::addCacheTags()
fromto:
(Just playing devil's advocate here. I'm not sure whether that's actually clearer. The current code confused @beejeebus in #105, so trying to think of alternatives.)
Comment #108
BerdirNot quit sure if the reason why we exclude that cache tag is clear from the comment.
We do this because things like renaming or creating duplicates of an entity would otherwise keep that cache tag which would then still reference the old object. Not a huge deal, but IIRC it came up in manual testing or caused some test fails. So it's not just a performance optimization or so.
That said, moving that logic into the entity sounds nice, I like that. Not quite sure what that means for DateFormat, though. Because the cache tag to invalidate for that is 'rendered', so code like the above would *not* filter them out, which would in turn result in them being added to all kinds of things...
Comment #110
pfrenssenRegarding #5: This indeed warrants some more detailed explanation.
This is an edge case that is only present in ConfigEntities. Most cacheable objects are standalone, but config entities are 100% dependant on their Config, and thus always inherit the cache tags from the Config object. The cache tag of a ConfigEntity object is identical to the cache tag of the Config object it is derived from.
A ConfigEntity must inherit all the cache tags from its parent Config object, but when you call
Config::getCacheTags()
this also includes the tag that refers to the Config object. This is how the self-referring tag ends up in a ConfigEntity. This doesn't happen for any other cacheable objects since they are not so closely coupled to another cacheable object.It is useless for a cacheable object to reference itself in its cache tags, none of the cacheable objects do this. ConfigEntity shouldn't be an exception.
Now this whole debate is probably rather academic, the presence of the self-referring tag is not causing any harm, but this will have some impact on performance since this self-referring cache tag might get merged hundreds of times when bubbling up.
I like the suggestion to move this to
ConfigEntity::getCacheTags()
though.edit: @berdir is correct in #108, we did encounter some test failures when this was originally added, I forgot about that. We should investigate a bit.
Comment #111
Gábor HojtsyDo the terminology discussions in #2525910: Ensure token replacements have cacheability + attachments metadata and that it is bubbled in any case affect how we should introduce new APIs in this patch? The names of public functions, etc? For consistency.
Comment #112
pfrenssen@Gabor Hojtsy, yes we should ensure to be consistent. I will follow up on that issue and backport any changes to the interfaces.
Comment #113
Wim LeersCurrently, the two issues are still consistent. But, yes, we should keep them in sync. Ideally, the other issue will land soon, that'd make things easier :)
Comment #114
dawehnerJust some drive by review ...
Did we discussed about making it plural by default to save a little bit of function calls?
Beside from that, why do overrides not implement
\Drupal\Core\Cache\CacheableDependencyInterface
?I think some explanation about why would be nice
In case we still have to adapt things here, let's just use ->willReturn(...) it is so much better to read
Comment #115
Berdir1. plural would make it more complex since we pass along the name of the object. Not quite sure how that would work. I'd say we should keep it unless we have numbers from profiling that show we have a problem there? And about the interface, same reason, the additional argument. We want to be able to allow an override that only every acts on block config entities to not apply the cache context or whatever to every config in the system.
Comment #116
Gábor HojtsyDid not apply anymore in BlockViewBuilder, reroll first.
Comment #118
Gábor HojtsyWorking on addressing at least some of the feedback.
Comment #119
Gábor HojtsyResolving #107 (except /5) and #114 (except /1), those two are still in discussion.
1. RefinableCacheableDependencyInterface::addCacheableMetadata(CacheableMetadata $metadata) renamed to RefinableCacheableDependencyInterface::addCacheableDependency($other_object)
2. Adapted implementation in RefinableCacheableDependencyTrait.php, eg. it did not return $this before even though the interface mandates it
3. Discussed propagateConfigOverriderCacheability() with Wim and the name is confusing, because we never call them overrideRs, we call them overrides. The interface, etc. Only tests have overriders (which provide overrides BTW). So renamed to propagateConfigOverriderCacheability()
4. Removed inline comments around it because they were not added value. Updated phpdoc.
5. Restored the $name phpdoc in ConfigFactoryOverrideInterface
6. Simplified the adding of dependency info in ConfigEntityStorage
7. Simplified the propagation of cache info in ViewUI (and removed a bunch of unused uses which are now obsolete)
8. Using willReturn now in ConfigEntityStorageTest (that was quite a bit of changes, not entirely sure it should be in scope here, but well)
Comment #120
Wim Leers#114.1: Berdir already explained this in #115. But in case it wasn't clear yet: because
CacheableDependencyInterface
only works for value objects (i.e. the same tags/contexts/max-age are returned always); but config overrides are not a value object: they apply different cacheability metadata to different config (i.e. different tags/contexts/max-age are returned).Interdiff looks great! Since you applied my suggestion in #107.4, but did not address #107.5, I'm curious to see if that'll cause any failures.
I don't think the changes in this file, all similar to the one I quoted here, are actually necessary? (EDIT: you say the same in your point 8.)
They're harmless though. So I'm fine with this personally. EDIT: @dawehner asked for this in #114.3, Gábor just updated the existing
->will()
calls as well, for local consistency. Makes sense. Just a bit distracting is all.Comment #121
Gábor HojtsyCannot work on this anymore today, cannot assign back to pfrenssen, so just unasssigning.
Comment #122
dawehnerWell yeah I did not asked about changing existing ones, just new ones would be nice to do it right from the start.
This difference needs at least some explanation, but honestly I don't get why this is not part of Cache::mergeMaxAges
We use FQCNs here
Comment #123
pfrenssenLet's have a look how this can move forward.
Comment #124
Gábor Hojtsy@dawehner: re
#122/0: the file is a big mess; I can undo the work of fixing the existing ones, regardless of what we add here, the file has all kinds of array and willReturn() or not approaches all over the place
#122/1: I guess it is sort of part of that, it is just doing it differently. CacheableMetadata::merge() does these same shortcuts, but it claims its called many times a request. We don't need to do the same shortcuts here.
#122/2: Easy to fix.
Addressed all of these.
Comment #126
pfrenssenIt looks like all remarks have been addressed, except #107-5. Answers have been provided in #108 and #110, but we still need to give some more context and investigate whether the logic can be moved to a better place, or if this turns out not to be possible we should at least provide better documentation.
Short recap of this remark:
ConfigEntityStorage::doLoadMultiple()
. The documentation only mentions this is done for performance reasons. Maybe a better place for this isConfigEntity::addCacheTags()
orRefinableCacheableDependencyTrait::addCacheTags()
.Comment #127
pfrenssenAddressed the first test failure, and started to look into the second one but my time is up for today.
Comment #128
pfrenssenComment #130
Gábor HojtsyFixing more places in ConfigEntityStorageTest for number of calls of getCacheTags, similar to 127.
Fixing a missing self-referential cache tag missing in tests in Views' plugin CacheTest.
Theoretically this should be green now. :)
Comment #131
Wim LeersSo with the patch green again, the only thing not yet addressed is my remark in #107.4-5, which explored a solution to what @beejeebus raised as being confusing in #105. But… we can clean it up further in a non-critical follow-up.
So I think that if the reasons Berdir gave in #108 for the current implementation being the way it is, this can be committed.
Comment #132
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedGiving this a review.
Comment #133
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedGiven the comment below is a minor problem and this is a critical issue, setting to RTBC status for now.
The patch looks fantastic!
Yeah, the removal and adding currently is a no-op.
As addCacheTags per design can only add tags and not set them.
Probably the "easiest" way to solve this would be:
$cacheable_metadata = CacheableMetadata::createFromObject($configs[$id]);
// getCacheTags()
// filter()
// setCacheTags()
$entity->addCacheableDependency($cacheable_metadata);
Could be fixed in a quick follow-up.
Comment #134
alexpottI can't see any explicit test coverage of this logic - unlike the coverage for the similar change in AccessResult
Not used.
Not used.
Comment #135
Wim Leers#134.
AccessResult
portion of #2526326 in this issue?Comment #136
alexpottI'm happy with the reasons that @Wim Leers gives in #135. Once AccessResult uses the trait we have coverage. Points 3 and 4 can be fixed on commit.
Comment #137
alexpottI still don't feel we've addresses all the feedback surrounding this comment. There has been talk of moving it into getCacheTags and of the need to improve the documentation because it is not only about performance.
Comment #139
Wim Leers#137: but:
See bottom of #105 + #108. That's why I wrote #135.
inSo… I'm not quite sure what you mean, @alexpott? Back to RTBC to get feedback.
Comment #140
alexpottIn #126 @pfrenssen:
Comment #141
alexpottDiscussed with @xjm, @catch, @effulgentsia and @webchick and we agreed that this issue is critical because of the cache poisoning possibilities once contrib adds configuration overrriders.
Comment #142
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedOkay, agree then, lets fix #133.
@Wim: This is not about elegance, but currently the cache tag is not removed.
And yes as this is a bug that is "green" we need to add explicit test coverage for that.
Comment #143
pfrenssen@FabianX has a point, there is something wrong here. It looks like the cache tags are set in
$entity->addCacheableDependency($configs[$id])
, so the following code will be ineffective.I will have some time to dig into this this afternoon.
Comment #144
pfrenssenFor the moment let's try just remove it, if it is really ineffective then the tests should be green. We could also solve this part in a follow-up, since this doesn't affect the critical part of the issue.
Comment #145
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commented#144: After alexpott's comment that this might affect clones, I am pretty sure we need to solve this properly to avoid the config entity cache tag being present.
My pseudo-code was:
which should work.
Comment #146
pfrenssen@FabianX thanks! Will address it when I have some time this afternoon.
Actually since this code was ineffective the remark from @berdir in #108 that the self-referring tag broke existing test coverage is now also answered. Since #119
Drupal\views\Tests\Plugin\CacheTest
started failing because the self-refererring tag was present:This failure was fixed in #130.
Comment #148
Wim LeersAddresses all of the feedback regarding Config entities inheriting cache tags from their underlying Config objects. Adds full documentation, implements the suggested solution from Fabianx (that I arrived independently at), and fixed the brokenness in
CacheTest
that it caused since #119 as @pfrenssen pointed out in #146 (which I failed to notice in my reviews).Note that the duplication test is kinda ugly, because config entities can only be saved if they have an ID, but creating a duplicate removes the original ID (which is obviously necessary), but then leaves you with no way to specify a new ID. However, for the purposes of this test, that's actually not a problem.
Worse, the renaming test is impossible to write because renaming config entities is broken AFAICT — opened #2539116: Renaming Config entities does not actually rename them for that. Wrote the test coverage, but for now it will fail, so I commented it out with a
@todo
.Interdiff is relative to #130, not #144.
Comment #149
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedBack to RTBC
#134 3. and 4. bits can be fixed without impeding RTBC status.
Comment #150
Wim LeersThanks @Berdir for pointing out I was renaming config entities all wrong. (Turns out it's both more intuitive than expected *and* less, depending on how you look at it.) Closed #2539116: Renaming Config entities does not actually rename them.
Fixed the nits in #134.3 + 4.
@Berdir++
Comment #151
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedAaaand back to RTBC, thanks for fixing the nits, too.
Comment #153
dawehnershould we typehint for |object instead? mixed includes more than that
It is always better to either have a 3 or 5 dot list :P
Comment #154
pfrenssenQuickly sneaked in @dawehner's suggestion about using `object` instead of `mixed`.
Comment #155
dawehner@pfrenssen
This has been there in more than one place.
Comment #156
pfrenssen@dawehner oops! I have to sneak another nitpick in the RTBC issue while @alexpott is not looking...
Comment #158
pfrenssenFixing test failures.
Comment #159
Wim Leers#158 are trivial fixes.
Comment #162
alexpottCommitted 61603f5 and pushed to 8.0.x. Thanks!
Comment #165
plachThere are a couple of draft CR mentioning this. Should they be published?
Comment #166
Wim LeersYes. Which ones aren't published?
Comment #167
plach@dawehner published them meanwhile :)
https://www.drupal.org/node/2532870
https://www.drupal.org/node/2532882
Comment #168
Gábor HojtsySuperb, glad to see this one landing :) Thanks all especially @pfrenssen for sticking to it :)