polyphony 0.99.4 → 0.99.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +10 -0
  3. data/examples/pipes/gzip_http_server.rb +2 -2
  4. data/examples/pipes/http_server.rb +1 -1
  5. data/examples/pipes/tcp_proxy.rb +1 -1
  6. data/ext/polyphony/backend_common.c +4 -4
  7. data/ext/polyphony/backend_io_uring.c +8 -8
  8. data/ext/polyphony/backend_libev.c +5 -5
  9. data/ext/polyphony/fiber.c +32 -41
  10. data/ext/polyphony/io_extensions.c +50 -37
  11. data/ext/polyphony/pipe.c +6 -18
  12. data/ext/polyphony/polyphony.c +63 -133
  13. data/ext/polyphony/queue.c +25 -63
  14. data/ext/polyphony/thread.c +3 -12
  15. data/lib/polyphony/adapters/process.rb +1 -2
  16. data/lib/polyphony/adapters/sequel.rb +2 -2
  17. data/lib/polyphony/core/debug.rb +1 -1
  18. data/lib/polyphony/core/exceptions.rb +1 -1
  19. data/lib/polyphony/core/global_api.rb +24 -38
  20. data/lib/polyphony/core/resource_pool.rb +7 -8
  21. data/lib/polyphony/core/sync.rb +1 -2
  22. data/lib/polyphony/core/thread_pool.rb +2 -5
  23. data/lib/polyphony/core/throttler.rb +1 -5
  24. data/lib/polyphony/core/timer.rb +24 -25
  25. data/lib/polyphony/extensions/fiber.rb +507 -540
  26. data/lib/polyphony/extensions/io.rb +3 -12
  27. data/lib/polyphony/extensions/openssl.rb +2 -23
  28. data/lib/polyphony/extensions/pipe.rb +4 -15
  29. data/lib/polyphony/extensions/socket.rb +15 -109
  30. data/lib/polyphony/extensions/thread.rb +0 -13
  31. data/lib/polyphony/extensions/timeout.rb +0 -1
  32. data/lib/polyphony/net.rb +5 -8
  33. data/lib/polyphony/version.rb +1 -1
  34. data/lib/polyphony.rb +2 -6
  35. data/test/test_io.rb +221 -221
  36. data/test/test_socket.rb +3 -3
  37. data/test/test_trace.rb +2 -2
  38. metadata +1 -1
@@ -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