polyphony 0.29 → 0.30

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/.gitignore +1 -0
  3. data/CHANGELOG.md +14 -0
  4. data/Gemfile.lock +1 -1
  5. data/TODO.md +15 -10
  6. data/docs/getting-started/tutorial.md +3 -3
  7. data/docs/index.md +2 -3
  8. data/docs/{technical-overview → main-concepts}/concurrency.md +62 -15
  9. data/docs/{technical-overview → main-concepts}/design-principles.md +21 -8
  10. data/docs/{technical-overview → main-concepts}/exception-handling.md +80 -38
  11. data/docs/{technical-overview → main-concepts}/extending.md +4 -3
  12. data/docs/{technical-overview → main-concepts}/fiber-scheduling.md +3 -3
  13. data/docs/{technical-overview.md → main-concepts.md} +2 -2
  14. data/examples/core/xx-at_exit.rb +29 -0
  15. data/examples/core/xx-fork-terminate.rb +27 -0
  16. data/examples/core/xx-pingpong.rb +18 -0
  17. data/examples/core/xx-stop.rb +20 -0
  18. data/ext/gyro/async.c +1 -1
  19. data/ext/gyro/extconf.rb +0 -3
  20. data/ext/gyro/gyro.c +7 -8
  21. data/ext/gyro/gyro.h +2 -0
  22. data/ext/gyro/queue.c +6 -6
  23. data/ext/gyro/selector.c +32 -1
  24. data/ext/gyro/thread.c +55 -9
  25. data/ext/gyro/timer.c +1 -0
  26. data/lib/polyphony/core/exceptions.rb +4 -1
  27. data/lib/polyphony/core/global_api.rb +1 -6
  28. data/lib/polyphony/core/thread_pool.rb +3 -3
  29. data/lib/polyphony/extensions/core.rb +7 -1
  30. data/lib/polyphony/extensions/fiber.rb +159 -72
  31. data/lib/polyphony/extensions/io.rb +2 -4
  32. data/lib/polyphony/extensions/openssl.rb +0 -17
  33. data/lib/polyphony/extensions/thread.rb +46 -22
  34. data/lib/polyphony/version.rb +1 -1
  35. data/lib/polyphony.rb +20 -18
  36. data/test/coverage.rb +1 -1
  37. data/test/helper.rb +7 -3
  38. data/test/test_fiber.rb +285 -72
  39. data/test/test_global_api.rb +7 -52
  40. data/test/test_io.rb +8 -0
  41. data/test/test_signal.rb +1 -0
  42. data/test/test_thread.rb +76 -56
  43. data/test/test_thread_pool.rb +27 -5
  44. data/test/test_throttler.rb +1 -0
  45. metadata +12 -12
  46. data/lib/polyphony/core/supervisor.rb +0 -114
  47. data/lib/polyphony/line_reader.rb +0 -82
  48. data/test/test_gyro.rb +0 -25
  49. data/test/test_supervisor.rb +0 -180
@@ -11,10 +11,12 @@ module FiberControl
11
11
  return @result.is_a?(Exception) ? (Kernel.raise @result) : @result
12
12
  end
13
13
 
14
- @waiting_fiber = Fiber.current
14
+ fiber = Fiber.current
15
+ @waiting_fibers ||= {}
16
+ @waiting_fibers[fiber] = true
15
17
  suspend
16
18
  ensure
17
- @waiting_fiber = nil
19
+ @waiting_fibers&.delete(fiber)
18
20
  end
19
21
  alias_method :join, :await
20
22
 
@@ -22,7 +24,6 @@ module FiberControl
22
24
  return if @running == false
23
25
 
24
26
  schedule Exceptions::MoveOn.new(nil, value)
25
- snooze
26
27
  end
27
28
  alias_method :stop, :interrupt
28
29
 
@@ -30,13 +31,17 @@ module FiberControl
30
31
  return if @running == false
31
32
 
32
33
  schedule Exceptions::Cancel.new
33
- snooze
34
+ end
35
+
36
+ def terminate
37
+ return if @running == false
38
+
39
+ schedule Exceptions::Terminate.new
34
40
  end
35
41
 
36
42
  def raise(*args)
37
43
  error = error_from_raise_args(args)
38
44
  schedule(error)
39
- snooze
40
45
  end
41
46
 
42
47
  def error_from_raise_args(args)
@@ -49,123 +54,196 @@ module FiberControl
49
54
  end
50
55
  end
51
56
 
52
- # Messaging functionality
53
- module FiberMessaging
54
- def <<(value)
55
- if @receive_waiting && @running
56
- schedule value
57
- else
58
- @queued_messages ||= Gyro::Queue.new
59
- @queued_messages << value
57
+ # Class methods for controlling fibers (namely await and select)
58
+ module FiberControlClassMethods
59
+ def await(*fibers)
60
+ return [] if fibers.empty?
61
+
62
+ state = setup_await_select_state(fibers)
63
+ await_setup_monitoring(fibers, state)
64
+ suspend
65
+ fibers.map(&:result)
66
+ ensure
67
+ await_select_cleanup(state)
68
+ end
69
+ alias_method :join, :await
70
+
71
+ def setup_await_select_state(fibers)
72
+ {
73
+ awaiter: Fiber.current,
74
+ pending: fibers.each_with_object({}) { |f, h| h[f] = true }
75
+ }
76
+ end
77
+
78
+ def await_setup_monitoring(fibers, state)
79
+ fibers.each do |f|
80
+ f.when_done { |r| await_fiber_done(f, r, state) }
60
81
  end
61
- snooze
62
82
  end
63
- alias_method :send, :<<
64
83
 
65
- def receive
66
- if !@queued_messages || @queued_messages&.empty?
67
- wait_for_message
68
- else
69
- value = @queued_messages.shift
70
- snooze
71
- value
84
+ def await_fiber_done(fiber, result, state)
85
+ state[:pending].delete(fiber)
86
+
87
+ if state[:cleanup]
88
+ state[:awaiter].schedule if state[:pending].empty?
89
+ elsif !state[:done] && (result.is_a?(Exception) || state[:pending].empty?)
90
+ state[:awaiter].schedule(result)
91
+ state[:done] = true
72
92
  end
73
93
  end
74
94
 
75
- def wait_for_message
76
- Gyro.ref
77
- @receive_waiting = true
95
+ def await_select_cleanup(state)
96
+ return if state[:pending].empty?
97
+
98
+ move_on = Exceptions::MoveOn.new
99
+ state[:cleanup] = true
100
+ state[:pending].each_key { |f| f.schedule(move_on) }
101
+ suspend
102
+ end
103
+
104
+ def select(*fibers)
105
+ state = setup_await_select_state(fibers)
106
+ select_setup_monitoring(fibers, state)
78
107
  suspend
79
108
  ensure
80
- Gyro.unref
81
- @receive_waiting = nil
109
+ await_select_cleanup(state)
110
+ end
111
+
112
+ def select_setup_monitoring(fibers, state)
113
+ fibers.each do |f|
114
+ f.when_done { |r| select_fiber_done(f, r, state) }
115
+ end
116
+ end
117
+
118
+ def select_fiber_done(fiber, result, state)
119
+ state[:pending].delete(fiber)
120
+ if state[:cleanup]
121
+ # in cleanup mode the selector is resumed if no more pending fibers
122
+ state[:awaiter].schedule if state[:pending].empty?
123
+ elsif !state[:selected]
124
+ # first fiber to complete, we schedule the result
125
+ state[:awaiter].schedule([fiber, result])
126
+ state[:selected] = true
127
+ end
82
128
  end
83
129
  end
84
130
 
85
- # Fiber extensions
86
- class ::Fiber
87
- prepend FiberControl
88
- include FiberMessaging
131
+ # Messaging functionality
132
+ module FiberMessaging
133
+ def <<(value)
134
+ @mailbox << value
135
+ snooze
136
+ end
137
+ alias_method :send, :<<
89
138
 
90
- def self.reset!
91
- @running_fibers_map = { Thread.current.main_fiber => true }
139
+ def receive
140
+ @mailbox.shift
92
141
  end
142
+ end
93
143
 
94
- reset!
144
+ # Methods for controlling child fibers
145
+ module ChildFiberControl
146
+ def children
147
+ (@children ||= {}).keys
148
+ end
95
149
 
96
- def self.map
97
- @running_fibers_map
150
+ def spin(tag = nil, orig_caller = caller, &block)
151
+ f = Fiber.new { |v| f.run(v) }
152
+ f.prepare(tag, block, orig_caller)
153
+ (@children ||= {})[f] = true
154
+ f
98
155
  end
99
156
 
100
- def self.list
101
- @running_fibers_map.keys
157
+ def child_done(child_fiber)
158
+ @children.delete(child_fiber)
102
159
  end
103
160
 
104
- def self.count
105
- @running_fibers_map.size
161
+ def terminate_all_children
162
+ return unless @children
163
+
164
+ e = Exceptions::Terminate.new
165
+ @children.each_key { |c| c.raise e }
106
166
  end
107
167
 
108
- def self.spin(tag = nil, orig_caller = caller, &block)
109
- f = new { |v| f.run(v) }
110
- f.setup(tag, block, orig_caller)
111
- f
168
+ def await_all_children
169
+ return unless @children && !@children.empty?
170
+
171
+ Fiber.await(*@children.keys)
112
172
  end
173
+ end
174
+
175
+ # Fiber extensions
176
+ class ::Fiber
177
+ prepend FiberControl
178
+ include FiberMessaging
179
+ include ChildFiberControl
180
+
181
+ extend FiberControlClassMethods
113
182
 
114
183
  attr_accessor :tag, :thread
115
184
 
116
- def setup(tag, block, caller)
185
+ def prepare(tag, block, caller)
117
186
  __fiber_trace__(:fiber_create, self)
118
187
  @thread = Thread.current
119
188
  @tag = tag
120
- @calling_fiber = Fiber.current
189
+ @parent = Fiber.current
121
190
  @caller = caller
122
191
  @block = block
192
+ @mailbox = Gyro::Queue.new
123
193
  schedule
124
194
  end
125
195
 
126
196
  def setup_main_fiber
197
+ @main = true
127
198
  @tag = :main
128
199
  @thread = Thread.current
129
200
  @running = true
201
+ @children&.clear
202
+ @mailbox = Gyro::Queue.new
130
203
  end
131
204
 
132
205
  def run(first_value)
133
- Kernel.raise first_value if first_value.is_a?(Exception)
134
-
135
- start_execution(first_value)
136
- rescue ::Interrupt, ::SystemExit => e
137
- Thread.current.main_fiber.transfer e.class.new
138
- rescue ::SignalException => e
139
- Thread.current.main_fiber.transfer e
140
- rescue Exceptions::MoveOn => e
141
- finish_execution(e.value)
206
+ setup(first_value)
207
+ uncaught = nil
208
+ result = @block.(first_value)
209
+ rescue Exceptions::MoveOn, Exceptions::Terminate => e
210
+ result = e.value
142
211
  rescue Exception => e
143
- finish_execution(e, true)
212
+ result = e
213
+ uncaught = true
214
+ ensure
215
+ finalize(result, uncaught)
144
216
  end
145
217
 
146
- def start_execution(first_value)
218
+ def setup(first_value)
219
+ Kernel.raise first_value if first_value.is_a?(Exception)
220
+
147
221
  @running = true
148
- self.class.map[self] = true
149
- result = @block.(first_value)
150
- finish_execution(result)
151
222
  end
152
223
 
153
- def finish_execution(result, uncaught_exception = false)
224
+ def finalize(result, uncaught_exception = false)
225
+ terminate_all_children
226
+ await_all_children
154
227
  __fiber_trace__(:fiber_terminate, self, result)
155
228
  @result = result
156
229
  @running = false
157
- self.class.map.delete(self)
158
- @when_done&.(result)
159
- @waiting_fiber&.schedule(result)
160
- return unless uncaught_exception && !@waiting_fiber
161
-
162
- exception_receiving_fiber.schedule(result)
230
+ inform_dependants(result, uncaught_exception)
163
231
  ensure
164
232
  Thread.current.switch_fiber
165
233
  end
166
234
 
167
- def exception_receiving_fiber
168
- @calling_fiber.running? ? @calling_fiber : Thread.current.main_fiber
235
+ def inform_dependants(result, uncaught_exception)
236
+ @parent.child_done(self)
237
+ @when_done_procs&.each { |p| p.(result) }
238
+ has_waiting_fibers = nil
239
+ @waiting_fibers&.each_key do |f|
240
+ has_waiting_fibers = true
241
+ f.schedule(result)
242
+ end
243
+ return unless uncaught_exception && !has_waiting_fibers
244
+
245
+ # propagate unaught exception to parent
246
+ @parent.schedule(result)
169
247
  end
170
248
 
171
249
  attr_reader :result
@@ -175,7 +253,7 @@ class ::Fiber
175
253
  end
176
254
 
177
255
  def when_done(&block)
178
- @when_done = block
256
+ (@when_done_procs ||= []) << block
179
257
  end
180
258
 
181
259
  def inspect
@@ -189,12 +267,21 @@ class ::Fiber
189
267
 
190
268
  def caller
191
269
  spin_caller = @caller || []
192
- if @calling_fiber
193
- spin_caller + @calling_fiber.caller
270
+ if @parent
271
+ spin_caller + @parent.caller
194
272
  else
195
273
  spin_caller
196
274
  end
197
275
  end
276
+
277
+ def main?
278
+ @main
279
+ end
198
280
  end
199
281
 
200
282
  Fiber.current.setup_main_fiber
283
+
284
+ at_exit do
285
+ Fiber.current.terminate_all_children
286
+ Fiber.current.await_all_children
287
+ end
@@ -178,10 +178,8 @@ class ::IO
178
178
  end
179
179
 
180
180
  until eof?
181
- result = outbuf ? readpartial(8192, outbuf) : readpartial(8192)
182
- break unless result
183
-
184
- outbuf = result
181
+ outbuf ||= +''
182
+ outbuf << readpartial(8192)
185
183
  end
186
184
  outbuf
187
185
  end
@@ -39,23 +39,6 @@ class ::OpenSSL::SSL::SSLSocket
39
39
  # @sync = osync
40
40
  end
41
41
 
42
- # def do_write(s)
43
- # @wbuffer = "" unless defined? @wbuffer
44
- # @wbuffer << s
45
- # @wbuffer.force_encoding(Encoding::BINARY)
46
- # @sync ||= false
47
- # if @sync or @wbuffer.size > BLOCK_SIZE
48
- # until @wbuffer.empty?
49
- # begin
50
- # nwrote = syswrite(@wbuffer)
51
- # rescue Errno::EAGAIN
52
- # retry
53
- # end
54
- # @wbuffer[0, nwrote] = ""
55
- # end
56
- # end
57
- # end
58
-
59
42
  def syswrite(buf)
60
43
  read_watcher = nil
61
44
  write_watcher = nil
@@ -4,45 +4,69 @@ Exceptions = import '../core/exceptions'
4
4
 
5
5
  # Thread extensions
6
6
  class ::Thread
7
- def self.join_queue_mutex
8
- @join_queue_mutex ||= Mutex.new
9
- end
10
-
11
7
  attr_reader :main_fiber
12
8
 
13
9
  alias_method :orig_initialize, :initialize
14
10
  def initialize(*args, &block)
15
11
  @join_wait_queue = Gyro::Queue.new
12
+ @args = args
16
13
  @block = block
17
- orig_initialize do
18
- Fiber.current.setup_main_fiber
19
- setup_fiber_scheduling
20
- block.(*args)
21
- ensure
22
- signal_waiters
23
- stop_event_selector
24
- end
14
+ orig_initialize { execute }
25
15
  end
26
16
 
27
- def signal_waiters
28
- @join_wait_queue.shift_each { |w| w.signal!(self) }
17
+ def execute
18
+ setup
19
+ result = @block.(*@args)
20
+ rescue Exceptions::MoveOn, Exceptions::Terminate => e
21
+ result = e.value
22
+ rescue Exception => e
23
+ result = e
24
+ ensure
25
+ finalize(result)
29
26
  end
30
27
 
31
- alias_method :orig_join, :join
32
- def join(timeout = nil)
33
- async = Gyro::Async.new
34
- Thread.join_queue_mutex.synchronize do
35
- return unless alive?
28
+ def setup
29
+ @main_fiber = Fiber.current
30
+ @main_fiber.setup_main_fiber
31
+ setup_fiber_scheduling
32
+ end
36
33
 
37
- @join_wait_queue << async
34
+ def finalize(result)
35
+ unless Fiber.current.children.empty?
36
+ Fiber.current.terminate_all_children
37
+ Fiber.current.await_all_children
38
38
  end
39
+ signal_waiters(result)
40
+ stop_event_selector
41
+ end
39
42
 
43
+ def signal_waiters(result)
44
+ @join_wait_queue.shift_each { |w| w.signal!(result) }
45
+ end
46
+
47
+ alias_method :orig_join, :join
48
+ def join(timeout = nil)
40
49
  if timeout
41
- move_on_after(timeout) { async.await }
50
+ move_on_after(timeout) { join_perform }
42
51
  else
43
- async.await
52
+ join_perform
44
53
  end
45
54
  end
55
+ alias_method :await, :join
56
+
57
+ alias_method :orig_raise, :raise
58
+ def raise(error = nil)
59
+ Thread.pass until @main_fiber
60
+ error = RuntimeError.new if error.nil?
61
+ error = RuntimeError.new(error) if error.is_a?(String)
62
+ error = error.new if error.is_a?(Class)
63
+ @main_fiber.raise(error)
64
+ end
65
+
66
+ alias_method :orig_kill, :kill
67
+ def kill
68
+ raise Exceptions::Terminate
69
+ end
46
70
 
47
71
  alias_method :orig_inspect, :inspect
48
72
  def inspect
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.29'
4
+ VERSION = '0.30'
5
5
  end
data/lib/polyphony.rb CHANGED
@@ -21,8 +21,9 @@ module Polyphony
21
21
  ::Object.include GlobalAPI
22
22
 
23
23
  exceptions = import './polyphony/core/exceptions'
24
- Cancel = exceptions::Cancel
25
- MoveOn = exceptions::MoveOn
24
+ Cancel = exceptions::Cancel
25
+ MoveOn = exceptions::MoveOn
26
+ Terminate = exceptions::Terminate
26
27
 
27
28
  Net = import './polyphony/net'
28
29
 
@@ -31,7 +32,6 @@ module Polyphony
31
32
  Channel: './polyphony/core/channel',
32
33
  FS: './polyphony/fs',
33
34
  ResourcePool: './polyphony/core/resource_pool',
34
- Supervisor: './polyphony/core/supervisor',
35
35
  Sync: './polyphony/core/sync',
36
36
  ThreadPool: './polyphony/core/thread_pool',
37
37
  Throttler: './polyphony/core/throttler',
@@ -40,21 +40,13 @@ module Polyphony
40
40
  )
41
41
 
42
42
  class << self
43
- # def trap(sig, ref = false, &callback)
44
- # sig = Signal.list[sig.to_s.upcase] if sig.is_a?(Symbol)
45
- # puts "sig = #{sig.inspect}"
46
- # watcher = Gyro::Signal.new(sig, &callback)
47
- # # Gyro.unref unless ref
48
- # watcher
49
- # end
50
-
51
43
  def wait_for_signal(sig)
52
44
  fiber = Fiber.current
53
45
  Gyro.ref
54
- trap(sig) do
55
- trap(sig, :DEFAULT)
46
+ old_trap = trap(sig) do
56
47
  Gyro.unref
57
- fiber.transfer(sig)
48
+ fiber.schedule(sig)
49
+ trap(sig, old_trap)
58
50
  end
59
51
  suspend
60
52
  end
@@ -64,13 +56,23 @@ module Polyphony
64
56
  Gyro.post_fork
65
57
  Fiber.current.setup_main_fiber
66
58
  block.()
59
+ ensure
60
+ Fiber.current.terminate_all_children
61
+ Fiber.current.await_all_children
67
62
  end
68
63
  pid
69
64
  end
65
+ end
66
+ end
70
67
 
71
- def reset!
72
- Thread.current.reset_fiber_scheduling
73
- Fiber.reset!
74
- end
68
+ # install signal handlers
69
+
70
+ def install_terminating_signal_handler(signal, exception_class)
71
+ trap(signal) do
72
+ exception = exception_class.new
73
+ Thread.current.break_out_of_ev_loop(exception)
75
74
  end
76
75
  end
76
+
77
+ install_terminating_signal_handler('SIGTERM', SystemExit)
78
+ install_terminating_signal_handler('SIGINT', Interrupt)
data/test/coverage.rb CHANGED
@@ -46,7 +46,7 @@ class << SimpleCov::LinesClassifier
46
46
  # apparently TracePoint tracing does not cover lines including only keywords
47
47
  # such as begin end etc, so here we mark those lines as whitespace, so they
48
48
  # won't count towards the coverage score.
49
- line.strip =~ /^(begin|end|ensure|else|\})|(\s*rescue\s.+)$/ ||
49
+ line.strip =~ /^(begin|end|ensure|else|\{|\})|(\s*rescue\s.+)$/ ||
50
50
  orig_whitespace_line?(line)
51
51
  end
52
52
  end
data/test/helper.rb CHANGED
@@ -20,13 +20,17 @@ Minitest::Reporters.use! [
20
20
 
21
21
  class MiniTest::Test
22
22
  def setup
23
+ if Fiber.current.children.size > 0
24
+ puts "Children left: #{Fiber.current.children.inspect}"
25
+ exit!
26
+ end
23
27
  Fiber.current.setup_main_fiber
28
+ sleep 0
24
29
  end
25
30
 
26
31
  def teardown
27
- # wait for any remaining scheduled work
28
- Thread.current.switch_fiber
29
- Polyphony.reset!
32
+ Fiber.current.terminate_all_children
33
+ Fiber.current.await_all_children
30
34
  end
31
35
  end
32
36