Handle broken transactions in DB settings getattr

This expands the role of the log database error context manager
and will actually make itself an exception to the standard
ORM behavior of raising an error when any queries are executed
inside of a broken transaction.

In this particular case it is less risky to continue on
with a database query and push the data to memcache than
it would be to use default settings values in violation of
user's intent. (hopefully)
This commit is contained in:
AlanCoding
2018-05-24 13:27:20 -04:00
parent b5cb4e8290
commit 4e6fd59180

View File

@@ -15,7 +15,7 @@ from django.conf import LazySettings
from django.conf import settings, UserSettingsHolder from django.conf import settings, UserSettingsHolder
from django.core.cache import cache as django_cache from django.core.cache import cache as django_cache
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import ProgrammingError, OperationalError from django.db import ProgrammingError, OperationalError, transaction, connection
from django.utils.functional import cached_property from django.utils.functional import cached_property
# Django REST Framework # Django REST Framework
@@ -61,24 +61,66 @@ __all__ = ['SettingsWrapper', 'get_settings_to_cache', 'SETTING_CACHE_NOTSET']
@contextlib.contextmanager @contextlib.contextmanager
def _log_database_error(): def _ctit_db_wrapper(trans_safe=False):
'''
Wrapper to avoid undesired actions by Django ORM when managing settings
if only getting a setting, can use trans_safe=True, which will avoid
throwing errors if the prior context was a broken transaction.
Any database errors will be logged, but exception will be suppressed.
'''
rollback_set = None
is_atomic = None
try: try:
if trans_safe:
is_atomic = connection.in_atomic_block
if is_atomic:
rollback_set = transaction.get_rollback()
if rollback_set:
logger.debug('Obtaining database settings in spite of broken transaction.')
transaction.set_rollback(False)
yield yield
except (ProgrammingError, OperationalError): except (ProgrammingError, OperationalError):
if 'migrate' in sys.argv and get_tower_migration_version() < '310': if 'migrate' in sys.argv and get_tower_migration_version() < '310':
logger.info('Using default settings until version 3.1 migration.') logger.info('Using default settings until version 3.1 migration.')
else: else:
# Somewhat ugly - craming the full stack trace into the log message # We want the _full_ traceback with the context
# the available exc_info does not give information about the real caller # First we get the current call stack, which constitutes the "top",
# TODO: replace in favor of stack_info kwarg in python 3 # it has the context up to the point where the context manager is used
sio = StringIO.StringIO() top_stack = StringIO.StringIO()
traceback.print_stack(file=sio) traceback.print_stack(file=top_stack)
sinfo = sio.getvalue() top_lines = top_stack.getvalue().strip('\n').split('\n')
sio.close() top_stack.close()
sinfo = sinfo.strip('\n') # Get "bottom" stack from the local error that happened
logger.warning('Database settings are not available, using defaults, logged from:\n{}'.format(sinfo)) # inside of the "with" block this wraps
exc_type, exc_value, exc_traceback = sys.exc_info()
bottom_stack = StringIO.StringIO()
traceback.print_tb(exc_traceback, file=bottom_stack)
bottom_lines = bottom_stack.getvalue().strip('\n').split('\n')
# Glue together top and bottom where overlap is found
bottom_cutoff = 0
for i, line in enumerate(bottom_lines):
if line in top_lines:
# start of overlapping section, take overlap from bottom
top_lines = top_lines[:top_lines.index(line)]
bottom_cutoff = i
break
bottom_lines = bottom_lines[bottom_cutoff:]
tb_lines = top_lines + bottom_lines
tb_string = '\n'.join(
['Traceback (most recent call last):'] +
tb_lines +
['{}: {}'.format(exc_type.__name__, str(exc_value))]
)
bottom_stack.close()
# Log the combined stack
if trans_safe:
logger.warning('Database settings are not available, using defaults, error:\n{}'.format(tb_string))
else:
logger.error('Error modifying something related to database settings.\n{}'.format(tb_string))
finally: finally:
pass if trans_safe and is_atomic and rollback_set:
transaction.set_rollback(rollback_set)
def filter_sensitive(registry, key, value): def filter_sensitive(registry, key, value):
@@ -398,7 +440,7 @@ class SettingsWrapper(UserSettingsHolder):
def __getattr__(self, name): def __getattr__(self, name):
value = empty value = empty
if name in self.all_supported_settings: if name in self.all_supported_settings:
with _log_database_error(): with _ctit_db_wrapper(trans_safe=True):
value = self._get_local(name) value = self._get_local(name)
if value is not empty: if value is not empty:
return value return value
@@ -430,7 +472,7 @@ class SettingsWrapper(UserSettingsHolder):
def __setattr__(self, name, value): def __setattr__(self, name, value):
if name in self.all_supported_settings: if name in self.all_supported_settings:
with _log_database_error(): with _ctit_db_wrapper():
self._set_local(name, value) self._set_local(name, value)
else: else:
setattr(self.default_settings, name, value) setattr(self.default_settings, name, value)
@@ -446,14 +488,14 @@ class SettingsWrapper(UserSettingsHolder):
def __delattr__(self, name): def __delattr__(self, name):
if name in self.all_supported_settings: if name in self.all_supported_settings:
with _log_database_error(): with _ctit_db_wrapper():
self._del_local(name) self._del_local(name)
else: else:
delattr(self.default_settings, name) delattr(self.default_settings, name)
def __dir__(self): def __dir__(self):
keys = [] keys = []
with _log_database_error(): with _ctit_db_wrapper(trans_safe=True):
for setting in Setting.objects.filter( for setting in Setting.objects.filter(
key__in=self.all_supported_settings, user__isnull=True): key__in=self.all_supported_settings, user__isnull=True):
# Skip returning settings that have been overridden but are # Skip returning settings that have been overridden but are
@@ -470,7 +512,7 @@ class SettingsWrapper(UserSettingsHolder):
def is_overridden(self, setting): def is_overridden(self, setting):
set_locally = False set_locally = False
if setting in self.all_supported_settings: if setting in self.all_supported_settings:
with _log_database_error(): with _ctit_db_wrapper(trans_safe=True):
set_locally = Setting.objects.filter(key=setting, user__isnull=True).exists() set_locally = Setting.objects.filter(key=setting, user__isnull=True).exists()
set_on_default = getattr(self.default_settings, 'is_overridden', lambda s: False)(setting) set_on_default = getattr(self.default_settings, 'is_overridden', lambda s: False)(setting)
return (set_locally or set_on_default) return (set_locally or set_on_default)