mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-02 13:28:49 +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."""
|
"""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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user