polyphony 0.99.4 → 0.99.6

Sign up to get free protection for your applications and to get access to all the features.
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