fiber_stream 0.2.0 → 0.3.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 +37 -0
- data/README.md +102 -9
- data/examples/README.md +5 -0
- data/examples/ractor_merge_ports_and_map.rb +116 -0
- data/lib/fiber_stream/errors.rb +4 -1
- data/lib/fiber_stream/flow.rb +37 -1
- data/lib/fiber_stream/pull/async_boundary.rb +28 -11
- data/lib/fiber_stream/pull/buffer_boundary.rb +28 -10
- data/lib/fiber_stream/pull/concat.rb +9 -1
- data/lib/fiber_stream/pull/grouped.rb +46 -0
- data/lib/fiber_stream/pull/merge.rb +230 -0
- data/lib/fiber_stream/pull/parallel_map_boundary.rb +28 -24
- data/lib/fiber_stream/pull/ractor_map_boundary.rb +103 -79
- data/lib/fiber_stream/pull/ractor_merge_ports_source.rb +358 -0
- data/lib/fiber_stream/pull/ractor_port_source.rb +14 -14
- data/lib/fiber_stream/pull/split.rb +134 -0
- data/lib/fiber_stream/pull.rb +23 -3
- data/lib/fiber_stream/ractor_port.rb +3 -1
- data/lib/fiber_stream/running_pipeline.rb +18 -8
- data/lib/fiber_stream/source.rb +105 -3
- data/lib/fiber_stream/version.rb +1 -1
- data/sig/fiber_stream.rbs +7 -0
- metadata +7 -2
|
@@ -10,7 +10,18 @@ module FiberStream
|
|
|
10
10
|
# is below `workers`, preserving bounded backpressure and ordered output.
|
|
11
11
|
class RactorMapBoundary
|
|
12
12
|
TERMINAL_RESULT_CAPACITY = 1
|
|
13
|
-
|
|
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(:sequence)
|
|
21
|
+
ResultError = ::Data.define(:sequence, :error)
|
|
22
|
+
|
|
23
|
+
private_constant :Job, :Shutdown, :Ready, :WorkerValue, :WorkerFailure, :Stopped
|
|
24
|
+
private_constant :ResultValue, :ResultDone, :ResultError
|
|
14
25
|
|
|
15
26
|
def initialize(upstream, workers, input_transfer, output_transfer, transform)
|
|
16
27
|
@upstream = upstream
|
|
@@ -53,6 +64,8 @@ module FiberStream
|
|
|
53
64
|
@done = true
|
|
54
65
|
close_error = close_upstream
|
|
55
66
|
close_admission(close_upstream: false)
|
|
67
|
+
close_ready_queue
|
|
68
|
+
close_result_queue
|
|
56
69
|
request_worker_shutdown
|
|
57
70
|
wait_for_workers
|
|
58
71
|
close_error ||= @upstream_close_error
|
|
@@ -103,7 +116,7 @@ module FiberStream
|
|
|
103
116
|
break unless worker
|
|
104
117
|
|
|
105
118
|
message = pull_job_message
|
|
106
|
-
if message.
|
|
119
|
+
if message.is_a?(Job)
|
|
107
120
|
@in_flight += 1
|
|
108
121
|
break unless deliver_job(worker, message)
|
|
109
122
|
else
|
|
@@ -120,19 +133,23 @@ module FiberStream
|
|
|
120
133
|
|
|
121
134
|
sequence = @next_sequence
|
|
122
135
|
@next_sequence += 1
|
|
123
|
-
|
|
136
|
+
Job.new(sequence, value)
|
|
124
137
|
rescue StandardError => error
|
|
125
138
|
close_upstream(record_error: false)
|
|
126
|
-
|
|
139
|
+
ResultError.new(sequence: @next_sequence, error:)
|
|
127
140
|
end
|
|
128
141
|
|
|
129
142
|
def terminal_done_message
|
|
130
143
|
close_error = close_upstream
|
|
131
|
-
|
|
144
|
+
if close_error
|
|
145
|
+
ResultError.new(sequence: @next_sequence, error: close_error)
|
|
146
|
+
else
|
|
147
|
+
ResultDone.new(sequence: @next_sequence)
|
|
148
|
+
end
|
|
132
149
|
end
|
|
133
150
|
|
|
134
151
|
def deliver_job(worker, message)
|
|
135
|
-
sequence = message.
|
|
152
|
+
sequence = message.sequence
|
|
136
153
|
track_worker_job(worker, sequence)
|
|
137
154
|
|
|
138
155
|
if @input_transfer == :move
|
|
@@ -143,8 +160,7 @@ module FiberStream
|
|
|
143
160
|
true
|
|
144
161
|
rescue StandardError => error
|
|
145
162
|
clear_worker_job(worker)
|
|
146
|
-
sequence
|
|
147
|
-
record_result([:error, sequence, build_ractor_map_error(sequence, :input_transfer, error)])
|
|
163
|
+
record_result(ResultError.new(sequence:, error: build_ractor_map_error(sequence, :input_transfer, error)))
|
|
148
164
|
false
|
|
149
165
|
end
|
|
150
166
|
|
|
@@ -162,28 +178,23 @@ module FiberStream
|
|
|
162
178
|
end
|
|
163
179
|
|
|
164
180
|
def emit(message)
|
|
165
|
-
case message
|
|
166
|
-
|
|
167
|
-
emit_value(
|
|
168
|
-
|
|
181
|
+
case message
|
|
182
|
+
in ResultValue[sequence:, value:]
|
|
183
|
+
emit_value(sequence, value)
|
|
184
|
+
in ResultDone
|
|
169
185
|
complete
|
|
170
|
-
|
|
171
|
-
fail_with_ordered_error(
|
|
186
|
+
in ResultError[sequence:, error:]
|
|
187
|
+
fail_with_ordered_error(sequence, error)
|
|
172
188
|
end
|
|
173
189
|
end
|
|
174
190
|
|
|
175
|
-
def emit_value(
|
|
176
|
-
sequence = message.fetch(1)
|
|
177
|
-
value = message.fetch(2)
|
|
191
|
+
def emit_value(sequence, value)
|
|
178
192
|
@next_emit_sequence = sequence + 1
|
|
179
193
|
@in_flight -= 1 if @in_flight.positive?
|
|
180
194
|
value
|
|
181
195
|
end
|
|
182
196
|
|
|
183
|
-
def fail_with_ordered_error(
|
|
184
|
-
sequence = message.fetch(1)
|
|
185
|
-
error = message.fetch(2)
|
|
186
|
-
|
|
197
|
+
def fail_with_ordered_error(sequence, error)
|
|
187
198
|
if @failure_sequence && sequence > @failure_sequence
|
|
188
199
|
@next_emit_sequence = sequence + 1
|
|
189
200
|
@in_flight -= 1 if @in_flight.positive?
|
|
@@ -207,14 +218,14 @@ module FiberStream
|
|
|
207
218
|
end
|
|
208
219
|
|
|
209
220
|
def record_result(message)
|
|
210
|
-
if message.
|
|
211
|
-
sequence = message.
|
|
221
|
+
if message.is_a?(ResultError)
|
|
222
|
+
sequence = message.sequence
|
|
212
223
|
@failure_sequence = sequence if @failure_sequence.nil? || sequence < @failure_sequence
|
|
213
224
|
close_admission
|
|
214
225
|
request_worker_shutdown
|
|
215
226
|
end
|
|
216
227
|
|
|
217
|
-
@pending[message.
|
|
228
|
+
@pending[message.sequence] = message
|
|
218
229
|
end
|
|
219
230
|
|
|
220
231
|
def drain_available_results
|
|
@@ -282,39 +293,39 @@ module FiberStream
|
|
|
282
293
|
end
|
|
283
294
|
|
|
284
295
|
def handle_worker_message(message, live_workers)
|
|
285
|
-
case message
|
|
286
|
-
|
|
287
|
-
deliver_ready_worker(
|
|
296
|
+
case message
|
|
297
|
+
in Ready[worker_id]
|
|
298
|
+
deliver_ready_worker(worker_id)
|
|
288
299
|
0
|
|
289
|
-
|
|
300
|
+
in WorkerValue
|
|
290
301
|
handle_worker_value_message(message)
|
|
291
302
|
0
|
|
292
|
-
|
|
303
|
+
in WorkerFailure
|
|
293
304
|
handle_worker_error_message(message)
|
|
294
305
|
0
|
|
295
|
-
|
|
306
|
+
in Stopped
|
|
296
307
|
handle_worker_stopped_message(message, live_workers)
|
|
297
308
|
end
|
|
298
309
|
end
|
|
299
310
|
|
|
300
311
|
def handle_worker_value_message(message)
|
|
301
|
-
worker = worker_for_id(message.
|
|
302
|
-
sequence = message.
|
|
303
|
-
value = message.
|
|
312
|
+
worker = worker_for_id(message.worker_id)
|
|
313
|
+
sequence = message.sequence
|
|
314
|
+
value = message.value
|
|
304
315
|
|
|
305
316
|
clear_worker_job(worker)
|
|
306
|
-
deliver_result(
|
|
317
|
+
deliver_result(ResultValue.new(sequence:, value:))
|
|
307
318
|
end
|
|
308
319
|
|
|
309
320
|
def handle_worker_error_message(message)
|
|
310
|
-
worker = worker_for_id(message.
|
|
321
|
+
worker = worker_for_id(message.worker_id)
|
|
311
322
|
|
|
312
323
|
clear_worker_job(worker)
|
|
313
324
|
deliver_result(normalize_worker_error_message(message))
|
|
314
325
|
end
|
|
315
326
|
|
|
316
327
|
def handle_worker_stopped_message(message, live_workers)
|
|
317
|
-
worker = worker_for_id(message.
|
|
328
|
+
worker = worker_for_id(message.worker_id)
|
|
318
329
|
live_workers.delete(worker)
|
|
319
330
|
sequence = clear_worker_job(worker)
|
|
320
331
|
deliver_worker_termination_error(worker, sequence) if sequence && !@closed && !@worker_shutdown_sent
|
|
@@ -339,38 +350,34 @@ module FiberStream
|
|
|
339
350
|
cause: cause
|
|
340
351
|
)
|
|
341
352
|
|
|
342
|
-
deliver_result(
|
|
353
|
+
deliver_result(ResultError.new(sequence:, error:))
|
|
343
354
|
end
|
|
344
355
|
|
|
345
356
|
def deliver_ready_worker(worker_id)
|
|
346
357
|
return if @closed
|
|
347
358
|
|
|
348
|
-
push_until_delivered_or_closed(@ready_workers, worker_for_id(worker_id)
|
|
359
|
+
push_until_delivered_or_closed(@ready_workers, worker_for_id(worker_id))
|
|
349
360
|
end
|
|
350
361
|
|
|
351
362
|
def deliver_result(message)
|
|
352
363
|
return if @closed
|
|
353
364
|
|
|
354
|
-
push_until_delivered_or_closed(@results, message
|
|
365
|
+
push_until_delivered_or_closed(@results, message)
|
|
355
366
|
end
|
|
356
367
|
|
|
357
|
-
def push_until_delivered_or_closed(queue, message
|
|
358
|
-
|
|
359
|
-
return if @closed && suppress_data
|
|
360
|
-
return if @closed && !suppress_data
|
|
368
|
+
def push_until_delivered_or_closed(queue, message)
|
|
369
|
+
return if @closed
|
|
361
370
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
sleep READY_WAIT_INTERVAL
|
|
366
|
-
end
|
|
371
|
+
queue.push(message)
|
|
372
|
+
rescue ThreadError, ClosedQueueError
|
|
373
|
+
nil
|
|
367
374
|
end
|
|
368
375
|
|
|
369
376
|
def normalize_worker_error_message(message)
|
|
370
|
-
sequence = message.
|
|
371
|
-
kind = message.
|
|
372
|
-
cause_class_name = message.
|
|
373
|
-
cause_message = message.
|
|
377
|
+
sequence = message.sequence
|
|
378
|
+
kind = message.kind
|
|
379
|
+
cause_class_name = message.cause_class_name
|
|
380
|
+
cause_message = message.cause_message
|
|
374
381
|
error =
|
|
375
382
|
RactorMapError.new(
|
|
376
383
|
sequence: sequence,
|
|
@@ -379,7 +386,7 @@ module FiberStream
|
|
|
379
386
|
cause_message: cause_message
|
|
380
387
|
)
|
|
381
388
|
|
|
382
|
-
|
|
389
|
+
ResultError.new(sequence:, error:)
|
|
383
390
|
end
|
|
384
391
|
|
|
385
392
|
def worker_for_id(worker_id)
|
|
@@ -411,7 +418,7 @@ module FiberStream
|
|
|
411
418
|
|
|
412
419
|
@worker_shutdown_sent = true
|
|
413
420
|
@workers.each do |worker|
|
|
414
|
-
worker.send(
|
|
421
|
+
worker.send(Shutdown.new)
|
|
415
422
|
rescue StandardError
|
|
416
423
|
nil
|
|
417
424
|
end
|
|
@@ -420,7 +427,6 @@ module FiberStream
|
|
|
420
427
|
def wait_for_workers
|
|
421
428
|
return unless @coordinator
|
|
422
429
|
|
|
423
|
-
sleep READY_WAIT_INTERVAL while @coordinator.alive?
|
|
424
430
|
@coordinator.join
|
|
425
431
|
end
|
|
426
432
|
|
|
@@ -456,40 +462,58 @@ module FiberStream
|
|
|
456
462
|
def self.spawn_worker(worker_id, result_port, transform, output_transfer)
|
|
457
463
|
Ractor.new(worker_id, result_port, transform, output_transfer) do |id, port, mapper, transfer|
|
|
458
464
|
current_sequence = nil
|
|
465
|
+
send_control =
|
|
466
|
+
lambda do |message|
|
|
467
|
+
port.send(message)
|
|
468
|
+
true
|
|
469
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
470
|
+
false
|
|
471
|
+
end
|
|
472
|
+
send_failure =
|
|
473
|
+
lambda do |sequence, kind, error|
|
|
474
|
+
send_control.call(WorkerFailure.new(id, sequence, kind, error.class.name, error.message))
|
|
475
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
476
|
+
false
|
|
477
|
+
end
|
|
459
478
|
|
|
460
479
|
begin
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
else
|
|
480
|
+
if send_control.call(Ready.new(id))
|
|
481
|
+
loop do
|
|
482
|
+
message = Ractor.receive
|
|
483
|
+
case message
|
|
484
|
+
in Shutdown
|
|
485
|
+
break
|
|
486
|
+
in Job[sequence, value]
|
|
487
|
+
current_sequence = sequence
|
|
488
|
+
else
|
|
489
|
+
raise TypeError, "invalid ractor_map worker message: #{message.class}"
|
|
490
|
+
end
|
|
491
|
+
|
|
474
492
|
begin
|
|
475
|
-
|
|
476
|
-
port.send([:value, id, current_sequence, mapped_value], move: true)
|
|
477
|
-
else
|
|
478
|
-
port.send([:value, id, current_sequence, mapped_value])
|
|
479
|
-
end
|
|
493
|
+
mapped_value = mapper.call(value)
|
|
480
494
|
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
481
|
-
|
|
495
|
+
break unless send_failure.call(current_sequence, :worker, error)
|
|
496
|
+
else
|
|
497
|
+
begin
|
|
498
|
+
if transfer == :move
|
|
499
|
+
port.send(WorkerValue.new(id, current_sequence, mapped_value), move: true)
|
|
500
|
+
else
|
|
501
|
+
port.send(WorkerValue.new(id, current_sequence, mapped_value))
|
|
502
|
+
end
|
|
503
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
504
|
+
break unless send_failure.call(current_sequence, :output_transfer, error)
|
|
505
|
+
end
|
|
482
506
|
end
|
|
483
|
-
end
|
|
484
507
|
|
|
485
|
-
|
|
486
|
-
|
|
508
|
+
current_sequence = nil
|
|
509
|
+
break unless send_control.call(Ready.new(id))
|
|
510
|
+
end
|
|
487
511
|
end
|
|
488
512
|
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
489
513
|
sequence = current_sequence || -1
|
|
490
|
-
|
|
514
|
+
send_failure.call(sequence, :worker_termination, error)
|
|
491
515
|
ensure
|
|
492
|
-
|
|
516
|
+
send_control.call(Stopped.new(id))
|
|
493
517
|
end
|
|
494
518
|
end
|
|
495
519
|
end
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberStream
|
|
4
|
+
module Pull
|
|
5
|
+
# Pull stream for `Source.ractor_merge_ports`.
|
|
6
|
+
#
|
|
7
|
+
# A coordinator thread owns blocking Ractor waits and forwards producer
|
|
8
|
+
# messages through a bounded result mailbox. Each producer receives at most
|
|
9
|
+
# one outstanding ack, and downstream demand replenishes only the producer
|
|
10
|
+
# that emitted the previous value.
|
|
11
|
+
class RactorMergePortsSource
|
|
12
|
+
PortPair = Data.define(:side, :port, :ack_port)
|
|
13
|
+
StartCommand = Data.define
|
|
14
|
+
RequestAckCommand = Data.define(:side)
|
|
15
|
+
ShutdownCommand = Data.define
|
|
16
|
+
ValueResult = Data.define(:side, :value)
|
|
17
|
+
DoneResult = Data.define(:side)
|
|
18
|
+
ErrorResult = Data.define(:side, :error)
|
|
19
|
+
private_constant :PortPair, :StartCommand, :RequestAckCommand, :ShutdownCommand
|
|
20
|
+
private_constant :ValueResult, :DoneResult, :ErrorResult
|
|
21
|
+
|
|
22
|
+
def initialize(port_pairs, ack_transfer, cancel)
|
|
23
|
+
@pairs = port_pairs.each_with_index.map do |pair, side|
|
|
24
|
+
PortPair.new(side:, port: pair.port, ack_port: pair.ack_port)
|
|
25
|
+
end.freeze
|
|
26
|
+
@ack_transfer = ack_transfer
|
|
27
|
+
@cancel_enabled = cancel
|
|
28
|
+
@result_mailbox = RactorMergeResultMailbox.new(@pairs.size)
|
|
29
|
+
@control_port = nil
|
|
30
|
+
@coordinator = nil
|
|
31
|
+
@state_mutex = Mutex.new
|
|
32
|
+
@producer_terminal = @pairs.to_h { |pair| [pair.side, false] }
|
|
33
|
+
@side_done = @pairs.to_h { |pair| [pair.side, false] }
|
|
34
|
+
@cancel_sent = @pairs.to_h { |pair| [pair.side, false] }
|
|
35
|
+
@pending_ack_sides = {}
|
|
36
|
+
@started = false
|
|
37
|
+
@closed = false
|
|
38
|
+
@done = false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def next
|
|
42
|
+
return DONE if closed_or_done?
|
|
43
|
+
|
|
44
|
+
start
|
|
45
|
+
request_pending_acks
|
|
46
|
+
next_result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def close
|
|
50
|
+
started = mark_closed
|
|
51
|
+
return if started.nil?
|
|
52
|
+
return unless started
|
|
53
|
+
|
|
54
|
+
close_result_mailbox
|
|
55
|
+
wake_coordinator
|
|
56
|
+
wait_for_coordinator
|
|
57
|
+
cancel_error = cancel_non_terminal_producers
|
|
58
|
+
raise cancel_error if cancel_error
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def closed_or_done?
|
|
64
|
+
@state_mutex.synchronize { @closed || @done }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def mark_closed
|
|
68
|
+
already_closed = false
|
|
69
|
+
started = false
|
|
70
|
+
|
|
71
|
+
@state_mutex.synchronize do
|
|
72
|
+
already_closed = @closed
|
|
73
|
+
started = @started
|
|
74
|
+
@closed = true
|
|
75
|
+
@done = true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
already_closed ? nil : started
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def start
|
|
82
|
+
started_now = false
|
|
83
|
+
|
|
84
|
+
@state_mutex.synchronize do
|
|
85
|
+
return if @started || @closed
|
|
86
|
+
|
|
87
|
+
@control_port = Ractor::Port.new
|
|
88
|
+
@started = true
|
|
89
|
+
@coordinator = Thread.new { run_coordinator }
|
|
90
|
+
started_now = true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
send_control_command(StartCommand.new) if started_now && !closed_or_done?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def request_pending_acks
|
|
97
|
+
sides =
|
|
98
|
+
@state_mutex.synchronize do
|
|
99
|
+
pending = @pending_ack_sides.keys
|
|
100
|
+
@pending_ack_sides.clear
|
|
101
|
+
pending
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
sides.each { |side| send_control_command(RequestAckCommand.new(side:)) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def next_result
|
|
108
|
+
loop do
|
|
109
|
+
result = @result_mailbox.pop
|
|
110
|
+
return complete if result.nil?
|
|
111
|
+
|
|
112
|
+
case result
|
|
113
|
+
in ValueResult[side:, value:]
|
|
114
|
+
record_pending_ack(side)
|
|
115
|
+
return value
|
|
116
|
+
in DoneResult[side:]
|
|
117
|
+
mark_side_done(side)
|
|
118
|
+
return complete if all_done?
|
|
119
|
+
in ErrorResult[error:]
|
|
120
|
+
mark_done
|
|
121
|
+
raise_error(error)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
rescue RactorMergeResultMailbox::Closed
|
|
125
|
+
complete
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def record_pending_ack(side)
|
|
129
|
+
@state_mutex.synchronize do
|
|
130
|
+
@pending_ack_sides[side] = true unless @closed || @producer_terminal.fetch(side)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def mark_side_done(side)
|
|
135
|
+
@state_mutex.synchronize { @side_done[side] = true }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def all_done?
|
|
139
|
+
@state_mutex.synchronize { @side_done.values.all? }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def mark_done
|
|
143
|
+
@state_mutex.synchronize { @done = true }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def complete
|
|
147
|
+
mark_done
|
|
148
|
+
DONE
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def run_coordinator
|
|
152
|
+
outstanding_ack = @pairs.to_h { |pair| [pair.side, false] }
|
|
153
|
+
active_ports = @pairs.map(&:port)
|
|
154
|
+
pair_by_port = @pairs.to_h { |pair| [pair.port, pair] }
|
|
155
|
+
|
|
156
|
+
loop do
|
|
157
|
+
break if active_ports.empty?
|
|
158
|
+
|
|
159
|
+
selected, message = Ractor.select(@control_port, *active_ports)
|
|
160
|
+
if selected == @control_port
|
|
161
|
+
break if handle_control_message(message, outstanding_ack)
|
|
162
|
+
else
|
|
163
|
+
pair = pair_by_port.fetch(selected)
|
|
164
|
+
outstanding_ack[pair.side] = false
|
|
165
|
+
result = build_result(pair, message)
|
|
166
|
+
active_ports.delete(pair.port) if producer_terminal?(pair.side)
|
|
167
|
+
deliver_result(result)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
rescue StandardError => error
|
|
171
|
+
deliver_result(ErrorResult.new(side: nil, error: build_error(:receive, error)))
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def handle_control_message(message, outstanding_ack)
|
|
175
|
+
case message
|
|
176
|
+
in StartCommand
|
|
177
|
+
@pairs.each { |pair| ack_pair(pair, outstanding_ack) }
|
|
178
|
+
false
|
|
179
|
+
in RequestAckCommand[side:]
|
|
180
|
+
pair = @pairs.fetch(side)
|
|
181
|
+
ack_pair(pair, outstanding_ack)
|
|
182
|
+
false
|
|
183
|
+
in ShutdownCommand
|
|
184
|
+
true
|
|
185
|
+
else
|
|
186
|
+
false
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def ack_pair(pair, outstanding_ack)
|
|
191
|
+
return if outstanding_ack.fetch(pair.side)
|
|
192
|
+
return if producer_terminal?(pair.side)
|
|
193
|
+
|
|
194
|
+
ack_error = send_ack(pair)
|
|
195
|
+
if ack_error
|
|
196
|
+
deliver_result(ErrorResult.new(side: pair.side, error: ack_error))
|
|
197
|
+
else
|
|
198
|
+
outstanding_ack[pair.side] = true
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def send_ack(pair)
|
|
203
|
+
send_control(pair.ack_port, RactorPort::Ack.new)
|
|
204
|
+
nil
|
|
205
|
+
rescue StandardError => error
|
|
206
|
+
build_error(:ack_transfer, error)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def build_result(pair, message)
|
|
210
|
+
side = pair.side
|
|
211
|
+
|
|
212
|
+
case message
|
|
213
|
+
in RactorPort::Element[value]
|
|
214
|
+
ValueResult.new(side:, value:)
|
|
215
|
+
in RactorPort::Complete
|
|
216
|
+
mark_producer_terminal(side)
|
|
217
|
+
DoneResult.new(side:)
|
|
218
|
+
in RactorPort::Failure[String => cause_class_name, String => cause_message]
|
|
219
|
+
mark_producer_terminal(side)
|
|
220
|
+
ErrorResult.new(
|
|
221
|
+
side:,
|
|
222
|
+
error: RactorPortSourceError.new(
|
|
223
|
+
kind: :producer_failure,
|
|
224
|
+
cause_class_name: cause_class_name,
|
|
225
|
+
cause_message: cause_message
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
in RactorPort::Failure
|
|
229
|
+
ErrorResult.new(side:, error: invalid_message_error(message, "Failure payloads must be Strings"))
|
|
230
|
+
else
|
|
231
|
+
ErrorResult.new(side:, error: invalid_message_error(message, "invalid RactorPort message"))
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def invalid_message_error(message, cause_message)
|
|
236
|
+
RactorPortSourceError.new(
|
|
237
|
+
kind: :invalid_message,
|
|
238
|
+
cause_class_name: message.class.name,
|
|
239
|
+
cause_message: cause_message
|
|
240
|
+
)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def build_error(kind, error)
|
|
244
|
+
RactorPortSourceError.new(
|
|
245
|
+
kind: kind,
|
|
246
|
+
cause_class_name: error.class.name,
|
|
247
|
+
cause_message: error.message,
|
|
248
|
+
cause: error
|
|
249
|
+
)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def mark_producer_terminal(side)
|
|
253
|
+
@state_mutex.synchronize { @producer_terminal[side] = true }
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def producer_terminal?(side)
|
|
257
|
+
@state_mutex.synchronize { @producer_terminal.fetch(side) }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def cancel_non_terminal_producers
|
|
261
|
+
first_error = nil
|
|
262
|
+
|
|
263
|
+
@pairs.each do |pair|
|
|
264
|
+
next unless should_cancel_pair?(pair)
|
|
265
|
+
|
|
266
|
+
cancel_error = send_cancel(pair)
|
|
267
|
+
first_error ||= cancel_error
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
first_error
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def should_cancel_pair?(pair)
|
|
274
|
+
@state_mutex.synchronize do
|
|
275
|
+
return false unless @cancel_enabled
|
|
276
|
+
return false if @producer_terminal.fetch(pair.side)
|
|
277
|
+
return false if @cancel_sent.fetch(pair.side)
|
|
278
|
+
|
|
279
|
+
@cancel_sent[pair.side] = true
|
|
280
|
+
true
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def send_cancel(pair)
|
|
285
|
+
send_control(pair.ack_port, RactorPort::Cancel.new(:closed))
|
|
286
|
+
nil
|
|
287
|
+
rescue StandardError => error
|
|
288
|
+
build_error(:cancel_transfer, error)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def send_control(port, message)
|
|
292
|
+
if @ack_transfer == :move
|
|
293
|
+
port.send(message, move: true)
|
|
294
|
+
else
|
|
295
|
+
port.send(message)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def raise_error(error)
|
|
300
|
+
if error.is_a?(RactorPortSourceError) && error.original_cause
|
|
301
|
+
raise error, cause: error.original_cause
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
raise error
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def deliver_result(result)
|
|
308
|
+
@result_mailbox.push(result)
|
|
309
|
+
rescue RactorMergeResultMailbox::Closed
|
|
310
|
+
nil
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def send_control_command(command)
|
|
314
|
+
@control_port&.send(command)
|
|
315
|
+
rescue StandardError
|
|
316
|
+
nil
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def wake_coordinator
|
|
320
|
+
send_control_command(ShutdownCommand.new)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def wait_for_coordinator
|
|
324
|
+
return unless @coordinator
|
|
325
|
+
|
|
326
|
+
@coordinator.join
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def close_result_mailbox
|
|
330
|
+
@result_mailbox.close
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
class RactorMergeResultMailbox
|
|
334
|
+
Closed = Class.new(StandardError)
|
|
335
|
+
|
|
336
|
+
def initialize(capacity)
|
|
337
|
+
@queue = Thread::SizedQueue.new(capacity)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def push(result)
|
|
341
|
+
@queue << result
|
|
342
|
+
rescue ClosedQueueError
|
|
343
|
+
raise Closed
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def pop
|
|
347
|
+
@queue.pop
|
|
348
|
+
rescue ClosedQueueError
|
|
349
|
+
raise Closed
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def close
|
|
353
|
+
@queue.close
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|