mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	Plugin sequence fix (#10057)
* Fix error message * Add registry ready flag * Add is_ready func * Cleaner init code * Protect against plugin access until ready * Add decorator for registry entrypoint funcs * Tweak return value * Only add plugin URLs if registry is ready * Fix load logic * Refactor @registry_entrypoint decorator * Tweak * Typing * Reimplement is_ready property
This commit is contained in:
		| @@ -9,7 +9,7 @@ from django.conf import settings | |||||||
| from django.contrib.auth import get_user_model | from django.contrib.auth import get_user_model | ||||||
| from django.core.exceptions import AppRegistryNotReady | from django.core.exceptions import AppRegistryNotReady | ||||||
| from django.db import transaction | from django.db import transaction | ||||||
| from django.db.utils import IntegrityError, OperationalError | from django.db.utils import IntegrityError | ||||||
|  |  | ||||||
| import structlog | import structlog | ||||||
| from allauth.socialaccount.signals import social_account_updated | from allauth.socialaccount.signals import social_account_updated | ||||||
| @@ -65,7 +65,8 @@ class InvenTreeConfig(AppConfig): | |||||||
|             self.start_background_tasks() |             self.start_background_tasks() | ||||||
|  |  | ||||||
|             if not InvenTree.ready.isInTestMode():  # pragma: no cover |             if not InvenTree.ready.isInTestMode():  # pragma: no cover | ||||||
|                 self.update_exchange_rates() |                 # Update exchange rates | ||||||
|  |                 InvenTree.tasks.offload_task(InvenTree.tasks.update_exchange_rates) | ||||||
|                 # Let the background worker check for migrations |                 # Let the background worker check for migrations | ||||||
|                 InvenTree.tasks.offload_task(InvenTree.tasks.check_for_migrations) |                 InvenTree.tasks.offload_task(InvenTree.tasks.check_for_migrations) | ||||||
|  |  | ||||||
| @@ -179,6 +180,8 @@ class InvenTreeConfig(AppConfig): | |||||||
|                 InvenTree.tasks.offload_task( |                 InvenTree.tasks.offload_task( | ||||||
|                     InvenTree.tasks.heartbeat, force_async=True, group='heartbeat' |                     InvenTree.tasks.heartbeat, force_async=True, group='heartbeat' | ||||||
|                 ) |                 ) | ||||||
|  |         except AppRegistryNotReady:  # pragma: no cover | ||||||
|  |             pass | ||||||
|         except Exception: |         except Exception: | ||||||
|             pass |             pass | ||||||
|  |  | ||||||
| @@ -194,63 +197,6 @@ class InvenTreeConfig(AppConfig): | |||||||
|                 except Exception as e:  # pragma: no cover |                 except Exception as e:  # pragma: no cover | ||||||
|                     logger.exception('Error loading tasks for %s: %s', app_name, e) |                     logger.exception('Error loading tasks for %s: %s', app_name, e) | ||||||
|  |  | ||||||
|     def update_exchange_rates(self):  # pragma: no cover |  | ||||||
|         """Update exchange rates each time the server is started. |  | ||||||
|  |  | ||||||
|         Only runs *if*: |  | ||||||
|         a) Have not been updated recently (one day or less) |  | ||||||
|         b) The base exchange rate has been altered |  | ||||||
|         """ |  | ||||||
|         try: |  | ||||||
|             from djmoney.contrib.exchange.models import ExchangeBackend |  | ||||||
|  |  | ||||||
|             from common.currency import currency_code_default |  | ||||||
|             from InvenTree.tasks import update_exchange_rates |  | ||||||
|         except AppRegistryNotReady:  # pragma: no cover |  | ||||||
|             pass |  | ||||||
|  |  | ||||||
|         base_currency = currency_code_default() |  | ||||||
|  |  | ||||||
|         update = False |  | ||||||
|  |  | ||||||
|         try: |  | ||||||
|             backend = ExchangeBackend.objects.filter(name='InvenTreeExchange') |  | ||||||
|  |  | ||||||
|             if backend.exists(): |  | ||||||
|                 backend = backend.first() |  | ||||||
|  |  | ||||||
|                 last_update = backend.last_update |  | ||||||
|  |  | ||||||
|                 if last_update is None: |  | ||||||
|                     # Never been updated |  | ||||||
|                     logger.info('Exchange backend has never been updated') |  | ||||||
|                     update = True |  | ||||||
|  |  | ||||||
|                 # Backend currency has changed? |  | ||||||
|                 if base_currency != backend.base_currency: |  | ||||||
|                     logger.info( |  | ||||||
|                         'Base currency changed from %s to %s', |  | ||||||
|                         backend.base_currency, |  | ||||||
|                         base_currency, |  | ||||||
|                     ) |  | ||||||
|                     update = True |  | ||||||
|  |  | ||||||
|         except ExchangeBackend.DoesNotExist: |  | ||||||
|             logger.info('Exchange backend not found - updating') |  | ||||||
|             update = True |  | ||||||
|  |  | ||||||
|         except Exception: |  | ||||||
|             # Some other error - potentially the tables are not ready yet |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         if update: |  | ||||||
|             try: |  | ||||||
|                 update_exchange_rates() |  | ||||||
|             except OperationalError: |  | ||||||
|                 logger.warning('Could not update exchange rates - database not ready') |  | ||||||
|             except Exception as e: |  | ||||||
|                 logger.exception('Error updating exchange rates: %s (%s)', e, type(e)) |  | ||||||
|  |  | ||||||
|     def update_site_url(self): |     def update_site_url(self): | ||||||
|         """Update the site URL setting. |         """Update the site URL setting. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,12 +15,14 @@ import InvenTree.helpers | |||||||
| logger = structlog.get_logger('inventree') | logger = structlog.get_logger('inventree') | ||||||
|  |  | ||||||
|  |  | ||||||
| def currency_code_default(): | def currency_code_default(create: bool = True): | ||||||
|     """Returns the default currency code (or USD if not specified).""" |     """Returns the default currency code (or USD if not specified).""" | ||||||
|     from common.settings import get_global_setting |     from common.settings import get_global_setting | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|         code = get_global_setting('INVENTREE_DEFAULT_CURRENCY', create=True, cache=True) |         code = get_global_setting( | ||||||
|  |             'INVENTREE_DEFAULT_CURRENCY', create=create, cache=True | ||||||
|  |         ) | ||||||
|     except Exception:  # pragma: no cover |     except Exception:  # pragma: no cover | ||||||
|         # Database may not yet be ready, no need to throw an error here |         # Database may not yet be ready, no need to throw an error here | ||||||
|         code = '' |         code = '' | ||||||
|   | |||||||
| @@ -32,23 +32,11 @@ class PluginAppConfig(AppConfig): | |||||||
|         else: |         else: | ||||||
|             logger.info('Loading InvenTree plugins') |             logger.info('Loading InvenTree plugins') | ||||||
|  |  | ||||||
|             if not registry.is_loading: |             if not registry.is_ready: | ||||||
|                 # this is the first startup |                 # Mark the registry as ready | ||||||
|                 try: |                 # This ensures that other apps cannot access the registry before it is fully initialized | ||||||
|                     from common.models import InvenTreeSetting |                 logger.info('Plugin registry is ready - performing initial load') | ||||||
|  |                 registry.set_ready() | ||||||
|                     if InvenTreeSetting.get_setting( |  | ||||||
|                         'PLUGIN_ON_STARTUP', create=False, cache=False |  | ||||||
|                     ): |  | ||||||
|                         # make sure all plugins are installed |  | ||||||
|                         registry.install_plugin_file() |  | ||||||
|                 except Exception:  # pragma: no cover |  | ||||||
|                     pass |  | ||||||
|  |  | ||||||
|                 # Perform a full reload of the plugin registry |  | ||||||
|                 registry.reload_plugins( |  | ||||||
|                     full_reload=True, force_reload=True, collect=True |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|                 # drop out of maintenance |                 # drop out of maintenance | ||||||
|                 # makes sure we did not have an error in reloading and maintenance is still active |                 # makes sure we did not have an error in reloading and maintenance is still active | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
| - Manages setup and teardown of plugin class instances | - Manages setup and teardown of plugin class instances | ||||||
| """ | """ | ||||||
|  |  | ||||||
|  | import functools | ||||||
| import importlib | import importlib | ||||||
| import importlib.util | import importlib.util | ||||||
| import os | import os | ||||||
| @@ -43,6 +44,37 @@ from .plugin import InvenTreePlugin | |||||||
| logger = structlog.get_logger('inventree') | logger = structlog.get_logger('inventree') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def registry_entrypoint(check_reload: bool = True, default_value: Any = None) -> Any: | ||||||
|  |     """Function decorator for registry entrypoints methods. | ||||||
|  |  | ||||||
|  |     - Ensure that the registry is ready before calling the method. | ||||||
|  |     - Check if the registry needs to be reloaded. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def decorator(method): | ||||||
|  |         """Decorator to ensure registry is ready before calling the method.""" | ||||||
|  |  | ||||||
|  |         @functools.wraps(method) | ||||||
|  |         def wrapper(self, *args, **kwargs): | ||||||
|  |             """Wrapper function to ensure registry is ready.""" | ||||||
|  |             if not self.ready: | ||||||
|  |                 logger.warning( | ||||||
|  |                     "Plugin registry is not ready - cannot call method '%s'", | ||||||
|  |                     method.__name__, | ||||||
|  |                 ) | ||||||
|  |                 return default_value | ||||||
|  |  | ||||||
|  |             # Check if the registry needs to be reloaded | ||||||
|  |             if check_reload: | ||||||
|  |                 self.check_reload() | ||||||
|  |  | ||||||
|  |             return method(self, *args, **kwargs) | ||||||
|  |  | ||||||
|  |         return wrapper | ||||||
|  |  | ||||||
|  |     return decorator | ||||||
|  |  | ||||||
|  |  | ||||||
| class PluginsRegistry: | class PluginsRegistry: | ||||||
|     """The PluginsRegistry class.""" |     """The PluginsRegistry class.""" | ||||||
|  |  | ||||||
| @@ -68,6 +100,8 @@ class PluginsRegistry: | |||||||
|         'parameter-exporter', |         'parameter-exporter', | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |     ready: bool | ||||||
|  |  | ||||||
|     def __init__(self) -> None: |     def __init__(self) -> None: | ||||||
|         """Initialize registry. |         """Initialize registry. | ||||||
|  |  | ||||||
| @@ -82,6 +116,8 @@ class PluginsRegistry: | |||||||
|             str, InvenTreePlugin |             str, InvenTreePlugin | ||||||
|         ] = {}  # List of all plugin instances |         ] = {}  # List of all plugin instances | ||||||
|  |  | ||||||
|  |         self.ready = False  # Marks if the registry is ready to be used | ||||||
|  |  | ||||||
|         # Keep an internal hash of the plugin registry state |         # Keep an internal hash of the plugin registry state | ||||||
|         self.registry_hash = None |         self.registry_hash = None | ||||||
|  |  | ||||||
| @@ -100,11 +136,35 @@ class PluginsRegistry: | |||||||
|  |  | ||||||
|         self.installed_apps = []  # Holds all added plugin_paths |         self.installed_apps = []  # Holds all added plugin_paths | ||||||
|  |  | ||||||
|  |     def set_ready(self): | ||||||
|  |         """Set the registry as ready to be used. | ||||||
|  |  | ||||||
|  |         This method should only be called once per application start, | ||||||
|  |         after all apps have been loaded and the registry is fully initialized. | ||||||
|  |         """ | ||||||
|  |         from common.models import InvenTreeSetting | ||||||
|  |  | ||||||
|  |         self.ready = True | ||||||
|  |  | ||||||
|  |         # Install plugins from file (if required) | ||||||
|  |         if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP', create=False, cache=False): | ||||||
|  |             # make sure all plugins are installed | ||||||
|  |             registry.install_plugin_file() | ||||||
|  |  | ||||||
|  |         # Perform initial plugin discovery | ||||||
|  |         self.reload_plugins(full_reload=True, force_reload=True, collect=True) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def is_ready(self) -> bool: | ||||||
|  |         """Return True if the plugin registry is ready to be used.""" | ||||||
|  |         return self.ready | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def is_loading(self) -> bool: |     def is_loading(self) -> bool: | ||||||
|         """Return True if the plugin registry is currently loading.""" |         """Return True if the plugin registry is currently loading.""" | ||||||
|         return self.loading_lock.locked() |         return self.loading_lock.locked() | ||||||
|  |  | ||||||
|  |     @registry_entrypoint() | ||||||
|     def get_plugin( |     def get_plugin( | ||||||
|         self, slug: str, active: bool = True, with_mixin: Optional[str] = None |         self, slug: str, active: bool = True, with_mixin: Optional[str] = None | ||||||
|     ) -> InvenTreePlugin: |     ) -> InvenTreePlugin: | ||||||
| @@ -118,9 +178,6 @@ class PluginsRegistry: | |||||||
|         Returns: |         Returns: | ||||||
|             InvenTreePlugin or None: The plugin instance if found, otherwise None. |             InvenTreePlugin or None: The plugin instance if found, otherwise None. | ||||||
|         """ |         """ | ||||||
|         # Check if the registry needs to be reloaded |  | ||||||
|         self.check_reload() |  | ||||||
|  |  | ||||||
|         if slug not in self.plugins: |         if slug not in self.plugins: | ||||||
|             logger.warning("Plugin registry has no record of plugin '%s'", slug) |             logger.warning("Plugin registry has no record of plugin '%s'", slug) | ||||||
|             return None |             return None | ||||||
| @@ -169,6 +226,7 @@ class PluginsRegistry: | |||||||
|  |  | ||||||
|         return cfg |         return cfg | ||||||
|  |  | ||||||
|  |     @registry_entrypoint() | ||||||
|     def set_plugin_state(self, slug: str, state: bool): |     def set_plugin_state(self, slug: str, state: bool): | ||||||
|         """Set the state(active/inactive) of a plugin. |         """Set the state(active/inactive) of a plugin. | ||||||
|  |  | ||||||
| @@ -176,9 +234,6 @@ class PluginsRegistry: | |||||||
|             slug (str): Plugin slug |             slug (str): Plugin slug | ||||||
|             state (bool): Plugin state - true = active, false = inactive |             state (bool): Plugin state - true = active, false = inactive | ||||||
|         """ |         """ | ||||||
|         # Check if the registry needs to be reloaded |  | ||||||
|         self.check_reload() |  | ||||||
|  |  | ||||||
|         if slug not in self.plugins_full: |         if slug not in self.plugins_full: | ||||||
|             logger.warning("Plugin registry has no record of plugin '%s'", slug) |             logger.warning("Plugin registry has no record of plugin '%s'", slug) | ||||||
|             return |             return | ||||||
| @@ -190,6 +245,7 @@ class PluginsRegistry: | |||||||
|         # Update the registry hash value |         # Update the registry hash value | ||||||
|         self.update_plugin_hash() |         self.update_plugin_hash() | ||||||
|  |  | ||||||
|  |     @registry_entrypoint() | ||||||
|     def call_plugin_function(self, slug: str, func: str, *args, **kwargs): |     def call_plugin_function(self, slug: str, func: str, *args, **kwargs): | ||||||
|         """Call a member function (named by 'func') of the plugin named by 'slug'. |         """Call a member function (named by 'func') of the plugin named by 'slug'. | ||||||
|  |  | ||||||
| @@ -198,9 +254,6 @@ class PluginsRegistry: | |||||||
|  |  | ||||||
|         Instead, any error messages are returned to the worker. |         Instead, any error messages are returned to the worker. | ||||||
|         """ |         """ | ||||||
|         # Check if the registry needs to be reloaded |  | ||||||
|         self.check_reload() |  | ||||||
|  |  | ||||||
|         raise_error = kwargs.pop('raise_error', True) |         raise_error = kwargs.pop('raise_error', True) | ||||||
|  |  | ||||||
|         plugin = self.get_plugin(slug) |         plugin = self.get_plugin(slug) | ||||||
| @@ -220,6 +273,8 @@ class PluginsRegistry: | |||||||
|         return plugin_func(*args, **kwargs) |         return plugin_func(*args, **kwargs) | ||||||
|  |  | ||||||
|     # region registry functions |     # region registry functions | ||||||
|  |  | ||||||
|  |     @registry_entrypoint(default_value=[]) | ||||||
|     def with_mixin( |     def with_mixin( | ||||||
|         self, mixin: str, active: bool = True, builtin: Optional[bool] = None |         self, mixin: str, active: bool = True, builtin: Optional[bool] = None | ||||||
|     ) -> list[InvenTreePlugin]: |     ) -> list[InvenTreePlugin]: | ||||||
| @@ -230,9 +285,6 @@ class PluginsRegistry: | |||||||
|             active (bool, optional): Filter by 'active' status of plugin. Defaults to True. |             active (bool, optional): Filter by 'active' status of plugin. Defaults to True. | ||||||
|             builtin (bool, optional): Filter by 'builtin' status of plugin. Defaults to None. |             builtin (bool, optional): Filter by 'builtin' status of plugin. Defaults to None. | ||||||
|         """ |         """ | ||||||
|         # Check if the registry needs to be loaded |  | ||||||
|         self.check_reload() |  | ||||||
|  |  | ||||||
|         mixin = str(mixin).lower().strip() |         mixin = str(mixin).lower().strip() | ||||||
|  |  | ||||||
|         result = [] |         result = [] | ||||||
| @@ -304,6 +356,7 @@ class PluginsRegistry: | |||||||
|  |  | ||||||
|         logger.info('Finished unloading plugins') |         logger.info('Finished unloading plugins') | ||||||
|  |  | ||||||
|  |     @registry_entrypoint(check_reload=False) | ||||||
|     def reload_plugins( |     def reload_plugins( | ||||||
|         self, |         self, | ||||||
|         full_reload: bool = False, |         full_reload: bool = False, | ||||||
| @@ -369,7 +422,7 @@ class PluginsRegistry: | |||||||
|             logger.info('Plugin Registry: Loaded %s plugins', len(self.plugins)) |             logger.info('Plugin Registry: Loaded %s plugins', len(self.plugins)) | ||||||
|  |  | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             logger.exception('Expected error during plugin reload: %s', e) |             logger.exception('Unexpected error during plugin reload: %s', e) | ||||||
|             log_error('reload_plugins', scope='plugins') |             log_error('reload_plugins', scope='plugins') | ||||||
|  |  | ||||||
|         finally: |         finally: | ||||||
| @@ -900,6 +953,7 @@ class PluginsRegistry: | |||||||
|  |  | ||||||
|         return str(data.hexdigest()) |         return str(data.hexdigest()) | ||||||
|  |  | ||||||
|  |     @registry_entrypoint(default_value=False, check_reload=False) | ||||||
|     def check_reload(self): |     def check_reload(self): | ||||||
|         """Determine if the registry needs to be reloaded. |         """Determine if the registry needs to be reloaded. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -18,7 +18,11 @@ def get_plugin_urls(): | |||||||
|  |  | ||||||
|     urls = [] |     urls = [] | ||||||
|  |  | ||||||
|     if get_global_setting('ENABLE_PLUGINS_URL', False) or settings.PLUGIN_TESTING_SETUP: |     if registry.is_ready: | ||||||
|  |         if ( | ||||||
|  |             get_global_setting('ENABLE_PLUGINS_URL', False) | ||||||
|  |             or settings.PLUGIN_TESTING_SETUP | ||||||
|  |         ): | ||||||
|             for plugin in registry.with_mixin(PluginMixinEnum.URLS): |             for plugin in registry.with_mixin(PluginMixinEnum.URLS): | ||||||
|                 try: |                 try: | ||||||
|                     if plugin_urls := plugin.urlpatterns: |                     if plugin_urls := plugin.urlpatterns: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user