Problem/Motivation
I was investigating the failures in #2429617-160: Make D8 2x as fast: Dynamic Page Cache: context-dependent page caching (for *all* users!), the toolbar failures there were unexpected.
That's how I discovered that AFAICT we forgot about something in this issue all this time:
user.permissionscontinues to work when a user receives *additional* roles. And because we "fixed" the issue here by adding role cache tags when optimizing awayuser.permissions, that case is *still* broken: it only captures the changes of permissions assigned to roles, it does not capture the changes of roles assigned to the user!
But fortunately we already have the necessary infrastructure in place thanks to this issue, because we solved it generically here. At least one solution is to use the same approach that we used foruser.node_grants: consideruser.permissionsimpossible to optimize away.
Proposed resolution
Those child cache contexts which may change if the User entity is changed, should specify the User entity's cache tag in their cacheability metadata.
Remaining tasks
None.
User interface changes
None.
API changes
None.
Data model changes
None.
Beta phase evaluation
| Issue category | Bug because incorrect cache invalidation. |
|---|---|
| Issue priority | Major because in some cases in HEAD, there is incorrect cache invalidation. |
| Prioritized changes | The main goal of this issue is cacheability. |
| Disruption | Zero disruption. |
| Comment | File | Size | Author |
|---|---|---|---|
| #32 | interdiff.txt | 1.59 KB | lauriii |
| #31 | add_the_user_entity-2533768-31.patch | 4.38 KB | lauriii |
| #28 | add_the_user_entity-2533768-28.patch | 4.39 KB | lauriii |
| #28 | add_the_user_entity-2533768-28-test-only.patch | 2.17 KB | lauriii |
| #23 | 2533768-23.patch | 2.49 KB | wim leers |
Comments
Comment #1
lauriiiAFAIK the cacheable metadata would not get loaded unless the context is optimized away
Comment #2
fabianx commentedYeah, we need an IS update with the bug described.
Comment #3
berdirYes, we either need to change it so that it's always added or we need to add it to the subcontexts.. (because that is unexpected if it changes) and if code depends on something from the user then it also needs to add relevent cache tags.
Comment #4
wim leersIS updated.
Basically, this is a new issue created from #2512866-132: CacheContextsManager::optimizeTokens() optimizes ['user', 'user.permissions'] to ['user'] without adding cache tags to invalidate that when the user's roles are modified, with two relevant discussion comments: the subsequent comments #133 and #134 there.
Comment #5
wim leersA recap!
So what @catch proposed in #133 of that issue () doesn't really make sense, for the reason @lauriii mentions in #1: we only add cacheability metadata of a cache context if it is optimized away.
A solution to that problem would be what @Berdir says in #3: always add the cacheability metadata of a cache context, even when it is not optimized away. But that'd be a significant behavior change, and it took us many days and many discussions to arrive at the solution we found in #2512866: CacheContextsManager::optimizeTokens() optimizes ['user', 'user.permissions'] to ['user'] without adding cache tags to invalidate that when the user's roles are modified. So I'm kinda reluctant to just do it. (Even though I was advocating for that in #2512866 originally.)
@Berdir also briefly mentions , i.e. have a parent cache context provide cacheability metadata for its children in case it is optimized away. I like the idea at first sight, but I don't think that actually makes sense. Not only would the implementation be messy, but if we look at all
user.*cache contexts (copy/pasted from https://www.drupal.org/developing/api/8/cache/contexts):Then we see that it doesn't apply universally: it does not apply to the
user.is_super_usercache context.Comment #6
wim leersSo after the recap in #5 of everything that was discussed, here's my proposal: just have those child cache contexts for which this does matter just specify the user cache tag.
That'd look simply like this.
Comment #7
berdirWhether this is good enough comes down to one question:
What is the expectation when adding the the user cache context and you do something that depends on e.g. a field of the user. Now that field changes. Are you responsible for adding the user cache tag in that case yourself or would you expect that "vary by user" takes care of that?
If the first (you have to explicitly say "is invalidated when the user changes) then it's fine. If not, then we still have a problem.
I'm not sure :)
Not sure this makes sense. Per the documentation, cache tags are ignored if max-age is zero, so there's no point in defining it.
And if we don't return max-age then that just means that there are no node grant implementations, which doesn't need to be invalidated. unless code or the module list changes (I still think we should delete all caches if a module is installed, but that's another topic).
Also, AFAIK the "security" tag should only be used for actual security issues.. the other one should be security enhancements or something like that. So, security shouldn't be used on non-critical issues...
Comment #8
berdirNote: That is different from user.roles or user.permissions. If you say it varies by the permissions of the user, then any action that can affect the permissions (or roles) of a user should be taken care of for you, definitely. And that's what we're doing with this patch.
Comment #9
fabianx commented#7: I agree, it makes sense for user.permissions and user.roles, huge +1, I think we should leave user.node_grants alone ...
I just changed my mind.
Even if there are no node grants active, when the user gets added some node grants then ... we should invalidate, but we do indeed not invalidate the user then ...
So probably node_grants should be another follow-up and keep this focused to the issue at hand.
Major as it soft blocks smart-cache.
Comment #10
wim leersYes, if you render something that not just varies by user, but contains some property of the user, then you must add
User::getCacheTags()to that rendered thing's cacheability. This has always been the case.Oh heh :P They'd indeed be ignored. But the cache tag also does no harm. I'm thinking: what if contrib comes in and just subclasses this class, and overrides the max-age to a non-zero value?
Good point. So what if I just move the
user:Xcache tag just a little bit, like this?Comment #11
wim leersComment #14
fabianx commentedRTBC - if tests pass :).
Comment #15
borisson_Tested the previously failing test manually as well, couldn't find anything wrong, so RTBC +1
Comment #16
wim leersUpdated IS, added beta eval.
Comment #17
lauriiiLooks good! I'm happy to see that the solution on the parent issue actually created API which solves this kind of issues. I think what we still need is better documentation about how this works to make people actually understand. Now there is way larger difference between applying user and user.roles cache contexts than it was before and people should understand that.
Just as a last note, shouldn't we have tests for this since this is a bug?
Comment #18
wim leersIndeed :)
That's already done: https://www.drupal.org/node/2459039/revisions/view/8630634/8652850
Comment #19
catchChange looks fine, but we should add inline docs to explain why this needs both the user ID cache tag and the role cache tag - just 'Permissions may change when either a role is updated to have different permissions, or a user is updated to have different roles' should do it?
If max age is 0 what does the cache tag do here?
Comment #20
berdir2) This has been discussed a bit, see second part of #7 and the response in #10. If you disagree with that then it can be removed, but that's the reason.
Comment #21
catch#7/#10 is OK just needs to be reflected in the inline comment that it's done for correctness, that the max-age might change etc.
Comment #22
wim leersI'll reroll this in 1-2 hours.
Comment #23
wim leersOk, got sidetracked in #2429617: Make D8 2x as fast: Dynamic Page Cache: context-dependent page caching (for *all* users!) for a while. So, with a small delay, here's an updated patch.
Comment #24
fabianx commentedRTBC + 1 again
Comment #25
effulgentsia commentedYes.
Comment #26
lauriiiI will write the tests so we can get this in from blocking the Smart Cache
Comment #27
wim leersWoot! Thanks @lauriii :)
Comment #28
lauriiiThis was a little simpler than I thought in the beginning. No interdiff since test-only patch works as interdiff.
Comment #30
wim leersLooking great :)
s/is defining cacheable metadata/still works when it is optimized away/
s/$user1/$root_user/
You're trying to get the role other than the
'authenticated'one, I think?I think this only works because usually, the randomly generated role name will not alphabetically be something like
abXXXX. But it could. So I think this introduces a random fail.s/cacheable metadata/cache tag/
Nit: needs
\nin between.Comment #31
lauriiiThanks for the review!
#30.3:
'authenticated'is a special case. User::getRoles() is always setting the'authenticated'user role as first manually.Comment #32
lauriiiInterdiff for the previous patch
Comment #33
fabianx commentedHello I am a RTBC fairy, I saw this nice little patch and just could not go further without RTBC'ing :p.
Seriously:
Great work, back to RTBC.
Comment #34
catchCommitted/pushed to 8.0.x, thanks!
Comment #36
wim leersWoohoo!
This was the final blocker to #2429617: Make D8 2x as fast: Dynamic Page Cache: context-dependent page caching (for *all* users!) :)
Comment #37
loopduplicate commentedI think there is a typo in the code comments. What is "user entity changes23"?
Comment #38
wim leersThat typo snuck in in the #31/#32 reroll. Can you file a follow-up issue to fix that?
Comment #39
loopduplicate commentedFollow up issue added here: https://www.drupal.org/node/2540764
Comment #42
dewalt commentedSorry that I comment so old issue, but I'm creating custom cache context similar to "UserRolesCacheContext", and I don't understand why "user:ID" cache tag is needed exactly here. (Or I understand cache context implementations wrong)
As far as I understand "::getContext()" value is always calculated, and "::getCacheableMetadata()" is merged with cache data, that uses the context.
In this way lets get users 2 and 3 that have the same role "viewer", and some abstract block with "user.roles" cache context.
Case:
But for user 3 the block is still valid, looks like cache invalidation isn't needed.
Other case:
Looks like everything works without "user:ID" tag.
Reading comments I see that issues take place when lower "user.roles" context is optimized to use with "user" context. And "user:ID" is needed to invalidate caches when target user is changed. But why the cache metadata implemented not on "UserCacheContext" level?
Looks like it leads to the same result and looks more correct:
Buggy case as I see it for user 4:
Current case:
Correct case: