Improve request caching for plugin config objects (#12430)

* Improve request caching for plugin config objects

* Cache configs dict further
This commit is contained in:
Oliver 2026-07-21 10:23:50 +10:00 committed by GitHub
parent 4c4c86e180
commit 7973057ad9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 85 additions and 38 deletions

View File

@ -212,39 +212,89 @@ class PluginsRegistry:
return plg
def get_plugin_config(self, slug: str, name: str | None = None):
def get_plugin_configs(self) -> Optional[dict[str, Any]]:
"""Return a per-request cached mapping of {slug: PluginConfig} for all plugins.
This is a general-purpose cache, shared by any registry method which needs to
look up (potentially many) PluginConfig instances - e.g. get_plugin_config(),
with_mixin(), calculate_plugin_hash() - so that a single request only ever
pre-fetches the full PluginConfig table once, rather than issuing a
per-plugin database query.
Returns:
dict[str, PluginConfig]: Mapping of plugin slug to PluginConfig instance.
None if the database is not ready to be queried.
"""
# As we have already checked the registry hash, this is a valid cache key
cache_key = f'plugin_configs:{self.registry_hash}'
configs = InvenTree.cache.get_session_cache(cache_key)
if configs is None:
try:
# Pre-fetch the PluginConfig objects to avoid multiple database queries
from plugin.models import PluginConfig
configs = {config.key: config for config in PluginConfig.objects.all()}
InvenTree.cache.set_session_cache(cache_key, configs)
except (ProgrammingError, OperationalError):
# The database is not ready yet
logger.warning('plugin.registry.get_plugin_configs: Database not ready')
return None
return configs
def get_plugin_config(
self, slug: str, name: str | None = None, configs: dict | None = None
):
"""Return the matching PluginConfig instance for a given plugin.
Arguments:
slug: The plugin slug
name: The plugin name (optional)
configs: A pre-fetched mapping of {slug: PluginConfig} to avoid multiple database queries (optional)
Returns:
PluginConfig | None: The matching PluginConfig instance, or None if not found.
"""
import InvenTree.ready
from plugin.models import PluginConfig
# Under certain circumstances, we want to avoid creating new PluginConfig instances in the database
can_create = (
InvenTree.ready.canAppAccessDatabase(
allow_plugins=False, allow_shell=True, allow_test=True
)
and not InvenTree.ready.isReadOnlyCommand()
)
if configs is None:
configs = self.get_plugin_configs()
try:
cfg = PluginConfig.objects.filter(key=slug).first()
cfg = configs.get(slug) if configs is not None else None
if not cfg and can_create:
logger.debug(
"get_plugin_config: Creating new PluginConfig for '%s'", slug
if not cfg:
# Under certain circumstances, we want to avoid creating new PluginConfig instances in the database
can_create = (
InvenTree.ready.canAppAccessDatabase(
allow_plugins=False, allow_shell=True, allow_test=True
)
cfg = PluginConfig.objects.create(key=slug)
and not InvenTree.ready.isReadOnlyCommand()
)
except PluginConfig.DoesNotExist: # pragma: no cover
return None
except (IntegrityError, OperationalError, ProgrammingError): # pragma: no cover
return None
try:
cfg = PluginConfig.objects.filter(key=slug).first()
if name and cfg.name != name:
if not cfg and can_create:
logger.debug(
"get_plugin_config: Creating new PluginConfig for '%s'", slug
)
cfg = PluginConfig.objects.create(key=slug)
except (
IntegrityError,
OperationalError,
ProgrammingError,
): # pragma: no cover
return None
if cfg is not None and configs is not None:
# Backfill the cache, so repeated lookups this request are free
configs[slug] = cfg
if cfg and name and cfg.name != name:
# Update the name if it has changed
try:
cfg.name = name
@ -313,25 +363,15 @@ class PluginsRegistry:
active (bool, optional): Filter by 'active' status of plugin. Defaults to True.
builtin (bool, optional): Filter by 'builtin' status of plugin. Defaults to None.
"""
# We can store the PluginConfig objects against the session cache,
# which allows us to avoid hitting the database multiple times (per session)
# As we have already checked the registry hash, this is a valid cache key
cache_key = f'plugin_configs:{self.registry_hash}'
# Pre-fetch (and cache) the PluginConfig objects, to avoid hitting the
# database multiple times (per plugin) below
configs = InvenTree.cache.get_session_cache(cache_key)
configs = self.get_plugin_configs()
if not configs:
try:
# Pre-fetch the PluginConfig objects to avoid multiple database queries
from plugin.models import PluginConfig
plugin_configs = PluginConfig.objects.all()
configs = {config.key: config for config in plugin_configs}
InvenTree.cache.set_session_cache(cache_key, configs)
except (ProgrammingError, OperationalError):
# The database is not ready yet
logger.warning('plugin.registry.with_mixin: Database not ready')
return []
if configs is None:
# The database is not ready yet
logger.warning('plugin.registry.with_mixin: Database not ready')
return []
mixin = str(mixin).lower().strip()
@ -344,7 +384,7 @@ class PluginsRegistry:
except MixinNotImplementedError:
continue
config = configs.get(plugin.slug) or plugin.plugin_config()
config = self.get_plugin_config(plugin.slug, configs=configs)
# No config - cannot use this plugin
if not config:
@ -1025,6 +1065,10 @@ class PluginsRegistry:
data = md5()
# Pre-fetch (and cache) the PluginConfig objects, to avoid hitting the
# database once per plugin below
configs = self.get_plugin_configs() or {}
# Hash for all loaded plugins
# Note: Sort by slug, so the hash is independent of discovery order.
# Different processes can discover the same plugins in a different
@ -1034,7 +1078,10 @@ class PluginsRegistry:
data.update(str(slug).encode())
data.update(str(plug.name).encode())
data.update(str(plug.version).encode())
data.update(str(plug.is_active()).encode())
config = configs.get(slug) or self.get_plugin_config(slug)
active = config.is_active() if config else False
data.update(str(active).encode())
for k in self.plugin_settings_keys():
try: