From c443b4e9b8597c06008179edd00cd9716fa3a0de Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 4 Dec 2025 19:30:14 +1100 Subject: [PATCH] App ready warning (#10938) * Fix for currency functions - Prevent database access until after the 'common' app has loaded * Add decorator to selectively ignore warnings * Add reference to PR * Fix variable assignment * Use functools.wraps * Add wrapper for loading machine registry * Move decorator to ready.py * Add missing code * Set backup values to match default currency codes * Bump API version --- .../InvenTree/InvenTree/api_version.py | 6 ++- src/backend/InvenTree/InvenTree/apps.py | 17 ++++++-- src/backend/InvenTree/InvenTree/ready.py | 42 +++++++++++++++++++ src/backend/InvenTree/common/apps.py | 6 +++ src/backend/InvenTree/common/currency.py | 30 ++++++++----- src/backend/InvenTree/machine/apps.py | 16 ++++--- src/backend/InvenTree/plugin/apps.py | 8 +++- tasks.py | 2 +- 8 files changed, 105 insertions(+), 22 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index f198138e53..c220b37925 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,15 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 428 +INVENTREE_API_VERSION = 429 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v429 -> 2025-12-04 : https://github.com/inventree/InvenTree/pull/10938 + - Adjust default values for currency codes in the API schema + - Note that this does not change any functional behavior, only the schema documentation + v428 -> 2025-11-28 : https://github.com/inventree/InvenTree/pull/10926 - Various typo fixes in API - no functional changes diff --git a/src/backend/InvenTree/InvenTree/apps.py b/src/backend/InvenTree/InvenTree/apps.py index 9d94ba5843..16e634e023 100644 --- a/src/backend/InvenTree/InvenTree/apps.py +++ b/src/backend/InvenTree/InvenTree/apps.py @@ -18,6 +18,7 @@ import InvenTree.conversion import InvenTree.ready import InvenTree.tasks from InvenTree.config import get_setting +from InvenTree.ready import ignore_ready_warning logger = structlog.get_logger('inventree') MIGRATIONS_CHECK_DONE = False @@ -70,9 +71,7 @@ class InvenTreeConfig(AppConfig): InvenTree.tasks.offload_task(InvenTree.tasks.check_for_migrations) self.update_site_url() - - # Ensure the unit registry is loaded - InvenTree.conversion.get_unit_registry() + self.load_unit_registry() if InvenTree.ready.canAppAccessDatabase() or settings.TESTING_ENV: self.add_user_on_startup() @@ -84,6 +83,7 @@ class InvenTreeConfig(AppConfig): social_account_updated.connect(sso.ensure_sso_groups) + @ignore_ready_warning def remove_obsolete_tasks(self): """Delete any obsolete scheduled tasks in the database.""" obsolete = [ @@ -112,6 +112,7 @@ class InvenTreeConfig(AppConfig): except Exception: logger.exception('Failed to remove obsolete tasks - database not ready') + @ignore_ready_warning def start_background_tasks(self): """Start all background tests for InvenTree.""" logger.info('Starting background tasks...') @@ -171,6 +172,7 @@ class InvenTreeConfig(AppConfig): logger.info('Started %s scheduled background tasks...', len(tasks)) + @ignore_ready_warning def add_heartbeat(self): """Ensure there is at least one background task in the queue.""" import django_q.models @@ -185,6 +187,7 @@ class InvenTreeConfig(AppConfig): except Exception: pass + @ignore_ready_warning def collect_tasks(self): """Collect all background tasks.""" for app_name, app in apps.app_configs.items(): @@ -197,6 +200,7 @@ class InvenTreeConfig(AppConfig): except Exception as e: # pragma: no cover logger.exception('Error loading tasks for %s: %s', app_name, e) + @ignore_ready_warning def update_site_url(self): """Update the site URL setting. @@ -223,6 +227,12 @@ class InvenTreeConfig(AppConfig): except Exception: pass + @ignore_ready_warning + def load_unit_registry(self): + """Ensure the unit registry is loaded.""" + InvenTree.conversion.get_unit_registry() + + @ignore_ready_warning def add_user_on_startup(self): """Add a user on startup.""" # stop if checks were already created @@ -281,6 +291,7 @@ class InvenTreeConfig(AppConfig): except IntegrityError: logger.warning('The user "%s" could not be created', add_user) + @ignore_ready_warning def add_user_from_file(self): """Add the superuser from a file.""" # stop if checks were already created diff --git a/src/backend/InvenTree/InvenTree/ready.py b/src/backend/InvenTree/InvenTree/ready.py index fd4aa565d6..2fe3cf5846 100644 --- a/src/backend/InvenTree/InvenTree/ready.py +++ b/src/backend/InvenTree/InvenTree/ready.py @@ -1,8 +1,31 @@ """Functions to check if certain parts of InvenTree are ready.""" +import functools import inspect import os import sys +import warnings + +# Keep track of loaded apps, to prevent multiple executions of ready functions +_loaded_apps = set() + + +def clearLoadedApps(): + """Clear the set of loaded apps.""" + global _loaded_apps + _loaded_apps = set() + + +def setAppLoaded(app_name: str): + """Mark an app as loaded.""" + global _loaded_apps + _loaded_apps.add(app_name) + + +def isAppLoaded(app_name: str) -> bool: + """Return True if the app has been marked as loaded.""" + global _loaded_apps + return app_name in _loaded_apps def isInTestMode(): @@ -157,3 +180,22 @@ def isPluginRegistryLoaded(): from plugin import registry return registry.plugins_loaded + + +def ignore_ready_warning(func): + """Decorator to ignore 'AppRegistryNotReady' warnings in functions called during app ready phase. + + Ref: https://github.com/inventree/InvenTree/issues/10806 + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + with warnings.catch_warnings(): + warnings.filterwarnings( + 'ignore', + message='Accessing the database during app initialization is discouraged', + category=RuntimeWarning, + ) + return func(*args, **kwargs) + + return wrapper diff --git a/src/backend/InvenTree/common/apps.py b/src/backend/InvenTree/common/apps.py index d795c7136c..ad1f5c321e 100644 --- a/src/backend/InvenTree/common/apps.py +++ b/src/backend/InvenTree/common/apps.py @@ -6,6 +6,7 @@ import structlog import InvenTree.ready from common.settings import get_global_setting, set_global_setting +from InvenTree.ready import ignore_ready_warning logger = structlog.get_logger('inventree') @@ -20,11 +21,16 @@ class CommonConfig(AppConfig): def ready(self): """Initialize restart flag clearance on startup.""" + from InvenTree.ready import setAppLoaded + + setAppLoaded(self.name) + if InvenTree.ready.isRunningMigrations(): # pragma: no cover return self.clear_restart_flag() + @ignore_ready_warning def clear_restart_flag(self): """Clear the SERVER_RESTART_REQUIRED setting.""" try: diff --git a/src/backend/InvenTree/common/currency.py b/src/backend/InvenTree/common/currency.py index 307e54d6d5..6146c321c7 100644 --- a/src/backend/InvenTree/common/currency.py +++ b/src/backend/InvenTree/common/currency.py @@ -11,6 +11,7 @@ import structlog from moneyed import CURRENCIES import InvenTree.helpers +import InvenTree.ready logger = structlog.get_logger('inventree') @@ -19,15 +20,18 @@ def currency_code_default(create: bool = True): """Returns the default currency code (or USD if not specified).""" from common.settings import get_global_setting - try: - code = get_global_setting( - 'INVENTREE_DEFAULT_CURRENCY', create=create, cache=True - ) - except Exception: # pragma: no cover - # Database may not yet be ready, no need to throw an error here - code = '' + code = '' - if code not in CURRENCIES: + if InvenTree.ready.isAppLoaded('common'): + try: + code = get_global_setting( + 'INVENTREE_DEFAULT_CURRENCY', create=create, cache=True + ) + except Exception: # pragma: no cover + # Database may not yet be ready, no need to throw an error here + code = '' + + if not code or code not in CURRENCIES: code = 'USD' # pragma: no cover return code @@ -47,9 +51,13 @@ def currency_codes() -> list: """Returns the current currency codes.""" from common.settings import get_global_setting - codes = get_global_setting( - 'CURRENCY_CODES', create=False, environment_key='INVENTREE_CURRENCY_CODES' - ).strip() + codes = None + + # Ensure we do not hit the database until the common app is loaded + if InvenTree.ready.isAppLoaded('common'): + codes = get_global_setting( + 'CURRENCY_CODES', create=False, environment_key='INVENTREE_CURRENCY_CODES' + ).strip() if not codes: codes = currency_codes_default_list() diff --git a/src/backend/InvenTree/machine/apps.py b/src/backend/InvenTree/machine/apps.py index 95ab925dc1..7130393cce 100755 --- a/src/backend/InvenTree/machine/apps.py +++ b/src/backend/InvenTree/machine/apps.py @@ -7,6 +7,7 @@ import structlog from InvenTree.ready import ( canAppAccessDatabase, + ignore_ready_warning, isImportingData, isInMainThread, isPluginRegistryLoaded, @@ -32,12 +33,17 @@ class MachineConfig(AppConfig): logger.debug('Machine app: Skipping machine loading sequence') return - from machine import registry - try: - logger.info('Loading InvenTree machines') - if not registry.is_ready: - registry.initialize(main=isInMainThread()) + self.initialize_registry() except (OperationalError, ProgrammingError): # Database might not yet be ready logger.warn('Database was not ready for initializing machines') + + @ignore_ready_warning + def initialize_registry(self): + """Initialize the machine registry.""" + from machine import registry + + if not registry.is_ready: + logger.info('Loading InvenTree machines') + registry.initialize(main=isInMainThread()) diff --git a/src/backend/InvenTree/plugin/apps.py b/src/backend/InvenTree/plugin/apps.py index f37e91d006..86655cf70b 100644 --- a/src/backend/InvenTree/plugin/apps.py +++ b/src/backend/InvenTree/plugin/apps.py @@ -9,7 +9,12 @@ from django.apps import AppConfig import structlog from maintenance_mode.core import set_maintenance_mode -from InvenTree.ready import canAppAccessDatabase, isInMainThread, isInWorkerThread +from InvenTree.ready import ( + canAppAccessDatabase, + ignore_ready_warning, + isInMainThread, + isInWorkerThread, +) from plugin import registry logger = structlog.get_logger('inventree') @@ -24,6 +29,7 @@ class PluginAppConfig(AppConfig): """The ready method is extended to initialize plugins.""" self.reload_plugin_registry() + @ignore_ready_warning def reload_plugin_registry(self): """Reload the plugin registry.""" if not isInMainThread() and not isInWorkerThread(): diff --git a/tasks.py b/tasks.py index 7dbc5ca68a..a1941c2ca6 100644 --- a/tasks.py +++ b/tasks.py @@ -1466,7 +1466,7 @@ def schema( 'False' # Disable plugins to ensure they are kep out of schema ) envs['INVENTREE_CURRENCY_CODES'] = ( - 'AUD,CNY,EUR,USD' # Default currency codes to ensure they are stable + 'AUD,CAD,CNY,EUR,GBP,JPY,NZD,USD' # Default currency codes to ensure they are stable ) manage(c, cmd, pty=True, env=envs)