@@ -205,15 +205,19 @@ def closed(self) -> bool:
205
205
class _TransportPlaceholder :
206
206
"""placeholder for BaseConnector.connect function"""
207
207
208
- __slots__ = ("closed" ,)
208
+ __slots__ = ("closed" , "transport" )
209
209
210
210
def __init__ (self , closed_future : asyncio .Future [Optional [Exception ]]) -> None :
211
211
"""Initialize a placeholder for a transport."""
212
212
self .closed = closed_future
213
+ self .transport = None
213
214
214
215
def close (self ) -> None :
215
216
"""Close the placeholder."""
216
217
218
+ def abort (self ) -> None :
219
+ """Abort the placeholder (does nothing)."""
220
+
217
221
218
222
class BaseConnector :
219
223
"""Base connector class.
@@ -431,17 +435,22 @@ def _cleanup_closed(self) -> None:
431
435
timeout_ceil_threshold = self ._timeout_ceil_threshold ,
432
436
)
433
437
434
- async def close (self ) -> None :
435
- """Close all opened transports."""
436
- waiters = self ._close_immediately ()
438
+ async def close (self , * , abort_ssl : bool = False ) -> None :
439
+ """Close all opened transports.
440
+
441
+ :param abort_ssl: If True, SSL connections will be aborted immediately
442
+ without performing the shutdown handshake. This provides
443
+ faster cleanup at the cost of less graceful disconnection.
444
+ """
445
+ waiters = self ._close_immediately (abort_ssl = abort_ssl )
437
446
if waiters :
438
447
results = await asyncio .gather (* waiters , return_exceptions = True )
439
448
for res in results :
440
449
if isinstance (res , Exception ):
441
450
err_msg = "Error while closing connector: " + repr (res )
442
451
client_logger .debug (err_msg )
443
452
444
- def _close_immediately (self ) -> List [Awaitable [object ]]:
453
+ def _close_immediately (self , * , abort_ssl : bool = False ) -> List [Awaitable [object ]]:
445
454
waiters : List [Awaitable [object ]] = []
446
455
447
456
if self ._closed :
@@ -463,12 +472,26 @@ def _close_immediately(self) -> List[Awaitable[object]]:
463
472
464
473
for data in self ._conns .values ():
465
474
for proto , _ in data :
466
- proto .close ()
475
+ if (
476
+ abort_ssl
477
+ and proto .transport
478
+ and proto .transport .get_extra_info ("sslcontext" ) is not None
479
+ ):
480
+ proto .abort ()
481
+ else :
482
+ proto .close ()
467
483
if closed := proto .closed :
468
484
waiters .append (closed )
469
485
470
486
for proto in self ._acquired :
471
- proto .close ()
487
+ if (
488
+ abort_ssl
489
+ and proto .transport
490
+ and proto .transport .get_extra_info ("sslcontext" ) is not None
491
+ ):
492
+ proto .abort ()
493
+ else :
494
+ proto .close ()
472
495
if closed := proto .closed :
473
496
waiters .append (closed )
474
497
@@ -838,11 +861,12 @@ class TCPConnector(BaseConnector):
838
861
socket_factory - A SocketFactoryType function that, if supplied,
839
862
will be used to create sockets given an
840
863
AddrInfoType.
841
- ssl_shutdown_timeout - Grace period for SSL shutdown handshake on TLS
842
- connections. Default is 0.1 seconds. This usually
843
- allows for a clean SSL shutdown by notifying the
844
- remote peer of connection closure, while avoiding
845
- excessive delays during connector cleanup.
864
+ ssl_shutdown_timeout - DEPRECATED. Will be removed in aiohttp 4.0.
865
+ Grace period for SSL shutdown handshake on TLS
866
+ connections. Default is 0 seconds (immediate abort).
867
+ This parameter allowed for a clean SSL shutdown by
868
+ notifying the remote peer of connection closure,
869
+ while avoiding excessive delays during connector cleanup.
846
870
Note: Only takes effect on Python 3.11+.
847
871
"""
848
872
@@ -866,7 +890,7 @@ def __init__(
866
890
happy_eyeballs_delay : Optional [float ] = 0.25 ,
867
891
interleave : Optional [int ] = None ,
868
892
socket_factory : Optional [SocketFactoryType ] = None ,
869
- ssl_shutdown_timeout : Optional [ float ] = 0.1 ,
893
+ ssl_shutdown_timeout : Union [ _SENTINEL , None , float ] = sentinel ,
870
894
):
871
895
super ().__init__ (
872
896
keepalive_timeout = keepalive_timeout ,
@@ -903,26 +927,57 @@ def __init__(
903
927
self ._interleave = interleave
904
928
self ._resolve_host_tasks : Set ["asyncio.Task[List[ResolveResult]]" ] = set ()
905
929
self ._socket_factory = socket_factory
906
- self ._ssl_shutdown_timeout = ssl_shutdown_timeout
930
+ self ._ssl_shutdown_timeout : Optional [ float ]
907
931
908
- def _close_immediately (self ) -> List [Awaitable [object ]]:
932
+ # Handle ssl_shutdown_timeout with warning for Python < 3.11
933
+ if ssl_shutdown_timeout is sentinel :
934
+ self ._ssl_shutdown_timeout = 0
935
+ else :
936
+ # Deprecation warning for ssl_shutdown_timeout parameter
937
+ warnings .warn (
938
+ "The ssl_shutdown_timeout parameter is deprecated and will be removed in aiohttp 4.0" ,
939
+ DeprecationWarning ,
940
+ stacklevel = 2 ,
941
+ )
942
+ if (
943
+ sys .version_info < (3 , 11 )
944
+ and ssl_shutdown_timeout is not None
945
+ and ssl_shutdown_timeout != 0
946
+ ):
947
+ warnings .warn (
948
+ f"ssl_shutdown_timeout={ ssl_shutdown_timeout } is ignored on Python < 3.11; "
949
+ "only ssl_shutdown_timeout=0 is supported. The timeout will be ignored." ,
950
+ RuntimeWarning ,
951
+ stacklevel = 2 ,
952
+ )
953
+ self ._ssl_shutdown_timeout = ssl_shutdown_timeout
954
+
955
+ async def close (self , * , abort_ssl : bool = False ) -> None :
956
+ """Close all opened transports.
957
+
958
+ :param abort_ssl: If True, SSL connections will be aborted immediately
959
+ without performing the shutdown handshake. If False (default),
960
+ the behavior is determined by ssl_shutdown_timeout:
961
+ - If ssl_shutdown_timeout=0: connections are aborted
962
+ - If ssl_shutdown_timeout>0: graceful shutdown is performed
963
+ """
964
+ if self ._resolver_owner :
965
+ await self ._resolver .close ()
966
+ # Use abort_ssl param if explicitly set, otherwise use ssl_shutdown_timeout default
967
+ await super ().close (abort_ssl = abort_ssl or self ._ssl_shutdown_timeout == 0 )
968
+
969
+ def _close_immediately (self , * , abort_ssl : bool = False ) -> List [Awaitable [object ]]:
909
970
for fut in chain .from_iterable (self ._throttle_dns_futures .values ()):
910
971
fut .cancel ()
911
972
912
- waiters = super ()._close_immediately ()
973
+ waiters = super ()._close_immediately (abort_ssl = abort_ssl )
913
974
914
975
for t in self ._resolve_host_tasks :
915
976
t .cancel ()
916
977
waiters .append (t )
917
978
918
979
return waiters
919
980
920
- async def close (self ) -> None :
921
- """Close all opened transports."""
922
- if self ._resolver_owner :
923
- await self ._resolver .close ()
924
- await super ().close ()
925
-
926
981
@property
927
982
def family (self ) -> int :
928
983
"""Socket family like AF_INET."""
@@ -1155,7 +1210,7 @@ async def _wrap_create_connection(
1155
1210
# Add ssl_shutdown_timeout for Python 3.11+ when SSL is used
1156
1211
if (
1157
1212
kwargs .get ("ssl" )
1158
- and self ._ssl_shutdown_timeout is not None
1213
+ and self ._ssl_shutdown_timeout
1159
1214
and sys .version_info >= (3 , 11 )
1160
1215
):
1161
1216
kwargs ["ssl_shutdown_timeout" ] = self ._ssl_shutdown_timeout
@@ -1233,10 +1288,7 @@ async def _start_tls_connection(
1233
1288
):
1234
1289
try :
1235
1290
# ssl_shutdown_timeout is only available in Python 3.11+
1236
- if (
1237
- sys .version_info >= (3 , 11 )
1238
- and self ._ssl_shutdown_timeout is not None
1239
- ):
1291
+ if sys .version_info >= (3 , 11 ) and self ._ssl_shutdown_timeout :
1240
1292
tls_transport = await self ._loop .start_tls (
1241
1293
underlying_transport ,
1242
1294
tls_proto ,
@@ -1257,7 +1309,10 @@ async def _start_tls_connection(
1257
1309
# We need to close the underlying transport since
1258
1310
# `start_tls()` probably failed before it had a
1259
1311
# chance to do this:
1260
- underlying_transport .close ()
1312
+ if self ._ssl_shutdown_timeout == 0 :
1313
+ underlying_transport .abort ()
1314
+ else :
1315
+ underlying_transport .close ()
1261
1316
raise
1262
1317
if isinstance (tls_transport , asyncio .Transport ):
1263
1318
fingerprint = self ._get_fingerprint (req )
0 commit comments