7
7
8
8
from .client_exceptions import ClientError , ServerTimeoutError
9
9
from .client_reqrep import ClientResponse
10
- from .helpers import call_later , set_result
10
+ from .helpers import calculate_timeout_when , set_result
11
11
from .http import (
12
12
WS_CLOSED_MESSAGE ,
13
13
WS_CLOSING_MESSAGE ,
@@ -72,6 +72,7 @@ def __init__(
72
72
self ._autoping = autoping
73
73
self ._heartbeat = heartbeat
74
74
self ._heartbeat_cb : Optional [asyncio .TimerHandle ] = None
75
+ self ._heartbeat_when : float = 0.0
75
76
if heartbeat is not None :
76
77
self ._pong_heartbeat = heartbeat / 2.0
77
78
self ._pong_response_cb : Optional [asyncio .TimerHandle ] = None
@@ -85,52 +86,64 @@ def __init__(
85
86
self ._reset_heartbeat ()
86
87
87
88
def _cancel_heartbeat (self ) -> None :
88
- if self ._pong_response_cb is not None :
89
- self ._pong_response_cb .cancel ()
90
- self ._pong_response_cb = None
91
-
89
+ self ._cancel_pong_response_cb ()
92
90
if self ._heartbeat_cb is not None :
93
91
self ._heartbeat_cb .cancel ()
94
92
self ._heartbeat_cb = None
95
93
96
- def _reset_heartbeat (self ) -> None :
97
- self ._cancel_heartbeat ()
94
+ def _cancel_pong_response_cb (self ) -> None :
95
+ if self ._pong_response_cb is not None :
96
+ self ._pong_response_cb .cancel ()
97
+ self ._pong_response_cb = None
98
98
99
- if self ._heartbeat is not None :
100
- self ._heartbeat_cb = call_later (
101
- self ._send_heartbeat ,
102
- self ._heartbeat ,
103
- self ._loop ,
104
- timeout_ceil_threshold = (
105
- self ._conn ._connector ._timeout_ceil_threshold
106
- if self ._conn is not None
107
- else 5
108
- ),
109
- )
99
+ def _reset_heartbeat (self ) -> None :
100
+ if self ._heartbeat is None :
101
+ return
102
+ self ._cancel_pong_response_cb ()
103
+ loop = self ._loop
104
+ assert loop is not None
105
+ conn = self ._conn
106
+ timeout_ceil_threshold = (
107
+ conn ._connector ._timeout_ceil_threshold if conn is not None else 5
108
+ )
109
+ now = loop .time ()
110
+ when = calculate_timeout_when (now , self ._heartbeat , timeout_ceil_threshold )
111
+ self ._heartbeat_when = when
112
+ if self ._heartbeat_cb is None :
113
+ # We do not cancel the previous heartbeat_cb here because
114
+ # it generates a significant amount of TimerHandle churn
115
+ # which causes asyncio to rebuild the heap frequently.
116
+ # Instead _send_heartbeat() will reschedule the next
117
+ # heartbeat if it fires too early.
118
+ self ._heartbeat_cb = loop .call_at (when , self ._send_heartbeat )
110
119
111
120
def _send_heartbeat (self ) -> None :
112
- if self ._heartbeat is not None and not self ._closed :
113
- # fire-and-forget a task is not perfect but maybe ok for
114
- # sending ping. Otherwise we need a long-living heartbeat
115
- # task in the class.
116
- self ._loop .create_task (self ._writer .ping ()) # type: ignore[unused-awaitable]
117
-
118
- if self ._pong_response_cb is not None :
119
- self ._pong_response_cb .cancel ()
120
- self ._pong_response_cb = call_later (
121
- self ._pong_not_received ,
122
- self ._pong_heartbeat ,
123
- self ._loop ,
124
- timeout_ceil_threshold = (
125
- self ._conn ._connector ._timeout_ceil_threshold
126
- if self ._conn is not None
127
- else 5
128
- ),
121
+ self ._heartbeat_cb = None
122
+ loop = self ._loop
123
+ now = loop .time ()
124
+ if now < self ._heartbeat_when :
125
+ # Heartbeat fired too early, reschedule
126
+ self ._heartbeat_cb = loop .call_at (
127
+ self ._heartbeat_when , self ._send_heartbeat
129
128
)
129
+ return
130
+
131
+ # fire-and-forget a task is not perfect but maybe ok for
132
+ # sending ping. Otherwise we need a long-living heartbeat
133
+ # task in the class.
134
+ loop .create_task (self ._writer .ping ()) # type: ignore[unused-awaitable]
135
+
136
+ conn = self ._conn
137
+ timeout_ceil_threshold = (
138
+ conn ._connector ._timeout_ceil_threshold if conn is not None else 5
139
+ )
140
+ when = calculate_timeout_when (now , self ._pong_heartbeat , timeout_ceil_threshold )
141
+ self ._cancel_pong_response_cb ()
142
+ self ._pong_response_cb = loop .call_at (when , self ._pong_not_received )
130
143
131
144
def _pong_not_received (self ) -> None :
132
145
if not self ._closed :
133
- self ._closed = True
146
+ self ._set_closed ()
134
147
self ._close_code = WSCloseCode .ABNORMAL_CLOSURE
135
148
self ._exception = ServerTimeoutError ()
136
149
self ._response .close ()
@@ -139,6 +152,22 @@ def _pong_not_received(self) -> None:
139
152
WSMessage (WSMsgType .ERROR , self ._exception , None )
140
153
)
141
154
155
+ def _set_closed (self ) -> None :
156
+ """Set the connection to closed.
157
+
158
+ Cancel any heartbeat timers and set the closed flag.
159
+ """
160
+ self ._closed = True
161
+ self ._cancel_heartbeat ()
162
+
163
+ def _set_closing (self ) -> None :
164
+ """Set the connection to closing.
165
+
166
+ Cancel any heartbeat timers and set the closing flag.
167
+ """
168
+ self ._closing = True
169
+ self ._cancel_heartbeat ()
170
+
142
171
@property
143
172
def closed (self ) -> bool :
144
173
return self ._closed
@@ -203,13 +232,12 @@ async def close(self, *, code: int = WSCloseCode.OK, message: bytes = b"") -> bo
203
232
if self ._waiting and not self ._closing :
204
233
assert self ._loop is not None
205
234
self ._close_wait = self ._loop .create_future ()
206
- self ._closing = True
235
+ self ._set_closing ()
207
236
self ._reader .feed_data (WS_CLOSING_MESSAGE )
208
237
await self ._close_wait
209
238
210
239
if not self ._closed :
211
- self ._cancel_heartbeat ()
212
- self ._closed = True
240
+ self ._set_closed ()
213
241
try :
214
242
await self ._writer .close (code , message )
215
243
except asyncio .CancelledError :
@@ -278,7 +306,8 @@ async def receive(self, timeout: Optional[float] = None) -> WSMessage:
278
306
await self .close ()
279
307
return WSMessage (WSMsgType .CLOSED , None , None )
280
308
except ClientError :
281
- self ._closed = True
309
+ # Likely ServerDisconnectedError when connection is lost
310
+ self ._set_closed ()
282
311
self ._close_code = WSCloseCode .ABNORMAL_CLOSURE
283
312
return WS_CLOSED_MESSAGE
284
313
except WebSocketError as exc :
@@ -287,19 +316,19 @@ async def receive(self, timeout: Optional[float] = None) -> WSMessage:
287
316
return WSMessage (WSMsgType .ERROR , exc , None )
288
317
except Exception as exc :
289
318
self ._exception = exc
290
- self ._closing = True
319
+ self ._set_closing ()
291
320
self ._close_code = WSCloseCode .ABNORMAL_CLOSURE
292
321
await self .close ()
293
322
return WSMessage (WSMsgType .ERROR , exc , None )
294
323
295
324
if msg .type is WSMsgType .CLOSE :
296
- self ._closing = True
325
+ self ._set_closing ()
297
326
self ._close_code = msg .data
298
327
# Could be closed elsewhere while awaiting reader
299
328
if not self ._closed and self ._autoclose : # type: ignore[redundant-expr]
300
329
await self .close ()
301
330
elif msg .type is WSMsgType .CLOSING :
302
- self ._closing = True
331
+ self ._set_closing ()
303
332
elif msg .type is WSMsgType .PING and self ._autoping :
304
333
await self .pong (msg .data )
305
334
continue
0 commit comments