From 419aeb2956350d04653bc09a4cb635a8f2d8ae18 Mon Sep 17 00:00:00 2001 From: Dmitry Kropachev Date: Sat, 14 Feb 2026 08:01:21 -0400 Subject: [PATCH 1/2] Fix factory() returning dead connections on close during setup (#614) After connected_event.wait() returns, factory() only checks last_error and whether the event is set. If close() was called (not defunct) and the reactor didn't set last_error, factory() returns a closed connection that immediately fails on use. Add an is_closed/is_defunct check after the last_error check to catch this case and raise ConnectionShutdown instead of returning a dead connection. --- cassandra/connection.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cassandra/connection.py b/cassandra/connection.py index 87f860f32b..32037000ca 100644 --- a/cassandra/connection.py +++ b/cassandra/connection.py @@ -866,6 +866,9 @@ def factory(cls, endpoint, timeout, host_conn = None, *args, **kwargs): if conn.is_unsupported_proto_version: raise ProtocolVersionUnsupported(endpoint, conn.protocol_version) raise conn.last_error + elif conn.is_closed or conn.is_defunct: + raise ConnectionShutdown( + "Connection to %s was closed during setup" % (endpoint,)) elif not conn.connected_event.is_set(): conn.close() raise OperationTimedOut("Timed out creating connection (%s seconds)" % timeout) From e24e1cbd1aff52e87eb7b24529835c9887555201 Mon Sep 17 00:00:00 2001 From: Dmitry Kropachev Date: Sun, 22 Feb 2026 11:02:40 -0400 Subject: [PATCH 2/2] Add unit tests for factory() close-during-setup fix Verify that Connection.factory() raises ConnectionShutdown when a connection becomes closed or defunct during setup, instead of silently returning a dead connection. --- tests/unit/test_connection.py | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/unit/test_connection.py b/tests/unit/test_connection.py index 6ac63ff761..d6f7f5ade9 100644 --- a/tests/unit/test_connection.py +++ b/tests/unit/test_connection.py @@ -571,3 +571,41 @@ def test_generate_is_repeatable_with_same_mock(self, mock_randrange): second_run = list(itertools.islice(gen.generate(0, 2), 5)) assert first_run == second_run + + +class FactoryCloseRaceTest(unittest.TestCase): + """Tests for Connection.factory() handling connections closed during setup.""" + + def _make_fake_connection_class(self, is_closed=False, is_defunct=False, last_error=None): + """Create a fake connection class whose __init__ sets up minimal state + needed by factory() without actually connecting to anything.""" + from threading import Event + + class FakeConnection(Connection): + def __init__(self, endpoint, *args, **kwargs): # noqa - intentionally skips super().__init__ + self.connected_event = Event() + self.connected_event.set() + self.is_closed = is_closed + self.is_defunct = is_defunct + self.is_unsupported_proto_version = False + self.last_error = last_error + self.endpoint = endpoint + + return FakeConnection + + def test_factory_raises_on_closed_during_setup(self): + FakeConn = self._make_fake_connection_class(is_closed=True) + with pytest.raises(ConnectionShutdown, match="closed during setup"): + FakeConn.factory(DefaultEndPoint('1.2.3.4'), timeout=5) + + def test_factory_raises_on_defunct_during_setup(self): + FakeConn = self._make_fake_connection_class(is_defunct=True) + with pytest.raises(ConnectionShutdown, match="closed during setup"): + FakeConn.factory(DefaultEndPoint('1.2.3.4'), timeout=5) + + def test_factory_returns_conn_when_connected_normally(self): + FakeConn = self._make_fake_connection_class(is_closed=False, is_defunct=False) + result = FakeConn.factory(DefaultEndPoint('1.2.3.4'), timeout=5) + assert result is not None + assert not result.is_closed + assert not result.is_defunct