2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-27 19:16:44 +00:00

feat(backend): clearer behaviour on missing migrations (#9527)

* feat(backend): better warning on missing migrations

* add debug info to some tasks

* ensure db would even be accessed before raising concerns

* add more markers

* Add decorator to log flow

* reduce calls

* reduce fnc down
This commit is contained in:
Matthias Mair 2025-04-19 00:03:16 +02:00 committed by GitHub
parent 9890246180
commit 9a49c9f19c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 73 additions and 3 deletions

View File

@ -1,5 +1,6 @@
"""AppConfig for InvenTree app."""
import sys
from importlib import import_module
from pathlib import Path
@ -20,6 +21,7 @@ from common.settings import get_global_setting, set_global_setting
from InvenTree.config import get_setting
logger = structlog.get_logger('inventree')
MIGRATIONS_CHECK_DONE = False
class InvenTreeConfig(AppConfig):
@ -56,6 +58,9 @@ class InvenTreeConfig(AppConfig):
return
if InvenTree.ready.canAppAccessDatabase() or settings.TESTING_ENV:
# Ensure there are no open migrations
self.ensure_migrations_done()
self.remove_obsolete_tasks()
self.collect_tasks()
self.start_background_tasks()
@ -382,3 +387,14 @@ class InvenTreeConfig(AppConfig):
from generic.states import storage
storage.collect()
def ensure_migrations_done(self=None):
"""Ensures there are no open migrations, stop if inconsistent state."""
global MIGRATIONS_CHECK_DONE
if MIGRATIONS_CHECK_DONE:
return
if not InvenTree.tasks.check_for_migrations():
logger.error('INVE-W8: Database Migrations required')
sys.exit(1)
MIGRATIONS_CHECK_DONE = True

View File

@ -629,10 +629,12 @@ def get_migration_plan():
@scheduled_task(ScheduledTask.DAILY)
def check_for_migrations(force: bool = False, reload_registry: bool = True):
def check_for_migrations(force: bool = False, reload_registry: bool = True) -> bool:
"""Checks if migrations are needed.
If the setting auto_update is enabled we will start updating.
Returns bool indicating if migrations are up to date
"""
from plugin import registry
@ -654,14 +656,14 @@ def check_for_migrations(force: bool = False, reload_registry: bool = True):
# Check if there are any open migrations
if not plan:
set_pending_migrations(0)
return
return True
set_pending_migrations(n)
# Test if auto-updates are enabled
if not force and not get_setting('INVENTREE_AUTO_UPDATE', 'auto_update'):
logger.info('Auto-update is disabled - skipping migrations')
return
return False
# Log open migrations
for migration in plan:
@ -696,6 +698,8 @@ def check_for_migrations(force: bool = False, reload_registry: bool = True):
# are loaded fully in their new state.
registry.reload_plugins(full_reload=True, force_reload=True, collect=True)
return True
def email_user(user_id: int, subject: str, message: str) -> None:
"""Send a message to a user."""

View File

@ -6,6 +6,7 @@ from django.db.utils import OperationalError, ProgrammingError
import structlog
import InvenTree.ready
from InvenTree.apps import InvenTreeConfig
logger = structlog.get_logger('inventree')
@ -28,6 +29,9 @@ class PartConfig(AppConfig):
return
if InvenTree.ready.canAppAccessDatabase():
# Ensure there are no open migrations
InvenTreeConfig.ensure_migrations_done()
self.update_trackable_status()
self.reset_part_pricing_flags()

View File

@ -7,6 +7,7 @@ import re
import shutil
import subprocess
import sys
from functools import wraps
from pathlib import Path
from platform import python_version
from typing import Optional
@ -30,6 +31,15 @@ def is_rtd_environment():
return is_true(os.environ.get('READTHEDOCS', 'False'))
def is_deb_environment():
"""Check if the InvenTree environment is running in a debug environment."""
from src.backend.InvenTree.InvenTree.config import is_true
return is_true(os.environ.get('INVENTREE_DEBUG', 'False')) or is_true(
os.environ.get('RUNNER_DEBUG', 'False')
)
def task_exception_handler(t, v, tb):
"""Handle exceptions raised by tasks.
@ -82,6 +92,30 @@ def info(*args):
print(wrap_color(msg, '94'))
def state_logger(fn=None, method_name=None):
"""Decorator to log state markers before/after function execution, optionally accepting arguments."""
def decorator(func):
func.method_name = method_name or f'invoke task named `{func.__name__}`'
@wraps(func)
def wrapped(c, *args, **kwargs):
do_log = is_deb_environment()
if do_log:
info(f'# {func.method_name}| start')
func(c, *args, **kwargs)
if do_log:
info(f'# {func.method_name}| done')
return wrapped
if fn and callable(fn):
return decorator(fn)
elif fn and isinstance(fn, str):
method_name = fn
return decorator
def checkInvokeVersion():
"""Check that the installed invoke version meets minimum requirements."""
MIN_INVOKE_VERSION = '2.0.0'
@ -315,6 +349,7 @@ def node_available(versions: bool = False, bypass_yarn: bool = False):
return ret(yarn_passes and node_version, node_version, yarn_version)
@state_logger
def check_file_existence(filename: Path, overwrite: bool = False):
"""Checks if a file exists and asks the user if it should be overwritten.
@ -335,6 +370,7 @@ def check_file_existence(filename: Path, overwrite: bool = False):
# Install tasks
@task(help={'uv': 'Use UV (experimental package manager)'})
@state_logger('TSK01')
def plugins(c, uv=False):
"""Installs all plugins as specified in 'plugins.txt'."""
from src.backend.InvenTree.InvenTree.config import get_plugin_file
@ -360,6 +396,7 @@ def plugins(c, uv=False):
'skip_plugins': 'Skip plugin installation',
}
)
@state_logger('TSK02')
def install(c, uv=False, skip_plugins=False):
"""Installs required python packages."""
# Ensure path is relative to *this* directory
@ -447,6 +484,7 @@ def rebuild_thumbnails(c):
@task
@state_logger('TSK09')
def clean_settings(c):
"""Clean the setting tables of old settings."""
info('Cleaning old settings from the database')
@ -471,6 +509,7 @@ def remove_mfa(c, mail=''):
'skip_plugins': 'Ignore collection of plugin static files',
}
)
@state_logger('TSK08')
def static(c, frontend=False, clear=True, skip_plugins=False):
"""Copies required static files to the STATIC_ROOT directory, as per Django requirements."""
if frontend and node_available():
@ -521,6 +560,7 @@ def translate(c, ignore_static=False, no_frontend=False):
'path': 'Specify path for generated backup files (leave blank for default path)',
}
)
@state_logger('TSK04')
def backup(c, clean=False, path=None):
"""Backup the database and media files."""
info('Backing up InvenTree database...')
@ -597,6 +637,7 @@ def restore(
@task(post=[rebuild_models, rebuild_thumbnails])
@state_logger('TSK05')
def migrate(c):
"""Performs database migrations.
@ -629,6 +670,7 @@ def showmigrations(c, app=''):
'uv': 'Use UV (experimental package manager)',
},
)
@state_logger('TSK03')
def update(
c,
skip_backup: bool = False,
@ -935,6 +977,7 @@ def import_fixtures(c):
# Execution tasks
@task
@state_logger('TSK10')
def wait(c):
"""Wait until the database connection is ready."""
info('Waiting for database connection...')
@ -1224,6 +1267,7 @@ def setup_test(
'no_default': 'Do not use default settings for schema (default = off/False)',
}
)
@state_logger('TSK11')
def schema(
c, filename='schema.yml', overwrite=False, ignore_warnings=False, no_default=False
):
@ -1366,6 +1410,7 @@ def frontend_check(c):
@task
@state_logger('TSK06')
def frontend_compile(c):
"""Generate react frontend.
@ -1437,6 +1482,7 @@ def frontend_server(c):
'clean': 'Delete old files from InvenTree/web/static/web first, default: True',
}
)
@state_logger('TSK07')
def frontend_download(
c,
ref=None,