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 .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,32 @@ 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
86
109
87
- # Flow control.
110
+ # Flow control
111
+ # ------------
88
112
self ._next_sequence_number : int = 0
89
113
self ._last_ack : int = - 1
90
114
self ._sliding_window_size : Optional [int ] = self .DEFAULT_SLIDING_WINDOW_SIZE
91
115
116
+ def get_next_seq (self ) -> int :
117
+ assert self ._state is not StreamerState .CLOSING
118
+ assert self ._state is not StreamerState .CLOSED
119
+ seq = self ._next_sequence_number
120
+ self ._next_sequence_number += 1
121
+ return seq
122
+
92
123
def set_sliding_window_size (self , size : Optional [int ]) -> None :
93
124
"""Set a new sliding window size for flow control. If size is none, disables flow control.
94
125
"""
@@ -102,87 +133,136 @@ def set_ack(self, ack: int) -> None:
102
133
103
134
If the new value is bigger than the previous value, the streaming might be resumed.
104
135
"""
105
- if ack < = self ._last_ack :
136
+ if ack = = self ._last_ack :
106
137
# We might receive outdated or duplicate ACKs, and we can safely ignore them.
138
+ return
139
+ if ack < self ._last_ack :
140
+ # ACK got smaller. Something is wrong...
107
141
self .send_message (StreamErrorMessage (
108
142
id = self .stream_id ,
109
- errmsg = f'Outdated ACK received. Skipping it... (ack={ ack } )'
143
+ errmsg = f'Outdated ACK received (ack={ ack } )'
110
144
))
145
+ self .stop (False )
111
146
return
112
147
if ack >= self ._next_sequence_number :
148
+ # ACK is higher than the last message sent. Something is wrong...
113
149
self .send_message (StreamErrorMessage (
114
150
id = self .stream_id ,
115
- errmsg = f'Received ACK is higher than the last sent message. Skipping it... (ack={ ack } )'
151
+ errmsg = f'Received ACK is higher than the last sent message (ack={ ack } )'
116
152
))
153
+ self .stop (False )
117
154
return
118
155
self ._last_ack = ack
119
- self .resume_if_possible ()
156
+ if self ._state is not StreamerState .CLOSING :
157
+ closing_ack = self ._next_sequence_number - 1
158
+ if ack == closing_ack :
159
+ self .stop (True )
160
+ else :
161
+ self .resume_if_possible ()
120
162
121
163
def resume_if_possible (self ) -> None :
122
- if not self ._started :
164
+ """Resume sending messages if possible."""
165
+ if self ._state is StreamerState .PAUSED :
166
+ return
167
+ if not self ._state .can_transition_to (StreamerState .ACTIVE ):
168
+ return
169
+ if self ._is_main_loop_running :
123
170
return
124
- if not self .should_pause_streaming () and not self ._is_running :
125
- self .resumeProducing ()
171
+ if self .should_pause_streaming ():
172
+ return
173
+ self ._run ()
174
+
175
+ def set_state (self , new_state : StreamerState ) -> None :
176
+ """Set a new state for the streamer."""
177
+ if self ._state == new_state :
178
+ return
179
+ assert self ._state .can_transition_to (new_state )
180
+ self ._state = new_state
126
181
127
182
def start (self ) -> Deferred [bool ]:
128
183
"""Start streaming items."""
184
+ assert self ._state is StreamerState .NOT_STARTED
185
+
129
186
# The websocket connection somehow instantiates an twisted.web.http.HTTPChannel object
130
187
# which register a producer. It seems the HTTPChannel is not used anymore after switching
131
188
# to websocket but it keep registered. So we have to unregister before registering a new
132
189
# producer.
133
190
if self .protocol .transport .producer :
134
191
self .protocol .unregisterProducer ()
135
-
136
192
self .protocol .registerProducer (self , True )
137
193
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 ()
194
+ self .send_message (StreamBeginMessage (
195
+ id = self .stream_id ,
196
+ seq = self .get_next_seq (),
197
+ window_size = self ._sliding_window_size ,
198
+ ))
199
+ self .resume_if_possible ()
142
200
return self .deferred
143
201
144
202
def stop (self , success : bool ) -> None :
145
203
"""Stop streaming items."""
146
- assert self ._started
147
- self ._stop = True
148
- self ._started = False
204
+ if not self ._state .can_transition_to (StreamerState .CLOSED ):
205
+ # Do nothing if the streamer has already been stopped.
206
+ self .protocol .log .warn ('stop called in an unexpected state' , state = self ._state )
207
+ return
208
+ self .set_state (StreamerState .CLOSED )
149
209
self .protocol .unregisterProducer ()
150
210
self .deferred .callback (success )
151
211
212
+ def gracefully_close (self ) -> None :
213
+ """Gracefully close the stream by sending the StreamEndMessage and waiting for its ack."""
214
+ if not self ._state .can_transition_to (StreamerState .CLOSING ):
215
+ return
216
+ self .send_message (StreamEndMessage (id = self .stream_id , seq = self .get_next_seq ()))
217
+ self .set_state (StreamerState .CLOSING )
218
+
152
219
def pauseProducing (self ) -> None :
153
220
"""Pause streaming. Called by twisted."""
154
- self ._paused = True
221
+ if not self ._state .can_transition_to (StreamerState .PAUSED ):
222
+ self .protocol .log .warn ('pause requested in an unexpected state' , state = self ._state )
223
+ return
224
+ self .set_state (StreamerState .PAUSED )
155
225
156
226
def stopProducing (self ) -> None :
157
227
"""Stop streaming. Called by twisted."""
158
- self ._stop = True
228
+ if not self ._state .can_transition_to (StreamerState .CLOSED ):
229
+ self .protocol .log .warn ('stopped requested in an unexpected state' , state = self ._state )
230
+ return
159
231
self .stop (False )
160
232
161
233
def resumeProducing (self ) -> None :
162
234
"""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 )
235
+ if not self ._state .can_transition_to (StreamerState .ACTIVE ):
236
+ self .protocol .log .warn ('resume requested in an unexpected state' , state = self ._state )
237
+ return
238
+ self .set_state (StreamerState .ACTIVE )
239
+ self .resume_if_possible ()
170
240
171
241
def should_pause_streaming (self ) -> bool :
242
+ """Return true if the streaming should pause due to the flow control mechanism."""
172
243
if self ._sliding_window_size is None :
173
244
return False
174
245
stop_value = self ._last_ack + self ._sliding_window_size + 1
175
246
if self ._next_sequence_number < stop_value :
176
247
return False
177
248
return True
178
249
250
+ def _run (self ) -> None :
251
+ """Run the streaming main loop."""
252
+ if not self ._state .can_transition_to (StreamerState .ACTIVE ):
253
+ self .protocol .log .warn ('_run() called in an unexpected state' , state = self ._state )
254
+ return
255
+ coro = self ._async_run ()
256
+ Deferred .fromCoroutine (coro )
257
+
179
258
async def _async_run (self ):
180
- assert not self ._is_running
181
- self ._is_running = True
259
+ assert not self ._is_main_loop_running
260
+ self .set_state (StreamerState .ACTIVE )
261
+ self ._is_main_loop_running = True
182
262
try :
183
263
await self ._async_run_unsafe ()
184
264
finally :
185
- self ._is_running = False
265
+ self ._is_main_loop_running = False
186
266
187
267
async def _async_run_unsafe (self ):
188
268
"""Internal method that runs the streaming main loop."""
@@ -204,7 +284,7 @@ async def _async_run_unsafe(self):
204
284
self .stats_sent_addresses += 1
205
285
self .send_message (StreamAddressMessage (
206
286
id = self .stream_id ,
207
- seq = self ._next_sequence_number ,
287
+ seq = self .get_next_seq () ,
208
288
index = item .index ,
209
289
address = item .address ,
210
290
subscribed = subscribed ,
@@ -214,42 +294,39 @@ async def _async_run_unsafe(self):
214
294
self .stats_sent_vertices += 1
215
295
self .send_message (StreamVertexMessage (
216
296
id = self .stream_id ,
217
- seq = self ._next_sequence_number ,
297
+ seq = self .get_next_seq () ,
218
298
data = item .vertex .to_json_extended (),
219
299
))
220
300
221
301
case _:
222
302
assert False
223
303
224
- self ._next_sequence_number += 1
225
304
if self .should_pause_streaming ():
226
305
break
227
306
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
307
self .stats_total_messages += 1
235
308
if self .stats_total_messages % self .stats_log_interval == 0 :
236
309
self .protocol .log .info ('websocket streaming statistics' ,
237
310
total_messages = self .stats_total_messages ,
238
311
sent_vertices = self .stats_sent_vertices ,
239
312
sent_addresses = self .stats_sent_addresses )
240
313
314
+ # The methods `pauseProducing()` and `stopProducing()` might be called during the
315
+ # call to `self.protocol.sendMessage()`. So the streamer state might change during
316
+ # the loop.
317
+ if self ._state is not StreamerState .ACTIVE :
318
+ break
319
+
320
+ # Limit blocking of the event loop to a maximum of N seconds.
241
321
dt = self .reactor .seconds () - t0
242
322
if dt > self .max_seconds_locking_event_loop :
243
323
# Let the event loop run at least once.
244
324
await deferLater (self .reactor , 0 , lambda : None )
245
325
t0 = self .reactor .seconds ()
246
326
247
327
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 )
328
+ # Iterator is empty so we can close the stream.
329
+ self .gracefully_close ()
253
330
254
331
def send_message (self , message : StreamBase ) -> None :
255
332
"""Send a message to the websocket connection."""
0 commit comments