mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Maintenance Mode Improvements (#6451)
* Custom migration step in tasks.py - Add custom management command - Wraps migration step in maintenance mode * Rename custom management command to "runmigrations" - Add command to isRunningMigrations * Add new data checks * Update database readiness checks - Set maintenance mode while performing certain management commands * Remove unused import * Re-add syncdb command * Log warning msg * Catch another potential error vector
This commit is contained in:
		| @@ -8,7 +8,7 @@ from django.db.utils import IntegrityError, OperationalError, ProgrammingError | |||||||
| from maintenance_mode.backends import AbstractStateBackend | from maintenance_mode.backends import AbstractStateBackend | ||||||
|  |  | ||||||
| import common.models | import common.models | ||||||
| import InvenTree.helpers | import InvenTree.ready | ||||||
|  |  | ||||||
| logger = logging.getLogger('inventree') | logger = logging.getLogger('inventree') | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,60 +3,73 @@ | |||||||
| - This is crucial after importing any fixtures, etc | - This is crucial after importing any fixtures, etc | ||||||
| """ | """ | ||||||
|  |  | ||||||
|  | import logging | ||||||
|  |  | ||||||
| from django.core.management.base import BaseCommand | from django.core.management.base import BaseCommand | ||||||
|  |  | ||||||
|  | from maintenance_mode.core import maintenance_mode_on, set_maintenance_mode | ||||||
|  |  | ||||||
|  | logger = logging.getLogger('inventree') | ||||||
|  |  | ||||||
|  |  | ||||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||||
|     """Rebuild all database models which leverage the MPTT structure.""" |     """Rebuild all database models which leverage the MPTT structure.""" | ||||||
|  |  | ||||||
|     def handle(self, *args, **kwargs): |     def handle(self, *args, **kwargs): | ||||||
|         """Rebuild all database models which leverage the MPTT structure.""" |         """Rebuild all database models which leverage the MPTT structure.""" | ||||||
|  |         with maintenance_mode_on(): | ||||||
|  |             self.rebuild_models() | ||||||
|  |  | ||||||
|  |         set_maintenance_mode(False) | ||||||
|  |  | ||||||
|  |     def rebuild_models(self): | ||||||
|  |         """Rebuild all MPTT models in the database.""" | ||||||
|         # Part model |         # Part model | ||||||
|         try: |         try: | ||||||
|             print('Rebuilding Part objects') |             logger.info('Rebuilding Part objects') | ||||||
|  |  | ||||||
|             from part.models import Part |             from part.models import Part | ||||||
|  |  | ||||||
|             Part.objects.rebuild() |             Part.objects.rebuild() | ||||||
|         except Exception: |         except Exception: | ||||||
|             print('Error rebuilding Part objects') |             logger.info('Error rebuilding Part objects') | ||||||
|  |  | ||||||
|         # Part category |         # Part category | ||||||
|         try: |         try: | ||||||
|             print('Rebuilding PartCategory objects') |             logger.info('Rebuilding PartCategory objects') | ||||||
|  |  | ||||||
|             from part.models import PartCategory |             from part.models import PartCategory | ||||||
|  |  | ||||||
|             PartCategory.objects.rebuild() |             PartCategory.objects.rebuild() | ||||||
|         except Exception: |         except Exception: | ||||||
|             print('Error rebuilding PartCategory objects') |             logger.info('Error rebuilding PartCategory objects') | ||||||
|  |  | ||||||
|         # StockItem model |         # StockItem model | ||||||
|         try: |         try: | ||||||
|             print('Rebuilding StockItem objects') |             logger.info('Rebuilding StockItem objects') | ||||||
|  |  | ||||||
|             from stock.models import StockItem |             from stock.models import StockItem | ||||||
|  |  | ||||||
|             StockItem.objects.rebuild() |             StockItem.objects.rebuild() | ||||||
|         except Exception: |         except Exception: | ||||||
|             print('Error rebuilding StockItem objects') |             logger.info('Error rebuilding StockItem objects') | ||||||
|  |  | ||||||
|         # StockLocation model |         # StockLocation model | ||||||
|         try: |         try: | ||||||
|             print('Rebuilding StockLocation objects') |             logger.info('Rebuilding StockLocation objects') | ||||||
|  |  | ||||||
|             from stock.models import StockLocation |             from stock.models import StockLocation | ||||||
|  |  | ||||||
|             StockLocation.objects.rebuild() |             StockLocation.objects.rebuild() | ||||||
|         except Exception: |         except Exception: | ||||||
|             print('Error rebuilding StockLocation objects') |             logger.info('Error rebuilding StockLocation objects') | ||||||
|  |  | ||||||
|         # Build model |         # Build model | ||||||
|         try: |         try: | ||||||
|             print('Rebuilding Build objects') |             logger.info('Rebuilding Build objects') | ||||||
|  |  | ||||||
|             from build.models import Build |             from build.models import Build | ||||||
|  |  | ||||||
|             Build.objects.rebuild() |             Build.objects.rebuild() | ||||||
|         except Exception: |         except Exception: | ||||||
|             print('Error rebuilding Build objects') |             logger.info('Error rebuilding Build objects') | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								InvenTree/InvenTree/management/commands/runmigrations.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								InvenTree/InvenTree/management/commands/runmigrations.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | """Check if there are any pending database migrations, and run them.""" | ||||||
|  |  | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | from django.core.management.base import BaseCommand | ||||||
|  |  | ||||||
|  | from InvenTree.tasks import check_for_migrations | ||||||
|  |  | ||||||
|  | logger = logging.getLogger('inventree') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Command(BaseCommand): | ||||||
|  |     """Check if there are any pending database migrations, and run them.""" | ||||||
|  |  | ||||||
|  |     def handle(self, *args, **kwargs): | ||||||
|  |         """Check for any pending database migrations.""" | ||||||
|  |         logger.info('Checking for pending database migrations') | ||||||
|  |         check_for_migrations(force=True, reload_registry=False) | ||||||
|  |         logger.info('Database migrations complete') | ||||||
| @@ -10,13 +10,45 @@ def isInTestMode(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def isImportingData(): | def isImportingData(): | ||||||
|     """Returns True if the database is currently importing 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 'loaddata' in sys.argv |     return any((x in sys.argv for x in ['flush', 'loaddata', 'dumpdata'])) | ||||||
|  |  | ||||||
|  |  | ||||||
| def isRunningMigrations(): | def isRunningMigrations(): | ||||||
|     """Return True if the database is currently running migrations.""" |     """Return True if the database is currently running migrations.""" | ||||||
|     return any((x in sys.argv for x in ['migrate', 'makemigrations', 'showmigrations'])) |     return any( | ||||||
|  |         ( | ||||||
|  |             x in sys.argv | ||||||
|  |             for x in ['migrate', 'makemigrations', 'showmigrations', 'runmigrations'] | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def isRebuildingData(): | ||||||
|  |     """Return true if any of the rebuilding commands are being executed.""" | ||||||
|  |     return any( | ||||||
|  |         ( | ||||||
|  |             x in sys.argv | ||||||
|  |             for x in ['prerender', 'rebuild_models', 'rebuild_thumbnails', 'rebuild'] | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def isRunningBackup(): | ||||||
|  |     """Return true if any of the backup commands are being executed.""" | ||||||
|  |     return any( | ||||||
|  |         ( | ||||||
|  |             x in sys.argv | ||||||
|  |             for x in [ | ||||||
|  |                 'backup', | ||||||
|  |                 'restore', | ||||||
|  |                 'dbbackup', | ||||||
|  |                 'dbresotore', | ||||||
|  |                 'mediabackup', | ||||||
|  |                 'mediarestore', | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def isInWorkerThread(): | def isInWorkerThread(): | ||||||
| @@ -58,26 +90,30 @@ def canAppAccessDatabase( | |||||||
|     There are some circumstances where we don't want the ready function in apps.py |     There are some circumstances where we don't want the ready function in apps.py | ||||||
|     to touch the database |     to touch the database | ||||||
|     """ |     """ | ||||||
|  |     # Prevent database access if we are running backups | ||||||
|  |     if isRunningBackup(): | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |     # Prevent database access if we are importing data | ||||||
|  |     if isImportingData(): | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |     # Prevent database access if we are rebuilding data | ||||||
|  |     if isRebuildingData(): | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |     # Prevent database access if we are running migrations | ||||||
|  |     if not allow_plugins and isRunningMigrations(): | ||||||
|  |         return False | ||||||
|  |  | ||||||
|     # If any of the following management commands are being executed, |     # If any of the following management commands are being executed, | ||||||
|     # prevent custom "on load" code from running! |     # prevent custom "on load" code from running! | ||||||
|     excluded_commands = [ |     excluded_commands = [ | ||||||
|         'flush', |  | ||||||
|         'loaddata', |  | ||||||
|         'dumpdata', |  | ||||||
|         'check', |         'check', | ||||||
|         'createsuperuser', |         'createsuperuser', | ||||||
|         'wait_for_db', |         'wait_for_db', | ||||||
|         'prerender', |  | ||||||
|         'rebuild_models', |  | ||||||
|         'rebuild_thumbnails', |  | ||||||
|         'makemessages', |         'makemessages', | ||||||
|         'compilemessages', |         'compilemessages', | ||||||
|         'backup', |  | ||||||
|         'dbbackup', |  | ||||||
|         'mediabackup', |  | ||||||
|         'restore', |  | ||||||
|         'dbrestore', |  | ||||||
|         'mediarestore', |  | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     if not allow_shell: |     if not allow_shell: | ||||||
| @@ -88,12 +124,7 @@ def canAppAccessDatabase( | |||||||
|         excluded_commands.append('test') |         excluded_commands.append('test') | ||||||
|  |  | ||||||
|     if not allow_plugins: |     if not allow_plugins: | ||||||
|         excluded_commands.extend([ |         excluded_commands.extend(['collectstatic']) | ||||||
|             'makemigrations', |  | ||||||
|             'showmigrations', |  | ||||||
|             'migrate', |  | ||||||
|             'collectstatic', |  | ||||||
|         ]) |  | ||||||
|  |  | ||||||
|     for cmd in excluded_commands: |     for cmd in excluded_commands: | ||||||
|         if cmd in sys.argv: |         if cmd in sys.argv: | ||||||
|   | |||||||
| @@ -644,7 +644,7 @@ def get_migration_plan(): | |||||||
|  |  | ||||||
|  |  | ||||||
| @scheduled_task(ScheduledTask.DAILY) | @scheduled_task(ScheduledTask.DAILY) | ||||||
| def check_for_migrations(): | def check_for_migrations(force: bool = False, reload_registry: bool = True): | ||||||
|     """Checks if migrations are needed. |     """Checks if migrations are needed. | ||||||
|  |  | ||||||
|     If the setting auto_update is enabled we will start updating. |     If the setting auto_update is enabled we will start updating. | ||||||
| @@ -659,6 +659,7 @@ def check_for_migrations(): | |||||||
|  |  | ||||||
|     logger.info('Checking for pending database migrations') |     logger.info('Checking for pending database migrations') | ||||||
|  |  | ||||||
|  |     if reload_registry: | ||||||
|         # Force plugin registry reload |         # Force plugin registry reload | ||||||
|         registry.check_reload() |         registry.check_reload() | ||||||
|  |  | ||||||
| @@ -674,7 +675,7 @@ def check_for_migrations(): | |||||||
|     set_pending_migrations(n) |     set_pending_migrations(n) | ||||||
|  |  | ||||||
|     # Test if auto-updates are enabled |     # Test if auto-updates are enabled | ||||||
|     if not get_setting('INVENTREE_AUTO_UPDATE', 'auto_update'): |     if not force and not get_setting('INVENTREE_AUTO_UPDATE', 'auto_update'): | ||||||
|         logger.info('Auto-update is disabled - skipping migrations') |         logger.info('Auto-update is disabled - skipping migrations') | ||||||
|         return |         return | ||||||
|  |  | ||||||
| @@ -706,6 +707,7 @@ def check_for_migrations(): | |||||||
|         set_maintenance_mode(False) |         set_maintenance_mode(False) | ||||||
|         logger.info('Manually released maintenance mode') |         logger.info('Manually released maintenance mode') | ||||||
|  |  | ||||||
|  |     if reload_registry: | ||||||
|         # We should be current now - triggering full reload to make sure all models |         # We should be current now - triggering full reload to make sure all models | ||||||
|         # are loaded fully in their new state. |         # are loaded fully in their new state. | ||||||
|         registry.reload_plugins(full_reload=True, force_reload=True, collect=True) |         registry.reload_plugins(full_reload=True, force_reload=True, collect=True) | ||||||
|   | |||||||
| @@ -677,6 +677,16 @@ class BaseInvenTreeSetting(models.Model): | |||||||
|                 setting = cls(key=key, **kwargs) |                 setting = cls(key=key, **kwargs) | ||||||
|             else: |             else: | ||||||
|                 return |                 return | ||||||
|  |         except (OperationalError, ProgrammingError): | ||||||
|  |             if not key.startswith('_'): | ||||||
|  |                 logger.warning("Database is locked, cannot set setting '%s'", key) | ||||||
|  |             # Likely the DB is locked - not much we can do here | ||||||
|  |             return | ||||||
|  |         except Exception as exc: | ||||||
|  |             logger.exception( | ||||||
|  |                 "Error setting setting '%s' for %s: %s", key, str(cls), str(type(exc)) | ||||||
|  |             ) | ||||||
|  |             return | ||||||
|  |  | ||||||
|         # Enforce standard boolean representation |         # Enforce standard boolean representation | ||||||
|         if setting.is_bool(): |         if setting.is_bool(): | ||||||
| @@ -703,6 +713,10 @@ class BaseInvenTreeSetting(models.Model): | |||||||
|                     attempts=attempts - 1, |                     attempts=attempts - 1, | ||||||
|                     **kwargs, |                     **kwargs, | ||||||
|                 ) |                 ) | ||||||
|  |         except (OperationalError, ProgrammingError): | ||||||
|  |             logger.warning("Database is locked, cannot set setting '%s'", key) | ||||||
|  |             # Likely the DB is locked - not much we can do here | ||||||
|  |             pass | ||||||
|         except Exception as exc: |         except Exception as exc: | ||||||
|             # Some other error |             # Some other error | ||||||
|             logger.exception( |             logger.exception( | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ from django.conf import settings | |||||||
| from django.core.exceptions import AppRegistryNotReady | from django.core.exceptions import AppRegistryNotReady | ||||||
| from django.db.utils import IntegrityError, OperationalError, ProgrammingError | from django.db.utils import IntegrityError, OperationalError, ProgrammingError | ||||||
|  |  | ||||||
|  | from maintenance_mode.core import maintenance_mode_on, set_maintenance_mode | ||||||
|  |  | ||||||
| import InvenTree.helpers | import InvenTree.helpers | ||||||
| import InvenTree.ready | import InvenTree.ready | ||||||
|  |  | ||||||
| @@ -32,13 +34,10 @@ class LabelConfig(AppConfig): | |||||||
|         ): |         ): | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         if InvenTree.ready.isRunningMigrations(): |         if not InvenTree.ready.canAppAccessDatabase(allow_test=False): | ||||||
|             return |             return  # pragma: no cover | ||||||
|  |  | ||||||
|         if ( |         with maintenance_mode_on(): | ||||||
|             InvenTree.ready.canAppAccessDatabase(allow_test=False) |  | ||||||
|             and not InvenTree.ready.isImportingData() |  | ||||||
|         ): |  | ||||||
|             try: |             try: | ||||||
|                 self.create_labels()  # pragma: no cover |                 self.create_labels()  # pragma: no cover | ||||||
|             except ( |             except ( | ||||||
| @@ -52,6 +51,8 @@ class LabelConfig(AppConfig): | |||||||
|                     'Database was not ready for creating labels', stacklevel=2 |                     'Database was not ready for creating labels', stacklevel=2 | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|  |         set_maintenance_mode(False) | ||||||
|  |  | ||||||
|     def create_labels(self): |     def create_labels(self): | ||||||
|         """Create all default templates.""" |         """Create all default templates.""" | ||||||
|         # Test if models are ready |         # Test if models are ready | ||||||
|   | |||||||
| @@ -11,6 +11,8 @@ from django.conf import settings | |||||||
| from django.core.exceptions import AppRegistryNotReady | from django.core.exceptions import AppRegistryNotReady | ||||||
| from django.db.utils import IntegrityError, OperationalError, ProgrammingError | from django.db.utils import IntegrityError, OperationalError, ProgrammingError | ||||||
|  |  | ||||||
|  | from maintenance_mode.core import maintenance_mode_on, set_maintenance_mode | ||||||
|  |  | ||||||
| import InvenTree.helpers | import InvenTree.helpers | ||||||
|  |  | ||||||
| logger = logging.getLogger('inventree') | logger = logging.getLogger('inventree') | ||||||
| @@ -32,18 +34,20 @@ class ReportConfig(AppConfig): | |||||||
|         ): |         ): | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         if InvenTree.ready.isRunningMigrations(): |         if not InvenTree.ready.canAppAccessDatabase(allow_test=False): | ||||||
|             return |             return  # pragma: no cover | ||||||
|  |  | ||||||
|         # Configure logging for PDF generation (disable "info" messages) |         # Configure logging for PDF generation (disable "info" messages) | ||||||
|         logging.getLogger('fontTools').setLevel(logging.WARNING) |         logging.getLogger('fontTools').setLevel(logging.WARNING) | ||||||
|         logging.getLogger('weasyprint').setLevel(logging.WARNING) |         logging.getLogger('weasyprint').setLevel(logging.WARNING) | ||||||
|  |  | ||||||
|         # Create entries for default report templates |         with maintenance_mode_on(): | ||||||
|         if ( |             self.create_reports() | ||||||
|             InvenTree.ready.canAppAccessDatabase(allow_test=False) |  | ||||||
|             and not InvenTree.ready.isImportingData() |         set_maintenance_mode(False) | ||||||
|         ): |  | ||||||
|  |     def create_reports(self): | ||||||
|  |         """Create default report templates.""" | ||||||
|         try: |         try: | ||||||
|             self.create_default_test_reports() |             self.create_default_test_reports() | ||||||
|             self.create_default_build_reports() |             self.create_default_build_reports() | ||||||
| @@ -59,9 +63,7 @@ class ReportConfig(AppConfig): | |||||||
|             ProgrammingError, |             ProgrammingError, | ||||||
|         ): |         ): | ||||||
|             # Database might not yet be ready |             # Database might not yet be ready | ||||||
|                 warnings.warn( |             warnings.warn('Database was not ready for creating reports', stacklevel=2) | ||||||
|                     'Database was not ready for creating reports', stacklevel=2 |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|     def create_default_reports(self, model, reports): |     def create_default_reports(self, model, reports): | ||||||
|         """Copy default report files across to the media directory.""" |         """Copy default report files across to the media directory.""" | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								tasks.py
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								tasks.py
									
									
									
									
									
								
							| @@ -369,10 +369,9 @@ def migrate(c): | |||||||
|     print('Running InvenTree database migrations...') |     print('Running InvenTree database migrations...') | ||||||
|     print('========================================') |     print('========================================') | ||||||
|  |  | ||||||
|     manage(c, 'makemigrations') |     # Run custom management command which wraps migrations in "maintenance mode" | ||||||
|     manage(c, 'migrate --noinput') |     manage(c, 'runmigrations', pty=True) | ||||||
|     manage(c, 'migrate --run-syncdb') |     manage(c, 'migrate --run-syncdb') | ||||||
|     manage(c, 'check') |  | ||||||
|  |  | ||||||
|     print('========================================') |     print('========================================') | ||||||
|     print('InvenTree database migrations completed!') |     print('InvenTree database migrations completed!') | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user