polyphony 0.29 → 0.30

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/.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