polyphony 0.99.3 → 0.99.5

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +10 -0
  3. data/.yardopts +3 -1
  4. data/README.md +1 -1
  5. data/docs/readme.md +102 -0
  6. data/examples/pipes/gzip_http_server.rb +2 -2
  7. data/examples/pipes/http_server.rb +1 -1
  8. data/examples/pipes/tcp_proxy.rb +1 -1
  9. data/ext/polyphony/backend_common.c +4 -4
  10. data/ext/polyphony/backend_io_uring.c +8 -8
  11. data/ext/polyphony/backend_libev.c +5 -5
  12. data/ext/polyphony/fiber.c +32 -41
  13. data/ext/polyphony/io_extensions.c +50 -37
  14. data/ext/polyphony/pipe.c +6 -18
  15. data/ext/polyphony/polyphony.c +63 -133
  16. data/ext/polyphony/queue.c +25 -63
  17. data/ext/polyphony/thread.c +3 -12
  18. data/lib/polyphony/adapters/process.rb +1 -2
  19. data/lib/polyphony/adapters/sequel.rb +2 -2
  20. data/lib/polyphony/core/debug.rb +1 -1
  21. data/lib/polyphony/core/exceptions.rb +1 -1
  22. data/lib/polyphony/core/global_api.rb +24 -38
  23. data/lib/polyphony/core/resource_pool.rb +7 -8
  24. data/lib/polyphony/core/sync.rb +1 -2
  25. data/lib/polyphony/core/thread_pool.rb +2 -5
  26. data/lib/polyphony/core/throttler.rb +1 -5
  27. data/lib/polyphony/core/timer.rb +24 -25
  28. data/lib/polyphony/extensions/fiber.rb +507 -540
  29. data/lib/polyphony/extensions/io.rb +3 -12
  30. data/lib/polyphony/extensions/openssl.rb +2 -23
  31. data/lib/polyphony/extensions/pipe.rb +4 -15
  32. data/lib/polyphony/extensions/socket.rb +15 -109
  33. data/lib/polyphony/extensions/thread.rb +0 -13
  34. data/lib/polyphony/extensions/timeout.rb +0 -1
  35. data/lib/polyphony/net.rb +5 -8
  36. data/lib/polyphony/version.rb +1 -1
  37. data/lib/polyphony.rb +2 -6
  38. data/test/test_io.rb +221 -221
  39. data/test/test_socket.rb +3 -3
  40. data/test/test_trace.rb +2 -2
  41. metadata +3 -2
@@ -2,228 +2,525 @@
2
2
 
3
3
  require_relative '../core/exceptions'
4
4
 
5
- module Polyphony
5
+ # Fiber extensions
6
+ class ::Fiber
7
+ attr_accessor :tag, :thread, :parent, :oob
8
+ attr_reader :result
6
9
 
7
- # Fiber control methods
8
- module FiberControl
9
- # Returns the fiber's monitoring mailbox queue, used for receiving fiber
10
- # monitoring messages.
11
- #
12
- # @return [Polyphony::Queue] monitoring mailbox queue
13
- def monitor_mailbox
14
- @monitor_mailbox ||= Polyphony::Queue.new
10
+ # Returns true if fiber is running.
11
+ #
12
+ # @return [bool] is fiber running
13
+ def running?
14
+ @running
15
+ end
16
+
17
+ # Returns a string representation of the fiber for debugging.
18
+ #
19
+ # @return [String] string representation
20
+ def inspect
21
+ if @tag
22
+ "#<Fiber #{tag}:#{object_id} #{location} (#{state})>"
23
+ else
24
+ "#<Fiber:#{object_id} #{location} (#{state})>"
15
25
  end
26
+ end
27
+ alias_method :to_s, :inspect
16
28
 
17
- # call-seq:
18
- # fiber.stop(value = nil) -> fiber
19
- # Fiber.interrupt(value = nil) -> fiber
20
- # Fiber.move_on(value = nil) -> fiber
21
- #
22
- # Stops the fiber by raising a `Polyphony::MoveOn` exception. The given
23
- # value will become the fiber's return value.
24
- #
25
- # @param value [any] fiber's eventual return value
26
- # @return [Fiber] self
27
- def interrupt(value = nil)
28
- return if @running == false
29
+ # Returns the source location for the fiber based on its caller.
30
+ #
31
+ # @return [String] source location
32
+ def location
33
+ if @oob
34
+ "#{@caller[0]} (oob)"
35
+ else
36
+ @caller ? @caller[0] : '(root)'
37
+ end
38
+ end
29
39
 
30
- schedule Polyphony::MoveOn.new(value)
31
- self
40
+ # Returns the fiber's caller.
41
+ #
42
+ # @return [Array<String>] caller
43
+ def caller
44
+ spin_caller = @caller || []
45
+ if @parent
46
+ spin_caller + @parent.caller
47
+ else
48
+ spin_caller
32
49
  end
33
- alias_method :stop, :interrupt
34
- alias_method :move_on, :interrupt
50
+ end
35
51
 
36
- # call-seq:
37
- # fiber.reset(value = nil) -> fiber
38
- # fiber.restart(value = nil) -> fiber
39
- #
40
- # Restarts the fiber, with the given value serving as the first value passed
41
- # to the fiber's block.
42
- #
43
- # @param value [any] value passed to fiber block
44
- # @return [Fiber] restarted fiber
45
- def restart(value = nil)
46
- raise "Can't restart main fiber" if @main
47
-
48
- if @running
49
- schedule Polyphony::Restart.new(value)
50
- return self
51
- end
52
+ # Sets the fiber's caller.
53
+ #
54
+ # @param caller [Array<String>] new caller
55
+ # @return [Fiber] self
56
+ def set_caller(caller)
57
+ @caller = caller
58
+ self
59
+ end
52
60
 
53
- fiber = parent.spin(@tag, @caller, &@block)
54
- @monitors&.each_key { |f| fiber.monitor(f) }
55
- fiber.schedule(value) unless value.nil?
56
- fiber
57
- end
58
- alias_method :reset, :restart
61
+ # Returns true if the fiber is the main fiber for its thread.
62
+ #
63
+ # @return [bool] is main fiber
64
+ def main?
65
+ @main
66
+ end
59
67
 
60
- # Stops a fiber by raising a Polyphony::Cancel exception.
61
- #
62
- # @param exception [Class, Exception] exception or exception class
63
- # @return [Fiber] self
64
- def cancel(exception = Polyphony::Cancel)
65
- return if @running == false
66
-
67
- value = (Class === exception) ? exception.new : exception
68
- schedule value
69
- self
70
- end
68
+ #########################
69
+ # Fiber control methods #
70
+ #########################
71
71
 
72
- # Sets the graceful shutdown flag for the fiber.
73
- #
74
- # @param graceful [bool] Whether or not to perform a graceful shutdown
75
- # @return [bool] graceful
76
- def graceful_shutdown=(graceful)
77
- @graceful_shutdown = graceful
78
- end
72
+ # Returns the fiber's monitoring mailbox queue, used for receiving fiber
73
+ # monitoring messages.
74
+ #
75
+ # @return [Polyphony::Queue] monitoring mailbox queue
76
+ def monitor_mailbox
77
+ @monitor_mailbox ||= Polyphony::Queue.new
78
+ end
79
79
 
80
- # Returns the graceful shutdown flag for the fiber.
81
- #
82
- # @return [bool] true if graceful shutdown, otherwise false
83
- def graceful_shutdown?
84
- @graceful_shutdown
85
- end
80
+ # Stops the fiber by raising a `Polyphony::MoveOn` exception. The given
81
+ # value will become the fiber's return value.
82
+ #
83
+ # @param value [any] fiber's eventual return value
84
+ # @return [Fiber] self
85
+ def interrupt(value = nil)
86
+ return if @running == false
86
87
 
87
- # Terminates the fiber, optionally setting the graceful shutdown flag.
88
- #
89
- # @param graceful [bool] Whether to perform a graceful shutdown
90
- # @return [Fiber] self
91
- def terminate(graceful = false)
92
- return if @running == false
93
-
94
- @graceful_shutdown = graceful
95
- schedule Polyphony::Terminate.new
96
- self
97
- end
88
+ schedule Polyphony::MoveOn.new(value)
89
+ self
90
+ end
91
+ alias_method :stop, :interrupt
92
+ alias_method :move_on, :interrupt
98
93
 
99
- # call-seq:
100
- # fiber.raise(message) -> fiber
101
- # fiber.raise(exception_class) -> fiber
102
- # fiber.raise(exception_class, exception_message) -> fiber
103
- # fiber.raise(exception) -> fiber
104
- #
105
- # Raises an exception in the context of the fiber
106
- #
107
- # @return [Fiber] self
108
- def raise(*args)
109
- error = error_from_raise_args(args)
110
- schedule(error)
111
- self
94
+ # Restarts the fiber, with the given value serving as the first value passed
95
+ # to the fiber's block.
96
+ #
97
+ # @param value [any] value passed to fiber block
98
+ # @return [Fiber] restarted fiber
99
+ def restart(value = nil)
100
+ raise "Can't restart main fiber" if @main
101
+
102
+ if @running
103
+ schedule Polyphony::Restart.new(value)
104
+ return self
112
105
  end
113
106
 
114
- # Adds an interjection to the fiber. The current operation undertaken by the
115
- # fiber will be interrupted, and the given block will be executed, and the
116
- # operation will be resumed. This API is experimental and might be removed
117
- # in the future.
118
- #
119
- # @yield [any] given block
120
- # @return [Fiber] self
121
- def interject(&block)
122
- raise Polyphony::Interjection.new(block)
107
+ fiber = parent.spin(@tag, @caller, &@block)
108
+ @monitors&.each_key { |f| fiber.monitor(f) }
109
+ fiber.schedule(value) unless value.nil?
110
+ fiber
111
+ end
112
+ alias_method :reset, :restart
113
+
114
+ # Stops a fiber by raising a Polyphony::Cancel exception.
115
+ #
116
+ # @param exception [Class, Exception] exception or exception class
117
+ # @return [Fiber] self
118
+ def cancel(exception = Polyphony::Cancel)
119
+ return if @running == false
120
+
121
+ value = (Class === exception) ? exception.new : exception
122
+ schedule value
123
+ self
124
+ end
125
+
126
+ # Sets the graceful shutdown flag for the fiber.
127
+ #
128
+ # @param graceful [bool] Whether or not to perform a graceful shutdown
129
+ # @return [bool] graceful
130
+ def graceful_shutdown=(graceful)
131
+ @graceful_shutdown = graceful
132
+ end
133
+
134
+ # Returns the graceful shutdown flag for the fiber.
135
+ #
136
+ # @return [bool] true if graceful shutdown, otherwise false
137
+ def graceful_shutdown?
138
+ @graceful_shutdown
139
+ end
140
+
141
+ # Terminates the fiber, optionally setting the graceful shutdown flag.
142
+ #
143
+ # @param graceful [bool] Whether to perform a graceful shutdown
144
+ # @return [Fiber] self
145
+ def terminate(graceful = false)
146
+ return if @running == false
147
+
148
+ @graceful_shutdown = graceful
149
+ schedule Polyphony::Terminate.new
150
+ self
151
+ end
152
+
153
+ # Raises an exception in the context of the fiber
154
+ #
155
+ # @overload fiber.raise(message)
156
+ # @param message [String] error message
157
+ # @return [Fiber] self
158
+ # @overload fiber.raise(exception_class)
159
+ # @param exception_class [Class] exception class to raise
160
+ # @return [Fiber] self
161
+ # @overload fiber.raise(exception_class, exception_message)
162
+ # @param exception_class [Class] exception class to raise
163
+ # @param exception_message [String] exception message to raise
164
+ # @return [Fiber] self
165
+ # @overload fiber.raise(exception)
166
+ # @param any [Exception] exception to raise
167
+ # @return [Fiber] self
168
+ def raise(*args)
169
+ error = error_from_raise_args(args)
170
+ schedule(error)
171
+ self
172
+ end
173
+
174
+ # Adds an interjection to the fiber. The current operation undertaken by the
175
+ # fiber will be interrupted, and the given block will be executed, and the
176
+ # operation will be resumed. This API is experimental and might be removed
177
+ # in the future.
178
+ #
179
+ # @return [Fiber] self
180
+ def interject(&block)
181
+ raise Polyphony::Interjection.new(block)
182
+ end
183
+
184
+ # Blocks until the fiber has terminated, returning its return value.
185
+ #
186
+ # @return [any] fiber's return value
187
+ def await
188
+ Fiber.await(self).first
189
+ end
190
+ alias_method :join, :await
191
+
192
+ #############################
193
+ # Fiber supervision methods #
194
+ #############################
195
+
196
+ # Supervises the given fibers or all child fibers. The fiber is put in
197
+ # supervision mode, which means any child added after calling `#supervise`
198
+ # will automatically be supervised. Depending on the given options, fibers
199
+ # may be automatically restarted.
200
+ #
201
+ # If a block is given, the block is called whenever a supervised fiber has
202
+ # terminated. If the `:on_done` option is given, that proc will be called
203
+ # when a supervised fiber has terminated. If the `:on_error` option is
204
+ # given, that proc will be called when a supervised fiber has terminated
205
+ # with an uncaught exception. If the `:restart` option equals `:always`,
206
+ # fibers will always be restarted. If the `:restart` option equals
207
+ # `:on_error`, fibers will be restarted only when terminated with an
208
+ # uncaught exception.
209
+ #
210
+ # This method blocks indefinitely.
211
+ #
212
+ # @param fibers [Array<Fiber>] fibers to supervise
213
+ # @option opts [Proc, nil] :on_done proc to call when a supervised fiber is terminated
214
+ # @option opts [Proc, nil] :on_error proc to call when a supervised fiber is terminated with an exception
215
+ # @option opts [:always, :on_error, nil] :restart whether to restart terminated fibers
216
+ # @return [void]
217
+ def supervise(*fibers, **opts, &block)
218
+ block ||= supervise_opts_to_block(opts)
219
+
220
+ @supervise_mode = true
221
+ fibers = children if fibers.empty?
222
+ fibers.each do |f|
223
+ f.attach_to(self) unless f.parent == self
224
+ f.monitor(self)
123
225
  end
124
226
 
125
- # Blocks until the fiber has terminated, returning its return value.
126
- #
127
- # @return [any] fiber's return value
128
- def await
129
- Fiber.await(self).first
227
+ mailbox = monitor_mailbox
228
+
229
+ while true
230
+ (fiber, result) = mailbox.shift
231
+ block&.call(fiber, result)
130
232
  end
131
- alias_method :join, :await
233
+ ensure
234
+ @supervise_mode = false
235
+ end
132
236
 
133
- private
237
+ ###############################
238
+ # Child fiber control methods #
239
+ ###############################
134
240
 
135
- # @!visibility private
136
- def error_from_raise_args(args)
137
- case (arg = args.shift)
138
- when String then RuntimeError.new(arg)
139
- when Class then arg.new(args.shift)
140
- when Exception then arg
141
- else RuntimeError.new
142
- end
241
+ # Returns the fiber's children.
242
+ #
243
+ # @return [Array<Fiber>] child fibers
244
+ def children
245
+ (@children ||= {}).keys
246
+ end
247
+
248
+ # Creates a new child fiber.
249
+ #
250
+ # child = fiber.spin { sleep 10; fiber.stop }
251
+ #
252
+ # @param tag [any] child fiber's tag
253
+ # @param orig_caller [Array<String>] caller to set for fiber
254
+ # @return [Fiber] child fiber
255
+ def spin(tag = nil, orig_caller = Kernel.caller, &block)
256
+ f = Fiber.new { |v| f.run(v) }
257
+ f.prepare(tag, block, orig_caller, self)
258
+ (@children ||= {})[f] = true
259
+ f.monitor(self) if @supervise_mode
260
+ f
261
+ end
262
+
263
+ # Terminates all child fibers. This method will return before the fibers are
264
+ # actually terminated.
265
+ #
266
+ # @param graceful [bool] whether to perform a graceful termination
267
+ # @return [Fiber] self
268
+ def terminate_all_children(graceful = false)
269
+ return self unless @children
270
+
271
+ e = Polyphony::Terminate.new
272
+ @children.each_key do |c|
273
+ c.graceful_shutdown = true if graceful
274
+ c.raise e
143
275
  end
276
+ self
144
277
  end
145
278
 
146
- # Fiber supervision methods
147
- module FiberSupervision
279
+ # Block until all child fibers have terminated. Returns the return values
280
+ # for all child fibers.
281
+ #
282
+ # @return [Array<any>] return values of child fibers
283
+ def await_all_children
284
+ return unless @children && !@children.empty?
148
285
 
149
- # call-seq:
150
- # fiber.supervise
151
- # fiber.supervise(fiber_a, fiber_b)
152
- # fiber.supervise { |f, r| handle_terminated_fiber(f, r) }
153
- # fiber.supervise(on_done: ->(f, r) { handle_terminated_fiber(f, r) })
154
- # fiber.supervise(on_error: ->(f, e) { handle_error(f, e) })
155
- # fiber.supervise(*fibers, restart: always)
156
- # fiber.supervise(*fibers, restart: on_error)
157
- #
158
- # Supervises the given fibers or all child fibers. The fiber is put in
159
- # supervision mode, which means any child added after calling `#supervise`
160
- # will automatically be supervised. Depending on the given options, fibers
161
- # may be automatically restarted.
162
- #
163
- # If a block is given, the block is called whenever a supervised fiber has
164
- # terminated. If the `:on_done` option is given, that proc will be called
165
- # when a supervised fiber has terminated. If the `:on_error` option is
166
- # given, that proc will be called when a supervised fiber has terminated
167
- # with an uncaught exception. If the `:restart` option equals `:always`,
168
- # fibers will always be restarted. If the `:restart` option equals
169
- # `:on_error`, fibers will be restarted only when terminated with an
170
- # uncaught exception.
171
- #
172
- # This method blocks indefinitely.
173
- #
174
- # @param fibers [Array<Fiber>] fibers to supervise
175
- # @option opts [Proc, nil] :on_done proc to call when a supervised fiber is terminated
176
- # @option opts [Proc, nil] :on_error proc to call when a supervised fiber is terminated with an exception
177
- # @option opts [:always, :on_error, nil] :restart whether to restart terminated fibers
178
- # @yield [] supervisor block
179
- # @return [void]
180
- def supervise(*fibers, **opts, &block)
181
- block ||= supervise_opts_to_block(opts)
286
+ Fiber.await(*@children.keys.reject { |c| c.dead? })
287
+ end
182
288
 
183
- @supervise_mode = true
184
- fibers = children if fibers.empty?
185
- fibers.each do |f|
186
- f.attach_to(self) unless f.parent == self
187
- f.monitor(self)
188
- end
289
+ # Terminates and blocks until all child fibers have terminated.
290
+ #
291
+ # @return [Fiber] self
292
+ def shutdown_all_children(graceful = false)
293
+ return self unless @children
189
294
 
190
- mailbox = monitor_mailbox
295
+ pending = []
296
+ @children.keys.each do |c|
297
+ next if c.dead?
191
298
 
192
- while true
193
- (fiber, result) = mailbox.shift
194
- block&.call(fiber, result)
195
- end
196
- ensure
197
- @supervise_mode = false
299
+ c.terminate(graceful)
300
+ pending << c
198
301
  end
302
+ Fiber.await(*pending)
303
+ self
304
+ end
199
305
 
200
- private
306
+ # Attaches all child fibers to a new parent.
307
+ #
308
+ # @param parent [Fiber] new parent
309
+ # @return [Fiber] self
310
+ def attach_all_children_to(parent)
311
+ @children&.keys.each { |c| c.attach_to(parent) }
312
+ self
313
+ end
201
314
 
202
- # @!visibility private
203
- def supervise_opts_to_block(opts)
204
- block = opts[:on_done] || opts[:on_error]
205
- restart = opts[:restart]
206
- return nil unless block || restart
207
-
208
- error_only = !!opts[:on_error]
209
- restart_always = (restart == :always) || (restart == true)
210
- restart_on_error = restart == :on_error
211
-
212
- ->(f, r) do
213
- is_error = r.is_a?(Exception)
214
- block.(f, r) if block && (!error_only || is_error)
215
- f.restart if restart_always || (restart_on_error && is_error)
216
- end
315
+ # Detaches the fiber from its current parent. The fiber will be made a child
316
+ # of the main fiber (for the current thread.)
317
+ #
318
+ # @return [Fiber] self
319
+ def detach
320
+ @parent.remove_child(self)
321
+ @parent = @thread.main_fiber
322
+ @parent.add_child(self)
323
+ self
324
+ end
325
+
326
+ # Attaches the fiber to a new parent.
327
+ #
328
+ # @param parent [Fiber] new parent
329
+ # @return [Fiber] self
330
+ def attach_to(parent)
331
+ @parent.remove_child(self)
332
+ @parent = parent
333
+ parent.add_child(self)
334
+ self
335
+ end
336
+
337
+ # Attaches the fiber to the new parent and monitors the new parent.
338
+ #
339
+ # @param parent [Fiber] new parent
340
+ # @return [Fiber] self
341
+ def attach_and_monitor(parent)
342
+ @parent.remove_child(self)
343
+ @parent = parent
344
+ parent.add_child(self)
345
+ monitor(parent)
346
+ self
347
+ end
348
+
349
+ # Adds a child fiber reference. Used internally.
350
+ #
351
+ # @param child_fiber [Fiber] child fiber
352
+ # @return [Fiber] self
353
+ def add_child(child_fiber)
354
+ (@children ||= {})[child_fiber] = true
355
+ child_fiber.monitor(self) if @supervise_mode
356
+ self
357
+ end
358
+
359
+ # Removes a child fiber reference. Used internally.
360
+ #
361
+ # @param child_fiber [Fiber] child fiber to be removed
362
+ # @return [Fiber] self
363
+ def remove_child(child_fiber)
364
+ @children.delete(child_fiber) if @children
365
+ self
366
+ end
367
+
368
+ ############################
369
+ # Fiber life cycle methods #
370
+ ############################
371
+
372
+ # Prepares a fiber for running.
373
+ #
374
+ # @param tag [any] fiber's tag
375
+ # @param block [Proc] fiber's block
376
+ # @param caller [Array<String>] fiber's caller
377
+ # @param parent [Fiber] fiber's parent
378
+ # @return [void]
379
+ def prepare(tag, block, caller, parent)
380
+ @thread = Thread.current
381
+ @tag = tag
382
+ @parent = parent
383
+ @caller = caller
384
+ @block = block
385
+ Thread.backend.trace(:spin, self, Kernel.caller[1..-1])
386
+ schedule
387
+ end
388
+
389
+ # Runs the fiber's block and handles uncaught exceptions.
390
+ #
391
+ # @param first_value [any] value passed to fiber on first resume
392
+ # @return [void]
393
+ def run(first_value)
394
+ Kernel.raise first_value if first_value.is_a?(Exception)
395
+ @running = true
396
+
397
+ Thread.backend.trace(:unblock, self, first_value, @caller)
398
+ result = @block.(first_value)
399
+ finalize(result)
400
+ rescue Polyphony::Restart => e
401
+ restart_self(e.value)
402
+ rescue Polyphony::MoveOn, Polyphony::Terminate => e
403
+ finalize(e.value)
404
+ rescue Exception => e
405
+ e.source_fiber = self
406
+ finalize(e, true)
407
+ end
408
+
409
+ # Performs setup for a "raw" Fiber created using Fiber.new. Note that this
410
+ # fiber is an orphan fiber (has no parent), since we cannot control how the
411
+ # fiber terminates after it has already been created. Calling #setup_raw
412
+ # allows the fiber to be scheduled and to receive messages.
413
+ #
414
+ # @return [void]
415
+ def setup_raw
416
+ @thread = Thread.current
417
+ @running = true
418
+ end
419
+
420
+ # Sets up the fiber as the main fiber for the current thread.
421
+ #
422
+ # @return [void]
423
+ def setup_main_fiber
424
+ @main = true
425
+ @tag = :main
426
+ @thread = Thread.current
427
+ @running = true
428
+ @children&.clear
429
+ end
430
+
431
+ # Resets the fiber's state and reruns the fiber.
432
+ #
433
+ # @param first_value [Fiber] first_value to pass to fiber after restarting
434
+ # @return [void]
435
+ def restart_self(first_value)
436
+ @mailbox = nil
437
+ run(first_value)
438
+ end
439
+
440
+ # Finalizes the fiber, handling its return value or any uncaught exception.
441
+ #
442
+ # @param result [any] return value
443
+ # @param uncaught_exception [Exception, nil] uncaught exception
444
+ # @return [void]
445
+ def finalize(result, uncaught_exception = false)
446
+ result, uncaught_exception = finalize_children(result, uncaught_exception)
447
+ Thread.backend.trace(:terminate, self, result)
448
+ @result = result
449
+
450
+ inform_monitors(result, uncaught_exception)
451
+ @running = false
452
+ ensure
453
+ @parent&.remove_child(self)
454
+ # Prevent fiber from being resumed after terminating
455
+ @thread.fiber_unschedule(self)
456
+ Thread.current.switch_fiber
457
+ end
458
+
459
+ # Shuts down all children of the current fiber. If any exception occurs while
460
+ # the children are shut down, it is returned along with the uncaught_exception
461
+ # flag set. Otherwise, it returns the given arguments.
462
+ #
463
+ # @param result [any] fiber's return value
464
+ # @param uncaught_exception [Exception, nil] uncaught exception
465
+ # @return [void]
466
+ def finalize_children(result, uncaught_exception)
467
+ shutdown_all_children(graceful_shutdown?)
468
+ [result, uncaught_exception]
469
+ rescue Exception => e
470
+ [e, true]
471
+ end
472
+
473
+ # Informs the fiber's monitors it is terminated.
474
+ #
475
+ # @param result [any] fiber's return value
476
+ # @param uncaught_exception [Exception, nil] uncaught exception
477
+ # @return [void]
478
+ def inform_monitors(result, uncaught_exception)
479
+ if @monitors
480
+ msg = [self, result]
481
+ @monitors.each_key { |f| f.monitor_mailbox << msg }
482
+ end
483
+
484
+ if uncaught_exception && @parent
485
+ parent_is_monitor = @monitors&.has_key?(@parent)
486
+ @parent.schedule_with_priority(result) unless parent_is_monitor
217
487
  end
218
488
  end
219
489
 
220
- # Fiber control class methods
221
- module FiberControlClassMethods
222
-
223
- # call-seq:
224
- # Fiber.await(*fibers) -> [*results]
225
- # Fiber.join(*fibers) -> [*results]
226
- #
490
+ # Adds a fiber to the list of monitoring fibers. Monitoring fibers will be
491
+ # notified on their monitor mailboxes when the fiber is terminated.
492
+ #
493
+ # @param fiber [Fiber] monitoring fiber
494
+ # @return [Fiber] self
495
+ def monitor(fiber)
496
+ (@monitors ||= {})[fiber] = true
497
+ self
498
+ end
499
+
500
+ # Removes a monitor fiber.
501
+ #
502
+ # @param fiber [Fiber] monitoring fiber
503
+ # @return [Fiber] self
504
+ def unmonitor(fiber)
505
+ (@monitors ||= []).delete(fiber)
506
+ self
507
+ end
508
+
509
+ # Returns the list of monitoring fibers.
510
+ #
511
+ # @return [Array<Fiber>] monitoring fibers
512
+ def monitors
513
+ @monitors&.keys || []
514
+ end
515
+
516
+ # Returns true if the fiber is dead.
517
+ #
518
+ # @return [bool] is fiber dead
519
+ def dead?
520
+ state == :dead
521
+ end
522
+
523
+ class << self
227
524
  # Waits for all given fibers to terminate, then returns the respective
228
525
  # return values for all terminated fibers. If any of the awaited fibers
229
526
  # terminates with an uncaught exception, `Fiber.await` will await all the
@@ -302,7 +599,6 @@ module Polyphony
302
599
  # also be scheduled with priority. This method is mainly used trapping
303
600
  # signals (see also the patched `Kernel#trap`)
304
601
  #
305
- # @yield [] given block
306
602
  # @return [void]
307
603
  def schedule_priority_oob_fiber(&block)
308
604
  oob_fiber = Fiber.new do
@@ -333,363 +629,34 @@ module Polyphony
333
629
  end
334
630
  end
335
631
 
336
- # Child fiber control methods
337
- module ChildFiberControl
338
-
339
- # Returns the fiber's children.
340
- #
341
- # @return [Array<Fiber>] child fibers
342
- def children
343
- (@children ||= {}).keys
344
- end
345
-
346
- # Creates a new child fiber.
347
- #
348
- # child = fiber.spin { sleep 10; fiber.stop }
349
- #
350
- # @param tag [any] child fiber's tag
351
- # @param orig_caller [Array<String>] caller to set for fiber
352
- # @yield [any] child fiber's block
353
- # @return [Fiber] child fiber
354
- def spin(tag = nil, orig_caller = Kernel.caller, &block)
355
- f = Fiber.new { |v| f.run(v) }
356
- f.prepare(tag, block, orig_caller, self)
357
- (@children ||= {})[f] = true
358
- f.monitor(self) if @supervise_mode
359
- f
360
- end
361
-
362
- # Terminates all child fibers. This method will return before the fibers are
363
- # actually terminated.
364
- #
365
- # @param graceful [bool] whether to perform a graceful termination
366
- # @return [Fiber] self
367
- def terminate_all_children(graceful = false)
368
- return self unless @children
369
-
370
- e = Polyphony::Terminate.new
371
- @children.each_key do |c|
372
- c.graceful_shutdown = true if graceful
373
- c.raise e
374
- end
375
- self
376
- end
377
-
378
- # Block until all child fibers have terminated. Returns the return values
379
- # for all child fibers.
380
- #
381
- # @return [Array<any>] return values of child fibers
382
- def await_all_children
383
- return unless @children && !@children.empty?
384
-
385
- Fiber.await(*@children.keys.reject { |c| c.dead? })
386
- end
387
-
388
- # Terminates and blocks until all child fibers have terminated.
389
- #
390
- # @return [Fiber] self
391
- def shutdown_all_children(graceful = false)
392
- return self unless @children
393
-
394
- pending = []
395
- @children.keys.each do |c|
396
- next if c.dead?
397
-
398
- c.terminate(graceful)
399
- pending << c
400
- end
401
- Fiber.await(*pending)
402
- self
403
- end
404
-
405
- # Attaches all child fibers to a new parent.
406
- #
407
- # @param parent [Fiber] new parent
408
- # @return [Fiber] self
409
- def attach_all_children_to(parent)
410
- @children&.keys.each { |c| c.attach_to(parent) }
411
- self
412
- end
413
-
414
- # Detaches the fiber from its current parent. The fiber will be made a child
415
- # of the main fiber (for the current thread.)
416
- #
417
- # @return [Fiber] self
418
- def detach
419
- @parent.remove_child(self)
420
- @parent = @thread.main_fiber
421
- @parent.add_child(self)
422
- self
423
- end
424
-
425
- # Attaches the fiber to a new parent.
426
- #
427
- # @param parent [Fiber] new parent
428
- # @return [Fiber] self
429
- def attach_to(parent)
430
- @parent.remove_child(self)
431
- @parent = parent
432
- parent.add_child(self)
433
- self
434
- end
435
-
436
- # Attaches the fiber to the new parent and monitors the new parent.
437
- #
438
- # @param parent [Fiber] new parent
439
- # @return [Fiber] self
440
- def attach_and_monitor(parent)
441
- @parent.remove_child(self)
442
- @parent = parent
443
- parent.add_child(self)
444
- monitor(parent)
445
- self
446
- end
447
-
448
- # Adds a child fiber reference. Used internally.
449
- #
450
- # @param child_fiber [Fiber] child fiber
451
- # @return [Fiber] self
452
- def add_child(child_fiber)
453
- (@children ||= {})[child_fiber] = true
454
- child_fiber.monitor(self) if @supervise_mode
455
- self
456
- end
457
-
458
- # Removes a child fiber reference. Used internally.
459
- #
460
- # @param child_fiber [Fiber] child fiber to be removed
461
- # @return [Fiber] self
462
- def remove_child(child_fiber)
463
- @children.delete(child_fiber) if @children
464
- self
465
- end
466
- end
467
-
468
- # Fiber life cycle methods
469
- module FiberLifeCycle
470
-
471
- # Prepares a fiber for running.
472
- #
473
- # @param tag [any] fiber's tag
474
- # @param block [Proc] fiber's block
475
- # @param caller [Array<String>] fiber's caller
476
- # @param parent [Fiber] fiber's parent
477
- # @return [void]
478
- def prepare(tag, block, caller, parent)
479
- @thread = Thread.current
480
- @tag = tag
481
- @parent = parent
482
- @caller = caller
483
- @block = block
484
- Thread.backend.trace(:spin, self, Kernel.caller[1..-1])
485
- schedule
486
- end
487
-
488
- # Runs the fiber's block and handles uncaught exceptions.
489
- #
490
- # @param first_value [any] value passed to fiber on first resume
491
- # @return [void]
492
- def run(first_value)
493
- Kernel.raise first_value if first_value.is_a?(Exception)
494
- @running = true
495
-
496
- Thread.backend.trace(:unblock, self, first_value, @caller)
497
- result = @block.(first_value)
498
- finalize(result)
499
- rescue Polyphony::Restart => e
500
- restart_self(e.value)
501
- rescue Polyphony::MoveOn, Polyphony::Terminate => e
502
- finalize(e.value)
503
- rescue Exception => e
504
- e.source_fiber = self
505
- finalize(e, true)
506
- end
507
-
508
- # Performs setup for a "raw" Fiber created using Fiber.new. Note that this
509
- # fiber is an orphan fiber (has no parent), since we cannot control how the
510
- # fiber terminates after it has already been created. Calling #setup_raw
511
- # allows the fiber to be scheduled and to receive messages.
512
- #
513
- # @return [void]
514
- def setup_raw
515
- @thread = Thread.current
516
- @running = true
517
- end
518
-
519
- # Sets up the fiber as the main fiber for the current thread.
520
- #
521
- # @return [void]
522
- def setup_main_fiber
523
- @main = true
524
- @tag = :main
525
- @thread = Thread.current
526
- @running = true
527
- @children&.clear
528
- end
529
-
530
- # Resets the fiber's state and reruns the fiber.
531
- #
532
- # @param first_value [Fiber] first_value to pass to fiber after restarting
533
- # @return [void]
534
- def restart_self(first_value)
535
- @mailbox = nil
536
- run(first_value)
537
- end
538
-
539
- # Finalizes the fiber, handling its return value or any uncaught exception.
540
- #
541
- # @param result [any] return value
542
- # @param uncaught_exception [Exception, nil] uncaught exception
543
- # @return [void]
544
- def finalize(result, uncaught_exception = false)
545
- result, uncaught_exception = finalize_children(result, uncaught_exception)
546
- Thread.backend.trace(:terminate, self, result)
547
- @result = result
548
-
549
- inform_monitors(result, uncaught_exception)
550
- @running = false
551
- ensure
552
- @parent&.remove_child(self)
553
- # Prevent fiber from being resumed after terminating
554
- @thread.fiber_unschedule(self)
555
- Thread.current.switch_fiber
556
- end
557
-
558
- # Shuts down all children of the current fiber. If any exception occurs while
559
- # the children are shut down, it is returned along with the uncaught_exception
560
- # flag set. Otherwise, it returns the given arguments.
561
- #
562
- # @param result [any] fiber's return value
563
- # @param uncaught_exception [Exception, nil] uncaught exception
564
- # @return [void]
565
- def finalize_children(result, uncaught_exception)
566
- shutdown_all_children(graceful_shutdown?)
567
- [result, uncaught_exception]
568
- rescue Exception => e
569
- [e, true]
570
- end
571
-
572
- # Informs the fiber's monitors it is terminated.
573
- #
574
- # @param result [any] fiber's return value
575
- # @param uncaught_exception [Exception, nil] uncaught exception
576
- # @return [void]
577
- def inform_monitors(result, uncaught_exception)
578
- if @monitors
579
- msg = [self, result]
580
- @monitors.each_key { |f| f.monitor_mailbox << msg }
581
- end
582
-
583
- if uncaught_exception && @parent
584
- parent_is_monitor = @monitors&.has_key?(@parent)
585
- @parent.schedule_with_priority(result) unless parent_is_monitor
586
- end
587
- end
632
+ private
588
633
 
589
- # Adds a fiber to the list of monitoring fibers. Monitoring fibers will be
590
- # notified on their monitor mailboxes when the fiber is terminated.
591
- #
592
- # @param fiber [Fiber] monitoring fiber
593
- # @return [Fiber] self
594
- def monitor(fiber)
595
- (@monitors ||= {})[fiber] = true
596
- self
597
- end
598
-
599
- # Removes a monitor fiber.
600
- #
601
- # @param fiber [Fiber] monitoring fiber
602
- # @return [Fiber] self
603
- def unmonitor(fiber)
604
- (@monitors ||= []).delete(fiber)
605
- self
606
- end
607
-
608
- # Returns the list of monitoring fibers.
609
- #
610
- # @return [Array<Fiber>] monitoring fibers
611
- def monitors
612
- @monitors&.keys || []
613
- end
614
-
615
- # Returns true if the fiber is dead.
616
- #
617
- # @return [bool] is fiber dead
618
- def dead?
619
- state == :dead
634
+ # @!visibility private
635
+ def error_from_raise_args(args)
636
+ case (arg = args.shift)
637
+ when String then RuntimeError.new(arg)
638
+ when Class then arg.new(args.shift)
639
+ when Exception then arg
640
+ else RuntimeError.new
620
641
  end
621
642
  end
622
- end
623
643
 
624
- # Fiber extensions
625
- class ::Fiber
626
- prepend Polyphony::FiberControl
627
- include Polyphony::FiberSupervision
628
- include Polyphony::ChildFiberControl
629
- include Polyphony::FiberLifeCycle
644
+ # @!visibility private
645
+ def supervise_opts_to_block(opts)
646
+ block = opts[:on_done] || opts[:on_error]
647
+ restart = opts[:restart]
648
+ return nil unless block || restart
630
649
 
631
- extend Polyphony::FiberControlClassMethods
650
+ error_only = !!opts[:on_error]
651
+ restart_always = (restart == :always) || (restart == true)
652
+ restart_on_error = restart == :on_error
632
653
 
633
- attr_accessor :tag, :thread, :parent, :oob
634
- attr_reader :result
635
-
636
- # Returns true if fiber is running.
637
- #
638
- # @return [bool] is fiber running
639
- def running?
640
- @running
641
- end
642
-
643
- # Returns a string representation of the fiber for debugging.
644
- #
645
- # @return [String] string representation
646
- def inspect
647
- if @tag
648
- "#<Fiber #{tag}:#{object_id} #{location} (#{state})>"
649
- else
650
- "#<Fiber:#{object_id} #{location} (#{state})>"
651
- end
652
- end
653
- alias_method :to_s, :inspect
654
-
655
- # Returns the source location for the fiber based on its caller.
656
- #
657
- # @return [String] source location
658
- def location
659
- if @oob
660
- "#{@caller[0]} (oob)"
661
- else
662
- @caller ? @caller[0] : '(root)'
663
- end
664
- end
665
-
666
- # Returns the fiber's caller.
667
- #
668
- # @return [Array<String>] caller
669
- def caller
670
- spin_caller = @caller || []
671
- if @parent
672
- spin_caller + @parent.caller
673
- else
674
- spin_caller
654
+ ->(f, r) do
655
+ is_error = r.is_a?(Exception)
656
+ block.(f, r) if block && (!error_only || is_error)
657
+ f.restart if restart_always || (restart_on_error && is_error)
675
658
  end
676
659
  end
677
-
678
- # Sets the fiber's caller.
679
- #
680
- # @param caller [Array<String>] new caller
681
- # @return [Fiber] self
682
- def set_caller(caller)
683
- @caller = caller
684
- self
685
- end
686
-
687
- # Returns true if the fiber is the main fiber for its thread.
688
- #
689
- # @return [bool] is main fiber
690
- def main?
691
- @main
692
- end
693
660
  end
694
661
 
695
662
  Fiber.current.setup_main_fiber