Upgrade exc_filters for 'engine' argument and connect behavior

This patch applies upgrades to the sqlalchemy/exc_filters.py and
sqlalchemy/compat/handle_error.py compatibility layers to accommodate
new changes in SQLAlchemy 1.0.  SQLA 1.0 will now route errors that
occur upon connect through the handle_error() event, just like any other,
so that when 1.0 is present we no longer need to use
exc_filters.handle_connect_error; the method becomes a passthrough
as far as running the event handler.  Additionally, SQLAlchemy 1.0
has added the "engine" parameter to ExceptionContext, specifically
to suit the case when the initial connect has failed and there is
no Connection object; the compatibility layer here now emulates
this behavior for SQLAlchemy versions prior to 1.0.

Change-Id: I61714f3c32625a621eaba501d20346519b8b12c7
This commit is contained in:
Mike Bayer 2014-12-05 15:01:16 -05:00
parent 161bbb2831
commit 32e5c6096a
4 changed files with 62 additions and 26 deletions

@ -23,6 +23,8 @@ from oslo.db.sqlalchemy.compat import handle_error as _h_err
# flake8 won't let me import handle_error directly
engine_connect = _e_conn.engine_connect
handle_error = _h_err.handle_error
ExceptionContextImpl = _h_err.ExceptionContextImpl
handle_connect_context = _h_err.handle_connect_context
__all__ = ['engine_connect', 'handle_error', 'ExceptionContextImpl']
__all__ = [
'engine_connect', 'handle_error',
'handle_connect_context']

@ -16,6 +16,7 @@ http://docs.sqlalchemy.org/en/rel_0_9/core/events.html.
"""
import contextlib
import sys
import six
@ -35,10 +36,17 @@ def handle_error(engine, listener):
in order to support safe re-raise of the exception.
"""
if utils.sqla_097:
if utils.sqla_100:
event.listen(engine, "handle_error", listener)
return
elif utils.sqla_097:
# ctx.engine added per
# https://bitbucket.org/zzzeek/sqlalchemy/issue/3266/
def wrap_listener(ctx):
ctx.engine = ctx.connection.engine
return listener(ctx)
event.listen(engine, "handle_error", wrap_listener)
return
assert isinstance(engine, Engine), \
"engine argument must be an Engine instance, not a Connection"
@ -92,7 +100,7 @@ def handle_error(engine, listener):
# new handle_error event
ctx = ExceptionContextImpl(
original_exception, sqlalchemy_exception,
self, cursor, statement,
self.engine, self, cursor, statement,
parameters, context, is_disconnect)
for fn in _oslo_handle_error_events:
@ -153,8 +161,9 @@ class ExceptionContextImpl(object):
"""
def __init__(self, exception, sqlalchemy_exception,
connection, cursor, statement, parameters,
engine, connection, cursor, statement, parameters,
context, is_disconnect):
self.engine = engine
self.connection = connection
self.sqlalchemy_exception = sqlalchemy_exception
self.original_exception = exception
@ -166,7 +175,17 @@ class ExceptionContextImpl(object):
connection = None
"""The :class:`.Connection` in use during the exception.
This member is always present.
This member is present, except in the case of a failure when
first connecting.
"""
engine = None
"""The :class:`.Engine` in use during the exception.
This member should always be present, even in the case of a failure
when first connecting.
"""
@ -247,3 +266,24 @@ class ExceptionContextImpl(object):
:meth:`.ConnectionEvents.handle_error` handler.
"""
@contextlib.contextmanager
def handle_connect_context(handler, engine):
"""Wrap connect() routines with a "handle error" context."""
try:
yield
except Exception as e:
if utils.sqla_100:
raise
if isinstance(e, sqla_exc.StatementError):
s_exc, orig = e, e.orig
else:
s_exc, orig = None, e
ctx = ExceptionContextImpl(
orig, s_exc, engine, None, None,
None, None, None, False
)
handler(ctx)

@ -19,6 +19,7 @@ _SQLA_VERSION = tuple(
for num in sqlalchemy.__version__.split(".")
)
sqla_100 = _SQLA_VERSION >= (1, 0, 0)
sqla_097 = _SQLA_VERSION >= (0, 9, 7)
sqla_094 = _SQLA_VERSION >= (0, 9, 4)
sqla_090 = _SQLA_VERSION >= (0, 9, 0)

@ -314,13 +314,13 @@ def handler(context):
more specific exception class are attempted first.
"""
def _dialect_registries(connection):
if connection.dialect.name in _registry:
yield _registry[connection.dialect.name]
def _dialect_registries(engine):
if engine.dialect.name in _registry:
yield _registry[engine.dialect.name]
if '*' in _registry:
yield _registry['*']
for per_dialect in _dialect_registries(context.connection):
for per_dialect in _dialect_registries(context.engine):
for exc in (
context.sqlalchemy_exception,
context.original_exception):
@ -334,7 +334,7 @@ def handler(context):
fn(
exc,
match,
context.connection.dialect.name,
context.engine.dialect.name,
context.is_disconnect)
except exception.DBConnectionError:
context.is_disconnect = True
@ -349,18 +349,11 @@ def handle_connect_error(engine):
"""Handle connect error.
Provide a special context that will allow on-connect errors
to be raised within the filtering context.
"""
try:
return engine.connect()
except Exception as e:
if isinstance(e, sqla_exc.StatementError):
s_exc, orig = e, e.orig
else:
s_exc, orig = None, e
to be treated within the filtering context.
ctx = compat.ExceptionContextImpl(
orig, s_exc, engine, None,
None, None, None, False
)
handler(ctx)
This routine is dependent on SQLAlchemy version, as version 1.0.0
provides this functionality natively.
"""
with compat.handle_connect_context(handler, engine):
return engine.connect()