6
6
7
7
from .client_exceptions import ClientError , ServerTimeoutError
8
8
from .client_reqrep import ClientResponse
9
- from .helpers import call_later , set_result
9
+ from .helpers import calculate_timeout_when , set_result
10
10
from .http import (
11
11
WS_CLOSED_MESSAGE ,
12
12
WS_CLOSING_MESSAGE ,
@@ -62,6 +62,7 @@ def __init__(
62
62
self ._autoping = autoping
63
63
self ._heartbeat = heartbeat
64
64
self ._heartbeat_cb : Optional [asyncio .TimerHandle ] = None
65
+ self ._heartbeat_when : float = 0.0
65
66
if heartbeat is not None :
66
67
self ._pong_heartbeat = heartbeat / 2.0
67
68
self ._pong_response_cb : Optional [asyncio .TimerHandle ] = None
@@ -75,52 +76,64 @@ def __init__(
75
76
self ._reset_heartbeat ()
76
77
77
78
def _cancel_heartbeat (self ) -> None :
78
- if self ._pong_response_cb is not None :
79
- self ._pong_response_cb .cancel ()
80
- self ._pong_response_cb = None
81
-
79
+ self ._cancel_pong_response_cb ()
82
80
if self ._heartbeat_cb is not None :
83
81
self ._heartbeat_cb .cancel ()
84
82
self ._heartbeat_cb = None
85
83
86
- def _reset_heartbeat (self ) -> None :
87
- self ._cancel_heartbeat ()
84
+ def _cancel_pong_response_cb (self ) -> None :
85
+ if self ._pong_response_cb is not None :
86
+ self ._pong_response_cb .cancel ()
87
+ self ._pong_response_cb = None
88
88
89
- if self ._heartbeat is not None :
90
- self ._heartbeat_cb = call_later (
91
- self ._send_heartbeat ,
92
- self ._heartbeat ,
93
- self ._loop ,
94
- timeout_ceil_threshold = (
95
- self ._conn ._connector ._timeout_ceil_threshold
96
- if self ._conn is not None
97
- else 5
98
- ),
99
- )
89
+ def _reset_heartbeat (self ) -> None :
90
+ if self ._heartbeat is None :
91
+ return
92
+ self ._cancel_pong_response_cb ()
93
+ loop = self ._loop
94
+ assert loop is not None
95
+ conn = self ._conn
96
+ timeout_ceil_threshold = (
97
+ conn ._connector ._timeout_ceil_threshold if conn is not None else 5
98
+ )
99
+ now = loop .time ()
100
+ when = calculate_timeout_when (now , self ._heartbeat , timeout_ceil_threshold )
101
+ self ._heartbeat_when = when
102
+ if self ._heartbeat_cb is None :
103
+ # We do not cancel the previous heartbeat_cb here because
104
+ # it generates a significant amount of TimerHandle churn
105
+ # which causes asyncio to rebuild the heap frequently.
106
+ # Instead _send_heartbeat() will reschedule the next
107
+ # heartbeat if it fires too early.
108
+ self ._heartbeat_cb = loop .call_at (when , self ._send_heartbeat )
100
109
101
110
def _send_heartbeat (self ) -> None :
102
- if self ._heartbeat is not None and not self ._closed :
103
- # fire-and-forget a task is not perfect but maybe ok for
104
- # sending ping. Otherwise we need a long-living heartbeat
105
- # task in the class.
106
- self ._loop .create_task (self ._writer .ping ())
107
-
108
- if self ._pong_response_cb is not None :
109
- self ._pong_response_cb .cancel ()
110
- self ._pong_response_cb = call_later (
111
- self ._pong_not_received ,
112
- self ._pong_heartbeat ,
113
- self ._loop ,
114
- timeout_ceil_threshold = (
115
- self ._conn ._connector ._timeout_ceil_threshold
116
- if self ._conn is not None
117
- else 5
118
- ),
111
+ self ._heartbeat_cb = None
112
+ loop = self ._loop
113
+ now = loop .time ()
114
+ if now < self ._heartbeat_when :
115
+ # Heartbeat fired too early, reschedule
116
+ self ._heartbeat_cb = loop .call_at (
117
+ self ._heartbeat_when , self ._send_heartbeat
119
118
)
119
+ return
120
+
121
+ # fire-and-forget a task is not perfect but maybe ok for
122
+ # sending ping. Otherwise we need a long-living heartbeat
123
+ # task in the class.
124
+ loop .create_task (self ._writer .ping ()) # type: ignore[unused-awaitable]
125
+
126
+ conn = self ._conn
127
+ timeout_ceil_threshold = (
128
+ conn ._connector ._timeout_ceil_threshold if conn is not None else 5
129
+ )
130
+ when = calculate_timeout_when (now , self ._pong_heartbeat , timeout_ceil_threshold )
131
+ self ._cancel_pong_response_cb ()
132
+ self ._pong_response_cb = loop .call_at (when , self ._pong_not_received )
120
133
121
134
def _pong_not_received (self ) -> None :
122
135
if not self ._closed :
123
- self ._closed = True
136
+ self ._set_closed ()
124
137
self ._close_code = WSCloseCode .ABNORMAL_CLOSURE
125
138
self ._exception = ServerTimeoutError ()
126
139
self ._response .close ()
@@ -129,6 +142,22 @@ def _pong_not_received(self) -> None:
129
142
WSMessage (WSMsgType .ERROR , self ._exception , None )
130
143
)
131
144
145
+ def _set_closed (self ) -> None :
146
+ """Set the connection to closed.
147
+
148
+ Cancel any heartbeat timers and set the closed flag.
149
+ """
150
+ self ._closed = True
151
+ self ._cancel_heartbeat ()
152
+
153
+ def _set_closing (self ) -> None :
154
+ """Set the connection to closing.
155
+
156
+ Cancel any heartbeat timers and set the closing flag.
157
+ """
158
+ self ._closing = True
159
+ self ._cancel_heartbeat ()
160
+
132
161
@property
133
162
def closed (self ) -> bool :
134
163
return self ._closed
@@ -193,13 +222,12 @@ async def close(self, *, code: int = WSCloseCode.OK, message: bytes = b"") -> bo
193
222
if self ._waiting and not self ._closing :
194
223
assert self ._loop is not None
195
224
self ._close_wait = self ._loop .create_future ()
196
- self ._closing = True
225
+ self ._set_closing ()
197
226
self ._reader .feed_data (WS_CLOSING_MESSAGE , 0 )
198
227
await self ._close_wait
199
228
200
229
if not self ._closed :
201
- self ._cancel_heartbeat ()
202
- self ._closed = True
230
+ self ._set_closed ()
203
231
try :
204
232
await self ._writer .close (code , message )
205
233
except asyncio .CancelledError :
@@ -266,7 +294,8 @@ async def receive(self, timeout: Optional[float] = None) -> WSMessage:
266
294
await self .close ()
267
295
return WSMessage (WSMsgType .CLOSED , None , None )
268
296
except ClientError :
269
- self ._closed = True
297
+ # Likely ServerDisconnectedError when connection is lost
298
+ self ._set_closed ()
270
299
self ._close_code = WSCloseCode .ABNORMAL_CLOSURE
271
300
return WS_CLOSED_MESSAGE
272
301
except WebSocketError as exc :
@@ -275,18 +304,18 @@ async def receive(self, timeout: Optional[float] = None) -> WSMessage:
275
304
return WSMessage (WSMsgType .ERROR , exc , None )
276
305
except Exception as exc :
277
306
self ._exception = exc
278
- self ._closing = True
307
+ self ._set_closing ()
279
308
self ._close_code = WSCloseCode .ABNORMAL_CLOSURE
280
309
await self .close ()
281
310
return WSMessage (WSMsgType .ERROR , exc , None )
282
311
283
312
if msg .type is WSMsgType .CLOSE :
284
- self ._closing = True
313
+ self ._set_closing ()
285
314
self ._close_code = msg .data
286
315
if not self ._closed and self ._autoclose :
287
316
await self .close ()
288
317
elif msg .type is WSMsgType .CLOSING :
289
- self ._closing = True
318
+ self ._set_closing ()
290
319
elif msg .type is WSMsgType .PING and self ._autoping :
291
320
await self .pong (msg .data )
292
321
continue
0 commit comments