2424
2525from twisted .internet import reactor , protocol
2626from twisted .internet .endpoints import connectProtocol , TCP4ClientEndpoint , SSL4ClientEndpoint
27+ from twisted .internet .error import ConnectionDone
2728from twisted .internet .interfaces import IOpenSSLClientConnectionCreator
2829from twisted .python .failure import Failure
2930from 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