Use the retrying lib. to do basic sqlalchemy engine validation

Instead of a custom loop and backoff just use the retrying
library to do this same code and delegate the loop complexity
to it instead.

Change-Id: Iaf02cc728d2a2cfc7077300e03d7ef25522717b7
This commit is contained in:
Joshua Harlow
2015-12-22 15:13:19 -08:00
committed by Joshua Harlow
parent 25a8e4b132
commit 25dca8eb59
3 changed files with 39 additions and 68 deletions

View File

@@ -43,6 +43,7 @@ automaton>=0.5.0 # Apache-2.0
# For common utilities # For common utilities
oslo.utils>=3.2.0 # Apache-2.0 oslo.utils>=3.2.0 # Apache-2.0
oslo.serialization>=1.10.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0
retrying>=1.2.3,!=1.3.0 # Apache-2.0
# For lru caches and such # For lru caches and such
cachetools>=1.0.0 # MIT License cachetools>=1.0.0 # MIT License

View File

@@ -23,6 +23,7 @@ import functools
import time import time
from oslo_utils import strutils from oslo_utils import strutils
import retrying
import six import six
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import exc as sa_exc from sqlalchemy import exc as sa_exc
@@ -35,7 +36,6 @@ from taskflow.persistence.backends.sqlalchemy import migration
from taskflow.persistence.backends.sqlalchemy import tables from taskflow.persistence.backends.sqlalchemy import tables
from taskflow.persistence import base from taskflow.persistence import base
from taskflow.persistence import models from taskflow.persistence import models
from taskflow.types import failure
from taskflow.utils import eventlet_utils from taskflow.utils import eventlet_utils
from taskflow.utils import misc from taskflow.utils import misc
@@ -246,6 +246,10 @@ class SQLAlchemyBackend(base.Backend):
self._engine = self._create_engine(self._conf) self._engine = self._create_engine(self._conf)
self._owns_engine = True self._owns_engine = True
self._validated = False self._validated = False
try:
self._max_retries = misc.as_int(self._conf.get('max_retries'))
except TypeError:
self._max_retries = 0
@staticmethod @staticmethod
def _create_engine(conf): def _create_engine(conf):
@@ -326,11 +330,7 @@ class SQLAlchemyBackend(base.Backend):
def get_connection(self): def get_connection(self):
conn = Connection(self) conn = Connection(self)
if not self._validated: if not self._validated:
try: conn.validate(max_retries=self._max_retries)
max_retries = misc.as_int(self._conf.get('max_retries', None))
except TypeError:
max_retries = 0
conn.validate(max_retries=max_retries)
self._validated = True self._validated = True
return conn return conn
@@ -356,47 +356,41 @@ class Connection(base.Connection):
return self._backend return self._backend
def validate(self, max_retries=0): def validate(self, max_retries=0):
"""Performs basic **connection** validation of a sqlalchemy engine."""
def verify_connect(failures): def _retry_on_exception(exc):
try: LOG.warn("Engine connection (validate) failed due to '%s'", exc)
# See if we can make a connection happen. if isinstance(exc, sa_exc.OperationalError) and \
# _is_db_connection_error(six.text_type(exc.args[0])):
# NOTE(harlowja): note that even though we are connecting # We may be able to fix this by retrying...
# once it does not mean that we will be able to connect in return True
# the future, so this is more of a sanity test and is not if isinstance(exc, (sa_exc.TimeoutError,
# complete connection insurance. sa_exc.ResourceClosedError,
with contextlib.closing(self._engine.connect()): sa_exc.DisconnectionError)):
pass # We may be able to fix this by retrying...
except sa_exc.OperationalError as ex: return True
if _is_db_connection_error(six.text_type(ex.args[0])): # Other failures we likely can't fix by retrying...
failures.append(failure.Failure()) return False
return False
return True
failures = [] @retrying.retry(stop_max_attempt_number=max(0, int(max_retries)),
if verify_connect(failures): # Ensure that the 2 ** retry number
return # is converted into milliseconds (thus why this
# multiplies by 1000.0) because thats what retrying
# lib. uses internally for whatever reason.
wait_exponential_multiplier=1000.0,
wrap_exception=False,
retry_on_exception=_retry_on_exception)
def _try_connect(engine):
# See if we can make a connection happen.
#
# NOTE(harlowja): note that even though we are connecting
# once it does not mean that we will be able to connect in
# the future, so this is more of a sanity test and is not
# complete connection insurance.
with contextlib.closing(engine.connect()):
pass
# Sorry it didn't work out... _try_connect(self._engine)
if max_retries <= 0:
failures[-1].reraise()
# Go through the exponential backoff loop and see if we can connect
# after a given number of backoffs (with a backoff sleeping period
# between each attempt)...
attempts_left = max_retries
for sleepy_secs in misc.ExponentialBackoff(max_retries):
LOG.warn("SQL connection failed due to '%s', %s attempts left.",
failures[-1].exc, attempts_left)
LOG.info("Attempting to test the connection again in %s seconds.",
sleepy_secs)
time.sleep(sleepy_secs)
if verify_connect(failures):
return
attempts_left -= 1
# Sorry it didn't work out...
failures[-1].reraise()
def upgrade(self): def upgrade(self):
try: try:

View File

@@ -35,7 +35,6 @@ from oslo_utils import importutils
from oslo_utils import netutils from oslo_utils import netutils
from oslo_utils import reflection from oslo_utils import reflection
import six import six
from six.moves import range as compat_range
from taskflow.types import failure from taskflow.types import failure
from taskflow.types import notifier from taskflow.types import notifier
@@ -452,29 +451,6 @@ def sequence_minus(seq1, seq2):
return result return result
class ExponentialBackoff(object):
"""An iterable object that will yield back an exponential delay sequence.
This objects provides for a configurable exponent, count of numbers
to generate, and a maximum number that will be returned. This object may
also be iterated over multiple times (yielding the same sequence each
time).
"""
def __init__(self, count, exponent=2, max_backoff=3600):
self.count = max(0, int(count))
self.exponent = exponent
self.max_backoff = max(0, int(max_backoff))
def __iter__(self):
if self.count <= 0:
raise StopIteration()
for i in compat_range(0, self.count):
yield min(self.exponent ** i, self.max_backoff)
def __str__(self):
return "ExponentialBackoff: %s" % ([str(v) for v in self])
def as_int(obj, quiet=False): def as_int(obj, quiet=False):
"""Converts an arbitrary value into a integer.""" """Converts an arbitrary value into a integer."""
# Try "2" -> 2 # Try "2" -> 2