diff options
author | Doug Hellmann <doug@doughellmann.com> | 2014-12-12 14:23:13 -0500 |
---|---|---|
committer | Roman Podoliaka <rpodolyaka@mirantis.com> | 2014-12-24 13:28:48 +0200 |
commit | 7063585c60205fe031e1c74289d88886705cfb57 (patch) | |
tree | 85d18890b25d9fb4e76365ce8dbbf282d8479711 /oslo_db/sqlalchemy/compat/handle_error.py | |
parent | 571433bfc4936d90602bfac4cbd7e9170c0a8d07 (diff) | |
download | oslo-db-7063585c60205fe031e1c74289d88886705cfb57.tar.gz |
Move files out of the namespace package
Move the public API out of oslo.db to oslo_db. Retain the ability to
import from the old namespace package for backwards compatibility for
this release cycle.
Blueprint: drop-namespace-packages
Change-Id: Ie96b482b9fbcb1d85203ad35bb65c1f43e912a44
Diffstat (limited to 'oslo_db/sqlalchemy/compat/handle_error.py')
-rw-r--r-- | oslo_db/sqlalchemy/compat/handle_error.py | 289 |
1 files changed, 289 insertions, 0 deletions
diff --git a/oslo_db/sqlalchemy/compat/handle_error.py b/oslo_db/sqlalchemy/compat/handle_error.py new file mode 100644 index 0000000..7e476a0 --- /dev/null +++ b/oslo_db/sqlalchemy/compat/handle_error.py @@ -0,0 +1,289 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Provide forwards compatibility for the handle_error event. + +See the "handle_error" event at +http://docs.sqlalchemy.org/en/rel_0_9/core/events.html. + + +""" +import contextlib +import sys + +import six +from sqlalchemy.engine import Engine +from sqlalchemy import event +from sqlalchemy import exc as sqla_exc + +from oslo_db.sqlalchemy.compat import utils + + +def handle_error(engine, listener): + """Add a handle_error listener for the given :class:`.Engine`. + + This listener uses the SQLAlchemy + :meth:`sqlalchemy.event.ConnectionEvents.handle_error` + event, however augments the listener for pre-0.9.7 versions of SQLAlchemy + in order to support safe re-raise of the exception. + + """ + 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" + + # use a Connection-wrapper class to wrap _handle_dbapi_exception. + if not getattr(engine._connection_cls, + '_oslo_handle_error_wrapper', False): + engine._oslo_handle_error_events = [] + + class Connection(engine._connection_cls): + _oslo_handle_error_wrapper = True + + def _handle_dbapi_exception(self, e, statement, parameters, + cursor, context): + + try: + super(Connection, self)._handle_dbapi_exception( + e, statement, parameters, cursor, context) + except Exception as reraised_exception: + # all versions: + # _handle_dbapi_exception reraises all DBAPI errors + # 0.8 and above: + # reraises all errors unconditionally + pass + else: + # 0.7.8: + # _handle_dbapi_exception does not unconditionally + # re-raise + reraised_exception = e + + _oslo_handle_error_events = getattr( + self.engine, + '_oslo_handle_error_events', + False) + + newraise = None + if _oslo_handle_error_events: + if isinstance(reraised_exception, + sqla_exc.StatementError): + sqlalchemy_exception = reraised_exception + original_exception = sqlalchemy_exception.orig + self._is_disconnect = is_disconnect = ( + isinstance(sqlalchemy_exception, + sqla_exc.DBAPIError) + and sqlalchemy_exception.connection_invalidated) + else: + sqlalchemy_exception = None + original_exception = reraised_exception + is_disconnect = False + + # new handle_error event + ctx = ExceptionContextImpl( + original_exception, sqlalchemy_exception, + self.engine, self, cursor, statement, + parameters, context, is_disconnect) + + for fn in _oslo_handle_error_events: + try: + # handler returns an exception; + # call next handler in a chain + per_fn = fn(ctx) + if per_fn is not None: + ctx.chained_exception = newraise = per_fn + except Exception as _raised: + # handler raises an exception - stop processing + newraise = _raised + break + + if sqlalchemy_exception and \ + self._is_disconnect != ctx.is_disconnect: + + if not ctx.is_disconnect: + raise NotImplementedError( + "Can't reset 'disconnect' status of exception " + "once it is set with this version of " + "SQLAlchemy") + + sqlalchemy_exception.connection_invalidated = \ + self._is_disconnect = ctx.is_disconnect + if self._is_disconnect: + self._do_disconnect(e) + + if newraise: + six.reraise(type(newraise), newraise, sys.exc_info()[2]) + else: + six.reraise(type(reraised_exception), + reraised_exception, sys.exc_info()[2]) + + def _do_disconnect(self, e): + del self._is_disconnect + if utils.sqla_094: + dbapi_conn_wrapper = self.connection + self.engine.pool._invalidate(dbapi_conn_wrapper, e) + self.invalidate(e) + else: + dbapi_conn_wrapper = self.connection + self.invalidate(e) + if not hasattr(dbapi_conn_wrapper, '_pool') or \ + dbapi_conn_wrapper._pool is self.engine.pool: + self.engine.dispose() + + engine._connection_cls = Connection + engine._oslo_handle_error_events.append(listener) + + +class ExceptionContextImpl(object): + """Encapsulate information about an error condition in progress. + + This is for forwards compatibility with the + ExceptionContext interface introduced in SQLAlchemy 0.9.7. + + """ + + def __init__(self, exception, sqlalchemy_exception, + engine, connection, cursor, statement, parameters, + context, is_disconnect): + self.engine = engine + self.connection = connection + self.sqlalchemy_exception = sqlalchemy_exception + self.original_exception = exception + self.execution_context = context + self.statement = statement + self.parameters = parameters + self.is_disconnect = is_disconnect + + connection = None + """The :class:`.Connection` in use during the exception. + + 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. + + """ + + cursor = None + """The DBAPI cursor object. + + May be None. + + """ + + statement = None + """String SQL statement that was emitted directly to the DBAPI. + + May be None. + + """ + + parameters = None + """Parameter collection that was emitted directly to the DBAPI. + + May be None. + + """ + + original_exception = None + """The exception object which was caught. + + This member is always present. + + """ + + sqlalchemy_exception = None + """The :class:`sqlalchemy.exc.StatementError` which wraps the original, + and will be raised if exception handling is not circumvented by the event. + + May be None, as not all exception types are wrapped by SQLAlchemy. + For DBAPI-level exceptions that subclass the dbapi's Error class, this + field will always be present. + + """ + + chained_exception = None + """The exception that was returned by the previous handler in the + exception chain, if any. + + If present, this exception will be the one ultimately raised by + SQLAlchemy unless a subsequent handler replaces it. + + May be None. + + """ + + execution_context = None + """The :class:`.ExecutionContext` corresponding to the execution + operation in progress. + + This is present for statement execution operations, but not for + operations such as transaction begin/end. It also is not present when + the exception was raised before the :class:`.ExecutionContext` + could be constructed. + + Note that the :attr:`.ExceptionContext.statement` and + :attr:`.ExceptionContext.parameters` members may represent a + different value than that of the :class:`.ExecutionContext`, + potentially in the case where a + :meth:`.ConnectionEvents.before_cursor_execute` event or similar + modified the statement/parameters to be sent. + + May be None. + + """ + + is_disconnect = None + """Represent whether the exception as occurred represents a "disconnect" + condition. + + This flag will always be True or False within the scope of the + :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) |