Problem/Motivation
The number of modules that access user roles has increased significantly. Consider scenarios like accessing roles in Rules (say that five times fast) or during batch operations. This issue is about implementing safe and effective role caching to increase performance.
Proposed resolution
The patch in #31 was committed to 8.x. The patch in #59 implements role caching, but does not cache roles for specific permissions, because most sites have enough permissions to make it very unlikely that this data will be requested frequently enough to justify the additional memory use. If we have a permission, or $user_roles is empty, we need to hit the database, otherwise, the cache is used.
Clear the user_* static caches before we invoke hook_user_role_delete(), so that implementing modules don't have to work with a stale cache.
Cautionary comments:
#33 - “adding this static cache could break D7 code that expects to be able to change roles during the request”
#34 - “Static caching in the caller doesn't help if you have four or five different contrib modules all doing their own calls to the function - you'll end up with 5 duplicate queries and 5 duplicate caches.”
Remaining tasks
The rerolled patch in #64 should be reviewed and further work is needed due to the changes introduced in #935062: Change role id to machine name. A backport to 7.x seems possible, but would be more complex due to #935062: Change role id to machine name.
User interface changes
None
API changes
To be determined
Original report by jik
user_roles should cache to avoid unnecessary redundant queries. See patch.
Comment | File | Size | Author |
---|---|---|---|
#69 | drupal_user_roles_cache-6463-do-not-test.patch | 2.54 KB | hefox |
#68 | drupal_user_roles_cache-6463-do-not-test.patch | 2.51 KB | hefox |
#68 | features_user_roles_cache-6463-do-not-test.patch | 340 bytes | hefox |
#64 | 6463-64.patch | 8.64 KB | star-szr |
#60 | user-roles-6463-60.patch | 8.71 KB | August1914 |
Comments
Comment #1
killes@www.drop.org CreditAttribution: killes@www.drop.org commentedI think that you should use a static variable instead of a global one.
Comment #2
jik CreditAttribution: jik commentedYou're right, I should. Here's a new patch against 4.4.0-rc.
Comment #3
Dries CreditAttribution: Dries commentedWhat makes you conclude that
user_roles()
gets called often, or that it represents a performance bottleneck?Comment #4
Chris Johnson CreditAttribution: Chris Johnson commentedThat's odd. user_roles() appears to get called once per admin user edit form. And even then, it's not that expensive -- it's way down the list when I run a profiler against it.
Comment #5
Dries CreditAttribution: Dries commentedThat's what I would think, Chris. I'm marking this "won't fix" for now.
Comment #6
xjmSix years later... I've seen a number of modules that need to access the roles list regularly (for example, access control modules) cache the list of roles with a static variable in their own _module_user_roles() function. (In fact, I've seen namespace collisions between modules that named them merely _user_roles().) It's definitely valuable when performing any sort of batch operations.
Are there any implications I'm missing?
Comment #7
hefox CreditAttribution: hefox commentedour custom stuff also has a wrapper around user_roles (we have a lot of stuff to do with to roles, permissions). It'd be quite nice to not have to do that.
Comment #8
3dloco CreditAttribution: 3dloco commentedAgree...I've got several user_load queries that are repeated...several of these showing up firebug>drupal>sql
user_load SELECT r.rid, r.name FROM role r INNER JOIN users_roles ur ON ur.rid = r.rid WHERE ur.uid =
Comment #9
Garrett Albright CreditAttribution: Garrett Albright commentedGave it a go. The parameters for user_roles() made things kind of tricky, but I think I nailed it.
Comment #10
Garrett Albright CreditAttribution: Garrett Albright commentedD6 backport.
Comment #11
marcingy CreditAttribution: marcingy commentedComment #12
marcingy CreditAttribution: marcingy commentedComment #13
Garrett Albright CreditAttribution: Garrett Albright commentedI managed to sucker catch into looking at this this evening. He pointed out that drupal_static() needs an & (d'oh) and that also the way that $roles is being assigned, the query might not be being run if a $permission value is passed - though after re-applying the patch and looking at the whole function, I think this assessment may be incorrect. I'll try to spend some more time with this over the weekend.
Comment #14
Garrett Albright CreditAttribution: Garrett Albright commentedReroll to add the &. As in my previous post, I can't see how the query wouldn't run when $permission is passed, so I think all is well.
Comment #15
msonnabaum CreditAttribution: msonnabaum commentedPerhaps a simpler approach?
Comment #16
msonnabaum CreditAttribution: msonnabaum commentedEven simpler.
Comment #17
msonnabaum CreditAttribution: msonnabaum commented…and fixing coding standards.
Comment #18
catchThis looks great, the caching is the same as #14 but it's a bit more self-contained. afaik core still doesn't call this function many times per page, but it definitely happens a lot on contrib modules (which is fair enough since sometimes you want to check roles rather than permissions).
Marking RTBC 7 years later!
Comment #19
catchComment #20
xjm#17 looks nice and clean. But do we want to cache cases where
$permission
is non-null as well?Comment #21
msonnabaum CreditAttribution: msonnabaum commentedWe certainly could, but it might be a micro-optimization.
Any time I've seen user_roles() show up when profiling its called with no args. If a perm is specified, that query should be indexed, so I don't know that we'd gain much.
Comment #22
hefox CreditAttribution: hefox commentedDitto; the likelyhood that two different modules are calling user_roles for the same permissions often enough to benefit seems a bit slim.
Comment #23
Dave ReidThis doesn't really make sense to me on reading it and could use an inline comment. I can't see any explanation why we're also not caching if $permission is specified?
1 days to next Drupal core point release.
Comment #24
catchYeah we should add a comment for no caching when $permission is specified definitely.
Comment #25
Anonymous (not verified) CreditAttribution: Anonymous commentedyep, i agree with #23, we need a comment, and we could make the code read more like the intent, which is 'use a different cache key based on $membersonly'.
like:
also, the if (isset($cid)) should prolly just check empty($permission) as it does earlier in the function, to make it clearer what's going on - we're not caching when permission is set, because the cache hits would likely be too low, and we wouldn't get any gain for the extra memory we'd be spending.
Comment #26
xjmComment #27
xjmRerolled for #23 through #25.
Comment #28
xjmVariant with a more detailed comment justifying the exclusion of specific permissions.
Comment #29
Anonymous (not verified) CreditAttribution: Anonymous commentedalrighty, this looks good to go.
EDIT: i should have waited for the bot to come back green...
Comment #30
msonnabaum CreditAttribution: msonnabaum commentedThis looks quite good to me as well.
Comment #31
msonnabaum CreditAttribution: msonnabaum commentedComments were a little redundant. Removed the first one and moved the bottom comment up.
Comment #32
catchCommitted to 8.x, moving to 7.x for webchick.
Comment #33
moshe weitzman CreditAttribution: moshe weitzman commentedPersonally I would rather leave this caching to the caller. But anyway, adding this static cache could break D7 code that expects to be able to change roles during the request. I would not commit it to 7, personally.
Comment #34
catchStatic caching in the caller doesn't help if you have four or five different contrib modules all doing their own calls to the function - you'll end up with 5 duplicate queries and 5 duplicate caches.
Moving to CNR for 7.x though.
Comment #35
David_Rothstein CreditAttribution: David_Rothstein commentedDoesn't this cache need to be cleared when user roles change?
Comment #36
Anonymous (not verified) CreditAttribution: Anonymous commentedonly if accessed during the same request - it's not a persistent cache.
Comment #37
msonnabaum CreditAttribution: msonnabaum commentedA reasonable request.
Attached patch should take care of added roles
Comment #38
msonnabaum CreditAttribution: msonnabaum commented[dupe]
Comment #39
Anonymous (not verified) CreditAttribution: Anonymous commenteduser_role_CRUD functions have this:
discussed this with catch, perhaps we should add user_roles_reset() or similar, and consolidate the static cache resets.
Comment #40
Anonymous (not verified) CreditAttribution: Anonymous commentedoh, and more awesome in here. seems that code that implements user_role_* hooks is some of the most likely to get bitten by stale static caches.
so this is probably not the order we are looking for, user_role_delete():
same pattern in user_role_save()....
Comment #41
David_Rothstein CreditAttribution: David_Rothstein commentedLet's start with this patch. These tests should fail, and we need to make them pass :)
A helper function as proposed in #39 makes sense to me.... and #40 is also a good point (the tests I wrote here won't actually catch that issue - we'd have to extend them to test the user role hooks also if we wanted to deal with that).
Comment #43
Anonymous (not verified) CreditAttribution: Anonymous commentedok, attached two patches.
one is test only, and extends David's patch in #41 to add tests for the hook invocations.
second one implements the fixes.
Comment #44
Anonymous (not verified) CreditAttribution: Anonymous commentedahem, ignore #43, git add -N FAIL.
this time with 100% more test module.
Comment #46
msonnabaum CreditAttribution: msonnabaum commented#43 looks fine to me. It's a bit more complex but the result is more sensible.
I think I'd like to see a comment above
if ($permission || !$user_roles) {
, it's pretty confusing as is. Maybe the one below could just be moved up there.Comment #47
xjmExcept it's failing testbot. ;)
Comment #48
Anonymous (not verified) CreditAttribution: Anonymous commentedupdated patch with more comments as per #46.
if this fails to apply for the bot again, i'm going to stab the bot, and maybe either find or write some docs about how to make a patch that adds files work for testing.
Comment #50
Anonymous (not verified) CreditAttribution: Anonymous commentedthanks to some help from boombatower, here's a patch that the bot will hopefully like.
Comment #52
Anonymous (not verified) CreditAttribution: Anonymous commentedaaarrrrggghhhh!
here's one without the module code copied twice.
Comment #53
msonnabaum CreditAttribution: msonnabaum commented#52 looks great. Comments are very clear.
Comment #54
David_Rothstein CreditAttribution: David_Rothstein commentedI think this breaks the feature where you're allowed to reorder the list of roles using drag-and-drop.... We need to make sure that user_roles() always returns the role list ordered by weight.
It's also slightly uncomfortable that it relies on a specific numeric value of DRUPAL_ANONYMOUS_RID (in relation to the other roles), though I suppose it's probably not the only code in Drupal that does that.
I don't think we should bother with this, actually. The goal here is to test that the role API works correctly, not to test the static cache (especially since a properly working static cache should be invisible to outside callers of the API), right? As long as some of the tests will fail when the static cache is broken (which they did), I don't think we should do special manipulations to make sure all of them will fail. It's better just to have the test be clean.
For similar reasons as above, I'd recommend calling this module something more like 'user_role_api_test'?
Comment #55
Anonymous (not verified) CreditAttribution: Anonymous commentedyes, i think i agree with all the points in #54. will reroll.
Comment #56
Anonymous (not verified) CreditAttribution: Anonymous commentedwell, actually the points about the test are kind of 'meh'.
i agree on the naming of the test, but i'm not sure i agree on writing a test where we deliberately don't test for situations that will happen in real code.
Comment #57
msonnabaum CreditAttribution: msonnabaum commentedRe-rolled #52 for the /core change and renamed the test module per #54.
Comment #59
xjmComment #60
August1914 CreditAttribution: August1914 commentedWrote a new issue summary. Feel free take it to another level.
Rerolled from 57 (which last applied at d13fd18f1e2dbd02a2efba5786dfbaa6dbd58cca)
Cleaned up whitespace and changed DrupalWebTestCase to WebTestBase.
This patch applies cleanly at faa63d2.
Comment #61
xjmSending to the bot. Thanks @August1914!
Comment #63
star-szrComment #64
star-szrRerolled because of #935062: Change role id to machine name and #1594260: Convert user tests to PSR-0. This was a messy reroll, and should be reviewed. Tests still do not pass.
A couple notes, due to the changes in #935062: Change role id to machine name:
Comment #65
star-szrComment #66.0
(not verified) CreditAttribution: commentedUpdated issue summary.
Comment #67
star-szrUpdated the issue summary.
Comment #67.0
star-szrUsing the issue summary template, and updating text here and there
Comment #67.1
star-szrCleaning up wording in first paragraph, other minor tweaks.
Comment #68
hefox CreditAttribution: hefox commentedThis looks like a bug.
Ignore these patches unless you're looking for d6 version of them.
Comment #69
hefox CreditAttribution: hefox commented(Again, ignore this, d6 -- was missing an array_filter)
Comment #69.0
hefox CreditAttribution: hefox commentedLinkifying comment numbers.
Comment #71
drummCorrecting version.
Comment #75
drummCorrecting version again.
Comment #76
dpiThe patch was already committed to Drupal 8 in #6463-32: Add static caching for user_roles() in commit #17329b1
Then the static caching was removed by #2572667: Deleting a role doesn't invalidate user_roles() static cache.
Changing to Drupal 7