2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-02-19 13:18:03 +00:00

[setup] invoke command updates (#11340)

* invoke command updates

- wait for db before migrating data
- improve task state reporting
- early return from isGeneratingSchema

* Disable warning message (for now)

* Fix typo

- This caused large delay when restoring data

* Remove debug statement

* Add warning message if isGeneratingSchema called falls through unexpectedly
This commit is contained in:
Oliver
2026-02-17 00:22:35 +11:00
committed by GitHub
parent 5f4dd49b12
commit 430dfbbae5
3 changed files with 55 additions and 28 deletions

View File

@@ -84,10 +84,11 @@ class InvenTreeMaintenanceModeBackend(AbstractStateBackend):
r -= 1 r -= 1
if r == 0: # Disable this warning message (for now) as it is confusing users with no upside
logger.warning( # if r == 0:
'Failed to set maintenance mode state after %s retries', retries # logger.warning(
) # 'Failed to set maintenance mode state after %s retries', retries
# )
class InvenTreeMailLoggingBackend(BaseEmailBackend): class InvenTreeMailLoggingBackend(BaseEmailBackend):

View File

@@ -6,6 +6,11 @@ import os
import sys import sys
import warnings import warnings
import structlog
logger = structlog.get_logger('inventree')
# Keep track of loaded apps, to prevent multiple executions of ready functions # Keep track of loaded apps, to prevent multiple executions of ready functions
_loaded_apps = set() _loaded_apps = set()
@@ -33,6 +38,11 @@ def isInTestMode():
return 'test' in sys.argv or sys.argv[0].endswith('pytest') return 'test' in sys.argv or sys.argv[0].endswith('pytest')
def isWaitingForDatabase():
"""Return True if we are currently waiting for the database to be ready."""
return 'wait_for_db' in sys.argv
def isImportingData(): def isImportingData():
"""Returns True if the database is currently importing (or exporting) data, e.g. 'loaddata' command is performed.""" """Returns True if the database is currently importing (or exporting) data, e.g. 'loaddata' command is performed."""
return any(x in sys.argv for x in ['flush', 'loaddata', 'dumpdata']) return any(x in sys.argv for x in ['flush', 'loaddata', 'dumpdata'])
@@ -61,7 +71,7 @@ def isRunningBackup():
'backup', 'backup',
'restore', 'restore',
'dbbackup', 'dbbackup',
'dbresotore', 'dbrestore',
'mediabackup', 'mediabackup',
'mediarestore', 'mediarestore',
] ]
@@ -82,11 +92,23 @@ def isGeneratingSchema():
if isInTestMode(): if isInTestMode():
return False return False
if isWaitingForDatabase():
return False
if 'schema' in sys.argv: if 'schema' in sys.argv:
return True return True
# This is a very inefficient call - so we only use it as a last resort # This is a very inefficient call - so we only use it as a last resort
return any('drf_spectacular' in frame.filename for frame in inspect.stack()) result = any('drf_spectacular' in frame.filename for frame in inspect.stack())
if not result:
# We should only get here if we *are* generating schema
# Any other time this is called, it should be from a server thread, worker thread, or test mode
logger.warning(
'isGeneratingSchema called outside of expected contexts - this may be a sign of a problem with the ready() function'
)
return result
def isInWorkerThread(): def isInWorkerThread():

View File

@@ -139,16 +139,16 @@ def state_logger(fn=None, method_name=None):
"""Decorator to log state markers before/after function execution, optionally accepting arguments.""" """Decorator to log state markers before/after function execution, optionally accepting arguments."""
def decorator(func): def decorator(func):
func.method_name = method_name or f'invoke task named `{func.__name__}`' func.method_name = method_name or func.__name__
@wraps(func) @wraps(func)
def wrapped(c, *args, **kwargs): def wrapped(c, *args, **kwargs):
do_log = is_debug_environment() do_log = is_debug_environment()
if do_log: if do_log:
info(f'# {func.method_name}| start') info(f'# task | {func.method_name} | start')
func(c, *args, **kwargs) func(c, *args, **kwargs)
if do_log: if do_log:
info(f'# {func.method_name}| done') info(f'# task | {func.method_name} | done')
return wrapped return wrapped
@@ -530,10 +530,18 @@ def check_file_existence(filename: Path, overwrite: bool = False):
sys.exit(1) sys.exit(1)
@task
@state_logger
def wait(c):
"""Wait until the database connection is ready."""
info('Waiting for database connection...')
return manage(c, 'wait_for_db')
# Install tasks # Install tasks
# region tasks # region tasks
@task(help={'uv': 'Use UV (experimental package manager)'}) @task(help={'uv': 'Use UV (experimental package manager)'})
@state_logger('TASK01') @state_logger
def plugins(c, uv=False): def plugins(c, uv=False):
"""Installs all plugins as specified in 'plugins.txt'.""" """Installs all plugins as specified in 'plugins.txt'."""
from src.backend.InvenTree.InvenTree.config import ( # type: ignore[import] from src.backend.InvenTree.InvenTree.config import ( # type: ignore[import]
@@ -553,7 +561,7 @@ def plugins(c, uv=False):
'dev': 'Install development requirements instead of production requirements', 'dev': 'Install development requirements instead of production requirements',
} }
) )
@state_logger('TASK02') @state_logger
def install(c, uv=False, skip_plugins=False, dev=False): def install(c, uv=False, skip_plugins=False, dev=False):
"""Installs required python packages.""" """Installs required python packages."""
if dev: if dev:
@@ -639,7 +647,7 @@ def rebuild_thumbnails(c):
@task @task
@state_logger('TASK09') @state_logger
def clean_settings(c): def clean_settings(c):
"""Clean the setting tables of old settings.""" """Clean the setting tables of old settings."""
info('Cleaning old settings from the database') info('Cleaning old settings from the database')
@@ -669,7 +677,7 @@ def remove_mfa(c, mail='', username=''):
'skip_plugins': 'Ignore collection of plugin static files', 'skip_plugins': 'Ignore collection of plugin static files',
} }
) )
@state_logger('TASK08') @state_logger
def static(c, frontend=False, clear=True, skip_plugins=False): def static(c, frontend=False, clear=True, skip_plugins=False):
"""Copies required static files to the STATIC_ROOT directory, as per Django requirements.""" """Copies required static files to the STATIC_ROOT directory, as per Django requirements."""
if frontend and node_available(): if frontend and node_available():
@@ -725,7 +733,7 @@ def translate(c, ignore_static=False, no_frontend=False):
'skip_media': 'Skip media backup step (only backup database files)', 'skip_media': 'Skip media backup step (only backup database files)',
} }
) )
@state_logger('TASK04') @state_logger
def backup( def backup(
c, c,
clean: bool = False, clean: bool = False,
@@ -838,15 +846,15 @@ def restore(
@task() @task()
@state_logger() @state_logger
def listbackups(c): def listbackups(c):
"""List available backup files.""" """List available backup files."""
info('Finding available backup files...') info('Finding available backup files...')
manage(c, 'listbackups') manage(c, 'listbackups')
@task(post=[rebuild_models, rebuild_thumbnails]) @task(pre=[wait], post=[rebuild_models, rebuild_thumbnails])
@state_logger('TASK05') @state_logger
def migrate(c): def migrate(c):
"""Performs database migrations. """Performs database migrations.
@@ -879,7 +887,7 @@ def showmigrations(c, app=''):
'uv': 'Use UV (experimental package manager)', 'uv': 'Use UV (experimental package manager)',
}, },
) )
@state_logger('TASK03') @state_logger
def update( def update(
c, c,
skip_backup: bool = False, skip_backup: bool = False,
@@ -953,7 +961,8 @@ def update(
'include_sso': 'Include SSO token data in the output file (default = False)', 'include_sso': 'Include SSO token data in the output file (default = False)',
'include_session': 'Include user session data in the output file (default = False)', 'include_session': 'Include user session data in the output file (default = False)',
'retain_temp': 'Retain temporary files (containing permissions) at end of process (default = False)', 'retain_temp': 'Retain temporary files (containing permissions) at end of process (default = False)',
} },
pre=[wait],
) )
def export_records( def export_records(
c, c,
@@ -1050,6 +1059,7 @@ def export_records(
'clear': 'Clear existing data before import', 'clear': 'Clear existing data before import',
'retain_temp': 'Retain temporary files at end of process (default = False)', 'retain_temp': 'Retain temporary files at end of process (default = False)',
}, },
pre=[wait],
post=[rebuild_models, rebuild_thumbnails], post=[rebuild_models, rebuild_thumbnails],
) )
def import_records( def import_records(
@@ -1199,12 +1209,6 @@ def import_fixtures(c):
# Execution tasks # Execution tasks
@task
@state_logger('TASK10')
def wait(c):
"""Wait until the database connection is ready."""
info('Waiting for database connection...')
return manage(c, 'wait_for_db')
@task( @task(
@@ -1506,7 +1510,7 @@ def setup_test(
'no_default': 'Do not use default settings for schema (default = off/False)', 'no_default': 'Do not use default settings for schema (default = off/False)',
} }
) )
@state_logger('TASK11') @state_logger
def schema( def schema(
c, filename='schema.yml', overwrite=False, ignore_warnings=False, no_default=False c, filename='schema.yml', overwrite=False, ignore_warnings=False, no_default=False
): ):
@@ -1668,7 +1672,7 @@ def frontend_check(c):
@task(help={'extract': 'Extract translation strings. Default: False'}) @task(help={'extract': 'Extract translation strings. Default: False'})
@state_logger('TASK06') @state_logger
def frontend_compile(c, extract: bool = False): def frontend_compile(c, extract: bool = False):
"""Generate react frontend. """Generate react frontend.
@@ -1781,7 +1785,7 @@ def frontend_test(c, host: str = '0.0.0.0'):
'clean': 'Delete old files from InvenTree/web/static/web first, default: True', 'clean': 'Delete old files from InvenTree/web/static/web first, default: True',
} }
) )
@state_logger('TASK07') @state_logger
def frontend_download( def frontend_download(
c, c,
ref=None, ref=None,