polyphony 0.99.4 → 0.99.6

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +11 -0
  3. data/.yardopts +0 -2
  4. data/README.md +1 -1
  5. data/docs/readme.md +1 -1
  6. data/docs/tutorial.md +2 -2
  7. data/examples/pipes/gzip_http_server.rb +2 -2
  8. data/examples/pipes/http_server.rb +1 -1
  9. data/examples/pipes/tcp_proxy.rb +1 -1
  10. data/ext/polyphony/backend_common.c +4 -4
  11. data/ext/polyphony/backend_io_uring.c +8 -8
  12. data/ext/polyphony/backend_libev.c +5 -5
  13. data/ext/polyphony/fiber.c +33 -42
  14. data/ext/polyphony/io_extensions.c +50 -37
  15. data/ext/polyphony/pipe.c +6 -20
  16. data/ext/polyphony/polyphony.c +72 -144
  17. data/ext/polyphony/queue.c +23 -63
  18. data/ext/polyphony/thread.c +4 -13
  19. data/lib/polyphony/adapters/process.rb +2 -5
  20. data/lib/polyphony/adapters/sequel.rb +2 -2
  21. data/lib/polyphony/core/debug.rb +1 -4
  22. data/lib/polyphony/core/exceptions.rb +1 -5
  23. data/lib/polyphony/core/resource_pool.rb +7 -8
  24. data/lib/polyphony/core/sync.rb +5 -8
  25. data/lib/polyphony/core/thread_pool.rb +3 -10
  26. data/lib/polyphony/core/throttler.rb +1 -5
  27. data/lib/polyphony/core/timer.rb +23 -30
  28. data/lib/polyphony/extensions/fiber.rb +513 -543
  29. data/lib/polyphony/extensions/io.rb +5 -14
  30. data/lib/polyphony/extensions/object.rb +283 -2
  31. data/lib/polyphony/extensions/openssl.rb +5 -26
  32. data/lib/polyphony/extensions/pipe.rb +6 -17
  33. data/lib/polyphony/extensions/socket.rb +24 -118
  34. data/lib/polyphony/extensions/thread.rb +3 -18
  35. data/lib/polyphony/extensions/timeout.rb +0 -1
  36. data/lib/polyphony/net.rb +5 -9
  37. data/lib/polyphony/version.rb +1 -1
  38. data/lib/polyphony.rb +2 -6
  39. data/test/test_io.rb +221 -221
  40. data/test/test_socket.rb +3 -3
  41. data/test/test_trace.rb +2 -2
  42. metadata +5 -9
  43. data/docs/index.md +0 -94
  44. data/docs/link_rewriter.rb +0 -17
  45. data/docs/main-concepts/index.md +0 -9
  46. data/lib/polyphony/core/global_api.rb +0 -309
  47. /data/{assets → docs/assets}/echo-fibers.svg +0 -0
  48. /data/{assets → docs/assets}/polyphony-logo.png +0 -0
  49. /data/{assets → docs/assets}/sleeping-fiber.svg +0 -0
@@ -2,228 +2,530 @@
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
+ def supervise(*fibers, **opts, &block)
217
+ block ||= supervise_opts_to_block(opts)
218
+
219
+ @supervise_mode = true
220
+ fibers = children if fibers.empty?
221
+ fibers.each do |f|
222
+ f.attach_to(self) unless f.parent == self
223
+ f.monitor(self)
123
224
  end
124
225
 
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
226
+ mailbox = monitor_mailbox
227
+
228
+ while true
229
+ (fiber, result) = mailbox.shift
230
+ block&.call(fiber, result)
130
231
  end
131
- alias_method :join, :await
232
+ ensure
233
+ @supervise_mode = false
234
+ end
132
235
 
133
- private
236
+ ###############################
237
+ # Child fiber control methods #
238
+ ###############################
134
239
 
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
240
+ # Returns the fiber's children.
241
+ #
242
+ # @return [Array<Fiber>] child fibers
243
+ def children
244
+ (@children ||= {}).keys
245
+ end
246
+
247
+ # Creates a new child fiber.
248
+ #
249
+ # child = fiber.spin { sleep 10; fiber.stop }
250
+ #
251
+ # @param tag [any] child fiber's tag
252
+ # @param orig_caller [Array<String>] caller to set for fiber
253
+ # @return [Fiber] child fiber
254
+ def spin(tag = nil, orig_caller = Kernel.caller, &block)
255
+ f = Fiber.new { |v| f.run(v) }
256
+ f.prepare(tag, block, orig_caller, self)
257
+ (@children ||= {})[f] = true
258
+ f.monitor(self) if @supervise_mode
259
+ f
260
+ end
261
+
262
+ # Terminates all child fibers. This method will return before the fibers are
263
+ # actually terminated.
264
+ #
265
+ # @param graceful [bool] whether to perform a graceful termination
266
+ # @return [Fiber] self
267
+ def terminate_all_children(graceful = false)
268
+ return self unless @children
269
+
270
+ e = Polyphony::Terminate.new
271
+ @children.each_key do |c|
272
+ c.graceful_shutdown = true if graceful
273
+ c.raise e
143
274
  end
275
+ self
144
276
  end
145
277
 
146
- # Fiber supervision methods
147
- module FiberSupervision
278
+ # Block until all child fibers have terminated. Returns the return values
279
+ # for all child fibers.
280
+ #
281
+ # @return [Array<any>] return values of child fibers
282
+ def await_all_children
283
+ return unless @children && !@children.empty?
148
284
 
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)
182
-
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
285
+ Fiber.await(*@children.keys.reject { |c| c.dead? })
286
+ end
189
287
 
190
- mailbox = monitor_mailbox
288
+ # Terminates and blocks until all child fibers have terminated.
289
+ #
290
+ # @return [Fiber] self
291
+ def shutdown_all_children(graceful = false)
292
+ return self unless @children
191
293
 
192
- while true
193
- (fiber, result) = mailbox.shift
194
- block&.call(fiber, result)
195
- end
196
- ensure
197
- @supervise_mode = false
294
+ pending = []
295
+ @children.keys.each do |c|
296
+ next if c.dead?
297
+
298
+ c.terminate(graceful)
299
+ pending << c
198
300
  end
301
+ Fiber.await(*pending)
302
+ self
303
+ end
199
304
 
200
- private
305
+ # Attaches all child fibers to a new parent.
306
+ #
307
+ # @param parent [Fiber] new parent
308
+ # @return [Fiber] self
309
+ def attach_all_children_to(parent)
310
+ @children&.keys.each { |c| c.attach_to(parent) }
311
+ self
312
+ end
201
313
 
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
314
+ # Detaches the fiber from its current parent. The fiber will be made a child
315
+ # of the main fiber (for the current thread.)
316
+ #
317
+ # @return [Fiber] self
318
+ def detach
319
+ @parent.remove_child(self)
320
+ @parent = @thread.main_fiber
321
+ @parent.add_child(self)
322
+ self
323
+ end
324
+
325
+ # Attaches the fiber to a new parent.
326
+ #
327
+ # @param parent [Fiber] new parent
328
+ # @return [Fiber] self
329
+ def attach_to(parent)
330
+ @parent.remove_child(self)
331
+ @parent = parent
332
+ parent.add_child(self)
333
+ self
334
+ end
335
+
336
+ # Attaches the fiber to the new parent and monitors the new parent.
337
+ #
338
+ # @param parent [Fiber] new parent
339
+ # @return [Fiber] self
340
+ def attach_and_monitor(parent)
341
+ @parent.remove_child(self)
342
+ @parent = parent
343
+ parent.add_child(self)
344
+ monitor(parent)
345
+ self
346
+ end
347
+
348
+ # Adds a child fiber reference. Used internally.
349
+ #
350
+ # @param child_fiber [Fiber] child fiber
351
+ # @return [Fiber] self
352
+ def add_child(child_fiber)
353
+ (@children ||= {})[child_fiber] = true
354
+ child_fiber.monitor(self) if @supervise_mode
355
+ self
356
+ end
357
+
358
+ # Removes a child fiber reference. Used internally.
359
+ #
360
+ # @param child_fiber [Fiber] child fiber to be removed
361
+ # @return [Fiber] self
362
+ def remove_child(child_fiber)
363
+ @children.delete(child_fiber) if @children
364
+ self
365
+ end
366
+
367
+ ############################
368
+ # Fiber life cycle methods #
369
+ ############################
370
+
371
+ # Prepares a fiber for running.
372
+ #
373
+ # @param tag [any] fiber's tag
374
+ # @param block [Proc] fiber's block
375
+ # @param caller [Array<String>] fiber's caller
376
+ # @param parent [Fiber] fiber's parent
377
+ # @return [Fiber] self
378
+ def prepare(tag, block, caller, parent)
379
+ @thread = Thread.current
380
+ @tag = tag
381
+ @parent = parent
382
+ @caller = caller
383
+ @block = block
384
+ Thread.backend.trace(:spin, self, Kernel.caller[1..-1])
385
+ schedule
386
+ self
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 [any] fiber result
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
+ result
401
+ rescue Polyphony::Restart => e
402
+ restart_self(e.value)
403
+ rescue Polyphony::MoveOn, Polyphony::Terminate => e
404
+ finalize(e.value)
405
+ rescue Exception => e
406
+ e.source_fiber = self
407
+ finalize(e, true)
408
+ end
409
+
410
+ # Performs setup for a "raw" Fiber created using Fiber.new. Note that this
411
+ # fiber is an orphan fiber (has no parent), since we cannot control how the
412
+ # fiber terminates after it has already been created. Calling #setup_raw
413
+ # allows the fiber to be scheduled and to receive messages.
414
+ #
415
+ # @return [Fiber] self
416
+ def setup_raw
417
+ @thread = Thread.current
418
+ @running = true
419
+ self
420
+ end
421
+
422
+ # Sets up the fiber as the main fiber for the current thread.
423
+ #
424
+ # @return [Fiber] self
425
+ def setup_main_fiber
426
+ @main = true
427
+ @tag = :main
428
+ @thread = Thread.current
429
+ @running = true
430
+ @children&.clear
431
+ self
432
+ end
433
+
434
+ # Resets the fiber's state and reruns the fiber.
435
+ #
436
+ # @param first_value [Fiber] first_value to pass to fiber after restarting
437
+ # @return [any] fiber result
438
+ def restart_self(first_value)
439
+ @mailbox = nil
440
+ run(first_value)
441
+ end
442
+
443
+ # Finalizes the fiber, handling its return value or any uncaught exception.
444
+ #
445
+ # @param result [any] return value
446
+ # @param uncaught_exception [Exception, nil] uncaught exception
447
+ # @return [false]
448
+ def finalize(result, uncaught_exception = false)
449
+ result, uncaught_exception = finalize_children(result, uncaught_exception)
450
+ Thread.backend.trace(:terminate, self, result)
451
+ @result = result
452
+
453
+ inform_monitors(result, uncaught_exception)
454
+ @running = false
455
+ ensure
456
+ @parent&.remove_child(self)
457
+ # Prevent fiber from being resumed after terminating
458
+ @thread.fiber_unschedule(self)
459
+ Thread.current.switch_fiber
460
+ end
461
+
462
+ # Shuts down all children of the current fiber. If any exception occurs while
463
+ # the children are shut down, it is returned along with the uncaught_exception
464
+ # flag set. Otherwise, it returns the given arguments.
465
+ #
466
+ # @param result [any] fiber's return value
467
+ # @param uncaught_exception [Exception, nil] uncaught exception
468
+ # @return [Array] array containing result and uncaught exception if any
469
+ def finalize_children(result, uncaught_exception)
470
+ shutdown_all_children(graceful_shutdown?)
471
+ [result, uncaught_exception]
472
+ rescue Exception => e
473
+ [e, true]
474
+ end
475
+
476
+ # Informs the fiber's monitors it is terminated.
477
+ #
478
+ # @param result [any] fiber's return value
479
+ # @param uncaught_exception [Exception, nil] uncaught exception
480
+ # @return [Fiber] self
481
+ def inform_monitors(result, uncaught_exception)
482
+ if @monitors
483
+ msg = [self, result]
484
+ @monitors.each_key { |f| f.monitor_mailbox << msg }
485
+ end
486
+
487
+ if uncaught_exception && @parent
488
+ parent_is_monitor = @monitors&.has_key?(@parent)
489
+ @parent.schedule_with_priority(result) unless parent_is_monitor
217
490
  end
491
+
492
+ self
218
493
  end
219
494
 
220
- # Fiber control class methods
221
- module FiberControlClassMethods
222
-
223
- # call-seq:
224
- # Fiber.await(*fibers) -> [*results]
225
- # Fiber.join(*fibers) -> [*results]
226
- #
495
+ # Adds a fiber to the list of monitoring fibers. Monitoring fibers will be
496
+ # notified on their monitor mailboxes when the fiber is terminated.
497
+ #
498
+ # @param fiber [Fiber] monitoring fiber
499
+ # @return [Fiber] self
500
+ def monitor(fiber)
501
+ (@monitors ||= {})[fiber] = true
502
+ self
503
+ end
504
+
505
+ # Removes a monitor fiber.
506
+ #
507
+ # @param fiber [Fiber] monitoring fiber
508
+ # @return [Fiber] self
509
+ def unmonitor(fiber)
510
+ (@monitors ||= []).delete(fiber)
511
+ self
512
+ end
513
+
514
+ # Returns the list of monitoring fibers.
515
+ #
516
+ # @return [Array<Fiber>] monitoring fibers
517
+ def monitors
518
+ @monitors&.keys || []
519
+ end
520
+
521
+ # Returns true if the fiber is dead.
522
+ #
523
+ # @return [bool] is fiber dead
524
+ def dead?
525
+ state == :dead
526
+ end
527
+
528
+ class << self
227
529
  # Waits for all given fibers to terminate, then returns the respective
228
530
  # return values for all terminated fibers. If any of the awaited fibers
229
531
  # terminates with an uncaught exception, `Fiber.await` will await all the
@@ -301,9 +603,6 @@ module Polyphony
301
603
  # running, it will bubble up to the main thread's main fiber, which will
302
604
  # also be scheduled with priority. This method is mainly used trapping
303
605
  # signals (see also the patched `Kernel#trap`)
304
- #
305
- # @yield [] given block
306
- # @return [void]
307
606
  def schedule_priority_oob_fiber(&block)
308
607
  oob_fiber = Fiber.new do
309
608
  Fiber.current.setup_raw
@@ -333,362 +632,33 @@ module Polyphony
333
632
  end
334
633
  end
335
634
 
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
635
+ private
404
636
 
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
637
+ # @!visibility private
638
+ def error_from_raise_args(args)
639
+ case (arg = args.shift)
640
+ when String then RuntimeError.new(arg)
641
+ when Class then arg.new(args.shift)
642
+ when Exception then arg
643
+ else RuntimeError.new
465
644
  end
466
645
  end
467
646
 
468
- # Fiber life cycle methods
469
- module FiberLifeCycle
647
+ # @!visibility private
648
+ def supervise_opts_to_block(opts)
649
+ block = opts[:on_done] || opts[:on_error]
650
+ restart = opts[:restart]
651
+ return nil unless block || restart
470
652
 
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
653
+ error_only = !!opts[:on_error]
654
+ restart_always = (restart == :always) || (restart == true)
655
+ restart_on_error = restart == :on_error
487
656
 
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
657
+ ->(f, r) do
658
+ is_error = r.is_a?(Exception)
659
+ block.(f, r) if block && (!error_only || is_error)
660
+ f.restart if restart_always || (restart_on_error && is_error)
528
661
  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
588
-
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
620
- end
621
- end
622
- end
623
-
624
- # Fiber extensions
625
- class ::Fiber
626
- prepend Polyphony::FiberControl
627
- include Polyphony::FiberSupervision
628
- include Polyphony::ChildFiberControl
629
- include Polyphony::FiberLifeCycle
630
-
631
- extend Polyphony::FiberControlClassMethods
632
-
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
675
- end
676
- 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
662
  end
693
663
  end
694
664