feat(rbac): Redis L2 cache for permission checks

- load_roleperms: store role_perms to Redis (sc:rbac:role_perms, TTL=600s)
- get_userroles: store user roles to Redis (sc:rbac:user_roles:{userid}, TTL=300s)
- is_user_has_path_perm: check Redis L2 cache before opening DB connection
- Convert rp_caches values from list to set for O(1) path lookup
- Add cache_invalidate() to 7 event handlers for cross-worker invalidation via Pub/Sub
This commit is contained in:
yumoqing 2026-08-06 14:42:22 +08:00
parent 13ae4e89c8
commit 24faae3978

View File

@ -5,7 +5,7 @@ from ahserver.serverenv import ServerEnv
from appPublic.Singleton import SingletonDecorator
from appPublic.log import debug, error
from appPublic.jsonConfig import getConfig
from appPublic.share_cache import cache_get, cache_invalidate
from appPublic.share_cache import cache_get, cache_set, cache_invalidate
def _cache_enabled(module_name='rbac'):
@ -114,6 +114,7 @@ class UserPermissions:
userid = getattr(data, 'id', None)
if userid:
self.invalidate_user_cache(userid)
asyncio.ensure_future(cache_invalidate('rbac', f'user_roles:{userid}'))
debug(f'RBAC cache invalidated for user id={userid} (users update)')
except Exception as e:
error(f'RBAC on_user_update handler error: {e}')
@ -126,6 +127,7 @@ class UserPermissions:
userid = getattr(data, 'id', None)
if userid:
self.invalidate_user_cache(userid)
asyncio.ensure_future(cache_invalidate('rbac', f'user_roles:{userid}'))
debug(f'RBAC cache invalidated for user id={userid} (users create)')
except Exception as e:
error(f'RBAC on_user_create handler error: {e}')
@ -138,6 +140,7 @@ class UserPermissions:
userid = getattr(data, 'id', None)
if userid:
self.invalidate_user_cache(userid)
asyncio.ensure_future(cache_invalidate('rbac', f'user_roles:{userid}'))
debug(f'RBAC cache invalidated for user id={userid} (users delete)')
except Exception as e:
error(f'RBAC on_user_delete handler error: {e}')
@ -148,6 +151,7 @@ class UserPermissions:
"""
try:
self.invalidate_rp_cache()
asyncio.ensure_future(cache_invalidate('rbac', 'role_perms'))
debug('RBAC role-permission cache invalidated (rolepermission change)')
except Exception as e:
error(f'RBAC on_rolepermission_change handler error: {e}')
@ -158,6 +162,7 @@ class UserPermissions:
"""
try:
self.invalidate_rp_cache()
asyncio.ensure_future(cache_invalidate('rbac', 'role_perms'))
debug('RBAC role-permission cache invalidated (permission change)')
except Exception as e:
error(f'RBAC on_permission_change handler error: {e}')
@ -170,6 +175,7 @@ class UserPermissions:
try:
self.invalidate_all_user_caches()
self.invalidate_rp_cache()
asyncio.ensure_future(cache_invalidate('rbac', 'role_perms'))
debug('RBAC all caches invalidated (role change)')
except Exception as e:
error(f'RBAC on_role_change handler error: {e}')
@ -182,6 +188,7 @@ class UserPermissions:
userid = getattr(data, 'userid', None)
if userid:
self.invalidate_user_cache(userid)
asyncio.ensure_future(cache_invalidate('rbac', f'user_roles:{userid}'))
debug(f'RBAC cache invalidated for user id={userid} (userrole change)')
except Exception as e:
error(f'RBAC on_userrole_change handler error: {e}')
@ -271,8 +278,13 @@ order by c.orgtypeid, c.name"""
arr.append(r.path)
new_caches[k] = arr
# Atomic swap: other coroutines see old cache or fully-loaded new cache, never {}
self.rp_caches = new_caches
self.rp_caches = {k: set(v) for k, v in new_caches.items()}
self.rp_cache_loaded_at = now
# Share to Redis for cross-worker reuse
try:
await cache_set("rbac", "role_perms", new_caches, ttl=self.rp_cache_ttl)
except Exception:
pass
async def get_userroles(self, sor, userid):
"""Load user roles from database and cache them.
@ -290,6 +302,11 @@ where a.id = c.userid
roles.append(f'*.{r.name}')
roles = sorted(list(set(roles)))
self.ur_caches.set(userid, roles)
# Share to Redis for cross-worker reuse
try:
await cache_set("rbac", f"user_roles:{userid}", roles, ttl=self.cache_ttl)
except Exception:
pass
return roles
def check_roles_path(self, roles, path):
@ -345,6 +362,19 @@ where a.id = c.userid
if userid is None:
roles = ['any', 'anonymous']
# L2: Try shared Redis cache before opening DB connection
if _cache_enabled('rbac'):
if self.rp_caches is None:
cached_rp = await cache_get('rbac', 'role_perms', ttl=self.rp_cache_ttl)
if cached_rp is not None:
self.rp_caches = {k: set(v) for k, v in cached_rp.items()}
self.rp_cache_loaded_at = __import__('time').time()
if not roles and userid is not None:
cached_roles = await cache_get('rbac', f'user_roles:{userid}', ttl=self.cache_ttl)
if cached_roles is not None:
roles = cached_roles
self.ur_caches.set(userid, roles)
if not _cache_enabled('rbac') or self.rp_caches is None or not roles:
env = ServerEnv()
async with get_sor_context(env, 'rbac') as sor: