From 7973057ad9cbb1b7e93abc32bdf62936eccd87fe Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 21 Jul 2026 10:23:50 +1000 Subject: [PATCH] Improve request caching for plugin config objects (#12430) * Improve request caching for plugin config objects * Cache configs dict further --- src/backend/InvenTree/plugin/registry.py | 123 ++++++++++++++++------- 1 file changed, 85 insertions(+), 38 deletions(-) diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py index 61b0d62607..6ee265a8c7 100644 --- a/src/backend/InvenTree/plugin/registry.py +++ b/src/backend/InvenTree/plugin/registry.py @@ -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: