polyphony 0.29 → 0.30
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile.lock +1 -1
- data/TODO.md +15 -10
- data/docs/getting-started/tutorial.md +3 -3
- data/docs/index.md +2 -3
- data/docs/{technical-overview → main-concepts}/concurrency.md +62 -15
- data/docs/{technical-overview → main-concepts}/design-principles.md +21 -8
- data/docs/{technical-overview → main-concepts}/exception-handling.md +80 -38
- data/docs/{technical-overview → main-concepts}/extending.md +4 -3
- data/docs/{technical-overview → main-concepts}/fiber-scheduling.md +3 -3
- data/docs/{technical-overview.md → main-concepts.md} +2 -2
- data/examples/core/xx-at_exit.rb +29 -0
- data/examples/core/xx-fork-terminate.rb +27 -0
- data/examples/core/xx-pingpong.rb +18 -0
- data/examples/core/xx-stop.rb +20 -0
- data/ext/gyro/async.c +1 -1
- data/ext/gyro/extconf.rb +0 -3
- data/ext/gyro/gyro.c +7 -8
- data/ext/gyro/gyro.h +2 -0
- data/ext/gyro/queue.c +6 -6
- data/ext/gyro/selector.c +32 -1
- data/ext/gyro/thread.c +55 -9
- data/ext/gyro/timer.c +1 -0
- data/lib/polyphony/core/exceptions.rb +4 -1
- data/lib/polyphony/core/global_api.rb +1 -6
- data/lib/polyphony/core/thread_pool.rb +3 -3
- data/lib/polyphony/extensions/core.rb +7 -1
- data/lib/polyphony/extensions/fiber.rb +159 -72
- data/lib/polyphony/extensions/io.rb +2 -4
- data/lib/polyphony/extensions/openssl.rb +0 -17
- data/lib/polyphony/extensions/thread.rb +46 -22
- data/lib/polyphony/version.rb +1 -1
- data/lib/polyphony.rb +20 -18
- data/test/coverage.rb +1 -1
- data/test/helper.rb +7 -3
- data/test/test_fiber.rb +285 -72
- data/test/test_global_api.rb +7 -52
- data/test/test_io.rb +8 -0
- data/test/test_signal.rb +1 -0
- data/test/test_thread.rb +76 -56
- data/test/test_thread_pool.rb +27 -5
- data/test/test_throttler.rb +1 -0
- metadata +12 -12
- data/lib/polyphony/core/supervisor.rb +0 -114
- data/lib/polyphony/line_reader.rb +0 -82
- data/test/test_gyro.rb +0 -25
- 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
|
-
|
14
|
+
fiber = Fiber.current
|
15
|
+
@waiting_fibers ||= {}
|
16
|
+
@waiting_fibers[fiber] = true
|
15
17
|
suspend
|
16
18
|
ensure
|
17
|
-
@
|
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
|
-
|
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
|
-
#
|
53
|
-
module
|
54
|
-
def
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
76
|
-
|
77
|
-
|
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
|
-
|
81
|
-
|
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
|
-
#
|
86
|
-
|
87
|
-
|
88
|
-
|
131
|
+
# Messaging functionality
|
132
|
+
module FiberMessaging
|
133
|
+
def <<(value)
|
134
|
+
@mailbox << value
|
135
|
+
snooze
|
136
|
+
end
|
137
|
+
alias_method :send, :<<
|
89
138
|
|
90
|
-
def
|
91
|
-
@
|
139
|
+
def receive
|
140
|
+
@mailbox.shift
|
92
141
|
end
|
142
|
+
end
|
93
143
|
|
94
|
-
|
144
|
+
# Methods for controlling child fibers
|
145
|
+
module ChildFiberControl
|
146
|
+
def children
|
147
|
+
(@children ||= {}).keys
|
148
|
+
end
|
95
149
|
|
96
|
-
def
|
97
|
-
|
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
|
101
|
-
@
|
157
|
+
def child_done(child_fiber)
|
158
|
+
@children.delete(child_fiber)
|
102
159
|
end
|
103
160
|
|
104
|
-
def
|
105
|
-
@
|
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
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
185
|
+
def prepare(tag, block, caller)
|
117
186
|
__fiber_trace__(:fiber_create, self)
|
118
187
|
@thread = Thread.current
|
119
188
|
@tag = tag
|
120
|
-
@
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
rescue ::
|
137
|
-
|
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
|
-
|
212
|
+
result = e
|
213
|
+
uncaught = true
|
214
|
+
ensure
|
215
|
+
finalize(result, uncaught)
|
144
216
|
end
|
145
217
|
|
146
|
-
def
|
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
|
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
|
-
|
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
|
168
|
-
@
|
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
|
-
@
|
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 @
|
193
|
-
spin_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
|
@@ -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
|
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
|
28
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
28
|
+
def setup
|
29
|
+
@main_fiber = Fiber.current
|
30
|
+
@main_fiber.setup_main_fiber
|
31
|
+
setup_fiber_scheduling
|
32
|
+
end
|
36
33
|
|
37
|
-
|
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) {
|
50
|
+
move_on_after(timeout) { join_perform }
|
42
51
|
else
|
43
|
-
|
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
|
data/lib/polyphony/version.rb
CHANGED
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
|
25
|
-
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.
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
28
|
-
|
29
|
-
Polyphony.reset!
|
32
|
+
Fiber.current.terminate_all_children
|
33
|
+
Fiber.current.await_all_children
|
30
34
|
end
|
31
35
|
end
|
32
36
|
|