Maintenance Mode Update (#6462)

* Adjust maintenance mode backend

- Save a timestamp to the setting, after which maintenance mode is not active
- Fallback to account for possibility that race condition / exception leaves maintenance mode active

* Update docstring

* Remove unused import

* Add unit tests for maintenance mode
This commit is contained in:
Oliver 2024-02-10 14:33:45 +11:00 committed by GitHub
parent 9c93130224
commit 903c65d08a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 83 additions and 12 deletions

View File

@ -1,5 +1,6 @@
"""Custom backend implementations."""
import datetime
import logging
import time
@ -8,7 +9,6 @@ from django.db.utils import IntegrityError, OperationalError, ProgrammingError
from maintenance_mode.backends import AbstractStateBackend
import common.models
import InvenTree.ready
logger = logging.getLogger('inventree')
@ -16,8 +16,7 @@ logger = logging.getLogger('inventree')
class InvenTreeMaintenanceModeBackend(AbstractStateBackend):
"""Custom backend for managing state of maintenance mode.
Stores the current state of the maintenance mode in the database,
using an InvenTreeSetting object.
Stores a timestamp in the database to determine when maintenance mode will elapse.
"""
SETTING_KEY = '_MAINTENANCE_MODE'
@ -30,26 +29,45 @@ class InvenTreeMaintenanceModeBackend(AbstractStateBackend):
"""
try:
setting = common.models.InvenTreeSetting.objects.get(key=self.SETTING_KEY)
value = InvenTree.helpers.str2bool(setting.value)
value = str(setting.value).strip()
except common.models.InvenTreeSetting.DoesNotExist:
# Database is accessible, but setting is not available - assume False
value = False
return False
except (IntegrityError, OperationalError, ProgrammingError):
# Database is inaccessible - assume we are not in maintenance mode
logger.warning('Failed to read maintenance mode state - assuming True')
value = True
logger.debug('Failed to read maintenance mode state - assuming True')
return True
logger.debug('Maintenance mode state: %s', value)
# Extract timestamp from string
try:
# If the timestamp is in the past, we are now *out* of maintenance mode
timestamp = datetime.datetime.fromisoformat(value)
return timestamp > datetime.datetime.now()
except ValueError:
# If the value is not a valid timestamp, assume maintenance mode is not active
return False
return value
def set_value(self, value: bool, retries: int = 5, minutes: int = 5):
"""Set the state of the maintenance mode.
def set_value(self, value: bool, retries: int = 5):
"""Set the state of the maintenance mode."""
Instead of simply writing "true" or "false" to the setting,
we write a timestamp to the setting, which is used to determine
when maintenance mode will elapse.
This ensures that we will always *exit* maintenance mode after a certain time period.
"""
logger.debug('Setting maintenance mode state: %s', value)
if value:
# Save as isoformat
timestamp = datetime.datetime.now() + datetime.timedelta(minutes=minutes)
timestamp = timestamp.isoformat()
else:
# Blank timestamp means maintenance mode is not active
timestamp = ''
while retries > 0:
try:
common.models.InvenTreeSetting.set_setting(self.SETTING_KEY, value)
common.models.InvenTreeSetting.set_setting(self.SETTING_KEY, timestamp)
# Read the value back to confirm
if self.get_value() == value:

View File

@ -19,6 +19,7 @@ import pint.errors
from djmoney.contrib.exchange.exceptions import MissingRate
from djmoney.contrib.exchange.models import Rate, convert_money
from djmoney.money import Money
from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode
from sesame.utils import get_user
import InvenTree.conversion
@ -1264,3 +1265,55 @@ class MagicLoginTest(InvenTreeTestCase):
self.assertEqual(resp.url, '/api/auth/login-redirect/')
# And we should be logged in again
self.assertEqual(resp.wsgi_request.user, self.user)
class MaintenanceModeTest(InvenTreeTestCase):
"""Unit tests for maintenance mode."""
def test_basic(self):
"""Test basic maintenance mode operation."""
for value in [False, True, False]:
set_maintenance_mode(value)
self.assertEqual(get_maintenance_mode(), value)
# API request is blocked in maintenance mode
set_maintenance_mode(True)
response = self.client.get('/api/')
self.assertEqual(response.status_code, 503)
set_maintenance_mode(False)
response = self.client.get('/api/')
self.assertEqual(response.status_code, 200)
def test_timestamp(self):
"""Test that the timestamp value is interpreted correctly."""
KEY = '_MAINTENANCE_MODE'
# Deleting the setting means maintenance mode is off
InvenTreeSetting.objects.filter(key=KEY).delete()
self.assertFalse(get_maintenance_mode())
def set_timestamp(value):
InvenTreeSetting.set_setting(KEY, value, None)
# Test blank value
set_timestamp('')
self.assertFalse(get_maintenance_mode())
# Test timestamp in the past
ts = datetime.now() - timedelta(minutes=10)
set_timestamp(ts.isoformat())
self.assertFalse(get_maintenance_mode())
# Test timestamp in the future
ts = datetime.now() + timedelta(minutes=10)
set_timestamp(ts.isoformat())
self.assertTrue(get_maintenance_mode())
# Set to false, check for empty string
set_maintenance_mode(False)
self.assertFalse(get_maintenance_mode())
self.assertEqual(InvenTreeSetting.get_setting(KEY, None), '')