2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-01 21:16:46 +00:00

[Enhancement] Request cache (#8956)

* Middleware for caching against request

* Create helpers for setting / getting session cache

* Settings objects check session cache first

* Ensure setting is removed from session cache when updated

* Cleaner implementation

* Fix cache cleanup

- ONLY allow access if there is a request object
- Ensure cache is deleted once session is over

* Skip plugin registry reload check
This commit is contained in:
Oliver 2025-01-27 14:45:11 +11:00 committed by GitHub
parent ddcb7980ff
commit 630d165c22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 119 additions and 19 deletions

View File

@ -1,6 +1,7 @@
"""Configuration options for InvenTree external cache.""" """Configuration options for InvenTree external cache."""
import socket import socket
import threading
import structlog import structlog
@ -9,9 +10,18 @@ import InvenTree.ready
logger = structlog.get_logger('inventree') logger = structlog.get_logger('inventree')
# Thread-local cache for caching data against the request object
thread_data = threading.local()
def cache_setting(name, default=None, **kwargs): def cache_setting(name, default=None, **kwargs):
"""Return a cache setting.""" """Return the value of a particular cache setting.
Arguments:
name: The name of the cache setting
default: The default value to return if the setting is not found
kwargs: Additional arguments to pass to the cache setting request
"""
return InvenTree.config.get_setting( return InvenTree.config.get_setting(
f'INVENTREE_CACHE_{name.upper()}', f'cache.{name.lower()}', default, **kwargs f'INVENTREE_CACHE_{name.upper()}', f'cache.{name.lower()}', default, **kwargs
) )
@ -22,12 +32,12 @@ def cache_host():
return cache_setting('host', None) return cache_setting('host', None)
def cache_port(): def cache_port() -> int:
"""Return the cache port.""" """Return the cache port."""
return cache_setting('port', '6379', typecast=int) return cache_setting('port', '6379', typecast=int)
def is_global_cache_enabled(): def is_global_cache_enabled() -> bool:
"""Check if the global cache is enabled. """Check if the global cache is enabled.
- Test if the user has enabled and configured global cache - Test if the user has enabled and configured global cache
@ -100,3 +110,42 @@ def get_cache_config(global_cache: bool) -> dict:
# Default: Use django local memory cache # Default: Use django local memory cache
return {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'} return {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}
def create_session_cache(request) -> None:
"""Create an empty session cache."""
thread_data.request = request
thread_data.request_cache = {}
def delete_session_cache() -> None:
"""Remove the session cache once the request is complete."""
if hasattr(thread_data, 'request'):
del thread_data.request
if hasattr(thread_data, 'request_cache'):
del thread_data.request_cache
def get_session_cache(key: str) -> any:
"""Return a cached value from the session cache."""
# Only return a cached value if the request object is available too
if not hasattr(thread_data, 'request'):
return None
request_cache = getattr(thread_data, 'request_cache', None)
if request_cache is not None:
val = request_cache.get(key, None)
return val
def set_session_cache(key: str, value: any) -> None:
"""Set a cached value in the session cache."""
# Only set a cached value if the request object is available too
if not hasattr(thread_data, 'request'):
return
request_cache = getattr(thread_data, 'request_cache', None)
if request_cache is not None:
request_cache[key] = value

View File

@ -7,12 +7,14 @@ from django.contrib.auth.middleware import PersistentRemoteUserMiddleware
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import Resolver404, include, path, resolve, reverse_lazy from django.urls import Resolver404, include, path, resolve, reverse_lazy
from django.utils.deprecation import MiddlewareMixin
import structlog import structlog
from allauth_2fa.middleware import AllauthTwoFactorMiddleware, BaseRequire2FAMiddleware from allauth_2fa.middleware import AllauthTwoFactorMiddleware, BaseRequire2FAMiddleware
from error_report.middleware import ExceptionProcessor from error_report.middleware import ExceptionProcessor
from common.settings import get_global_setting from common.settings import get_global_setting
from InvenTree.cache import create_session_cache, delete_session_cache
from InvenTree.urls import frontendpatterns from InvenTree.urls import frontendpatterns
from users.models import ApiToken from users.models import ApiToken
@ -215,3 +217,23 @@ class InvenTreeExceptionProcessor(ExceptionProcessor):
) )
error.save() error.save()
class InvenTreeRequestCacheMiddleware(MiddlewareMixin):
"""Middleware to perform caching against the request object.
This middleware is used to cache data against the request object,
which can be used to store data for the duration of the request.
In this fashion, we can avoid hitting the external cache multiple times,
much less the database!
"""
def process_request(self, request):
"""Create a request-specific cache object."""
create_session_cache(request)
def process_response(self, request, response):
"""Clear the cache object."""
delete_session_cache()
return response

View File

@ -330,6 +330,7 @@ MIDDLEWARE = CONFIG.get(
'InvenTree.middleware.Check2FAMiddleware', # Check if the user should be forced to use MFA 'InvenTree.middleware.Check2FAMiddleware', # Check if the user should be forced to use MFA
'maintenance_mode.middleware.MaintenanceModeMiddleware', 'maintenance_mode.middleware.MaintenanceModeMiddleware',
'InvenTree.middleware.InvenTreeExceptionProcessor', # Error reporting 'InvenTree.middleware.InvenTreeExceptionProcessor', # Error reporting
'InvenTree.middleware.InvenTreeRequestCacheMiddleware', # Request caching
'django_structlog.middlewares.RequestMiddleware', # Structured logging 'django_structlog.middlewares.RequestMiddleware', # Structured logging
], ],
) )

View File

@ -52,6 +52,7 @@ import users.models
from common.setting.type import InvenTreeSettingsKeyType, SettingsKeyType from common.setting.type import InvenTreeSettingsKeyType, SettingsKeyType
from generic.states import ColorEnum from generic.states import ColorEnum
from generic.states.custom import state_color_mappings from generic.states.custom import state_color_mappings
from InvenTree.cache import get_session_cache, set_session_cache
from InvenTree.sanitizer import sanitize_svg from InvenTree.sanitizer import sanitize_svg
logger = structlog.get_logger('inventree') logger = structlog.get_logger('inventree')
@ -156,6 +157,9 @@ class BaseInvenTreeSetting(models.Model):
if do_cache: if do_cache:
self.save_to_cache() self.save_to_cache()
# Remove the setting from the request cache
set_session_cache(self.cache_key, None)
# Execute after_save action # Execute after_save action
self._call_settings_function('after_save', args, kwargs) self._call_settings_function('after_save', args, kwargs)
@ -485,22 +489,20 @@ class BaseInvenTreeSetting(models.Model):
- Key is case-insensitive - Key is case-insensitive
- Returns None if no match is made - Returns None if no match is made
First checks the cache to see if this object has recently been accessed, As settings are accessed frequently, this function will attempt to access the cache first:
and returns the cached version if so.
1. Check the ephemeral request cache
2. Check the global cache
3. Query the database
""" """
key = str(key).strip().upper() key = str(key).strip().upper()
# Unless otherwise specified, attempt to create the setting # Unless otherwise specified, attempt to create the setting
create = kwargs.pop('create', True) create = kwargs.pop('create', True)
# Specify if cache lookup should be performed # Specify if global cache lookup should be performed
do_cache = kwargs.pop('cache', django_settings.GLOBAL_CACHE_ENABLED) # If not specified, determine based on whether global cache is enabled
access_global_cache = kwargs.pop('cache', django_settings.GLOBAL_CACHE_ENABLED)
filters = {
'key__iexact': key,
# Optionally filter by other keys
**cls.get_filters(**kwargs),
}
# Prevent saving to the database during certain operations # Prevent saving to the database during certain operations
if ( if (
@ -510,21 +512,36 @@ class BaseInvenTreeSetting(models.Model):
or InvenTree.ready.isRunningBackup() or InvenTree.ready.isRunningBackup()
): ):
create = False create = False
do_cache = False access_global_cache = False
cache_key = cls.create_cache_key(key, **kwargs) cache_key = cls.create_cache_key(key, **kwargs)
if do_cache: # Fist, attempt to pull the setting from the request cache
if setting := get_session_cache(cache_key):
return setting
if access_global_cache:
try: try:
# First attempt to find the setting object in the cache # First attempt to find the setting object in the cache
cached_setting = cache.get(cache_key) cached_setting = cache.get(cache_key)
if cached_setting is not None: if cached_setting is not None:
# Store the cached setting into the session cache
set_session_cache(cache_key, cached_setting)
return cached_setting return cached_setting
except Exception: except Exception:
# Cache is not ready yet # Cache is not ready yet
do_cache = False access_global_cache = False
# At this point, we need to query the database
filters = {
'key__iexact': key,
# Optionally filter by other keys
**cls.get_filters(**kwargs),
}
try: try:
settings = cls.objects.all() settings = cls.objects.all()
@ -551,9 +568,13 @@ class BaseInvenTreeSetting(models.Model):
# The setting failed validation - might be due to duplicate keys # The setting failed validation - might be due to duplicate keys
pass pass
if setting and do_cache: if setting:
# Cache this setting object # Cache this setting object to the request cache
setting.save_to_cache() set_session_cache(cache_key, setting)
if access_global_cache:
# Cache this setting object to the global cache
setting.save_to_cache()
return setting return setting

View File

@ -26,6 +26,7 @@ from django.utils.translation import gettext_lazy as _
import structlog import structlog
import InvenTree.cache
from common.settings import get_global_setting, set_global_setting from common.settings import get_global_setting, set_global_setting
from InvenTree.config import get_plugin_dir from InvenTree.config import get_plugin_dir
from InvenTree.ready import canAppAccessDatabase from InvenTree.ready import canAppAccessDatabase
@ -836,6 +837,12 @@ class PluginsRegistry:
# Skip check if database cannot be accessed # Skip check if database cannot be accessed
return return
if InvenTree.cache.get_session_cache('plugin_registry_checked'):
# Return early if the registry has already been checked (for this request)
return
InvenTree.cache.set_session_cache('plugin_registry_checked', True)
logger.debug('Checking plugin registry hash') logger.debug('Checking plugin registry hash')
# If not already cached, calculate the hash # If not already cached, calculate the hash