]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
ensure pool.reset event always called for reset
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 25 Oct 2022 20:00:50 +0000 (16:00 -0400)
committermike bayer <mike_mp@zzzcomputing.com>
Sun, 30 Oct 2022 18:18:38 +0000 (18:18 +0000)
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)

12 files changed:
doc/build/changelog/unreleased_14/8717.rst [new file with mode: 0644]
doc/build/conf.py
doc/build/core/pooling.rst
lib/sqlalchemy/dialects/mssql/base.py
lib/sqlalchemy/dialects/postgresql/base.py
lib/sqlalchemy/engine/base.py
lib/sqlalchemy/engine/create.py
lib/sqlalchemy/event/legacy.py
lib/sqlalchemy/pool/base.py
lib/sqlalchemy/pool/events.py
test/engine/test_logging.py
test/engine/test_pool.py

diff --git a/doc/build/changelog/unreleased_14/8717.rst b/doc/build/changelog/unreleased_14/8717.rst
new file mode 100644 (file)
index 0000000..4f3c5cd
--- /dev/null
@@ -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.
+
index d1144b41bbc291a8f29d3707a08b6c99fc7fba08..86f1925303c8493d08ed6b71dbc57f3cb0625592 100644 (file)
@@ -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
index 138feace2866c157726a5edc0715a4456c0fd27b..c147b1d0b7d771252515b4c0493a824a95ccda78 100644 (file)
@@ -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 <dbapi_autocommit_understanding>` 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 <connection object ...>
+    DEBUG sqlalchemy.pool.impl.QueuePool Connection <connection object ...> checked out from pool
+    >>> c1.close()
+    DEBUG sqlalchemy.pool.impl.QueuePool Connection <connection object ...> being returned to pool
+    DEBUG sqlalchemy.pool.impl.QueuePool Connection <connection object ...> 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:
index 6c530b631c2559b4a1a83f9ed959c28cfe191642..0509413062fd466344aeec091eb59d0a443d1e4b 100644 (file)
@@ -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 <pool_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
index eb841700d3b169e06c0f4dba5bc92c1aaeef31e7..6820aa60154a8166ce127ace44f05347bf9f6b30 100644 (file)
@@ -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 <pool_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
index f126eb0c56e1813cef0b7094c054f63af9e8f0df..00e1be77669af1884a435430b77055967ffcd770 100644 (file)
@@ -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()
 
index b9886b701b75bebeed07948273faa5748fccd918..8c929ccc4abf80ba50063c12d9ae94f5815584c9 100644 (file)
@@ -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
index d9f6ce57354906b0549ec07e359625c5cf9b58f9..686e4c5bf5da7389a26100225557641ea5b7e0d8 100644 (file)
@@ -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 "",
         }
     )
index 9f16c654334e271eecf0c06a72811b9bf5544358..a8234c53093cf78df92d296d7410b8f954a0fd7f 100644 (file)
@@ -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)
index 2829a58ae30681bc34b638d94b3065bef94a1a98..f0f97832bf1d791ae284737de4c467087fe006db 100644 (file)
@@ -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.
 
index 5b0d6c762e26de3313a71d80fde9d3729b57c067..ded91490396d8fd78ff5586ab530af7365356e20 100644 (file)
@@ -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",
             ]
index 879369a9ffd344d0ce709cabe141bceceb894e78..7a3b8ed58dc5ec51dd3c6fcddba044b6cc9d5631 100644 (file)
@@ -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")