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:
parent
ddcb7980ff
commit
630d165c22
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
],
|
||||
)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user