|
1 | 1 | """Utility functions for the Open edX AuthZ REST API.""" |
2 | 2 |
|
| 3 | +import threading |
| 4 | + |
3 | 5 | from django.contrib.auth import get_user_model |
4 | 6 | from django.db.models import Q |
5 | 7 |
|
|
9 | 11 | User = get_user_model() |
10 | 12 |
|
11 | 13 |
|
| 14 | +_user_cache_local = threading.local() |
| 15 | + |
| 16 | + |
12 | 17 | def get_generic_scope(scope: ScopeData) -> ScopeData: |
13 | 18 | """ |
14 | 19 | 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]: |
53 | 58 |
|
54 | 59 | def get_user_by_username_or_email(username_or_email: str) -> User: |
55 | 60 | """ |
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. |
57 | 67 |
|
58 | 68 | 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. |
60 | 71 |
|
61 | 72 | Returns: |
62 | | - User: The User object if found and not retired. |
| 73 | + User: The User object matching the provided username or email address. |
63 | 74 |
|
64 | 75 | 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 |
67 | 84 | """ |
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: |
70 | 101 | raise User.DoesNotExist |
71 | 102 | return user |
72 | 103 |
|
|
0 commit comments