fiber_stream 0.2.0 → 0.4.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.
@@ -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
- READY_WAIT_INTERVAL = 0.001
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
@@ -67,8 +80,7 @@ module FiberStream
67
80
  @started = true
68
81
  @result_port = Ractor::Port.new
69
82
  @workers_count.times do |worker_id|
70
- @workers << self.class.__send__(
71
- :spawn_worker,
83
+ @workers << self.class.spawn_worker(
72
84
  worker_id,
73
85
  @result_port,
74
86
  @transform,
@@ -103,7 +115,7 @@ module FiberStream
103
115
  break unless worker
104
116
 
105
117
  message = pull_job_message
106
- if message.fetch(0) == :job
118
+ if message.is_a?(Job)
107
119
  @in_flight += 1
108
120
  break unless deliver_job(worker, message)
109
121
  else
@@ -120,19 +132,23 @@ module FiberStream
120
132
 
121
133
  sequence = @next_sequence
122
134
  @next_sequence += 1
123
- [:job, sequence, value]
135
+ Job.new(sequence, value)
124
136
  rescue StandardError => error
125
137
  close_upstream(record_error: false)
126
- [:error, @next_sequence, error]
138
+ ResultError.new(sequence: @next_sequence, error:)
127
139
  end
128
140
 
129
141
  def terminal_done_message
130
142
  close_error = close_upstream
131
- close_error ? [:error, @next_sequence, close_error] : [:done, @next_sequence]
143
+ if close_error
144
+ ResultError.new(sequence: @next_sequence, error: close_error)
145
+ else
146
+ ResultDone.new(sequence: @next_sequence)
147
+ end
132
148
  end
133
149
 
134
150
  def deliver_job(worker, message)
135
- sequence = message.fetch(1)
151
+ sequence = message.sequence
136
152
  track_worker_job(worker, sequence)
137
153
 
138
154
  if @input_transfer == :move
@@ -143,8 +159,7 @@ module FiberStream
143
159
  true
144
160
  rescue StandardError => error
145
161
  clear_worker_job(worker)
146
- sequence = message.fetch(1)
147
- record_result([:error, sequence, build_ractor_map_error(sequence, :input_transfer, error)])
162
+ record_result(ResultError.new(sequence:, error: build_ractor_map_error(sequence, :input_transfer, error)))
148
163
  false
149
164
  end
150
165
 
@@ -162,28 +177,23 @@ module FiberStream
162
177
  end
163
178
 
164
179
  def emit(message)
165
- case message.fetch(0)
166
- when :value
167
- emit_value(message)
168
- when :done
180
+ case message
181
+ in ResultValue[sequence:, value:]
182
+ emit_value(sequence, value)
183
+ in ResultDone
169
184
  complete
170
- when :error
171
- fail_with_ordered_error(message)
185
+ in ResultError[sequence:, error:]
186
+ fail_with_ordered_error(sequence, error)
172
187
  end
173
188
  end
174
189
 
175
- def emit_value(message)
176
- sequence = message.fetch(1)
177
- value = message.fetch(2)
190
+ def emit_value(sequence, value)
178
191
  @next_emit_sequence = sequence + 1
179
192
  @in_flight -= 1 if @in_flight.positive?
180
193
  value
181
194
  end
182
195
 
183
- def fail_with_ordered_error(message)
184
- sequence = message.fetch(1)
185
- error = message.fetch(2)
186
-
196
+ def fail_with_ordered_error(sequence, error)
187
197
  if @failure_sequence && sequence > @failure_sequence
188
198
  @next_emit_sequence = sequence + 1
189
199
  @in_flight -= 1 if @in_flight.positive?
@@ -207,14 +217,14 @@ module FiberStream
207
217
  end
208
218
 
209
219
  def record_result(message)
210
- if message.fetch(0) == :error
211
- sequence = message.fetch(1)
220
+ if message.is_a?(ResultError)
221
+ sequence = message.sequence
212
222
  @failure_sequence = sequence if @failure_sequence.nil? || sequence < @failure_sequence
213
223
  close_admission
214
224
  request_worker_shutdown
215
225
  end
216
226
 
217
- @pending[message.fetch(1)] = message
227
+ @pending[message.sequence] = message
218
228
  end
219
229
 
220
230
  def drain_available_results
@@ -282,39 +292,39 @@ module FiberStream
282
292
  end
283
293
 
284
294
  def handle_worker_message(message, live_workers)
285
- case message.fetch(0)
286
- when :ready
287
- deliver_ready_worker(message.fetch(1))
295
+ case message
296
+ in Ready[worker_id]
297
+ deliver_ready_worker(worker_id)
288
298
  0
289
- when :value
299
+ in WorkerValue
290
300
  handle_worker_value_message(message)
291
301
  0
292
- when :error
302
+ in WorkerFailure
293
303
  handle_worker_error_message(message)
294
304
  0
295
- when :stopped
305
+ in Stopped
296
306
  handle_worker_stopped_message(message, live_workers)
297
307
  end
298
308
  end
299
309
 
300
310
  def handle_worker_value_message(message)
301
- worker = worker_for_id(message.fetch(1))
302
- sequence = message.fetch(2)
303
- value = message.fetch(3)
311
+ worker = worker_for_id(message.worker_id)
312
+ sequence = message.sequence
313
+ value = message.value
304
314
 
305
315
  clear_worker_job(worker)
306
- deliver_result([:value, sequence, value])
316
+ deliver_result(ResultValue.new(sequence:, value:))
307
317
  end
308
318
 
309
319
  def handle_worker_error_message(message)
310
- worker = worker_for_id(message.fetch(1))
320
+ worker = worker_for_id(message.worker_id)
311
321
 
312
322
  clear_worker_job(worker)
313
323
  deliver_result(normalize_worker_error_message(message))
314
324
  end
315
325
 
316
326
  def handle_worker_stopped_message(message, live_workers)
317
- worker = worker_for_id(message.fetch(1))
327
+ worker = worker_for_id(message.worker_id)
318
328
  live_workers.delete(worker)
319
329
  sequence = clear_worker_job(worker)
320
330
  deliver_worker_termination_error(worker, sequence) if sequence && !@closed && !@worker_shutdown_sent
@@ -339,38 +349,34 @@ module FiberStream
339
349
  cause: cause
340
350
  )
341
351
 
342
- deliver_result([:error, sequence, error])
352
+ deliver_result(ResultError.new(sequence:, error:))
343
353
  end
344
354
 
345
355
  def deliver_ready_worker(worker_id)
346
356
  return if @closed
347
357
 
348
- push_until_delivered_or_closed(@ready_workers, worker_for_id(worker_id), suppress_data: false)
358
+ push_until_delivered_or_closed(@ready_workers, worker_for_id(worker_id))
349
359
  end
350
360
 
351
361
  def deliver_result(message)
352
362
  return if @closed
353
363
 
354
- push_until_delivered_or_closed(@results, message, suppress_data: true)
364
+ push_until_delivered_or_closed(@results, message)
355
365
  end
356
366
 
357
- def push_until_delivered_or_closed(queue, message, suppress_data:)
358
- loop do
359
- return if @closed && suppress_data
360
- return if @closed && !suppress_data
367
+ def push_until_delivered_or_closed(queue, message)
368
+ return if @closed
361
369
 
362
- queue.push(message, true)
363
- return
364
- rescue ThreadError, ClosedQueueError
365
- sleep READY_WAIT_INTERVAL
366
- end
370
+ queue.push(message)
371
+ rescue ThreadError, ClosedQueueError
372
+ nil
367
373
  end
368
374
 
369
375
  def normalize_worker_error_message(message)
370
- sequence = message.fetch(2)
371
- kind = message.fetch(3)
372
- cause_class_name = message.fetch(4)
373
- cause_message = message.fetch(5)
376
+ sequence = message.sequence
377
+ kind = message.kind
378
+ cause_class_name = message.cause_class_name
379
+ cause_message = message.cause_message
374
380
  error =
375
381
  RactorMapError.new(
376
382
  sequence: sequence,
@@ -379,7 +385,7 @@ module FiberStream
379
385
  cause_message: cause_message
380
386
  )
381
387
 
382
- [:error, sequence, error]
388
+ ResultError.new(sequence:, error:)
383
389
  end
384
390
 
385
391
  def worker_for_id(worker_id)
@@ -411,7 +417,7 @@ module FiberStream
411
417
 
412
418
  @worker_shutdown_sent = true
413
419
  @workers.each do |worker|
414
- worker.send([:shutdown])
420
+ worker.send(Shutdown.new)
415
421
  rescue StandardError
416
422
  nil
417
423
  end
@@ -420,7 +426,6 @@ module FiberStream
420
426
  def wait_for_workers
421
427
  return unless @coordinator
422
428
 
423
- sleep READY_WAIT_INTERVAL while @coordinator.alive?
424
429
  @coordinator.join
425
430
  end
426
431
 
@@ -453,48 +458,66 @@ module FiberStream
453
458
  )
454
459
  end
455
460
 
456
- def self.spawn_worker(worker_id, result_port, transform, output_transfer)
457
- Ractor.new(worker_id, result_port, transform, output_transfer) do |id, port, mapper, transfer|
458
- current_sequence = nil
459
-
460
- begin
461
- port.send([:ready, id])
461
+ class << self
462
+ def spawn_worker(worker_id, result_port, transform, output_transfer) # :nodoc:
463
+ Ractor.new(worker_id, result_port, transform, output_transfer) do |id, port, mapper, transfer|
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
462
478
 
463
- loop do
464
- message = Ractor.receive
465
- break if message.fetch(0) == :shutdown
479
+ begin
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
466
491
 
467
- current_sequence = message.fetch(1)
468
- value = message.fetch(2)
469
- begin
470
- mapped_value = mapper.call(value)
471
- rescue Exception => error # rubocop:disable Lint/RescueException
472
- port.send([:error, id, current_sequence, :worker, error.class.name, error.message])
473
- else
474
- begin
475
- if transfer == :move
476
- port.send([:value, id, current_sequence, mapped_value], move: true)
492
+ begin
493
+ mapped_value = mapper.call(value)
494
+ rescue Exception => error # rubocop:disable Lint/RescueException
495
+ break unless send_failure.call(current_sequence, :worker, error)
477
496
  else
478
- port.send([:value, id, current_sequence, mapped_value])
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
479
506
  end
480
- rescue Exception => error # rubocop:disable Lint/RescueException
481
- port.send([:error, id, current_sequence, :output_transfer, error.class.name, error.message])
507
+
508
+ current_sequence = nil
509
+ break unless send_control.call(Ready.new(id))
482
510
  end
483
511
  end
484
-
485
- current_sequence = nil
486
- port.send([:ready, id])
512
+ rescue Exception => error # rubocop:disable Lint/RescueException
513
+ sequence = current_sequence || -1
514
+ send_failure.call(sequence, :worker_termination, error)
515
+ ensure
516
+ send_control.call(Stopped.new(id))
487
517
  end
488
- rescue Exception => error # rubocop:disable Lint/RescueException
489
- sequence = current_sequence || -1
490
- port.send([:error, id, sequence, :worker_termination, error.class.name, error.message])
491
- ensure
492
- port.send([:stopped, id])
493
518
  end
494
519
  end
495
520
  end
496
-
497
- private_class_method :spawn_worker
498
521
  end
499
522
  end
500
523
  end