Skip to content

Commit 0dcb688

Browse files
committed
connection: tolerate clean startup close
1 parent 9b5b037 commit 0dcb688

12 files changed

Lines changed: 11482 additions & 655 deletions

cassandra/cluster.py

Lines changed: 2822 additions & 273 deletions
Large diffs are not rendered by default.

cassandra/connection.py

Lines changed: 395 additions & 45 deletions
Large diffs are not rendered by default.

cassandra/io/twistedreactor.py

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from twisted.internet import reactor, protocol
2626
from twisted.internet.endpoints import connectProtocol, TCP4ClientEndpoint, SSL4ClientEndpoint
27+
from twisted.internet.error import ConnectionDone
2728
from twisted.internet.interfaces import IOpenSSLClientConnectionCreator
2829
from twisted.python.failure import Failure
2930
from zope.interface import implementer
@@ -198,6 +199,9 @@ def create_timer(cls, timeout, callback):
198199
cls._loop.add_timer(timer)
199200
return timer
200201

202+
def _is_clean_close_error(self, exc):
203+
return isinstance(exc, ConnectionDone) or Connection._is_clean_close_error(self, exc)
204+
201205
def __init__(self, *args, **kwargs):
202206
"""
203207
Initialization method.
@@ -209,7 +213,10 @@ def __init__(self, *args, **kwargs):
209213
"""
210214
Connection.__init__(self, *args, **kwargs)
211215

212-
self.is_closed = True
216+
# A scheduled or in-progress endpoint connection is still closeable.
217+
# Keeping it logically open lets pool shutdown cancel it before
218+
# connectionMade.
219+
self.is_closed = False
213220
self.connector = None
214221
self.transport = None
215222

@@ -226,9 +233,12 @@ def _check_pyopenssl(self):
226233

227234
def add_connection(self):
228235
"""
229-
Convenience function to connect and store the resulting
230-
connector.
236+
Convenience function to connect and store the resulting Deferred.
231237
"""
238+
with self.lock:
239+
if self.is_closed:
240+
return
241+
232242
host, port = self.endpoint.resolve()
233243
if self.ssl_context or self.ssl_options:
234244
# Can't use optionsForClientTLS here because it *forces* hostname verification.
@@ -257,16 +267,42 @@ def add_connection(self):
257267
port,
258268
timeout=self.connect_timeout
259269
)
260-
connectProtocol(endpoint, TwistedConnectionProtocol(self))
270+
271+
# connectProtocol returns a cancellable Deferred. Keep the final
272+
# shutdown check and assignment under the lock so close() either stops
273+
# this attempt before it starts or observes the Deferred and cancels it.
274+
with self.lock:
275+
if self.is_closed:
276+
return
277+
self.connector = connectProtocol(
278+
endpoint, TwistedConnectionProtocol(self))
279+
self.connector.addErrback(self._handle_connect_failure)
280+
281+
def _handle_connect_failure(self, failure):
282+
# Pool shutdown intentionally cancels the endpoint Deferred. Consume
283+
# that failure instead of leaving an unhandled cancellation in Twisted.
284+
with self.lock:
285+
if self.is_closed:
286+
return None
287+
self.defunct(failure.value)
288+
return None
261289

262290
def client_connection_made(self, transport):
263291
"""
264292
Called by twisted protocol when a connection attempt has
265293
succeeded.
266294
"""
267295
with self.lock:
268-
self.is_closed = False
269-
self.transport = transport
296+
if self.is_closed:
297+
close_transport = True
298+
else:
299+
close_transport = False
300+
self.transport = transport
301+
302+
if close_transport:
303+
reactor.callFromThread(transport.connector.disconnect)
304+
return
305+
270306
self._send_options_message()
271307

272308
def close(self):
@@ -277,18 +313,29 @@ def close(self):
277313
if self.is_closed:
278314
return
279315
self.is_closed = True
316+
connector = self.connector
317+
transport = self.transport
318+
319+
shutdown_error = None
320+
if not self.is_defunct:
321+
msg = "Connection to %s was closed" % self.endpoint
322+
if self.last_error:
323+
msg += ": %s" % (self.last_error,)
324+
shutdown_error = ConnectionShutdown(msg)
325+
if not self._startup_completed:
326+
self.last_error = shutdown_error
327+
# Wake Connection.factory before waiting for reactor cleanup.
328+
self.connected_event.set()
280329

281330
log.debug("Closing connection (%s) to %s", id(self), self.endpoint)
282-
reactor.callFromThread(self.transport.connector.disconnect)
331+
if transport is not None:
332+
reactor.callFromThread(transport.connector.disconnect)
333+
elif connector is not None:
334+
reactor.callFromThread(connector.cancel)
283335
log.debug("Closed socket to %s", self.endpoint)
284336

285-
if not self.is_defunct:
286-
msg = "Connection to %s was closed" % self.endpoint
287-
if self.last_error:
288-
msg += ": %s" % (self.last_error,)
289-
self.error_all_requests(ConnectionShutdown(msg))
290-
# don't leave in-progress operations hanging
291-
self.connected_event.set()
337+
if shutdown_error is not None:
338+
self.error_all_requests(shutdown_error)
292339

293340
def handle_read(self):
294341
"""

0 commit comments

Comments
 (0)