From: Mike Bayer Date: Tue, 25 Oct 2022 20:00:50 +0000 (-0400) Subject: ensure pool.reset event always called for reset X-Git-Tag: rel_1_4_43~9^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d593d63d81fe7db0bebaa2371366343db33ed576;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git ensure pool.reset event always called for reset Fixed issue where the :meth:`.PoolEvents.reset` event hook would not be called when a :class:`.Connection` were closed which already closed its own transaction. Logic that bypasses the "rollback on return" behavior of the pool was also skipping the event hook being emitted, preventing custom pool reset schemes from being used within this hook. This was a regression that appeared in version 1.4. For version 1.4, the hook is still not called in the case of an asyncio connection that is being discarded due to garbage collection. Version 2.0 will feature an improved version of :meth:`.PoolEvents.reset` which also receives contextual information about the reset, so that comprehensive "custom connection reset" schemes can be devised. Existing custom reset schemes that make use of :meth:`.PoolEvents.checkin` remain usable as they typically only need to deal with connections that are to be re-used. Change-Id: Ie17c4f55d02beb6f570b9de6b3044baffa7d6df6 Fixes: #8717 (cherry picked from commit bb8c36c5d2622e6e7033dc59dc98da0926ba7c00) --- diff --git a/doc/build/changelog/unreleased_14/8717.rst b/doc/build/changelog/unreleased_14/8717.rst new file mode 100644 index 0000000000..4f3c5cd472 --- /dev/null +++ b/doc/build/changelog/unreleased_14/8717.rst @@ -0,0 +1,19 @@ +.. change:: + :tags: bug, engine, regression + :tickets: 8717 + + Fixed issue where the :meth:`.PoolEvents.reset` event hook would not be + called when a :class:`.Connection` were closed which already called + ``.rollback()`` on its own transaction, due to an enhancement in the 1.4 + series that ensures ``.rollback()`` is only called once in this scenario, + rather than twice. This would prevent custom pool reset schemes from being + used within this hook. This was a regression that appeared in version 1.4. + + For version 1.4, the :meth:`.PoolEvents.checkin` likely remains a better + event to use for custom "reset" implementations. Version 2.0 will feature + an improved version of :meth:`.PoolEvents.reset` which is called for + additional scenarios such as termination of asyncio connections, and is + also passed contextual information about the reset, to allow for "custom + connection reset" schemes which can respond to different reset scenarios in + different ways. + diff --git a/doc/build/conf.py b/doc/build/conf.py index d1144b41bb..86f1925303 100644 --- a/doc/build/conf.py +++ b/doc/build/conf.py @@ -106,7 +106,7 @@ changelog_render_pullreq = { changelog_render_changeset = "https://www.sqlalchemy.org/trac/changeset/%s" -exclude_patterns = ["build", "**/unreleased*/*", "**/*_include.rst"] +exclude_patterns = ["build", "**/unreleased*/*", "**/*_include.rst", ".venv"] # zzzeeksphinx makes these conversions when it is rendering the # docstrings classes, methods, and functions within the scope of diff --git a/doc/build/core/pooling.rst b/doc/build/core/pooling.rst index 138feace28..c147b1d0b7 100644 --- a/doc/build/core/pooling.rst +++ b/doc/build/core/pooling.rst @@ -133,43 +133,116 @@ however and in particular is not supported with asyncio DBAPI drivers. Reset On Return --------------- -The pool also includes the a "reset on return" feature which will call the -``rollback()`` method of the DBAPI connection when the connection is returned -to the pool. This is so that any existing -transaction on the connection is removed, not only ensuring that no existing -state remains on next usage, but also so that table and row locks are released -as well as that any isolated data snapshots are removed. This ``rollback()`` -occurs in most cases even when using an :class:`_engine.Engine` object, -except in the case when the :class:`_engine.Connection` can guarantee -that a ``rollback()`` has been called immediately before the connection -is returned to the pool. - -For most DBAPIs, the call to ``rollback()`` is very inexpensive and if the +The pool includes "reset on return" behavior which will call the ``rollback()`` +method of the DBAPI connection when the connection is returned to the pool. +This is so that any existing transactional state is removed from the +connection, which includes not just uncommitted data but table and row locks as +well. For most DBAPIs, the call to ``rollback()`` is inexpensive, and if the DBAPI has already completed a transaction, the method should be a no-op. -However, for DBAPIs that incur performance issues with ``rollback()`` even if -there's no state on the connection, this behavior can be disabled using the -``reset_on_return`` option of :class:`_pool.Pool`. The behavior is safe -to disable under the following conditions: - -* If the database does not support transactions at all, such as using - MySQL with the MyISAM engine, or the DBAPI is used in autocommit - mode only, the behavior can be disabled. -* If the pool itself doesn't maintain a connection after it's checked in, - such as when using :class:`.NullPool`, the behavior can be disabled. -* Otherwise, it must be ensured that: - - * the application ensures that all :class:`_engine.Connection` - objects are explicitly closed out using a context manager (i.e. ``with`` - block) or a ``try/finally`` style block - * connections are never allowed to be garbage collected before being explicitly - closed. - * the DBAPI connection itself, e.g. ``connection.connection``, is not used - directly, or the application ensures that ``.rollback()`` is called - on this connection before releasing it back to the connection pool. - -The "reset on return" step may be logged using the ``logging.DEBUG`` + +Disabling Reset on Return for non-transactional connections +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For very specific cases where this ``rollback()`` is not useful, such as when +using a connection that is configured for +:ref:`autocommit ` or when using a database +that has no ACID capabilities such as the MyISAM engine of MySQL, the +reset-on-return behavior can be disabled, which is typically done for +performance reasons. This can be affected by using the +:paramref:`_pool.Pool.reset_on_return` parameter of :class:`_pool.Pool`, which +is also available from :func:`_sa.create_engine` as +:paramref:`_sa.create_engine.pool_reset_on_return`, passing a value of ``None``. +This is illustrated in the example below, in conjunction with the +:paramref:`.create_engine.isolation_level` parameter setting of +``AUTOCOMMIT``:: + + non_acid_engine = create_engine( + "mysql://scott:tiger@host/db", + pool_reset_on_return=None, + isolation_level="AUTOCOMMIT", + ) + +The above engine won't actually perform ROLLBACK when connections are returned +to the pool; since AUTOCOMMIT is enabled, the driver will also not perform +any BEGIN operation. + +Custom Reset-on-Return Schemes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +"reset on return" consisting of a single ``rollback()`` may not be sufficient +for some use cases; in particular, applications which make use of temporary +tables may wish for these tables to be automatically removed on connection +checkin. Some (but notably not all) backends include features that can "reset" +such tables within the scope of a database connection, which may be a desirable +behavior for connection pool reset. Other server resources such as prepared +statement handles and server-side statement caches may persist beyond the +checkin process, which may or may not be desirable, depending on specifics. +Again, some (but again not all) backends may provide for a means of resetting +this state. The two SQLAlchemy included dialects which are known to have +such reset schemes include Microsoft SQL Server, where an undocumented but +widely known stored procedure called ``sp_reset_connection`` is often used, +and PostgreSQL, which has a well-documented series of commands including +``DISCARD`` ``RESET``, ``DEALLOCATE``, and ``UNLISTEN``. + +.. note: next paragraph + example should match mssql/base.py example + +The following example illustrates how to replace reset on return with the +Microsoft SQL Server ``sp_reset_connection`` stored procedure, using the +:meth:`.PoolEvents.reset` event hook (**requires SQLAlchemy 1.4.43 or greater**). +The :paramref:`_sa.create_engine.pool_reset_on_return` parameter is set to +``None`` so that the custom scheme can replace the default behavior completely. +The custom hook implementation calls ``.rollback()`` in any case, as it's +usually important that the DBAPI's own tracking of commit/rollback will remain +consistent with the state of the transaction:: + + from sqlalchemy import create_engine + from sqlalchemy import event + + mssql_engine = create_engine( + "mssql+pyodbc://scott:tiger^5HHH@mssql2017:1433/test?driver=ODBC+Driver+17+for+SQL+Server", + # disable default reset-on-return scheme + pool_reset_on_return=None, + ) + + + @event.listens_for(mssql_engine, "reset") + def _reset_mssql(dbapi_connection, connection_record, reset_state): + dbapi_connection.execute("{call sys.sp_reset_connection}") + + # so that the DBAPI itself knows that the connection has been + # reset + dbapi_connection.rollback() + +.. versionchanged:: 1.4.43 Ensured the :meth:`.PoolEvents.reset` event + is invoked for all "reset" occurrences, so that it's appropriate + as a place for custom "reset" handlers. Previous schemes which + use the :meth:`.PoolEvents.checkin` handler remain usable as well. + +.. seealso:: + * :ref:`mssql_reset_on_return` - in the :ref:`mssql_toplevel` documentation + * :ref:`postgresql_reset_on_return` in the :ref:`postgresql_toplevel` documentation + +Logging reset-on-return events +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Logging for pool events including reset on return can be set +``logging.DEBUG`` log level along with the ``sqlalchemy.pool`` logger, or by setting -``echo_pool='debug'`` with :func:`_sa.create_engine`. +:paramref:`_sa.create_engine.echo_pool` to ``"debug"`` when using +:func:`_sa.create_engine`:: + + >>> from sqlalchemy import create_engine + >>> engine = create_engine("postgresql://scott:tiger@localhost/test", echo_pool="debug") + +The above pool will show verbose logging including reset on return:: + + >>> c1 = engine.connect() + DEBUG sqlalchemy.pool.impl.QueuePool Created new connection + DEBUG sqlalchemy.pool.impl.QueuePool Connection checked out from pool + >>> c1.close() + DEBUG sqlalchemy.pool.impl.QueuePool Connection being returned to pool + DEBUG sqlalchemy.pool.impl.QueuePool Connection rollback-on-return + Pool Events ----------- @@ -590,32 +663,22 @@ API Documentation - Available Pool Implementations -------------------------------------------------- .. autoclass:: sqlalchemy.pool.Pool - - .. automethod:: __init__ - .. automethod:: connect - .. automethod:: dispose - .. automethod:: recreate + :members: .. autoclass:: sqlalchemy.pool.QueuePool - - .. automethod:: __init__ - .. automethod:: connect + :members: .. autoclass:: SingletonThreadPool - - .. automethod:: __init__ + :members: .. autoclass:: AssertionPool - - .. automethod:: __init__ + :members: .. autoclass:: NullPool - - .. automethod:: __init__ + :members: .. autoclass:: StaticPool - - .. automethod:: __init__ + :members: .. autoclass:: _ConnectionFairy :members: diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 6c530b631c..0509413062 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -442,6 +442,60 @@ different isolation level settings. See the discussion at :ref:`dbapi_autocommit` +.. _mssql_reset_on_return: + +Temporary Table / Resource Reset for Connection Pooling +------------------------------------------------------- + +The :class:`.QueuePool` connection pool implementation used +by the SQLAlchemy :class:`_sa.Engine` object includes +:ref:`reset on return ` behavior that will invoke +the DBAPI ``.rollback()`` method when connections are returned to the pool. +While this rollback will clear out the immediate state used by the previous +transaction, it does not cover a wider range of session-level state, including +temporary tables as well as other server state such as prepared statement +handles and statement caches. An undocumented SQL Server procedure known +as ``sp_reset_connection`` is known to be a workaround for this issue which +will reset most of the session state that builds up on a connection, including +temporary tables. + +To install ``sp_reset_connection`` as the means of performing reset-on-return, +the :meth:`.PoolEvents.reset` event hook may be used, as demonstrated in the +example below (**requires SQLAlchemy 1.4.43 or greater**). The +:paramref:`_sa.create_engine.pool_reset_on_return` parameter is set to ``None`` +so that the custom scheme can replace the default behavior completely. The +custom hook implementation calls ``.rollback()`` in any case, as it's usually +important that the DBAPI's own tracking of commit/rollback will remain +consistent with the state of the transaction:: + + from sqlalchemy import create_engine + from sqlalchemy import event + + mssql_engine = create_engine( + "mssql+pyodbc://scott:tiger^5HHH@mssql2017:1433/test?driver=ODBC+Driver+17+for+SQL+Server", + + # disable default reset-on-return scheme + pool_reset_on_return=None, + ) + + + @event.listens_for(mssql_engine, "reset") + def _reset_mssql(dbapi_connection, connection_record, reset_state): + dbapi_connection.execute("{call sys.sp_reset_connection}") + + # so that the DBAPI itself knows that the connection has been + # reset + dbapi_connection.rollback() + +.. versionchanged:: 1.4.43 Ensured the :meth:`.PoolEvents.reset` event + is invoked for all "reset" occurrences, so that it's appropriate + as a place for custom "reset" handlers. Previous schemes which + use the :meth:`.PoolEvents.checkin` handler remain usable as well. + +.. seealso:: + + :ref:`pool_reset_on_return` - in the :ref:`pooling_toplevel` documentation + Nullability ----------- MSSQL has support for three levels of column nullability. The default diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index eb841700d3..6820aa6015 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -230,6 +230,68 @@ SERIALIZABLE isolation. .. versionadded:: 1.4 added support for the ``postgresql_readonly`` and ``postgresql_deferrable`` execution options. +.. _postgresql_reset_on_return: + +Temporary Table / Resource Reset for Connection Pooling +------------------------------------------------------- + +The :class:`.QueuePool` connection pool implementation used +by the SQLAlchemy :class:`_sa.Engine` object includes +:ref:`reset on return ` behavior that will invoke +the DBAPI ``.rollback()`` method when connections are returned to the pool. +While this rollback will clear out the immediate state used by the previous +transaction, it does not cover a wider range of session-level state, including +temporary tables as well as other server state such as prepared statement +handles and statement caches. The PostgreSQL database includes a variety +of commands which may be used to reset this state, including +``DISCARD``, ``RESET``, ``DEALLOCATE``, and ``UNLISTEN``. + + +To install +one or more of these commands as the means of performing reset-on-return, +the :meth:`.PoolEvents.reset` event hook may be used, as demonstrated +in the example below (**requires SQLAlchemy 1.4.43 or greater**). The implementation +will end transactions in progress as well as discard temporary tables +using the ``CLOSE``, ``RESET`` and ``DISCARD`` commands; see the PostgreSQL +documentation for background on what each of these statements do. + +The :paramref:`_sa.create_engine.pool_reset_on_return` parameter +is set to ``None`` so that the custom scheme can replace the default behavior +completely. The custom hook implementation calls ``.rollback()`` in any case, +as it's usually important that the DBAPI's own tracking of commit/rollback +will remain consistent with the state of the transaction:: + + + from sqlalchemy import create_engine + from sqlalchemy import event + + postgresql_engine = create_engine( + "postgresql+pyscopg2://scott:tiger@hostname/dbname", + + # disable default reset-on-return scheme + pool_reset_on_return=None, + ) + + + @event.listens_for(postgresql_engine, "reset") + def _reset_mssql(dbapi_connection, connection_record, reset_state): + dbapi_connection.execute("CLOSE ALL") + dbapi_connection.execute("RESET ALL") + dbapi_connection.execute("DISCARD TEMP") + + # so that the DBAPI itself knows that the connection has been + # reset + dbapi_connection.rollback() + +.. versionchanged:: 1.4.43 Ensured the :meth:`.PoolEvents.reset` event + is invoked for all "reset" occurrences, so that it's appropriate + as a place for custom "reset" handlers. Previous schemes which + use the :meth:`.PoolEvents.checkin` handler remain usable as well. + +.. seealso:: + + :ref:`pool_reset_on_return` - in the :ref:`pooling_toplevel` documentation + .. _postgresql_alternate_search_path: Setting Alternate Search Paths on Connect diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index f126eb0c56..00e1be7766 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1241,7 +1241,7 @@ class Connection(Connectable): # as we just closed the transaction, close the connection # pool connection without doing an additional reset if skip_reset: - conn._close_no_reset() + conn._close_special(transaction_reset=True) else: conn.close() diff --git a/lib/sqlalchemy/engine/create.py b/lib/sqlalchemy/engine/create.py index b9886b701b..8c929ccc4a 100644 --- a/lib/sqlalchemy/engine/create.py +++ b/lib/sqlalchemy/engine/create.py @@ -445,7 +445,7 @@ def create_engine(url, **kwargs): .. seealso:: - :paramref:`_pool.Pool.reset_on_return` + :ref:`pool_reset_on_return` :param pool_timeout=30: number of seconds to wait before giving up on getting a connection from the pool. This is only used diff --git a/lib/sqlalchemy/event/legacy.py b/lib/sqlalchemy/event/legacy.py index d9f6ce5735..686e4c5bf5 100644 --- a/lib/sqlalchemy/event/legacy.py +++ b/lib/sqlalchemy/event/legacy.py @@ -144,9 +144,9 @@ def _legacy_listen_examples(dispatch_collection, sample_target, fn): def _version_signature_changes(parent_dispatch_cls, dispatch_collection): since, args, conv = dispatch_collection.legacy_signatures[0] return ( - "\n.. deprecated:: %(since)s\n" - " The :class:`.%(clsname)s.%(event_name)s` event now accepts the \n" - " arguments ``%(named_event_arguments)s%(has_kw_arguments)s``.\n" + "\n.. versionchanged:: %(since)s\n" + " The :meth:`.%(clsname)s.%(event_name)s` event now accepts the \n" + " arguments %(named_event_arguments)s%(has_kw_arguments)s.\n" " Support for listener functions which accept the previous \n" ' argument signature(s) listed above as "deprecated" will be \n' " removed in a future release." @@ -154,7 +154,15 @@ def _version_signature_changes(parent_dispatch_cls, dispatch_collection): "since": since, "clsname": parent_dispatch_cls.__name__, "event_name": dispatch_collection.name, - "named_event_arguments": ", ".join(dispatch_collection.arg_names), + "named_event_arguments": ", ".join( + ":paramref:`.%(clsname)s.%(event_name)s.%(param_name)s`" + % { + "clsname": parent_dispatch_cls.__name__, + "event_name": dispatch_collection.name, + "param_name": param_name, + } + for param_name in dispatch_collection.arg_names + ), "has_kw_arguments": ", **kw" if dispatch_collection.has_kw else "", } ) diff --git a/lib/sqlalchemy/pool/base.py b/lib/sqlalchemy/pool/base.py index 9f16c65433..a8234c5309 100644 --- a/lib/sqlalchemy/pool/base.py +++ b/lib/sqlalchemy/pool/base.py @@ -115,34 +115,39 @@ class Pool(log.Identified): logging. :param reset_on_return: Determine steps to take on - connections as they are returned to the pool, which were - not otherwise handled by a :class:`_engine.Connection`. - - reset_on_return can have any of these values: - - * ``"rollback"`` - call rollback() on the connection, - to release locks and transaction resources. - This is the default value. The vast majority - of use cases should leave this value set. - * ``True`` - same as 'rollback', this is here for - backwards compatibility. - * ``"commit"`` - call commit() on the connection, - to release locks and transaction resources. - A commit here may be desirable for databases that - cache query plans if a commit is emitted, - such as Microsoft SQL Server. However, this - value is more dangerous than 'rollback' because - any data changes present on the transaction - are committed unconditionally. - * ``None`` - don't do anything on the connection. - This setting is only appropriate if the database / DBAPI - works in pure "autocommit" mode at all times, or if the - application uses the :class:`_engine.Engine` with consistent - connectivity patterns. See the section - :ref:`pool_reset_on_return` for more details. - - * ``False`` - same as None, this is here for - backwards compatibility. + connections as they are returned to the pool, which were + not otherwise handled by a :class:`_engine.Connection`. + Available from :func:`_sa.create_engine` via the + :paramref:`_sa.create_engine.pool_reset_on_return` parameter. + + :paramref:`_pool.Pool.reset_on_return` can have any of these values: + + * ``"rollback"`` - call rollback() on the connection, + to release locks and transaction resources. + This is the default value. The vast majority + of use cases should leave this value set. + * ``"commit"`` - call commit() on the connection, + to release locks and transaction resources. + A commit here may be desirable for databases that + cache query plans if a commit is emitted, + such as Microsoft SQL Server. However, this + value is more dangerous than 'rollback' because + any data changes present on the transaction + are committed unconditionally. + * ``None`` - don't do anything on the connection. + This setting may be appropriate if the database / DBAPI + works in pure "autocommit" mode at all times, or if + a custom reset handler is established using the + :meth:`.PoolEvents.reset` event handler. + * ``True`` - same as 'rollback', this is here for + backwards compatibility. + * ``False`` - same as None, this is here for + backwards compatibility. + + For further customization of reset on return, the + :meth:`.PoolEvents.reset` event hook may be used which can perform + any connection activity desired on reset. (requires version 1.4.43 + or greater) .. seealso:: @@ -495,7 +500,9 @@ class _ConnectionRecord(object): rec.fairy_ref = ref = weakref.ref( fairy, lambda ref: _finalize_fairy - and _finalize_fairy(None, rec, pool, ref, echo, True), + and _finalize_fairy( + None, rec, pool, ref, echo, transaction_was_reset=False + ), ) _strong_ref_connection_records[ref] = rec if echo: @@ -697,7 +704,7 @@ def _finalize_fairy( pool, ref, # this is None when called directly, not by the gc echo, - reset=True, + transaction_was_reset=False, fairy=None, ): """Cleanup for a :class:`._ConnectionFairy` whether or not it's already @@ -735,11 +742,8 @@ def _finalize_fairy( if dbapi_connection is not None: if connection_record and echo: pool.logger.debug( - "Connection %r being returned to pool%s", + "Connection %r being returned to pool", dbapi_connection, - ", transaction state was already reset by caller" - if not reset - else "", ) try: @@ -749,8 +753,8 @@ def _finalize_fairy( echo, ) assert fairy.dbapi_connection is dbapi_connection - if reset and can_manipulate_connection: - fairy._reset(pool) + if can_manipulate_connection: + fairy._reset(pool, transaction_was_reset) if detach: if connection_record: @@ -978,14 +982,14 @@ class _ConnectionFairy(object): def _checkout_existing(self): return _ConnectionFairy._checkout(self._pool, fairy=self) - def _checkin(self, reset=True): + def _checkin(self, transaction_was_reset=False): _finalize_fairy( self.dbapi_connection, self._connection_record, self._pool, None, self._echo, - reset=reset, + transaction_was_reset=transaction_was_reset, fairy=self, ) self.dbapi_connection = None @@ -993,15 +997,23 @@ class _ConnectionFairy(object): _close = _checkin - def _reset(self, pool): + def _reset(self, pool, transaction_was_reset=False): if pool.dispatch.reset: pool.dispatch.reset(self, self._connection_record) if pool._reset_on_return is reset_rollback: - if self._echo: - pool.logger.debug( - "Connection %s rollback-on-return", self.dbapi_connection - ) - pool._dialect.do_rollback(self) + if transaction_was_reset: + if self._echo: + pool.logger.debug( + "Connection %s reset, transaction already reset", + self.dbapi_connection, + ) + else: + if self._echo: + pool.logger.debug( + "Connection %s rollback-on-return", + self.dbapi_connection, + ) + pool._dialect.do_rollback(self) elif pool._reset_on_return is reset_commit: if self._echo: pool.logger.debug( @@ -1131,7 +1143,7 @@ class _ConnectionFairy(object): if self._counter == 0: self._checkin() - def _close_no_reset(self): + def _close_special(self, transaction_reset=False): self._counter -= 1 if self._counter == 0: - self._checkin(reset=False) + self._checkin(transaction_was_reset=transaction_reset) diff --git a/lib/sqlalchemy/pool/events.py b/lib/sqlalchemy/pool/events.py index 2829a58ae3..f0f97832bf 100644 --- a/lib/sqlalchemy/pool/events.py +++ b/lib/sqlalchemy/pool/events.py @@ -151,17 +151,37 @@ class PoolEvents(event.Events): def reset(self, dbapi_connection, connection_record): """Called before the "reset" action occurs for a pooled connection. - This event represents - when the ``rollback()`` method is called on the DBAPI connection - before it is returned to the pool. The behavior of "reset" can - be controlled, including disabled, using the ``reset_on_return`` - pool argument. - - + This event represents when the ``rollback()`` method is called on the + DBAPI connection before it is returned to the pool or discarded. A + custom "reset" strategy may be implemented using this event hook, which + may also be combined with disabling the default "reset" behavior using + the :paramref:`_pool.Pool.reset_on_return` parameter. + + The primary difference between the :meth:`_events.PoolEvents.reset` and + :meth:`_events.PoolEvents.checkin` events are that + :meth:`_events.PoolEvents.reset` is called not just for pooled + connections that are being returned to the pool, but also for + connections that were detached using the + :meth:`_engine.Connection.detach` method. + + Note that the event **is not** invoked for connections that were + invalidated using :meth:`_engine.Connection.invalidate`. These + events may be intercepted using the :meth:`.PoolEvents.soft_invalidate` + and :meth:`.PoolEvents.invalidate` event hooks, and all "connection + close" events may be intercepted using :meth:`.PoolEvents.close`. The :meth:`_events.PoolEvents.reset` event is usually followed by the - :meth:`_events.PoolEvents.checkin` event is called, except in those + :meth:`_events.PoolEvents.checkin` event, except in those cases where the connection is discarded immediately after reset. + In the 1.4 series, the event is also not invoked for asyncio + connections that are being garbage collected without their being + explicitly returned to the pool. This is due to the lack of an event + loop which prevents "reset" operations from taking place. Version 2.0 + will feature an enhanced version of :meth:`.PoolEvents.reset` which is + invoked in this scenario while passing additional contextual + information indicating that an event loop is not guaranteed + to be present. + :param dbapi_connection: a DBAPI connection. The :attr:`._ConnectionRecord.dbapi_connection` attribute. diff --git a/test/engine/test_logging.py b/test/engine/test_logging.py index 5b0d6c762e..ded9149039 100644 --- a/test/engine/test_logging.py +++ b/test/engine/test_logging.py @@ -449,6 +449,14 @@ class PoolLoggingTest(fixtures.TestBase): conn.close() conn = None + conn = q.connect() + conn._close_special(transaction_reset=True) + conn = None + + conn = q.connect() + conn._close_special(transaction_reset=False) + conn = None + conn = q.connect() conn = None del conn @@ -460,13 +468,19 @@ class PoolLoggingTest(fixtures.TestBase): [ "Created new connection %r", "Connection %r checked out from pool", - "Connection %r being returned to pool%s", + "Connection %r being returned to pool", + "Connection %s rollback-on-return", + "Connection %r checked out from pool", + "Connection %r being returned to pool", "Connection %s rollback-on-return", "Connection %r checked out from pool", - "Connection %r being returned to pool%s", + "Connection %r being returned to pool", + "Connection %s reset, transaction already reset", + "Connection %r checked out from pool", + "Connection %r being returned to pool", "Connection %s rollback-on-return", "Connection %r checked out from pool", - "Connection %r being returned to pool%s", + "Connection %r being returned to pool", "Connection %s rollback-on-return", "%s connection %r", ] diff --git a/test/engine/test_pool.py b/test/engine/test_pool.py index 879369a9ff..7a3b8ed58d 100644 --- a/test/engine/test_pool.py +++ b/test/engine/test_pool.py @@ -5,6 +5,7 @@ import time import weakref import sqlalchemy as tsa +from sqlalchemy import create_engine from sqlalchemy import event from sqlalchemy import pool from sqlalchemy import select @@ -1922,14 +1923,90 @@ class ResetOnReturnTest(PoolTestBase): pool.QueuePool(creator=lambda: dbapi.connect("foo.db"), **kw), ) - def test_plain_rollback(self): + def _engine_fixture(self, **kw): + dbapi = Mock() + + return dbapi, create_engine( + "postgresql://", + module=dbapi, + creator=lambda: dbapi.connect("foo.db"), + _initialize=False, + ) + + def test_custom(self): + dbapi, p = self._fixture(reset_on_return=None) + + @event.listens_for(p, "reset") + def custom_reset(dbapi_conn, record): + dbapi_conn.special_reset_method() + + c1 = p.connect() + c1.close() + + assert dbapi.connect().special_reset_method.called + assert not dbapi.connect().rollback.called + assert not dbapi.connect().commit.called + + @testing.combinations(True, False, argnames="assert_w_event") + @testing.combinations(True, False, argnames="use_engine_transaction") + def test_custom_via_engine(self, assert_w_event, use_engine_transaction): + dbapi, engine = self._engine_fixture(reset_on_return=None) + + if assert_w_event: + + @event.listens_for(engine, "reset") + def custom_reset(dbapi_conn, record): + dbapi_conn.special_reset_method() + + c1 = engine.connect() + if use_engine_transaction: + c1.begin() + c1.close() + assert dbapi.connect().rollback.called + + if assert_w_event: + assert dbapi.connect().special_reset_method.called + + @testing.combinations(True, False, argnames="assert_w_event") + def test_plain_rollback(self, assert_w_event): dbapi, p = self._fixture(reset_on_return="rollback") + if assert_w_event: + + @event.listens_for(p, "reset") + def custom_reset(dbapi_conn, record): + dbapi_conn.special_reset_method() + c1 = p.connect() c1.close() assert dbapi.connect().rollback.called assert not dbapi.connect().commit.called + if assert_w_event: + assert dbapi.connect().special_reset_method.called + + @testing.combinations(True, False, argnames="assert_w_event") + @testing.combinations(True, False, argnames="use_engine_transaction") + def test_plain_rollback_via_engine( + self, assert_w_event, use_engine_transaction + ): + dbapi, engine = self._engine_fixture(reset_on_return="rollback") + + if assert_w_event: + + @event.listens_for(engine, "reset") + def custom_reset(dbapi_conn, record): + dbapi_conn.special_reset_method() + + c1 = engine.connect() + if use_engine_transaction: + c1.begin() + c1.close() + assert dbapi.connect().rollback.called + + if assert_w_event: + assert dbapi.connect().special_reset_method.called + def test_plain_commit(self): dbapi, p = self._fixture(reset_on_return="commit")