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