--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 5034
+
+ Fixed issue where by if the "begin" of a transaction failed at the Core
+ engine/connection level, such as due to network error or database is locked
+ for some transactional recipes, within the context of the :class:`.Session`
+ procuring that connection from the conneciton pool and then immediately
+ returning it, the ORM :class:`.Session` would not close the connection
+ despite this connection not being stored within the state of that
+ :class:`.Session`. This would lead to the connection being cleaned out by
+ the connection pool weakref handler within garbage collection which is an
+ unpreferred codepath that in some special configurations can emit errors in
+ standard error.
)
return self._connections[bind][0]
+ local_connect = False
if self._parent:
conn = self._parent._connection_for_bind(bind, execution_options)
if not self.nested:
)
else:
conn = bind.connect()
+ local_connect = True
- if execution_options:
- conn = conn.execution_options(**execution_options)
+ try:
+ if execution_options:
+ conn = conn.execution_options(**execution_options)
- if self.session.twophase and self._parent is None:
- transaction = conn.begin_twophase()
- elif self.nested:
- transaction = conn.begin_nested()
+ if self.session.twophase and self._parent is None:
+ transaction = conn.begin_twophase()
+ elif self.nested:
+ transaction = conn.begin_nested()
+ else:
+ transaction = conn.begin()
+ except:
+ # connection will not not be associated with this Session;
+ # close it immediately so that it isn't closed under GC
+ if local_connect:
+ conn.close()
+ raise
else:
- transaction = conn.begin()
-
- self._connections[conn] = self._connections[conn.engine] = (
- conn,
- transaction,
- conn is not bind,
- )
- self.session.dispatch.after_begin(self.session, self, conn)
- return conn
+ self._connections[conn] = self._connections[conn.engine] = (
+ conn,
+ transaction,
+ conn is not bind,
+ )
+ self.session.dispatch.after_begin(self.session, self, conn)
+ return conn
def prepare(self):
if self._parent is not None or not self.session.twophase:
eq_(len(sess.query(User).all()), 1)
+ def test_begin_fails_connection_is_closed(self):
+ eng = engines.testing_engine()
+
+ state = []
+
+ @event.listens_for(eng, "begin")
+ def do_begin(conn):
+ state.append((conn, conn.connection))
+ raise Exception("failure")
+
+ s1 = Session(eng)
+
+ assert_raises_message(Exception, "failure", s1.execute, "select 1")
+
+ conn, fairy = state[0]
+ assert not fairy.is_valid
+ assert conn.closed
+ assert not conn.invalidated
+
+ s1.close()
+
+ # close does not occur because references were not saved, however
+ # the underlying DBAPI connection was closed
+ assert not fairy.is_valid
+ assert conn.closed
+ assert not conn.invalidated
+
+ def test_begin_savepoint_fails_connection_is_not_closed(self):
+ eng = engines.testing_engine()
+
+ state = []
+
+ @event.listens_for(eng, "savepoint")
+ def do_begin(conn, name):
+ state.append((conn, conn.connection))
+ raise Exception("failure")
+
+ s1 = Session(eng)
+
+ s1.begin_nested()
+ assert_raises_message(Exception, "failure", s1.execute, "select 1")
+
+ conn, fairy = state[0]
+ assert fairy.is_valid
+ assert not conn.closed
+ assert not conn.invalidated
+
+ s1.close()
+
+ assert conn.closed
+ assert not fairy.is_valid
+
def test_continue_flushing_on_commit(self):
"""test that post-flush actions get flushed also if
we're in commit()"""
bind.mock_calls,
[
mock.call.connect(),
- mock.call.connect().execution_options(
- isolation_level="FOO"
- ),
+ mock.call.connect().execution_options(isolation_level="FOO"),
mock.call.connect().execution_options().begin(),
],
)