--- /dev/null
+.. change::
+ :tags: bug, pool
+ :tickets: 4184
+
+ Fixed a fairly serious connection pool bug where a connection that is
+ acquired after being refreshed as a result of a user-defined
+ :class:`.DisconnectionError` or due to the 1.2-released "pre_ping" feature
+ would not be correctly reset if the connection were returned to the pool by
+ weakref cleanup (e.g. the front-facing object is garbage collected); the
+ weakref would still refer to the previously invalidated DBAPI connection
+ which would have the reset operation erroneously called upon it instead.
+ This would lead to stack traces in the logs and a connection being checked
+ into the pool without being reset, which can cause locking issues.
+
fairy,
lambda ref: _finalize_fairy and
_finalize_fairy(
- dbapi_connection,
+ None,
rec, pool, ref, echo)
)
_refs.add(rec)
"""
_refs.discard(connection_record)
- if ref is not None and \
- connection_record.fairy_ref is not ref:
- return
+ if ref is not None:
+ if connection_record.fairy_ref is not ref:
+ return
+ assert connection is None
+ connection = connection_record.connection
if connection is not None:
if connection_record and echo:
pool.logger.info(
"Disconnection detected on checkout, "
"invalidating all pooled connections prior to "
- "current timestamp: %r", e)
+ "current timestamp (reason: %r)", e)
fairy._connection_record.invalidate(e)
pool._invalidate(fairy, e, _checkin=False)
else:
pool.logger.info(
"Disconnection detected on checkout, "
- "invalidating individual connection: %r", e)
+ "invalidating individual connection %s (reason: %r)",
+ fairy.connection, e)
fairy._connection_record.invalidate(e)
try:
fairy.connection = \
self._assert_cleanup_on_pooled_reconnect(dbapi, p)
+ @testing.requires.predictable_gc
+ def test_userspace_disconnectionerror_weakref_finalizer(self):
+ dbapi, pool = self._queuepool_dbapi_fixture(
+ pool_size=1,
+ max_overflow=2)
+
+ @event.listens_for(pool, "checkout")
+ def handle_checkout_event(dbapi_con, con_record, con_proxy):
+ if getattr(dbapi_con, 'boom') == 'yes':
+ raise tsa.exc.DisconnectionError()
+
+ conn = pool.connect()
+ old_dbapi_conn = conn.connection
+ conn.close()
+
+ eq_(old_dbapi_conn.mock_calls, [call.rollback()])
+
+ old_dbapi_conn.boom = 'yes'
+
+ conn = pool.connect()
+ dbapi_conn = conn.connection
+ del conn
+ gc_collect()
+
+ # new connection was reset on return appropriately
+ eq_(dbapi_conn.mock_calls, [call.rollback()])
+
+ # old connection was just closed - did not get an
+ # erroneous reset on return
+ eq_(
+ old_dbapi_conn.mock_calls,
+ [call.rollback(), call.close()]
+ )
+
@testing.requires.timing_intensive
def test_recycle_pool_no_race(self):
def slow_close():
from sqlalchemy.testing.engines import testing_engine
from sqlalchemy.testing.mock import Mock, call, patch
from sqlalchemy import event
+from sqlalchemy.testing.util import gc_collect
class MockError(Exception):
cursor.execute, "foo"
)
+ @testing.requires.predictable_gc
+ def test_pre_ping_weakref_finalizer(self):
+ pool = self._pool_fixture(pre_ping=True)
+
+ conn = pool.connect()
+ old_dbapi_conn = conn.connection
+ conn.close()
+
+ eq_(old_dbapi_conn.mock_calls, [call.cursor(), call.rollback()])
+
+ self.dbapi.shutdown("execute", stop=True)
+ self.dbapi.restart()
+
+ conn = pool.connect()
+ dbapi_conn = conn.connection
+ del conn
+ gc_collect()
+
+ # new connection was reset on return appropriately
+ eq_(dbapi_conn.mock_calls, [call.cursor(), call.rollback()])
+
+ # old connection was just closed - did not get an
+ # erroneous reset on return
+ eq_(
+ old_dbapi_conn.mock_calls,
+ [call.cursor(), call.rollback(), call.cursor(), call.close()]
+ )
+
class MockReconnectTest(fixtures.TestBase):
def setup(self):