Skip to content

Commit 2fd75b3

Browse files
committed
feat: add thread-local caching for user lookup in get_user_by_username_or_email
1 parent 7abc3ce commit 2fd75b3

1 file changed

Lines changed: 38 additions & 7 deletions

File tree

openedx_authz/rest_api/utils.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Utility functions for the Open edX AuthZ REST API."""
22

3+
import threading
4+
35
from django.contrib.auth import get_user_model
46
from django.db.models import Q
57

@@ -9,6 +11,9 @@
911
User = get_user_model()
1012

1113

14+
_user_cache_local = threading.local()
15+
16+
1217
def get_generic_scope(scope: ScopeData) -> ScopeData:
1318
"""
1419
Create a generic scope from a given scope by replacing its key with a wildcard.
@@ -53,20 +58,46 @@ def get_user_map(usernames: list[str]) -> dict[str, User]:
5358

5459
def get_user_by_username_or_email(username_or_email: str) -> User:
5560
"""
56-
Retrieve a user by their username or email address.
61+
Retrieve a user by their username or email address with thread-local caching.
62+
63+
This function performs a flexible user lookup that accepts either a username or email
64+
address and returns the corresponding User object. Results are cached per-thread to
65+
avoid redundant database queries when the same user is looked up multiple times within
66+
the same request or thread context.
5767
5868
Args:
59-
username_or_email (str): The username or email address to search for.
69+
username_or_email (str): The username or email address to search for. The function
70+
will query both fields and return the first matching user.
6071
6172
Returns:
62-
User: The User object if found and not retired.
73+
User: The User object matching the provided username or email address.
6374
6475
Raises:
65-
User.DoesNotExist: If no user matches the provided username or email,
66-
or if the user has an associated retirement request.
76+
User.DoesNotExist: If no user is found with the given username or email, or if
77+
the user has been retired (has an associated userretirementrequest).
78+
79+
Note:
80+
- Uses thread-local storage for caching, so cache is isolated per thread/request
81+
- Negative lookups (non-existent users) are also cached to prevent repeated queries
82+
- Cache persists for the lifetime of the thread and is automatically cleaned up
83+
- Retired users (with userretirementrequest) are treated as non-existent
6784
"""
68-
user = User.objects.get(Q(email=username_or_email) | Q(username=username_or_email))
69-
if hasattr(user, "userretirementrequest"):
85+
cache = getattr(_user_cache_local, "data", None)
86+
if cache is None:
87+
cache = {}
88+
_user_cache_local.data = cache
89+
90+
if username_or_email not in cache:
91+
try:
92+
user = User.objects.get(Q(email=username_or_email) | Q(username=username_or_email))
93+
if hasattr(user, "userretirementrequest"):
94+
raise User.DoesNotExist
95+
cache[username_or_email] = user
96+
except User.DoesNotExist:
97+
cache[username_or_email] = None
98+
99+
user = cache[username_or_email]
100+
if user is None:
70101
raise User.DoesNotExist
71102
return user
72103

0 commit comments

Comments
 (0)