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

1"""Configuration options for InvenTree external cache.""" 

2 

3import socket 

4import threading 

5from typing import Any 

6 

7from django.db.utils import OperationalError, ProgrammingError 

8 

9import structlog 

10 

11import InvenTree.config 

12import InvenTree.ready 

13 

14logger = structlog.get_logger('inventree') 

15 

16# Thread-local cache for caching data against the request object 

17thread_data = threading.local() 

18 

19 

20def cache_setting(name, default=None, **kwargs): 

21 """Return the value of a particular cache setting. 

22 

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 ) 

31 

32 

33def cache_host(): 

34 """Return the cache host address.""" 

35 return cache_setting('host', None) 

36 

37 

38def cache_port() -> int: 

39 """Return the cache port.""" 

40 return cache_setting('port', '6379', typecast=int) 

41 

42 

43def cache_password(): 

44 """Return the cache password.""" 

45 return cache_setting('password', None) 

46 

47 

48def cache_user(): 

49 """Return the cash username.""" 

50 return cache_setting('user', None) 

51 

52 

53def cache_db(): 

54 """Return the cache database index.""" 

55 return cache_setting('db', 0, typecast=int) 

56 

57 

58def is_global_cache_enabled() -> bool: 

59 """Check if the global cache is enabled. 

60 

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() 

65 

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 

70 

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 

75 

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 

82 

83 logger.info('Global cache enabled') 

84 

85 return True 

86 

87 

88def get_cache_config(global_cache: bool) -> dict: 

89 """Return the cache configuration options. 

90 

91 Args: 

92 global_cache: True if the global cache is enabled. 

93 

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() 

103 

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}' 

108 

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 } 

115 

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 } 

135 

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'} 

142 

143 

144def create_session_cache(request) -> None: 

145 """Create an empty session cache.""" 

146 thread_data.request = request 

147 thread_data.request_cache = {} 

148 

149 

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 

154 

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 

157 

158 

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 

164 

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 

169 

170 

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 

176 

177 request_cache = getattr(thread_data, 'request_cache', None) 

178 

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 

181 

182 

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 

186 

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 

190 

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 = [] 

198 

199 return content_types