2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-01 13:06:45 +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."""
import socket
import threading
import structlog
@ -9,9 +10,18 @@ import InvenTree.ready
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):
"""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(
f'INVENTREE_CACHE_{name.upper()}', f'cache.{name.lower()}', default, **kwargs
)
@ -22,12 +32,12 @@ def cache_host():
return cache_setting('host', None)
def cache_port():
def cache_port() -> int:
"""Return the cache port."""
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.
- 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
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.shortcuts import redirect
from django.urls import Resolver404, include, path, resolve, reverse_lazy
from django.utils.deprecation import MiddlewareMixin
import structlog
from allauth_2fa.middleware import AllauthTwoFactorMiddleware, BaseRequire2FAMiddleware
from error_report.middleware import ExceptionProcessor
from common.settings import get_global_setting
from InvenTree.cache import create_session_cache, delete_session_cache
from InvenTree.urls import frontendpatterns
from users.models import ApiToken
@ -215,3 +217,23 @@ class InvenTreeExceptionProcessor(ExceptionProcessor):
)
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
'maintenance_mode.middleware.MaintenanceModeMiddleware',
'InvenTree.middleware.InvenTreeExceptionProcessor', # Error reporting
'InvenTree.middleware.InvenTreeRequestCacheMiddleware', # Request caching
'django_structlog.middlewares.RequestMiddleware', # Structured logging
],
)

View File

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

View File

@ -26,6 +26,7 @@ from django.utils.translation import gettext_lazy as _
import structlog
import InvenTree.cache
from common.settings import get_global_setting, set_global_setting
from InvenTree.config import get_plugin_dir
from InvenTree.ready import canAppAccessDatabase
@ -836,6 +837,12 @@ class PluginsRegistry:
# Skip check if database cannot be accessed
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')
# If not already cached, calculate the hash