Coverage for src/backend/InvenTree/InvenTree/cache.py: 79%
79 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Configuration options for InvenTree external cache."""
3import socket
4import threading
5from typing import Any
7from django.db.utils import OperationalError, ProgrammingError
9import structlog
11import InvenTree.config
12import InvenTree.ready
14logger = structlog.get_logger('inventree')
16# Thread-local cache for caching data against the request object
17thread_data = threading.local()
20def cache_setting(name, default=None, **kwargs):
21 """Return the value of a particular cache setting.
23 Arguments:
24 name: The name of the cache setting
25 default: The default value to return if the setting is not found
26 kwargs: Additional arguments to pass to the cache setting request
27 """
28 return InvenTree.config.get_setting(
29 f'INVENTREE_CACHE_{name.upper()}', f'cache.{name.lower()}', default, **kwargs
30 )
33def cache_host():
34 """Return the cache host address."""
35 return cache_setting('host', None)
38def cache_port() -> int:
39 """Return the cache port."""
40 return cache_setting('port', '6379', typecast=int)
43def cache_password():
44 """Return the cache password."""
45 return cache_setting('password', None)
48def cache_user():
49 """Return the cash username."""
50 return cache_setting('user', None)
53def cache_db():
54 """Return the cache database index."""
55 return cache_setting('db', 0, typecast=int)
58def is_global_cache_enabled() -> bool:
59 """Check if the global cache is enabled.
61 - Test if the user has enabled and configured global cache
62 - Test if it is appropriate to enable global cache based on the current operation.
63 """
64 host = cache_host()
66 # Test if cache is enabled
67 # If the cache host is set, then the "default" action is to enable the cache
68 if not cache_setting('enabled', host is not None, typecast=bool): 68 ↛ 72line 68 didn't jump to line 72 because the condition on line 68 was always true
69 return False
71 # Test if the cache is configured
72 if not cache_host():
73 logger.warning('Global cache is enabled, but no cache host is configured!')
74 return False
76 # The cache should not be used during certain operations
77 if not InvenTree.ready.canAppAccessDatabase(
78 allow_test=False, allow_plugins=False, allow_shell=True
79 ):
80 logger.info('Global cache bypassed for this operation')
81 return False
83 logger.info('Global cache enabled')
85 return True
88def get_cache_config(global_cache: bool) -> dict:
89 """Return the cache configuration options.
91 Args:
92 global_cache: True if the global cache is enabled.
94 Returns:
95 A dictionary containing the cache configuration options.
96 """
97 # Build Redis URL with optional password
98 password = cache_password()
99 user = cache_user() or ''
100 host = cache_host()
101 port = cache_port()
102 db = cache_db()
104 if password: 104 ↛ 105line 104 didn't jump to line 105 because the condition on line 104 was never true
105 redis_url = f'redis://{user}:{password}@{host}:{port}/{db}'
106 else:
107 redis_url = f'redis://{host}:{port}/{db}'
109 keepalive_options = {
110 'TCP_KEEPCNT': cache_setting('keepalive_count', 5, typecast=int),
111 'TCP_KEEPIDLE': cache_setting('keepalive_idle', 1, typecast=int),
112 'TCP_KEEPINTVL': cache_setting('keepalive_interval', 1, typecast=int),
113 'TCP_USER_TIMEOUT': cache_setting('user_timeout', 1000, typecast=int),
114 }
116 global_cache_config = {
117 'BACKEND': 'django_redis.cache.RedisCache',
118 'LOCATION': redis_url,
119 'OPTIONS': {
120 'CLIENT_CLASS': 'django_redis.client.DefaultClient',
121 'SOCKET_CONNECT_TIMEOUT': cache_setting('connect_timeout', 5, typecast=int),
122 'SOCKET_TIMEOUT': cache_setting('timeout', 3, typecast=int),
123 'CONNECTION_POOL_KWARGS': {
124 'socket_keepalive': cache_setting('tcp_keepalive', True, typecast=bool),
125 'socket_keepalive_options': {
126 # Only include options which are available on this platform
127 # e.g. MacOS does not have TCP_KEEPIDLE and TCP_USER_TIMEOUT
128 getattr(socket, key): value
129 for key, value in keepalive_options.items()
130 if hasattr(socket, key)
131 },
132 },
133 },
134 }
136 if global_cache: 136 ↛ 138line 136 didn't jump to line 138 because the condition on line 136 was never true
137 # Only return the global cache configuration if the global cache is enabled
138 return global_cache_config
139 else:
140 # Default: Use django local memory cache
141 return {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}
144def create_session_cache(request) -> None:
145 """Create an empty session cache."""
146 thread_data.request = request
147 thread_data.request_cache = {}
150def delete_session_cache() -> None:
151 """Remove the session cache once the request is complete."""
152 if hasattr(thread_data, 'request'): 152 ↛ 155line 152 didn't jump to line 155 because the condition on line 152 was always true
153 del thread_data.request
155 if hasattr(thread_data, 'request_cache'): 155 ↛ exitline 155 didn't return from function 'delete_session_cache' because the condition on line 155 was always true
156 del thread_data.request_cache
159def get_session_cache(key: str) -> Any:
160 """Return a cached value from the session cache."""
161 # Only return a cached value if the request object is available too
162 if not hasattr(thread_data, 'request'):
163 return None
165 request_cache = getattr(thread_data, 'request_cache', None)
166 if request_cache is not None: 166 ↛ exitline 166 didn't return from function 'get_session_cache' because the condition on line 166 was always true
167 val = request_cache.get(key, None)
168 return val
171def set_session_cache(key: str, value: Any) -> None:
172 """Set a cached value in the session cache."""
173 # Only set a cached value if the request object is available too
174 if not hasattr(thread_data, 'request'):
175 return
177 request_cache = getattr(thread_data, 'request_cache', None)
179 if request_cache is not None: 179 ↛ exitline 179 didn't return from function 'set_session_cache' because the condition on line 179 was always true
180 request_cache[key] = value
183def get_cached_content_types(cache_key: str = 'all_content_types') -> list:
184 """Return a list of all ContentType objects, using session cache if possible."""
185 from django.contrib.contenttypes.models import ContentType
187 # Attempt to retrieve a list of ContentType objects from session cache
188 if content_types := get_session_cache(cache_key):
189 return content_types
191 try:
192 content_types = list(ContentType.objects.all())
193 if len(content_types) > 0: 193 ↛ 199line 193 didn't jump to line 199 because the condition on line 193 was always true
194 set_session_cache(cache_key, content_types)
195 except (OperationalError, ProgrammingError):
196 # Database is likely not yet ready
197 content_types = []
199 return content_types