@@ -63,11 +63,10 @@ def __init__(
63
63
self .ping_interval = ping_interval
64
64
self .ping_timeout = ping_timeout
65
65
self .close_timeout = close_timeout
66
- self .max_queue : tuple [int | None , int | None ]
67
66
if isinstance (max_queue , int ) or max_queue is None :
68
- self .max_queue = ( max_queue , None )
67
+ self .max_queue_high , self . max_queue_low = max_queue , None
69
68
else :
70
- self .max_queue = max_queue
69
+ self .max_queue_high , self . max_queue_low = max_queue
71
70
if isinstance (write_limit , int ):
72
71
write_limit = (write_limit , None )
73
72
self .write_limit = write_limit
@@ -101,12 +100,12 @@ def __init__(
101
100
self .close_deadline : float | None = None
102
101
103
102
# Protect sending fragmented messages.
104
- self .fragmented_send_waiter : asyncio .Future [None ] | None = None
103
+ self .send_in_progress : asyncio .Future [None ] | None = None
105
104
106
105
# Mapping of ping IDs to pong waiters, in chronological order.
107
- self .pong_waiters : dict [bytes , tuple [asyncio .Future [float ], float ]] = {}
106
+ self .pending_pings : dict [bytes , tuple [asyncio .Future [float ], float ]] = {}
108
107
109
- self .latency : float = 0
108
+ self .latency : float = 0.0
110
109
"""
111
110
Latency of the connection, in seconds.
112
111
@@ -417,16 +416,16 @@ async def send(
417
416
418
417
You may override this behavior with the ``text`` argument:
419
418
420
- * Set ``text=True`` to send a bytestring or bytes-like object
421
- (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) as a
419
+ * Set ``text=True`` to send an UTF-8 bytestring or bytes-like object
420
+ (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) in a
422
421
Text_ frame. This improves performance when the message is already
423
422
UTF-8 encoded, for example if the message contains JSON and you're
424
423
using a JSON library that produces a bytestring.
425
424
* Set ``text=False`` to send a string (:class:`str`) in a Binary_
426
425
frame. This may be useful for servers that expect binary frames
427
426
instead of text frames.
428
427
429
- :meth:`send` also accepts an iterable or an asynchronous iterable of
428
+ :meth:`send` also accepts an iterable or asynchronous iterable of
430
429
strings, bytestrings, or bytes-like objects to enable fragmentation_.
431
430
Each item is treated as a message fragment and sent in its own frame.
432
431
All items must be of the same type, or else :meth:`send` will raise a
@@ -468,8 +467,8 @@ async def send(
468
467
"""
469
468
# While sending a fragmented message, prevent sending other messages
470
469
# until all fragments are sent.
471
- while self .fragmented_send_waiter is not None :
472
- await asyncio .shield (self .fragmented_send_waiter )
470
+ while self .send_in_progress is not None :
471
+ await asyncio .shield (self .send_in_progress )
473
472
474
473
# Unfragmented message -- this case must be handled first because
475
474
# strings and bytes-like objects are iterable.
@@ -502,8 +501,8 @@ async def send(
502
501
except StopIteration :
503
502
return
504
503
505
- assert self .fragmented_send_waiter is None
506
- self .fragmented_send_waiter = self .loop .create_future ()
504
+ assert self .send_in_progress is None
505
+ self .send_in_progress = self .loop .create_future ()
507
506
try :
508
507
# First fragment.
509
508
if isinstance (chunk , str ):
@@ -549,8 +548,8 @@ async def send(
549
548
raise
550
549
551
550
finally :
552
- self .fragmented_send_waiter .set_result (None )
553
- self .fragmented_send_waiter = None
551
+ self .send_in_progress .set_result (None )
552
+ self .send_in_progress = None
554
553
555
554
# Fragmented message -- async iterator.
556
555
@@ -561,8 +560,8 @@ async def send(
561
560
except StopAsyncIteration :
562
561
return
563
562
564
- assert self .fragmented_send_waiter is None
565
- self .fragmented_send_waiter = self .loop .create_future ()
563
+ assert self .send_in_progress is None
564
+ self .send_in_progress = self .loop .create_future ()
566
565
try :
567
566
# First fragment.
568
567
if isinstance (chunk , str ):
@@ -610,8 +609,8 @@ async def send(
610
609
raise
611
610
612
611
finally :
613
- self .fragmented_send_waiter .set_result (None )
614
- self .fragmented_send_waiter = None
612
+ self .send_in_progress .set_result (None )
613
+ self .send_in_progress = None
615
614
616
615
else :
617
616
raise TypeError ("data must be str, bytes, iterable, or async iterable" )
@@ -639,7 +638,7 @@ async def close(
639
638
# The context manager takes care of waiting for the TCP connection
640
639
# to terminate after calling a method that sends a close frame.
641
640
async with self .send_context ():
642
- if self .fragmented_send_waiter is not None :
641
+ if self .send_in_progress is not None :
643
642
self .protocol .fail (
644
643
CloseCode .INTERNAL_ERROR ,
645
644
"close during fragmented message" ,
@@ -681,9 +680,9 @@ async def ping(self, data: DataLike | None = None) -> Awaitable[float]:
681
680
682
681
::
683
682
684
- pong_waiter = await ws.ping()
683
+ pong_received = await ws.ping()
685
684
# only if you want to wait for the corresponding pong
686
- latency = await pong_waiter
685
+ latency = await pong_received
687
686
688
687
Raises:
689
688
ConnectionClosed: When the connection is closed.
@@ -700,19 +699,20 @@ async def ping(self, data: DataLike | None = None) -> Awaitable[float]:
700
699
701
700
async with self .send_context ():
702
701
# Protect against duplicates if a payload is explicitly set.
703
- if data in self .pong_waiters :
702
+ if data in self .pending_pings :
704
703
raise ConcurrencyError ("already waiting for a pong with the same data" )
705
704
706
705
# Generate a unique random payload otherwise.
707
- while data is None or data in self .pong_waiters :
706
+ while data is None or data in self .pending_pings :
708
707
data = struct .pack ("!I" , random .getrandbits (32 ))
709
708
710
- pong_waiter = self .loop .create_future ()
709
+ pong_received = self .loop .create_future ()
710
+ ping_timestamp = self .loop .time ()
711
711
# The event loop's default clock is time.monotonic(). Its resolution
712
712
# is a bit low on Windows (~16ms). This is improved in Python 3.13.
713
- self .pong_waiters [data ] = (pong_waiter , self . loop . time () )
713
+ self .pending_pings [data ] = (pong_received , ping_timestamp )
714
714
self .protocol .send_ping (data )
715
- return pong_waiter
715
+ return pong_received
716
716
717
717
async def pong (self , data : DataLike = b"" ) -> None :
718
718
"""
@@ -761,7 +761,7 @@ def acknowledge_pings(self, data: bytes) -> None:
761
761
762
762
"""
763
763
# Ignore unsolicited pong.
764
- if data not in self .pong_waiters :
764
+ if data not in self .pending_pings :
765
765
return
766
766
767
767
pong_timestamp = self .loop .time ()
@@ -770,41 +770,39 @@ def acknowledge_pings(self, data: bytes) -> None:
770
770
# Acknowledge all previous pings too in that case.
771
771
ping_id = None
772
772
ping_ids = []
773
- for ping_id , (pong_waiter , ping_timestamp ) in self .pong_waiters .items ():
773
+ for ping_id , (pong_received , ping_timestamp ) in self .pending_pings .items ():
774
774
ping_ids .append (ping_id )
775
775
latency = pong_timestamp - ping_timestamp
776
- if not pong_waiter .done ():
777
- pong_waiter .set_result (latency )
776
+ if not pong_received .done ():
777
+ pong_received .set_result (latency )
778
778
if ping_id == data :
779
779
self .latency = latency
780
780
break
781
781
else :
782
782
raise AssertionError ("solicited pong not found in pings" )
783
783
784
- # Remove acknowledged pings from self.pong_waiters .
784
+ # Remove acknowledged pings from self.pending_pings .
785
785
for ping_id in ping_ids :
786
- del self .pong_waiters [ping_id ]
786
+ del self .pending_pings [ping_id ]
787
787
788
- def abort_pings (self ) -> None :
788
+ def terminate_pending_pings (self ) -> None :
789
789
"""
790
- Raise ConnectionClosed in pending pings.
791
-
792
- They'll never receive a pong once the connection is closed.
790
+ Raise ConnectionClosed in pending pings when the connection is closed.
793
791
794
792
"""
795
793
assert self .protocol .state is CLOSED
796
794
exc = self .protocol .close_exc
797
795
798
- for pong_waiter , _ping_timestamp in self .pong_waiters .values ():
799
- if not pong_waiter .done ():
800
- pong_waiter .set_exception (exc )
796
+ for pong_received , _ping_timestamp in self .pending_pings .values ():
797
+ if not pong_received .done ():
798
+ pong_received .set_exception (exc )
801
799
# If the exception is never retrieved, it will be logged when ping
802
800
# is garbage-collected. This is confusing for users.
803
801
# Given that ping is done (with an exception), canceling it does
804
802
# nothing, but it prevents logging the exception.
805
- pong_waiter .cancel ()
803
+ pong_received .cancel ()
806
804
807
- self .pong_waiters .clear ()
805
+ self .pending_pings .clear ()
808
806
809
807
async def keepalive (self ) -> None :
810
808
"""
@@ -825,7 +823,7 @@ async def keepalive(self) -> None:
825
823
# connection to be closed before raising ConnectionClosed.
826
824
# However, connection_lost() cancels keepalive_task before
827
825
# it gets a chance to resume excuting.
828
- pong_waiter = await self .ping ()
826
+ pong_received = await self .ping ()
829
827
if self .debug :
830
828
self .logger .debug ("% sent keepalive ping" )
831
829
@@ -834,10 +832,11 @@ async def keepalive(self) -> None:
834
832
async with asyncio_timeout (self .ping_timeout ):
835
833
# connection_lost cancels keepalive immediately
836
834
# after setting a ConnectionClosed exception on
837
- # pong_waiter . A CancelledError is raised here,
835
+ # pong_received . A CancelledError is raised here,
838
836
# not a ConnectionClosed exception.
839
- latency = await pong_waiter
840
- self .logger .debug ("% received keepalive pong" )
837
+ latency = await pong_received
838
+ if self .debug :
839
+ self .logger .debug ("% received keepalive pong" )
841
840
except asyncio .TimeoutError :
842
841
if self .debug :
843
842
self .logger .debug ("- timed out waiting for keepalive pong" )
@@ -908,14 +907,13 @@ async def send_context(
908
907
# Check if the connection is expected to close soon.
909
908
if self .protocol .close_expected ():
910
909
wait_for_close = True
911
- # If the connection is expected to close soon, set the
912
- # close deadline based on the close timeout.
913
- # Since we tested earlier that protocol.state was OPEN
910
+ # Set the close deadline based on the close timeout.
911
+ # Since we tested earlier that protocol.state is OPEN
914
912
# (or CONNECTING), self.close_deadline is still None.
913
+ assert self .close_deadline is None
915
914
if self .close_timeout is not None :
916
- assert self .close_deadline is None
917
915
self .close_deadline = self .loop .time () + self .close_timeout
918
- # Write outgoing data to the socket and enforce flow control.
916
+ # Write outgoing data to the socket with flow control.
919
917
try :
920
918
self .send_data ()
921
919
await self .drain ()
@@ -933,9 +931,8 @@ async def send_context(
933
931
# will be closing soon if it isn't in the expected state.
934
932
wait_for_close = True
935
933
# Calculate close_deadline if it wasn't set yet.
936
- if self .close_timeout is not None :
937
- if self .close_deadline is None :
938
- self .close_deadline = self .loop .time () + self .close_timeout
934
+ if self .close_deadline is None and self .close_timeout is not None :
935
+ self .close_deadline = self .loop .time () + self .close_timeout
939
936
raise_close_exc = True
940
937
941
938
# If the connection is expected to close soon and the close timeout
@@ -966,9 +963,6 @@ def send_data(self) -> None:
966
963
"""
967
964
Send outgoing data.
968
965
969
- Raises:
970
- OSError: When a socket operations fails.
971
-
972
966
"""
973
967
for data in self .protocol .data_to_send ():
974
968
if data :
@@ -982,7 +976,7 @@ def send_data(self) -> None:
982
976
# OSError is plausible. uvloop can raise RuntimeError here.
983
977
try :
984
978
self .transport .write_eof ()
985
- except ( OSError , RuntimeError ) : # pragma: no cover
979
+ except Exception : # pragma: no cover
986
980
pass
987
981
# Else, close the TCP connection.
988
982
else : # pragma: no cover
@@ -1005,7 +999,8 @@ def set_recv_exc(self, exc: BaseException | None) -> None:
1005
999
def connection_made (self , transport : asyncio .BaseTransport ) -> None :
1006
1000
transport = cast (asyncio .Transport , transport )
1007
1001
self .recv_messages = Assembler (
1008
- * self .max_queue ,
1002
+ self .max_queue_high ,
1003
+ self .max_queue_low ,
1009
1004
pause = transport .pause_reading ,
1010
1005
resume = transport .resume_reading ,
1011
1006
)
@@ -1022,7 +1017,7 @@ def connection_lost(self, exc: Exception | None) -> None:
1022
1017
1023
1018
# Abort recv() and pending pings with a ConnectionClosed exception.
1024
1019
self .recv_messages .close ()
1025
- self .abort_pings ()
1020
+ self .terminate_pending_pings ()
1026
1021
1027
1022
if self .keepalive_task is not None :
1028
1023
self .keepalive_task .cancel ()
@@ -1092,12 +1087,10 @@ def data_received(self, data: bytes) -> None:
1092
1087
self .logger .debug ("! error while sending data" , exc_info = True )
1093
1088
self .set_recv_exc (exc )
1094
1089
1090
+ # If needed, set the close deadline based on the close timeout.
1095
1091
if self .protocol .close_expected ():
1096
- # If the connection is expected to close soon, set the
1097
- # close deadline based on the close timeout.
1098
- if self .close_timeout is not None :
1099
- if self .close_deadline is None :
1100
- self .close_deadline = self .loop .time () + self .close_timeout
1092
+ if self .close_deadline is None and self .close_timeout is not None :
1093
+ self .close_deadline = self .loop .time () + self .close_timeout
1101
1094
1102
1095
for event in events :
1103
1096
# This isn't expected to raise an exception.
@@ -1205,7 +1198,7 @@ def broadcast(
1205
1198
if connection .protocol .state is not OPEN :
1206
1199
continue
1207
1200
1208
- if connection .fragmented_send_waiter is not None :
1201
+ if connection .send_in_progress is not None :
1209
1202
if raise_exceptions :
1210
1203
exception = ConcurrencyError ("sending a fragmented message" )
1211
1204
exceptions .append (exception )
0 commit comments