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.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

killes@www.drop.org’s picture

I think that you should use a static variable instead of a global one.

jik’s picture

FileSize
636 bytes

You're right, I should. Here's a new patch against 4.4.0-rc.

Dries’s picture

What makes you conclude that user_roles() gets called often, or that it represents a performance bottleneck?

Chris Johnson’s picture

That'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.

Dries’s picture

That's what I would think, Chris. I'm marking this "won't fix" for now.

xjm’s picture

Version: » 7.x-dev
Status: Closed (won't fix) » Active

Six 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?

hefox’s picture

our 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.

3dloco’s picture

Issue tags: +Performance

Agree...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 =

Garrett Albright’s picture

Status: Active » Needs review
FileSize
2 KB

Gave it a go. The parameters for user_roles() made things kind of tricky, but I think I nailed it.

Garrett Albright’s picture

D6 backport.

marcingy’s picture

Version: 7.x-dev » 8.x-dev
Issue tags: +Needs backport to D7
marcingy’s picture

Category: feature » task
Garrett Albright’s picture

Status: Needs review » Needs work

I 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.

Garrett Albright’s picture

Status: Needs work » Needs review
FileSize
2 KB

Reroll 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.

msonnabaum’s picture

FileSize
874 bytes

Perhaps a simpler approach?

msonnabaum’s picture

FileSize
851 bytes

Even simpler.

msonnabaum’s picture

FileSize
852 bytes

…and fixing coding standards.

catch’s picture

Status: Needs review » Reviewed & tested by the community

This 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!

catch’s picture

Title: user_roles should cache » Add static caching for user_roles()
xjm’s picture

#17 looks nice and clean. But do we want to cache cases where $permission is non-null as well?

msonnabaum’s picture

We 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.

hefox’s picture

Ditto; the likelyhood that two different modules are calling user_roles for the same permissions often enough to benefit seems a bit slim.

Dave Reid’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/modules/user/user.moduleundefined
@@ -2785,6 +2785,15 @@ function user_mail_tokens(&$replacements, $data, $options) {
+    $cid = (int) $membersonly;
+    if (isset($user_roles[$cid])) {
+      return $user_roles[$cid];
+    }

This 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.

catch’s picture

Yeah we should add a comment for no caching when $permission is specified definitely.

Anonymous’s picture

yep, 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:

    $cid = $membersonly ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID;

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.

xjm’s picture

Assigned: Unassigned » xjm
xjm’s picture

Assigned: xjm » Unassigned
Status: Needs work » Needs review
FileSize
892 bytes
1007 bytes

Rerolled for #23 through #25.

xjm’s picture

Variant with a more detailed comment justifying the exclusion of specific permissions.

Anonymous’s picture

Status: Needs review » Reviewed & tested by the community

alrighty, this looks good to go.

EDIT: i should have waited for the bot to come back green...

msonnabaum’s picture

This looks quite good to me as well.

msonnabaum’s picture

FileSize
1.02 KB

Comments were a little redundant. Removed the first one and moved the bottom comment up.

catch’s picture

Version: 8.x-dev » 7.x-dev

Committed to 8.x, moving to 7.x for webchick.

moshe weitzman’s picture

Personally 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.

catch’s picture

Status: Reviewed & tested by the community » Needs review

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.

Moving to CNR for 7.x though.

David_Rothstein’s picture

Version: 7.x-dev » 8.x-dev
Status: Needs review » Needs work
Issue tags: +Needs tests

Doesn't this cache need to be cleared when user roles change?

Anonymous’s picture

only if accessed during the same request - it's not a persistent cache.

msonnabaum’s picture

Status: Needs work » Needs review
FileSize
402 bytes

A reasonable request.

Attached patch should take care of added roles

msonnabaum’s picture

Status: Needs work » Needs review

[dupe]

Anonymous’s picture

Status: Needs review » Needs work

user_role_CRUD functions have this:

  // Clear the user access cache.
  drupal_static_reset('user_access');
  drupal_static_reset('user_role_permissions');

discussed this with catch, perhaps we should add user_roles_reset() or similar, and consolidate the static cache resets.

Anonymous’s picture

Status: Needs review » Needs work

oh, 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():

  // Update the users who have this role set:
  db_delete('users_roles')
    ->condition('rid', $role->rid)
    ->execute();

  module_invoke_all('user_role_delete', $role);
  
  // Clear the user access cache.
  drupal_static_reset('user_access');
  drupal_static_reset('user_role_permissions');

same pattern in user_role_save()....

David_Rothstein’s picture

Status: Needs work » Needs review
FileSize
1.88 KB

Let'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).

Status: Needs review » Needs work

The last submitted patch, user-roles-6463-41.patch, failed testing.

Anonymous’s picture

Status: Needs work » Needs review
FileSize
7.08 KB
3.08 KB

ok, 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.

Anonymous’s picture

ahem, ignore #43, git add -N FAIL.

this time with 100% more test module.

Status: Needs review » Needs work

The last submitted patch, user.role_.patch, failed testing.

msonnabaum’s picture

Status: Needs work » Needs review

#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.

xjm’s picture

Status: Needs review » Needs work

Except it's failing testbot. ;)

Anonymous’s picture

Status: Needs work » Needs review
FileSize
8.96 KB

updated 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.

Status: Needs review » Needs work

The last submitted patch, user.role_.patch, failed testing.

Anonymous’s picture

Status: Needs work » Needs review
FileSize
10.53 KB

thanks to some help from boombatower, here's a patch that the bot will hopefully like.

Status: Needs review » Needs work

The last submitted patch, user.roles_.patch, failed testing.

Anonymous’s picture

Status: Needs work » Needs review
FileSize
9.09 KB

aaarrrrggghhhh!

here's one without the module code copied twice.

msonnabaum’s picture

Status: Needs review » Reviewed & tested by the community

#52 looks great. Comments are very clear.

David_Rothstein’s picture

Status: Reviewed & tested by the community » Needs work
+      // The $roles array is sorted by key so that the DRUPAL_ANONYMOUS_RID
+      // role is always the first element in the array, allowing us to use less
+      // memory by keeping one list of roles. The sort makes it easy to return
+      // a list with or without the anonymous role using array_slice().
+      ksort($roles);

I 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.

+    // Prime the user_roles() static cache.
+    user_roles();
....
+    // Clear the static cache, then reload it, so we can be sure that the role
+    // we're about to delete is there, otherwise we're not realing testing for
+    // staleness after the delete.
+    drupal_static_reset('user_roles');
+    user_roles();

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.

+++ b/modules/user/user_static_cache_test.info

For similar reasons as above, I'd recommend calling this module something more like 'user_role_api_test'?

Anonymous’s picture

yes, i think i agree with all the points in #54. will reroll.

Anonymous’s picture

well, 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.

msonnabaum’s picture

Status: Needs work » Needs review
FileSize
8.92 KB

Re-rolled #52 for the /core change and renamed the test module per #54.

Status: Needs review » Needs work

The last submitted patch, user-roles-6463-57.patch, failed testing.

xjm’s picture

August1914’s picture

FileSize
8.71 KB

Wrote 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.

xjm’s picture

Status: Needs work » Needs review

Sending to the bot. Thanks @August1914!

Status: Needs review » Needs work

The last submitted patch, user-roles-6463-60.patch, failed testing.

star-szr’s picture

Assigned: Unassigned » star-szr
star-szr’s picture

Status: Needs work » Needs review
FileSize
8.64 KB

Rerolled 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:

  1. The ksort() and array_slice() definitely needs to be reworked, since the key is no longer the role ID but the role's machine name.
  2. I think a 7.x backport will need to be reconsidered as well.
star-szr’s picture

Assigned: star-szr » Unassigned

Status: Needs review » Needs work

The last submitted patch, 6463-64.patch, failed testing.

Anonymous’s picture

Issue summary: View changes

Updated issue summary.

star-szr’s picture

Updated the issue summary.

star-szr’s picture

Issue summary: View changes

Using the issue summary template, and updating text here and there

star-szr’s picture

Issue summary: View changes

Cleaning up wording in first paragraph, other minor tweaks.

hefox’s picture

+++ b/core/modules/user/user.module
@@ -2638,32 +2638,36 @@ function user_mail_tokens(&$replacements, $data, $options) {
+    if ($membersonly) {
+      $query->condition('r.rid', DRUPAL_ANONYMOUS_RID, '!=');
+    }
...
+    return $membersonly ? array_slice($user_roles, 1, count($user_roles), TRUE) : $user_roles;

This looks like a bug.

Ignore these patches unless you're looking for d6 version of them.

hefox’s picture

(Again, ignore this, d6 -- was missing an array_filter)

hefox’s picture

Issue summary: View changes

Linkifying comment numbers.

Version: 8.0.x-dev » 8.2.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to upgrade to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

drumm’s picture

Version: 8.2.x-dev » 8.1.x-dev

Correcting version.

  • catch committed 17329b1 on 8.3.x
    Issue #6463 by msonnabaum, Garrett Albright, xjm: Add static caching for...

  • catch committed 17329b1 on 8.3.x
    Issue #6463 by msonnabaum, Garrett Albright, xjm: Add static caching for...

Version: 8.1.x-dev » 8.3.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

drumm’s picture

Version: 8.3.x-dev » 8.2.x-dev

Correcting version again.

dpi’s picture

Version: 8.2.x-dev » 7.x-dev
Status: Needs work » Active

The 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

  • catch committed 17329b1 on 8.4.x
    Issue #6463 by msonnabaum, Garrett Albright, xjm: Add static caching for...

  • catch committed 17329b1 on 8.4.x
    Issue #6463 by msonnabaum, Garrett Albright, xjm: Add static caching for...

  • catch committed 17329b1 on 9.1.x
    Issue #6463 by msonnabaum, Garrett Albright, xjm: Add static caching for...