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.
@@ -0,0 +1,373 @@
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, :producer_ractor)
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
+ producer_ractor = pair.respond_to?(:producer_ractor) ? pair.producer_ractor : nil
25
+ PortPair.new(side:, port: pair.port, ack_port: pair.ack_port, producer_ractor:)
26
+ end.freeze
27
+ @ack_transfer = ack_transfer
28
+ @cancel_enabled = cancel
29
+ @result_mailbox = RactorMergeResultMailbox.new(@pairs.size)
30
+ @control_port = nil
31
+ @coordinator = nil
32
+ @state_mutex = Mutex.new
33
+ @producer_terminal = @pairs.to_h { |pair| [pair.side, false] }
34
+ @side_done = @pairs.to_h { |pair| [pair.side, false] }
35
+ @cancel_sent = @pairs.to_h { |pair| [pair.side, false] }
36
+ @pending_ack_sides = {}
37
+ @started = false
38
+ @closed = false
39
+ @done = false
40
+ end
41
+
42
+ def next
43
+ return DONE if closed_or_done?
44
+
45
+ start
46
+ request_pending_acks
47
+ next_result
48
+ end
49
+
50
+ def close
51
+ started = mark_closed
52
+ return if started.nil?
53
+ return unless started
54
+
55
+ close_result_mailbox
56
+ wake_coordinator
57
+ wait_for_coordinator
58
+ cancel_error = cancel_non_terminal_producers
59
+ raise cancel_error if cancel_error
60
+ end
61
+
62
+ private
63
+
64
+ def closed_or_done?
65
+ @state_mutex.synchronize { @closed || @done }
66
+ end
67
+
68
+ def mark_closed
69
+ already_closed = false
70
+ started = false
71
+
72
+ @state_mutex.synchronize do
73
+ already_closed = @closed
74
+ started = @started
75
+ @closed = true
76
+ @done = true
77
+ end
78
+
79
+ already_closed ? nil : started
80
+ end
81
+
82
+ def start
83
+ started_now = false
84
+
85
+ @state_mutex.synchronize do
86
+ return if @started || @closed
87
+
88
+ @control_port = Ractor::Port.new
89
+ @started = true
90
+ @coordinator = Thread.new { run_coordinator }
91
+ started_now = true
92
+ end
93
+
94
+ send_control_command(StartCommand.new) if started_now && !closed_or_done?
95
+ end
96
+
97
+ def request_pending_acks
98
+ sides =
99
+ @state_mutex.synchronize do
100
+ pending = @pending_ack_sides.keys
101
+ @pending_ack_sides.clear
102
+ pending
103
+ end
104
+
105
+ sides.each { |side| send_control_command(RequestAckCommand.new(side:)) }
106
+ end
107
+
108
+ def next_result
109
+ loop do
110
+ result = @result_mailbox.pop
111
+ return complete if result.nil?
112
+
113
+ case result
114
+ in ValueResult[side:, value:]
115
+ record_pending_ack(side)
116
+ return value
117
+ in DoneResult[side:]
118
+ mark_side_done(side)
119
+ return complete if all_done?
120
+ in ErrorResult[error:]
121
+ mark_done
122
+ raise_error(error)
123
+ end
124
+ end
125
+ rescue RactorMergeResultMailbox::Closed
126
+ complete
127
+ end
128
+
129
+ def record_pending_ack(side)
130
+ @state_mutex.synchronize do
131
+ @pending_ack_sides[side] = true unless @closed || @producer_terminal.fetch(side)
132
+ end
133
+ end
134
+
135
+ def mark_side_done(side)
136
+ @state_mutex.synchronize { @side_done[side] = true }
137
+ end
138
+
139
+ def all_done?
140
+ @state_mutex.synchronize { @side_done.values.all? }
141
+ end
142
+
143
+ def mark_done
144
+ @state_mutex.synchronize { @done = true }
145
+ end
146
+
147
+ def complete
148
+ mark_done
149
+ DONE
150
+ end
151
+
152
+ def run_coordinator
153
+ outstanding_ack = @pairs.to_h { |pair| [pair.side, false] }
154
+ active_ports = @pairs.map(&:port)
155
+ active_ractors = @pairs.filter_map(&:producer_ractor)
156
+ pair_by_port = @pairs.to_h { |pair| [pair.port, pair] }
157
+ pair_by_ractor =
158
+ @pairs.each_with_object({}) do |pair, pairs|
159
+ pairs[pair.producer_ractor] = pair if pair.producer_ractor
160
+ end
161
+
162
+ loop do
163
+ break if active_ports.empty?
164
+
165
+ selected, message = Ractor.select(@control_port, *active_ports, *active_ractors)
166
+ if selected == @control_port
167
+ break if handle_control_message(message, outstanding_ack)
168
+ elsif pair_by_ractor.key?(selected)
169
+ pair = pair_by_ractor.fetch(selected)
170
+ active_ractors.delete(selected)
171
+ case message
172
+ in ProducerTerminal | ProducerCancelled
173
+ next
174
+ else
175
+ deliver_result(ErrorResult.new(side: pair.side, error: Pull.ractor_producer_termination_error(message)))
176
+ end
177
+ else
178
+ pair = pair_by_port.fetch(selected)
179
+ outstanding_ack[pair.side] = false
180
+ result = build_result(pair, message)
181
+ active_ports.delete(pair.port) if producer_terminal?(pair.side)
182
+ deliver_result(result)
183
+ end
184
+ end
185
+ rescue StandardError => error
186
+ deliver_result(ErrorResult.new(side: nil, error: build_error(:receive, error)))
187
+ end
188
+
189
+ def handle_control_message(message, outstanding_ack)
190
+ case message
191
+ in StartCommand
192
+ @pairs.each { |pair| ack_pair(pair, outstanding_ack) }
193
+ false
194
+ in RequestAckCommand[side:]
195
+ pair = @pairs.fetch(side)
196
+ ack_pair(pair, outstanding_ack)
197
+ false
198
+ in ShutdownCommand
199
+ true
200
+ else
201
+ false
202
+ end
203
+ end
204
+
205
+ def ack_pair(pair, outstanding_ack)
206
+ return if outstanding_ack.fetch(pair.side)
207
+ return if producer_terminal?(pair.side)
208
+
209
+ ack_error = send_ack(pair)
210
+ if ack_error
211
+ deliver_result(ErrorResult.new(side: pair.side, error: ack_error))
212
+ else
213
+ outstanding_ack[pair.side] = true
214
+ end
215
+ end
216
+
217
+ def send_ack(pair)
218
+ send_control(pair.ack_port, RactorPort::Ack.new)
219
+ nil
220
+ rescue StandardError => error
221
+ build_error(:ack_transfer, error)
222
+ end
223
+
224
+ def build_result(pair, message)
225
+ side = pair.side
226
+
227
+ case message
228
+ in RactorPort::Element[value]
229
+ ValueResult.new(side:, value:)
230
+ in RactorPort::Complete
231
+ mark_producer_terminal(side)
232
+ DoneResult.new(side:)
233
+ in RactorPort::Failure[String => cause_class_name, String => cause_message]
234
+ mark_producer_terminal(side)
235
+ ErrorResult.new(
236
+ side:,
237
+ error: RactorPortSourceError.new(
238
+ kind: :producer_failure,
239
+ cause_class_name: cause_class_name,
240
+ cause_message: cause_message
241
+ )
242
+ )
243
+ in RactorPort::Failure
244
+ ErrorResult.new(side:, error: invalid_message_error(message, "Failure payloads must be Strings"))
245
+ else
246
+ ErrorResult.new(side:, error: invalid_message_error(message, "invalid RactorPort message"))
247
+ end
248
+ end
249
+
250
+ def invalid_message_error(message, cause_message)
251
+ RactorPortSourceError.new(
252
+ kind: :invalid_message,
253
+ cause_class_name: message.class.name,
254
+ cause_message: cause_message
255
+ )
256
+ end
257
+
258
+ def build_error(kind, error)
259
+ RactorPortSourceError.new(
260
+ kind: kind,
261
+ cause_class_name: error.class.name,
262
+ cause_message: error.message,
263
+ cause: error
264
+ )
265
+ end
266
+
267
+ def mark_producer_terminal(side)
268
+ @state_mutex.synchronize { @producer_terminal[side] = true }
269
+ end
270
+
271
+ def producer_terminal?(side)
272
+ @state_mutex.synchronize { @producer_terminal.fetch(side) }
273
+ end
274
+
275
+ def cancel_non_terminal_producers
276
+ first_error = nil
277
+
278
+ @pairs.each do |pair|
279
+ next unless should_cancel_pair?(pair)
280
+
281
+ cancel_error = send_cancel(pair)
282
+ first_error ||= cancel_error
283
+ end
284
+
285
+ first_error
286
+ end
287
+
288
+ def should_cancel_pair?(pair)
289
+ @state_mutex.synchronize do
290
+ return false unless @cancel_enabled
291
+ return false if @producer_terminal.fetch(pair.side)
292
+ return false if @cancel_sent.fetch(pair.side)
293
+
294
+ @cancel_sent[pair.side] = true
295
+ true
296
+ end
297
+ end
298
+
299
+ def send_cancel(pair)
300
+ send_control(pair.ack_port, RactorPort::Cancel.new(:closed))
301
+ nil
302
+ rescue StandardError => error
303
+ build_error(:cancel_transfer, error)
304
+ end
305
+
306
+ def send_control(port, message)
307
+ if @ack_transfer == :move
308
+ port.send(message, move: true)
309
+ else
310
+ port.send(message)
311
+ end
312
+ end
313
+
314
+ def raise_error(error)
315
+ if error.is_a?(RactorPortSourceError) && error.original_cause
316
+ raise error, cause: error.original_cause
317
+ end
318
+
319
+ raise error
320
+ end
321
+
322
+ def deliver_result(result)
323
+ @result_mailbox.push(result)
324
+ rescue RactorMergeResultMailbox::Closed
325
+ nil
326
+ end
327
+
328
+ def send_control_command(command)
329
+ @control_port&.send(command)
330
+ rescue StandardError
331
+ nil
332
+ end
333
+
334
+ def wake_coordinator
335
+ send_control_command(ShutdownCommand.new)
336
+ end
337
+
338
+ def wait_for_coordinator
339
+ return unless @coordinator
340
+
341
+ @coordinator.join
342
+ end
343
+
344
+ def close_result_mailbox
345
+ @result_mailbox.close
346
+ end
347
+
348
+ class RactorMergeResultMailbox
349
+ Closed = Class.new(StandardError)
350
+
351
+ def initialize(capacity)
352
+ @queue = Thread::SizedQueue.new(capacity)
353
+ end
354
+
355
+ def push(result)
356
+ @queue << result
357
+ rescue ClosedQueueError
358
+ raise Closed
359
+ end
360
+
361
+ def pop
362
+ @queue.pop
363
+ rescue ClosedQueueError
364
+ raise Closed
365
+ end
366
+
367
+ def close
368
+ @queue.close
369
+ end
370
+ end
371
+ end
372
+ end
373
+ end
@@ -9,13 +9,21 @@ module FiberStream
9
9
  # a coordinator thread so scheduler-managed fibers do not call Ractor wait
10
10
  # APIs directly.
11
11
  class RactorPortSource
12
- WAIT_INTERVAL = 0.001
13
-
14
- def initialize(port, ack_port, ack_transfer, cancel)
12
+ ProtocolMessage = Data.define(:message)
13
+ ErrorMessage = Data.define(:error)
14
+ ClosedMessage = Data.define
15
+ SelectedProtocol = Data.define(:message)
16
+ SelectedShutdown = Data.define
17
+ SelectedProducerTerminated = Data.define(:result)
18
+ private_constant :ProtocolMessage, :ErrorMessage, :ClosedMessage
19
+ private_constant :SelectedProtocol, :SelectedShutdown, :SelectedProducerTerminated
20
+
21
+ def initialize(port, ack_port, ack_transfer, cancel, producer_ractor = nil)
15
22
  @port = port
16
23
  @ack_port = ack_port
17
24
  @ack_transfer = ack_transfer
18
25
  @cancel_enabled = cancel
26
+ @producer_ractor = producer_ractor
19
27
  @demands = Thread::SizedQueue.new(1)
20
28
  @results = Thread::SizedQueue.new(1)
21
29
  @shutdown_port = nil
@@ -25,6 +33,7 @@ module FiberStream
25
33
  @closed = false
26
34
  @done = false
27
35
  @producer_terminal = false
36
+ @producer_ractor_terminal = false
28
37
  @cancel_sent = false
29
38
  end
30
39
 
@@ -81,15 +90,13 @@ module FiberStream
81
90
  end
82
91
 
83
92
  def handle_result(result)
84
- tag = result.fetch(0)
85
-
86
- case tag
87
- when :message
88
- handle_protocol_message(result.fetch(1))
89
- when :error
93
+ case result
94
+ in ProtocolMessage[message:]
95
+ handle_protocol_message(message)
96
+ in ErrorMessage[error:]
90
97
  mark_done
91
- raise_error(result.fetch(1))
92
- when :closed
98
+ raise_error(error)
99
+ in ClosedMessage
93
100
  DONE
94
101
  end
95
102
  end
@@ -151,23 +158,50 @@ module FiberStream
151
158
 
152
159
  ack_error = send_ack
153
160
  if ack_error
154
- deliver_result([:error, ack_error])
161
+ deliver_result(ErrorMessage.new(error: ack_error))
155
162
  break
156
163
  end
157
164
 
158
- selected, message = select_message
159
- break if selected == @shutdown_port || closed?
160
-
161
- deliver_result([:message, message])
165
+ case select_message
166
+ in SelectedShutdown
167
+ break
168
+ in SelectedProducerTerminated[result:]
169
+ deliver_result(ErrorMessage.new(error: Pull.ractor_producer_termination_error(result)))
170
+ break
171
+ in SelectedProtocol[message:]
172
+ deliver_result(ProtocolMessage.new(message:))
173
+ end
162
174
  end
163
175
  rescue StandardError => error
164
- deliver_result([:error, build_error(:receive, error)])
176
+ deliver_result(ErrorMessage.new(error: build_error(:receive, error)))
165
177
  ensure
166
- deliver_result([:closed]) if closed?
178
+ deliver_result(ClosedMessage.new) if closed?
167
179
  end
168
180
 
169
181
  def select_message
170
- Ractor.select(@port, @shutdown_port)
182
+ loop do
183
+ selected, message =
184
+ if @producer_ractor && !@producer_ractor_terminal
185
+ Ractor.select(@port, @shutdown_port, @producer_ractor)
186
+ else
187
+ Ractor.select(@port, @shutdown_port)
188
+ end
189
+
190
+ if selected == @producer_ractor
191
+ @producer_ractor_terminal = true
192
+ case message
193
+ in ProducerTerminal | ProducerCancelled
194
+ next
195
+ else
196
+ return SelectedProducerTerminated.new(result: message)
197
+ end
198
+
199
+ end
200
+
201
+ return SelectedShutdown.new if selected == @shutdown_port || closed?
202
+
203
+ return SelectedProtocol.new(message:)
204
+ end
171
205
  end
172
206
 
173
207
  def send_ack
@@ -218,7 +252,6 @@ module FiberStream
218
252
  def wait_for_coordinator
219
253
  return unless @coordinator
220
254
 
221
- sleep WAIT_INTERVAL while @coordinator.alive?
222
255
  @coordinator.join
223
256
  end
224
257