fiber_stream 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/README.md +168 -12
- data/examples/README.md +5 -0
- data/examples/ractor_unordered_map_hashing.rb +62 -0
- data/lib/fiber_stream/flow.rb +123 -0
- data/lib/fiber_stream/pull/compact.rb +39 -0
- data/lib/fiber_stream/pull/filter_map.rb +41 -0
- data/lib/fiber_stream/pull/map_concat.rb +56 -0
- data/lib/fiber_stream/pull/ractor_unordered_map_boundary.rb +521 -0
- data/lib/fiber_stream/pull/reject.rb +40 -0
- data/lib/fiber_stream/pull/tap.rb +38 -0
- data/lib/fiber_stream/pull/throttle.rb +43 -0
- data/lib/fiber_stream/pull.rb +39 -3
- data/lib/fiber_stream/rate_limiter.rb +163 -0
- data/lib/fiber_stream/sink.rb +68 -0
- data/lib/fiber_stream/source.rb +66 -0
- data/lib/fiber_stream/version.rb +1 -1
- data/lib/fiber_stream.rb +1 -0
- data/sig/fiber_stream.rbs +30 -0
- metadata +11 -2
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberStream
|
|
4
|
+
module Pull
|
|
5
|
+
# One-to-many mapping stage.
|
|
6
|
+
#
|
|
7
|
+
# It expands one upstream element into the values yielded by one returned
|
|
8
|
+
# `#each` object. Only one expansion is active at a time, and the stage
|
|
9
|
+
# never pulls the next upstream element until the active expansion is
|
|
10
|
+
# exhausted.
|
|
11
|
+
class MapConcat
|
|
12
|
+
def initialize(upstream, transform)
|
|
13
|
+
@upstream = upstream
|
|
14
|
+
@transform = transform
|
|
15
|
+
@current_enumerator = nil
|
|
16
|
+
@closed = false
|
|
17
|
+
@done = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def next
|
|
21
|
+
return DONE if @closed || @done
|
|
22
|
+
|
|
23
|
+
loop do
|
|
24
|
+
if @current_enumerator
|
|
25
|
+
begin
|
|
26
|
+
return @current_enumerator.next
|
|
27
|
+
rescue StopIteration
|
|
28
|
+
@current_enumerator = nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
value = @upstream.next
|
|
33
|
+
if Pull.done?(value)
|
|
34
|
+
@done = true
|
|
35
|
+
return DONE
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
result = @transform.call(value)
|
|
39
|
+
unless result.respond_to?(:each)
|
|
40
|
+
raise TypeError, "map_concat block result must respond to each"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@current_enumerator = result.to_enum(:each)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def close
|
|
48
|
+
return if @closed
|
|
49
|
+
|
|
50
|
+
@closed = true
|
|
51
|
+
@current_enumerator = nil
|
|
52
|
+
@upstream.close
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberStream
|
|
4
|
+
module Pull
|
|
5
|
+
# Unordered Ractor-backed worker boundary for
|
|
6
|
+
# `Flow.ractor_unordered_map`.
|
|
7
|
+
#
|
|
8
|
+
# Upstream is pulled by the downstream caller, while blocking waits for
|
|
9
|
+
# Ractor worker messages are isolated in a coordinator thread. Downstream
|
|
10
|
+
# emits worker results in completion order and admission stays bounded by
|
|
11
|
+
# the number of workers.
|
|
12
|
+
class RactorUnorderedMapBoundary
|
|
13
|
+
Job = ::Data.define(:sequence, :value)
|
|
14
|
+
Shutdown = ::Data.define
|
|
15
|
+
Ready = ::Data.define(:worker_id)
|
|
16
|
+
WorkerValue = ::Data.define(:worker_id, :sequence, :value)
|
|
17
|
+
WorkerFailure = ::Data.define(:worker_id, :sequence, :kind, :cause_class_name, :cause_message)
|
|
18
|
+
Stopped = ::Data.define(:worker_id)
|
|
19
|
+
ResultValue = ::Data.define(:sequence, :value)
|
|
20
|
+
ResultDone = ::Data.define
|
|
21
|
+
ResultCloseError = ::Data.define(:sequence, :error)
|
|
22
|
+
ResultError = ::Data.define(:sequence, :error)
|
|
23
|
+
|
|
24
|
+
private_constant :Job, :Shutdown, :Ready, :WorkerValue, :WorkerFailure, :Stopped
|
|
25
|
+
private_constant :ResultValue, :ResultDone, :ResultCloseError, :ResultError
|
|
26
|
+
|
|
27
|
+
def initialize(upstream, workers, input_transfer, output_transfer, transform)
|
|
28
|
+
@upstream = upstream
|
|
29
|
+
@workers_count = workers
|
|
30
|
+
@input_transfer = input_transfer
|
|
31
|
+
@output_transfer = output_transfer
|
|
32
|
+
@transform = transform
|
|
33
|
+
@result_port = nil
|
|
34
|
+
@ready_workers = Thread::SizedQueue.new(workers)
|
|
35
|
+
@results = Thread::SizedQueue.new(workers)
|
|
36
|
+
@workers = []
|
|
37
|
+
@active_sequences = {}
|
|
38
|
+
@worker_state_mutex = Mutex.new
|
|
39
|
+
@coordinator = nil
|
|
40
|
+
@next_sequence = 0
|
|
41
|
+
@outstanding_jobs = 0
|
|
42
|
+
@terminal_message = nil
|
|
43
|
+
@started = false
|
|
44
|
+
@closed = false
|
|
45
|
+
@done = false
|
|
46
|
+
@admission_closed = false
|
|
47
|
+
@worker_shutdown_sent = false
|
|
48
|
+
@upstream_closed = false
|
|
49
|
+
@upstream_close_error = nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def next
|
|
53
|
+
return DONE if @closed || @done
|
|
54
|
+
|
|
55
|
+
start
|
|
56
|
+
next_message
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def close
|
|
60
|
+
return if @closed
|
|
61
|
+
|
|
62
|
+
@closed = true
|
|
63
|
+
@done = true
|
|
64
|
+
close_error = close_upstream
|
|
65
|
+
close_admission(close_upstream: false)
|
|
66
|
+
close_ready_queue
|
|
67
|
+
close_result_queue
|
|
68
|
+
request_worker_shutdown
|
|
69
|
+
wait_for_workers
|
|
70
|
+
close_error ||= @upstream_close_error
|
|
71
|
+
raise close_error if close_error
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def start
|
|
77
|
+
return if @started
|
|
78
|
+
|
|
79
|
+
@started = true
|
|
80
|
+
@result_port = Ractor::Port.new
|
|
81
|
+
@workers_count.times do |worker_id|
|
|
82
|
+
@workers << self.class.spawn_worker(
|
|
83
|
+
worker_id,
|
|
84
|
+
@result_port,
|
|
85
|
+
@transform,
|
|
86
|
+
@output_transfer
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
@coordinator = Thread.new { run_coordinator }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def next_message
|
|
93
|
+
return emit_terminal(@terminal_message) if terminal_ready?
|
|
94
|
+
|
|
95
|
+
ready = take_result(block: false)
|
|
96
|
+
return emit(ready) if ready
|
|
97
|
+
|
|
98
|
+
ready = fill_capacity
|
|
99
|
+
return emit(ready) if ready
|
|
100
|
+
|
|
101
|
+
return emit_terminal(@terminal_message) if terminal_ready?
|
|
102
|
+
|
|
103
|
+
message = take_result(block: true)
|
|
104
|
+
return complete unless message
|
|
105
|
+
|
|
106
|
+
emit(message)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def fill_capacity
|
|
110
|
+
return if @admission_closed
|
|
111
|
+
|
|
112
|
+
while @outstanding_jobs < @workers_count
|
|
113
|
+
ready = take_result(block: false)
|
|
114
|
+
return ready if ready
|
|
115
|
+
|
|
116
|
+
worker = take_ready_worker(block: @outstanding_jobs.zero? && @terminal_message.nil?)
|
|
117
|
+
break unless worker
|
|
118
|
+
|
|
119
|
+
message = pull_job_message
|
|
120
|
+
if message.is_a?(Job)
|
|
121
|
+
@outstanding_jobs += 1
|
|
122
|
+
break unless deliver_job(worker, message)
|
|
123
|
+
elsif upstream_failure?(message)
|
|
124
|
+
fail_with_error(message.sequence, message.error)
|
|
125
|
+
else
|
|
126
|
+
close_admission(close_upstream: false)
|
|
127
|
+
@terminal_message = message
|
|
128
|
+
break
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def pull_job_message
|
|
136
|
+
value = @upstream.next
|
|
137
|
+
return terminal_done_message if Pull.done?(value)
|
|
138
|
+
|
|
139
|
+
sequence = @next_sequence
|
|
140
|
+
@next_sequence += 1
|
|
141
|
+
Job.new(sequence, value)
|
|
142
|
+
rescue StandardError => error
|
|
143
|
+
close_upstream(record_error: false)
|
|
144
|
+
ResultError.new(sequence: @next_sequence, error:)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def terminal_done_message
|
|
148
|
+
close_error = close_upstream
|
|
149
|
+
if close_error
|
|
150
|
+
ResultCloseError.new(sequence: @next_sequence, error: close_error)
|
|
151
|
+
else
|
|
152
|
+
ResultDone.new
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def deliver_job(worker, message)
|
|
157
|
+
sequence = message.sequence
|
|
158
|
+
track_worker_job(worker, sequence)
|
|
159
|
+
|
|
160
|
+
if @input_transfer == :move
|
|
161
|
+
worker.send(message, move: true)
|
|
162
|
+
else
|
|
163
|
+
worker.send(message)
|
|
164
|
+
end
|
|
165
|
+
true
|
|
166
|
+
rescue StandardError => error
|
|
167
|
+
clear_worker_job(worker)
|
|
168
|
+
deliver_result(ResultError.new(sequence:, error: build_ractor_map_error(sequence, :input_transfer, error)))
|
|
169
|
+
close_admission
|
|
170
|
+
request_worker_shutdown
|
|
171
|
+
false
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def take_ready_worker(block:)
|
|
175
|
+
if block
|
|
176
|
+
loop do
|
|
177
|
+
worker = @ready_workers.pop
|
|
178
|
+
return worker if worker || @closed || @admission_closed || @ready_workers.closed?
|
|
179
|
+
end
|
|
180
|
+
else
|
|
181
|
+
@ready_workers.pop(true)
|
|
182
|
+
end
|
|
183
|
+
rescue ThreadError, ClosedQueueError
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def take_result(block:)
|
|
188
|
+
block ? @results.pop : @results.pop(true)
|
|
189
|
+
rescue ThreadError, ClosedQueueError
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def emit(message)
|
|
194
|
+
case message
|
|
195
|
+
in ResultValue[_sequence, value]
|
|
196
|
+
emit_value(value)
|
|
197
|
+
in ResultError[sequence:, error:]
|
|
198
|
+
fail_with_error(sequence, error)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def emit_value(value)
|
|
203
|
+
@outstanding_jobs -= 1 if @outstanding_jobs.positive?
|
|
204
|
+
value
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def terminal_ready?
|
|
208
|
+
@terminal_message && @outstanding_jobs.zero?
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def emit_terminal(message)
|
|
212
|
+
case message
|
|
213
|
+
in ResultDone
|
|
214
|
+
complete
|
|
215
|
+
in ResultCloseError[sequence:, error:]
|
|
216
|
+
fail_with_error(sequence, error, close_admission: false)
|
|
217
|
+
in ResultError[sequence:, error:]
|
|
218
|
+
fail_with_error(sequence, error, close_admission: false)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def upstream_failure?(message)
|
|
223
|
+
message.is_a?(ResultError)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def fail_with_error(_sequence, error, close_admission: true)
|
|
227
|
+
@done = true
|
|
228
|
+
close_admission() if close_admission
|
|
229
|
+
close_result_queue
|
|
230
|
+
request_worker_shutdown
|
|
231
|
+
if error.is_a?(RactorMapError) && error.original_cause
|
|
232
|
+
raise error, cause: error.original_cause
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
raise error
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def complete
|
|
239
|
+
@done = true
|
|
240
|
+
request_worker_shutdown
|
|
241
|
+
DONE
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def run_coordinator
|
|
245
|
+
stopped = 0
|
|
246
|
+
live_workers = @workers.dup
|
|
247
|
+
|
|
248
|
+
until stopped == @workers_count
|
|
249
|
+
selected, message = select_worker_message(live_workers)
|
|
250
|
+
if selected == :worker_remote_error
|
|
251
|
+
stopped += 1
|
|
252
|
+
elsif selected == @result_port
|
|
253
|
+
stopped += handle_worker_message(message, live_workers)
|
|
254
|
+
else
|
|
255
|
+
live_workers.delete(selected)
|
|
256
|
+
handle_worker_termination(selected)
|
|
257
|
+
stopped += 1
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
ensure
|
|
261
|
+
close_ready_queue
|
|
262
|
+
close_result_queue if @closed
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def select_worker_message(live_workers)
|
|
266
|
+
Ractor.select(@result_port, *live_workers)
|
|
267
|
+
rescue Ractor::RemoteError => error
|
|
268
|
+
worker = remote_error_worker(error, live_workers) || failed_worker_for_remote_error(live_workers)
|
|
269
|
+
live_workers.delete(worker) if worker
|
|
270
|
+
handle_worker_remote_error(worker, error)
|
|
271
|
+
[:worker_remote_error, nil]
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def remote_error_worker(error, live_workers)
|
|
275
|
+
return unless error.respond_to?(:ractor)
|
|
276
|
+
|
|
277
|
+
worker = error.ractor
|
|
278
|
+
live_workers.include?(worker) ? worker : nil
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def failed_worker_for_remote_error(live_workers)
|
|
282
|
+
@worker_state_mutex.synchronize do
|
|
283
|
+
live_workers
|
|
284
|
+
.select { |worker| @active_sequences.key?(worker) }
|
|
285
|
+
.min_by { |worker| @active_sequences.fetch(worker) }
|
|
286
|
+
end || live_workers.first
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def handle_worker_remote_error(worker, error)
|
|
290
|
+
sequence = worker ? clear_worker_job(worker) : nil
|
|
291
|
+
sequence ||= @next_sequence
|
|
292
|
+
return if @closed || @worker_shutdown_sent
|
|
293
|
+
|
|
294
|
+
deliver_worker_termination_error(worker, sequence, cause: error)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def handle_worker_message(message, live_workers)
|
|
298
|
+
case message
|
|
299
|
+
in Ready[worker_id]
|
|
300
|
+
deliver_ready_worker(worker_id)
|
|
301
|
+
0
|
|
302
|
+
in WorkerValue
|
|
303
|
+
handle_worker_value_message(message)
|
|
304
|
+
0
|
|
305
|
+
in WorkerFailure
|
|
306
|
+
handle_worker_error_message(message)
|
|
307
|
+
0
|
|
308
|
+
in Stopped
|
|
309
|
+
handle_worker_stopped_message(message, live_workers)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def handle_worker_value_message(message)
|
|
314
|
+
worker = worker_for_id(message.worker_id)
|
|
315
|
+
|
|
316
|
+
clear_worker_job(worker)
|
|
317
|
+
deliver_result(ResultValue.new(sequence: message.sequence, value: message.value))
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def handle_worker_error_message(message)
|
|
321
|
+
worker = worker_for_id(message.worker_id)
|
|
322
|
+
|
|
323
|
+
clear_worker_job(worker)
|
|
324
|
+
deliver_result(normalize_worker_error_message(message))
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def handle_worker_stopped_message(message, live_workers)
|
|
328
|
+
worker = worker_for_id(message.worker_id)
|
|
329
|
+
live_workers.delete(worker)
|
|
330
|
+
sequence = clear_worker_job(worker)
|
|
331
|
+
deliver_worker_termination_error(worker, sequence) if sequence && !@closed && !@worker_shutdown_sent
|
|
332
|
+
1
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def handle_worker_termination(worker)
|
|
336
|
+
sequence = clear_worker_job(worker) || @next_sequence
|
|
337
|
+
return if @closed || @worker_shutdown_sent
|
|
338
|
+
|
|
339
|
+
deliver_worker_termination_error(worker, sequence)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def deliver_worker_termination_error(worker, sequence, cause: nil)
|
|
343
|
+
close_ready_queue
|
|
344
|
+
error =
|
|
345
|
+
RactorMapError.new(
|
|
346
|
+
sequence: sequence,
|
|
347
|
+
kind: :worker_termination,
|
|
348
|
+
cause_class_name: cause&.class&.name || worker.class.name,
|
|
349
|
+
cause_message: cause&.message || "worker terminated without a lifecycle message",
|
|
350
|
+
cause: cause
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
deliver_result(ResultError.new(sequence:, error:))
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def deliver_ready_worker(worker_id)
|
|
357
|
+
return if @closed
|
|
358
|
+
|
|
359
|
+
push_until_delivered_or_closed(@ready_workers, worker_for_id(worker_id))
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def deliver_result(message)
|
|
363
|
+
return if @closed
|
|
364
|
+
|
|
365
|
+
push_until_delivered_or_closed(@results, message)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def push_until_delivered_or_closed(queue, message)
|
|
369
|
+
return if @closed
|
|
370
|
+
|
|
371
|
+
queue.push(message)
|
|
372
|
+
rescue ThreadError, ClosedQueueError
|
|
373
|
+
nil
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def normalize_worker_error_message(message)
|
|
377
|
+
sequence = message.sequence
|
|
378
|
+
error =
|
|
379
|
+
RactorMapError.new(
|
|
380
|
+
sequence: sequence,
|
|
381
|
+
kind: message.kind,
|
|
382
|
+
cause_class_name: message.cause_class_name,
|
|
383
|
+
cause_message: message.cause_message
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
ResultError.new(sequence:, error:)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def worker_for_id(worker_id)
|
|
390
|
+
@workers.fetch(worker_id)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def track_worker_job(worker, sequence)
|
|
394
|
+
@worker_state_mutex.synchronize do
|
|
395
|
+
@active_sequences[worker] = sequence
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def clear_worker_job(worker)
|
|
400
|
+
@worker_state_mutex.synchronize do
|
|
401
|
+
@active_sequences.delete(worker)
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def close_admission(close_upstream: true)
|
|
406
|
+
return if @admission_closed
|
|
407
|
+
|
|
408
|
+
@admission_closed = true
|
|
409
|
+
close_upstream(record_error: false) if close_upstream
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def request_worker_shutdown
|
|
413
|
+
return unless @started
|
|
414
|
+
return if @worker_shutdown_sent
|
|
415
|
+
|
|
416
|
+
@worker_shutdown_sent = true
|
|
417
|
+
@workers.each do |worker|
|
|
418
|
+
worker.send(Shutdown.new)
|
|
419
|
+
rescue StandardError
|
|
420
|
+
nil
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def wait_for_workers
|
|
425
|
+
return unless @coordinator
|
|
426
|
+
|
|
427
|
+
@coordinator.join
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def close_upstream(record_error: true)
|
|
431
|
+
return nil if @upstream_closed
|
|
432
|
+
|
|
433
|
+
@upstream_closed = true
|
|
434
|
+
@upstream.close
|
|
435
|
+
nil
|
|
436
|
+
rescue StandardError => error
|
|
437
|
+
@upstream_close_error ||= error if record_error
|
|
438
|
+
error
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def close_ready_queue
|
|
442
|
+
@ready_workers.close
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def close_result_queue
|
|
446
|
+
@results.close
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def build_ractor_map_error(sequence, kind, error)
|
|
450
|
+
RactorMapError.new(
|
|
451
|
+
sequence: sequence,
|
|
452
|
+
kind: kind,
|
|
453
|
+
cause_class_name: error.class.name,
|
|
454
|
+
cause_message: error.message,
|
|
455
|
+
cause: error
|
|
456
|
+
)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
class << self
|
|
460
|
+
def spawn_worker(worker_id, result_port, transform, output_transfer) # :nodoc:
|
|
461
|
+
Ractor.new(worker_id, result_port, transform, output_transfer) do |id, port, mapper, transfer|
|
|
462
|
+
current_sequence = nil
|
|
463
|
+
send_control =
|
|
464
|
+
lambda do |message|
|
|
465
|
+
port.send(message)
|
|
466
|
+
true
|
|
467
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
468
|
+
false
|
|
469
|
+
end
|
|
470
|
+
send_failure =
|
|
471
|
+
lambda do |sequence, kind, error|
|
|
472
|
+
send_control.call(WorkerFailure.new(id, sequence, kind, error.class.name, error.message))
|
|
473
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
474
|
+
false
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
begin
|
|
478
|
+
if send_control.call(Ready.new(id))
|
|
479
|
+
loop do
|
|
480
|
+
message = Ractor.receive
|
|
481
|
+
case message
|
|
482
|
+
in Shutdown
|
|
483
|
+
break
|
|
484
|
+
in Job[sequence, value]
|
|
485
|
+
current_sequence = sequence
|
|
486
|
+
else
|
|
487
|
+
raise TypeError, "invalid ractor_unordered_map worker message: #{message.class}"
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
begin
|
|
491
|
+
mapped_value = mapper.call(value)
|
|
492
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
493
|
+
break unless send_failure.call(current_sequence, :worker, error)
|
|
494
|
+
else
|
|
495
|
+
begin
|
|
496
|
+
if transfer == :move
|
|
497
|
+
port.send(WorkerValue.new(id, current_sequence, mapped_value), move: true)
|
|
498
|
+
else
|
|
499
|
+
port.send(WorkerValue.new(id, current_sequence, mapped_value))
|
|
500
|
+
end
|
|
501
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
502
|
+
break unless send_failure.call(current_sequence, :output_transfer, error)
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
current_sequence = nil
|
|
507
|
+
break unless send_control.call(Ready.new(id))
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
511
|
+
sequence = current_sequence || -1
|
|
512
|
+
send_failure.call(sequence, :worker_termination, error)
|
|
513
|
+
ensure
|
|
514
|
+
send_control.call(Stopped.new(id))
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberStream
|
|
4
|
+
module Pull
|
|
5
|
+
# Complement filtering stage.
|
|
6
|
+
#
|
|
7
|
+
# A single downstream demand may pull multiple upstream elements until the
|
|
8
|
+
# predicate retains a value or upstream completes. Rejected elements are
|
|
9
|
+
# discarded immediately and are not buffered.
|
|
10
|
+
class Reject
|
|
11
|
+
def initialize(upstream, predicate)
|
|
12
|
+
@upstream = upstream
|
|
13
|
+
@predicate = predicate
|
|
14
|
+
@closed = false
|
|
15
|
+
@done = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def next
|
|
19
|
+
return DONE if @closed || @done
|
|
20
|
+
|
|
21
|
+
loop do
|
|
22
|
+
value = @upstream.next
|
|
23
|
+
if Pull.done?(value)
|
|
24
|
+
@done = true
|
|
25
|
+
return DONE
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
return value unless @predicate.call(value)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def close
|
|
33
|
+
return if @closed
|
|
34
|
+
|
|
35
|
+
@closed = true
|
|
36
|
+
@upstream.close
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberStream
|
|
4
|
+
module Pull
|
|
5
|
+
# Stateless observing stage.
|
|
6
|
+
#
|
|
7
|
+
# It pulls one upstream element for each downstream demand, calls the
|
|
8
|
+
# observer for real elements, and emits the original element unchanged.
|
|
9
|
+
class Tap
|
|
10
|
+
def initialize(upstream, observer)
|
|
11
|
+
@upstream = upstream
|
|
12
|
+
@observer = observer
|
|
13
|
+
@closed = false
|
|
14
|
+
@done = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def next
|
|
18
|
+
return DONE if @closed || @done
|
|
19
|
+
|
|
20
|
+
value = @upstream.next
|
|
21
|
+
if Pull.done?(value)
|
|
22
|
+
@done = true
|
|
23
|
+
return DONE
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
@observer.call(value)
|
|
27
|
+
value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def close
|
|
31
|
+
return if @closed
|
|
32
|
+
|
|
33
|
+
@closed = true
|
|
34
|
+
@upstream.close
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberStream
|
|
4
|
+
module Pull
|
|
5
|
+
# Pull-driven rate-limiting stage.
|
|
6
|
+
#
|
|
7
|
+
# The stage pulls at most one upstream value, acquires one permit, and then
|
|
8
|
+
# emits that value unless the stage was closed while waiting.
|
|
9
|
+
class Throttle
|
|
10
|
+
def initialize(upstream, limiter)
|
|
11
|
+
@upstream = upstream
|
|
12
|
+
@limiter = limiter
|
|
13
|
+
@closed = false
|
|
14
|
+
@done = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def next
|
|
18
|
+
return DONE if @closed || @done
|
|
19
|
+
|
|
20
|
+
value = @upstream.next
|
|
21
|
+
if Pull.done?(value)
|
|
22
|
+
@done = true
|
|
23
|
+
return DONE
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
@limiter.acquire(permits: 1)
|
|
27
|
+
if @closed
|
|
28
|
+
@done = true
|
|
29
|
+
return DONE
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def close
|
|
36
|
+
return if @closed
|
|
37
|
+
|
|
38
|
+
@closed = true
|
|
39
|
+
@upstream.close
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|