12
12
# See the License for the specific language governing permissions and
13
13
# limitations under the License.
14
14
15
+ from enum import Enum , auto
15
16
from typing import TYPE_CHECKING , Optional
16
17
17
18
from twisted .internet .defer import Deferred
33
34
from hathor .websocket .protocol import HathorAdminWebsocketProtocol
34
35
35
36
37
+ class StreamerState (Enum ):
38
+ NOT_STARTED = auto ()
39
+ ACTIVE = auto ()
40
+ PAUSED = auto ()
41
+ CLOSING = auto ()
42
+ CLOSED = auto ()
43
+
44
+ def can_transition_to (self , destination : 'StreamerState' ) -> bool :
45
+ """Checks if the transition to the destination state is valid."""
46
+ return destination in VALID_TRANSITIONS [self ]
47
+
48
+
49
+ VALID_TRANSITIONS = {
50
+ StreamerState .NOT_STARTED : {StreamerState .ACTIVE },
51
+ StreamerState .ACTIVE : {StreamerState .PAUSED , StreamerState .CLOSING , StreamerState .CLOSED },
52
+ StreamerState .PAUSED : {StreamerState .ACTIVE , StreamerState .CLOSING , StreamerState .CLOSED },
53
+ StreamerState .CLOSING : {StreamerState .CLOSED },
54
+ StreamerState .CLOSED : set ()
55
+ }
56
+
57
+
36
58
@implementer (IPushProducer )
37
59
class HistoryStreamer :
38
60
"""A producer that pushes addresses and transactions to a websocket connection.
@@ -72,23 +94,34 @@ def __init__(self,
72
94
73
95
self .deferred : Deferred [bool ] = Deferred ()
74
96
75
- # Statistics.
97
+ # Statistics
98
+ # ----------
76
99
self .stats_log_interval = self .STATS_LOG_INTERVAL
77
100
self .stats_total_messages : int = 0
78
101
self .stats_sent_addresses : int = 0
79
102
self .stats_sent_vertices : int = 0
80
103
81
- # Execution control.
82
- self ._started = False
83
- self ._is_running = False
84
- self ._paused = False
85
- self ._stop = False
104
+ # Execution control
105
+ # -----------------
106
+ self ._state = StreamerState .NOT_STARTED
107
+ # Used to mark that the streamer is currently running its main loop and sending messages.
108
+ self ._is_main_loop_running = False
109
+ # Used to mark that the streamer was paused by the transport layer.
110
+ self ._is_paused_by_transport = False
86
111
87
- # Flow control.
112
+ # Flow control
113
+ # ------------
88
114
self ._next_sequence_number : int = 0
89
115
self ._last_ack : int = - 1
90
116
self ._sliding_window_size : Optional [int ] = self .DEFAULT_SLIDING_WINDOW_SIZE
91
117
118
+ def get_next_seq (self ) -> int :
119
+ assert self ._state is not StreamerState .CLOSING
120
+ assert self ._state is not StreamerState .CLOSED
121
+ seq = self ._next_sequence_number
122
+ self ._next_sequence_number += 1
123
+ return seq
124
+
92
125
def set_sliding_window_size (self , size : Optional [int ]) -> None :
93
126
"""Set a new sliding window size for flow control. If size is none, disables flow control.
94
127
"""
@@ -102,87 +135,130 @@ def set_ack(self, ack: int) -> None:
102
135
103
136
If the new value is bigger than the previous value, the streaming might be resumed.
104
137
"""
105
- if ack < = self ._last_ack :
138
+ if ack = = self ._last_ack :
106
139
# We might receive outdated or duplicate ACKs, and we can safely ignore them.
140
+ return
141
+ if ack < self ._last_ack :
142
+ # ACK got smaller. Something is wrong...
107
143
self .send_message (StreamErrorMessage (
108
144
id = self .stream_id ,
109
- errmsg = f'Outdated ACK received. Skipping it... (ack={ ack } )'
145
+ errmsg = f'Outdated ACK received (ack={ ack } )'
110
146
))
147
+ self .stop (False )
111
148
return
112
149
if ack >= self ._next_sequence_number :
150
+ # ACK is higher than the last message sent. Something is wrong...
113
151
self .send_message (StreamErrorMessage (
114
152
id = self .stream_id ,
115
- errmsg = f'Received ACK is higher than the last sent message. Skipping it... (ack={ ack } )'
153
+ errmsg = f'Received ACK is higher than the last sent message (ack={ ack } )'
116
154
))
155
+ self .stop (False )
117
156
return
118
157
self ._last_ack = ack
119
- self .resume_if_possible ()
158
+ if self ._state is not StreamerState .CLOSING :
159
+ closing_ack = self ._next_sequence_number - 1
160
+ if ack == closing_ack :
161
+ self .stop (True )
162
+ else :
163
+ self .resume_if_possible ()
120
164
121
165
def resume_if_possible (self ) -> None :
122
- if not self ._started :
166
+ """Resume sending messages if possible."""
167
+ if not self ._state .can_transition_to (StreamerState .ACTIVE ):
168
+ return
169
+ if self ._is_main_loop_running :
170
+ return
171
+ if self ._is_paused_by_transport :
123
172
return
124
- if not self .should_pause_streaming () and not self ._is_running :
125
- self .resumeProducing ()
173
+ if self .should_pause_streaming ():
174
+ return
175
+ self ._run ()
126
176
127
177
def start (self ) -> Deferred [bool ]:
128
178
"""Start streaming items."""
179
+ assert self ._state is StreamerState .NOT_STARTED
180
+
129
181
# The websocket connection somehow instantiates an twisted.web.http.HTTPChannel object
130
182
# which register a producer. It seems the HTTPChannel is not used anymore after switching
131
183
# to websocket but it keep registered. So we have to unregister before registering a new
132
184
# producer.
133
185
if self .protocol .transport .producer :
134
186
self .protocol .unregisterProducer ()
135
-
136
187
self .protocol .registerProducer (self , True )
137
188
138
- assert not self ._started
139
- self ._started = True
140
- self .send_message (StreamBeginMessage (id = self .stream_id , window_size = self ._sliding_window_size ))
141
- self .resumeProducing ()
189
+ self .send_message (StreamBeginMessage (
190
+ id = self .stream_id ,
191
+ seq = self .get_next_seq (),
192
+ window_size = self ._sliding_window_size ,
193
+ ))
194
+ self .resume_if_possible ()
142
195
return self .deferred
143
196
144
197
def stop (self , success : bool ) -> None :
145
198
"""Stop streaming items."""
146
- assert self ._started
147
- self ._stop = True
148
- self ._started = False
199
+ if not self ._state .can_transition_to (StreamerState .CLOSED ):
200
+ # Do nothing if the streamer has already been stopped.
201
+ self .protocol .log .warn ('stop called in an unexpected state' , state = self ._state )
202
+ return
203
+ self ._state = StreamerState .CLOSED
149
204
self .protocol .unregisterProducer ()
150
205
self .deferred .callback (success )
151
206
207
+ def gracefully_close (self ) -> None :
208
+ """Gracefully close the stream by sending the StreamEndMessage and waiting for its ack."""
209
+ if not self ._state .can_transition_to (StreamerState .CLOSING ):
210
+ return
211
+ self .send_message (StreamEndMessage (id = self .stream_id , seq = self .get_next_seq ()))
212
+ self ._state = StreamerState .CLOSING
213
+
152
214
def pauseProducing (self ) -> None :
153
215
"""Pause streaming. Called by twisted."""
154
- self ._paused = True
216
+ if not self ._state .can_transition_to (StreamerState .PAUSED ):
217
+ self .protocol .log .warn ('pause requested in an unexpected state' , state = self ._state )
218
+ return
219
+ self ._state = StreamerState .PAUSED
220
+ self ._is_paused_by_transport = True
155
221
156
222
def stopProducing (self ) -> None :
157
223
"""Stop streaming. Called by twisted."""
158
- self ._stop = True
224
+ if not self ._state .can_transition_to (StreamerState .CLOSED ):
225
+ self .protocol .log .warn ('stopped requested in an unexpected state' , state = self ._state )
226
+ return
159
227
self .stop (False )
160
228
161
229
def resumeProducing (self ) -> None :
162
230
"""Resume streaming. Called by twisted."""
163
- self ._paused = False
164
- self ._run ()
165
-
166
- def _run (self ) -> None :
167
- """Run the streaming main loop."""
168
- coro = self ._async_run ()
169
- Deferred .fromCoroutine (coro )
231
+ if not self ._state .can_transition_to (StreamerState .ACTIVE ):
232
+ self .protocol .log .warn ('resume requested in an unexpected state' , state = self ._state )
233
+ return
234
+ self ._is_paused_by_transport = False
235
+ self .resume_if_possible ()
170
236
171
237
def should_pause_streaming (self ) -> bool :
238
+ """Return true if the streaming should pause due to the flow control mechanism."""
172
239
if self ._sliding_window_size is None :
173
240
return False
174
241
stop_value = self ._last_ack + self ._sliding_window_size + 1
175
242
if self ._next_sequence_number < stop_value :
176
243
return False
177
244
return True
178
245
246
+ def _run (self ) -> None :
247
+ """Run the streaming main loop."""
248
+ if not self ._state .can_transition_to (StreamerState .ACTIVE ):
249
+ self .protocol .log .warn ('_run() called in an unexpected state' , state = self ._state )
250
+ return
251
+ coro = self ._async_run ()
252
+ Deferred .fromCoroutine (coro )
253
+
179
254
async def _async_run (self ):
180
- assert not self ._is_running
181
- self ._is_running = True
255
+ assert not self ._is_main_loop_running
256
+ self ._state = StreamerState .ACTIVE
257
+ self ._is_main_loop_running = True
182
258
try :
183
259
await self ._async_run_unsafe ()
184
260
finally :
185
- self ._is_running = False
261
+ self ._is_main_loop_running = False
186
262
187
263
async def _async_run_unsafe (self ):
188
264
"""Internal method that runs the streaming main loop."""
@@ -204,7 +280,7 @@ async def _async_run_unsafe(self):
204
280
self .stats_sent_addresses += 1
205
281
self .send_message (StreamAddressMessage (
206
282
id = self .stream_id ,
207
- seq = self ._next_sequence_number ,
283
+ seq = self .get_next_seq () ,
208
284
index = item .index ,
209
285
address = item .address ,
210
286
subscribed = subscribed ,
@@ -214,42 +290,39 @@ async def _async_run_unsafe(self):
214
290
self .stats_sent_vertices += 1
215
291
self .send_message (StreamVertexMessage (
216
292
id = self .stream_id ,
217
- seq = self ._next_sequence_number ,
293
+ seq = self .get_next_seq () ,
218
294
data = item .vertex .to_json_extended (),
219
295
))
220
296
221
297
case _:
222
298
assert False
223
299
224
- self ._next_sequence_number += 1
225
300
if self .should_pause_streaming ():
226
301
break
227
302
228
- # The methods `pauseProducing()` and `stopProducing()` might be called during the
229
- # call to `self.protocol.sendMessage()`. So both `_paused` and `_stop` might change
230
- # during the loop.
231
- if self ._paused or self ._stop :
232
- break
233
-
234
303
self .stats_total_messages += 1
235
304
if self .stats_total_messages % self .stats_log_interval == 0 :
236
305
self .protocol .log .info ('websocket streaming statistics' ,
237
306
total_messages = self .stats_total_messages ,
238
307
sent_vertices = self .stats_sent_vertices ,
239
308
sent_addresses = self .stats_sent_addresses )
240
309
310
+ # The methods `pauseProducing()` and `stopProducing()` might be called during the
311
+ # call to `self.protocol.sendMessage()`. So the streamer state might change during
312
+ # the loop.
313
+ if self ._state is not StreamerState .ACTIVE :
314
+ break
315
+
316
+ # Limit blocking of the event loop to a maximum of N seconds.
241
317
dt = self .reactor .seconds () - t0
242
318
if dt > self .max_seconds_locking_event_loop :
243
319
# Let the event loop run at least once.
244
320
await deferLater (self .reactor , 0 , lambda : None )
245
321
t0 = self .reactor .seconds ()
246
322
247
323
else :
248
- if self ._stop :
249
- # If the streamer has been stopped, there is nothing else to do.
250
- return
251
- self .send_message (StreamEndMessage (id = self .stream_id ))
252
- self .stop (True )
324
+ # Iterator is empty so we can close the stream.
325
+ self .gracefully_close ()
253
326
254
327
def send_message (self , message : StreamBase ) -> None :
255
328
"""Send a message to the websocket connection."""
0 commit comments