polyphony 0.32 → 0.33
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +20 -0
- data/.rubocop.yml +14 -1
- data/CHANGELOG.md +10 -2
- data/Gemfile.lock +1 -1
- data/README.md +4 -0
- data/TODO.md +116 -1
- data/docs/_sass/custom/custom.scss +4 -0
- data/docs/_sass/overrides.scss +4 -6
- data/docs/getting-started/installing.md +2 -2
- data/docs/getting-started/tutorial.md +17 -15
- data/docs/index.md +18 -23
- data/docs/main-concepts/concurrency.md +1 -1
- data/ext/gyro/async.c +27 -0
- data/ext/gyro/gyro.h +1 -0
- data/ext/gyro/queue.c +10 -9
- data/ext/gyro/selector.c +3 -5
- data/ext/gyro/thread.c +6 -17
- data/lib/polyphony.rb +1 -0
- data/lib/polyphony/core/exceptions.rb +4 -1
- data/lib/polyphony/core/global_api.rb +4 -0
- data/lib/polyphony/extensions/core.rb +4 -3
- data/lib/polyphony/extensions/fiber.rb +72 -11
- data/lib/polyphony/extensions/thread.rb +20 -7
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +2 -2
- data/test/stress.rb +20 -0
- data/test/test_fiber.rb +158 -4
- data/test/test_global_api.rb +21 -14
- data/test/test_kernel.rb +23 -0
- data/test/test_signal.rb +1 -1
- data/test/test_thread.rb +4 -3
- data/test/test_thread_pool.rb +2 -2
- metadata +6 -4
data/ext/gyro/gyro.h
CHANGED
data/ext/gyro/queue.c
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
#include "gyro.h"
|
2
2
|
|
3
3
|
struct Gyro_Queue {
|
4
|
-
VALUE self;
|
5
4
|
VALUE queue;
|
6
5
|
VALUE wait_queue;
|
7
6
|
};
|
@@ -45,7 +44,6 @@ static VALUE Gyro_Queue_initialize(VALUE self) {
|
|
45
44
|
struct Gyro_Queue *queue;
|
46
45
|
GetGyro_Queue(self, queue);
|
47
46
|
|
48
|
-
queue->self = self;
|
49
47
|
queue->queue = rb_ary_new();
|
50
48
|
queue->wait_queue = rb_ary_new();
|
51
49
|
|
@@ -69,15 +67,17 @@ VALUE Gyro_Queue_shift(VALUE self) {
|
|
69
67
|
struct Gyro_Queue *queue;
|
70
68
|
GetGyro_Queue(self, queue);
|
71
69
|
|
72
|
-
|
73
|
-
if (RARRAY_LEN(queue->queue) > 0) {
|
74
|
-
return rb_ary_shift(queue->queue);
|
75
|
-
}
|
76
|
-
|
70
|
+
if (RARRAY_LEN(queue->queue) == 0) {
|
77
71
|
VALUE async = rb_funcall(cGyro_Async, ID_new, 0);
|
78
72
|
rb_ary_push(queue->wait_queue, async);
|
79
|
-
|
73
|
+
VALUE ret = Gyro_Async_await_no_raise(async);
|
74
|
+
if (RTEST(rb_obj_is_kind_of(ret, rb_eException))) {
|
75
|
+
rb_ary_delete(queue->wait_queue, async);
|
76
|
+
return rb_funcall(rb_mKernel, ID_raise, 1, ret);
|
77
|
+
}
|
80
78
|
}
|
79
|
+
|
80
|
+
return rb_ary_shift(queue->queue);
|
81
81
|
}
|
82
82
|
|
83
83
|
VALUE Gyro_Queue_shift_no_wait(VALUE self) {
|
@@ -96,7 +96,8 @@ VALUE Gyro_Queue_shift_each(VALUE self) {
|
|
96
96
|
|
97
97
|
if (rb_block_given_p()) {
|
98
98
|
long len = RARRAY_LEN(old_queue);
|
99
|
-
|
99
|
+
long i;
|
100
|
+
for (i = 0; i < len; i++) {
|
100
101
|
rb_yield(RARRAY_AREF(old_queue, i));
|
101
102
|
}
|
102
103
|
RB_GC_GUARD(old_queue);
|
data/ext/gyro/selector.c
CHANGED
@@ -85,9 +85,7 @@ static VALUE Gyro_Selector_initialize(VALUE self, VALUE thread) {
|
|
85
85
|
|
86
86
|
ev_async_init(&selector->async, dummy_async_callback);
|
87
87
|
ev_async_start(selector->ev_loop, &selector->async);
|
88
|
-
|
89
88
|
ev_run(selector->ev_loop, EVRUN_NOWAIT);
|
90
|
-
|
91
89
|
return Qnil;
|
92
90
|
}
|
93
91
|
|
@@ -137,9 +135,9 @@ VALUE Gyro_Selector_break_out_of_ev_loop(VALUE self) {
|
|
137
135
|
|
138
136
|
if (selector->ev_loop_running) {
|
139
137
|
// Since the loop will run until at least one event has occurred, we signal
|
140
|
-
// the associated async watcher, which will cause the ev loop to
|
141
|
-
// contrast to using `ev_break` to break out of the loop, which
|
142
|
-
// called from the same thread (from within the ev_loop), using an
|
138
|
+
// the selector's associated async watcher, which will cause the ev loop to
|
139
|
+
// return. In contrast to using `ev_break` to break out of the loop, which
|
140
|
+
// should be called from the same thread (from within the ev_loop), using an
|
143
141
|
// `ev_async` allows us to interrupt the event loop across threads.
|
144
142
|
ev_async_send(selector->ev_loop, &selector->async);
|
145
143
|
return Qtrue;
|
data/ext/gyro/thread.c
CHANGED
@@ -7,7 +7,10 @@ static ID ID_empty;
|
|
7
7
|
static ID ID_fiber_ref_count;
|
8
8
|
static ID ID_ivar_event_selector_proc;
|
9
9
|
static ID ID_ivar_event_selector;
|
10
|
+
static ID ID_ivar_join_wait_queue;
|
10
11
|
static ID ID_ivar_main_fiber;
|
12
|
+
static ID ID_ivar_result;
|
13
|
+
static ID ID_ivar_terminated;
|
11
14
|
static ID ID_pop;
|
12
15
|
static ID ID_push;
|
13
16
|
static ID ID_run_queue;
|
@@ -247,21 +250,6 @@ VALUE Thread_fiber_break_out_of_ev_loop(VALUE self, VALUE fiber, VALUE resume_ob
|
|
247
250
|
return self;
|
248
251
|
}
|
249
252
|
|
250
|
-
VALUE Thread_join_perform(VALUE self) {
|
251
|
-
if (!RTEST(rb_funcall(self, rb_intern("alive?"), 0))) {
|
252
|
-
return self;
|
253
|
-
}
|
254
|
-
|
255
|
-
VALUE async = rb_funcall(cGyro_Async, ID_new, 0);
|
256
|
-
VALUE wait_queue = rb_ivar_get(self, rb_intern("@join_wait_queue"));
|
257
|
-
|
258
|
-
Gyro_Queue_push(wait_queue, async);
|
259
|
-
|
260
|
-
VALUE ret = Gyro_Async_await(async);
|
261
|
-
RB_GC_GUARD(async);
|
262
|
-
return ret;
|
263
|
-
}
|
264
|
-
|
265
253
|
void Init_Thread() {
|
266
254
|
cQueue = rb_const_get(rb_cObject, rb_intern("Queue"));
|
267
255
|
|
@@ -282,14 +270,15 @@ void Init_Thread() {
|
|
282
270
|
Thread_schedule_fiber_with_priority, 2);
|
283
271
|
rb_define_method(rb_cThread, "switch_fiber", Thread_switch_fiber, 0);
|
284
272
|
|
285
|
-
rb_define_method(rb_cThread, "join_perform", Thread_join_perform, 0);
|
286
|
-
|
287
273
|
ID_create_event_selector = rb_intern("create_event_selector");
|
288
274
|
ID_empty = rb_intern("empty?");
|
289
275
|
ID_fiber_ref_count = rb_intern("fiber_ref_count");
|
290
276
|
ID_ivar_event_selector = rb_intern("@event_selector");
|
291
277
|
ID_ivar_event_selector_proc = rb_intern("@event_selector_proc");
|
278
|
+
ID_ivar_join_wait_queue = rb_intern("@join_wait_queue");
|
292
279
|
ID_ivar_main_fiber = rb_intern("@main_fiber");
|
280
|
+
ID_ivar_result = rb_intern("@result");
|
281
|
+
ID_ivar_terminated = rb_intern("@terminated");
|
293
282
|
ID_pop = rb_intern("pop");
|
294
283
|
ID_push = rb_intern("push");
|
295
284
|
ID_run_queue = rb_intern("run_queue");
|
data/lib/polyphony.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
export :MoveOn, :Cancel, :Terminate
|
3
|
+
export :BaseException, :MoveOn, :Cancel, :Terminate, :Restart
|
4
4
|
|
5
5
|
# Common exception class for interrupting fibers. These exceptions allow
|
6
6
|
# control of fibers. BaseException exceptions can encapsulate a value and thus
|
@@ -26,3 +26,6 @@ class Cancel < BaseException; end
|
|
26
26
|
|
27
27
|
# Terminate is used to interrupt a fiber once its parent fiber has terminated.
|
28
28
|
class Terminate < BaseException; end
|
29
|
+
|
30
|
+
# Restart is used to restart a fiber
|
31
|
+
class Restart < BaseException; end
|
@@ -8,14 +8,15 @@ Exceptions = import('../core/exceptions')
|
|
8
8
|
|
9
9
|
# Exeption overrides
|
10
10
|
class ::Exception
|
11
|
+
EXIT_EXCEPTION_CLASSES = [::Interrupt, ::SystemExit].freeze
|
12
|
+
|
11
13
|
class << self
|
12
14
|
attr_accessor :__disable_sanitized_backtrace__
|
13
15
|
end
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
-
EXIT_EXCEPTION_CLASSES = [::Interrupt, ::SystemExit].freeze
|
17
|
+
attr_accessor :source_fiber
|
18
18
|
|
19
|
+
alias_method :orig_initialize, :initialize
|
19
20
|
def initialize(*args)
|
20
21
|
@__raising_fiber__ = Fiber.current
|
21
22
|
orig_initialize(*args)
|
@@ -27,6 +27,15 @@ module FiberControl
|
|
27
27
|
end
|
28
28
|
alias_method :stop, :interrupt
|
29
29
|
|
30
|
+
def restart(value = nil)
|
31
|
+
raise "Can''t restart main fiber" if @main
|
32
|
+
return parent.spin(&@block).tap { |f| f.schedule(value) } unless @running
|
33
|
+
|
34
|
+
schedule Exceptions::Restart.new(value)
|
35
|
+
self
|
36
|
+
end
|
37
|
+
alias_method :reset, :restart
|
38
|
+
|
30
39
|
def cancel!
|
31
40
|
return if @running == false
|
32
41
|
|
@@ -52,6 +61,35 @@ module FiberControl
|
|
52
61
|
else RuntimeError.new
|
53
62
|
end
|
54
63
|
end
|
64
|
+
|
65
|
+
def supervise(on_error: nil, &block)
|
66
|
+
@on_child_done = proc { schedule }
|
67
|
+
loop { supervise_perform(on_error, &block) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def supervise_perform(policy, &block)
|
71
|
+
suspend
|
72
|
+
rescue Polyphony::Restart
|
73
|
+
restart_all_children
|
74
|
+
rescue Exception => e
|
75
|
+
case e.source_fiber
|
76
|
+
when nil, self
|
77
|
+
Kernel.raise e
|
78
|
+
else
|
79
|
+
handle_supervisor_exception(e, e.source_fiber, policy, &block)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def handle_supervisor_exception(error, fiber, policy, &block)
|
84
|
+
return block.call(fiber, error) if block
|
85
|
+
|
86
|
+
case policy
|
87
|
+
when :restart
|
88
|
+
fiber.restart
|
89
|
+
when :restart_all
|
90
|
+
@children.keys.each(&:restart)
|
91
|
+
end
|
92
|
+
end
|
55
93
|
end
|
56
94
|
|
57
95
|
# Class methods for controlling fibers (namely await and select)
|
@@ -151,7 +189,7 @@ module ChildFiberControl
|
|
151
189
|
(@children ||= {}).keys
|
152
190
|
end
|
153
191
|
|
154
|
-
def spin(tag = nil, orig_caller = caller, &block)
|
192
|
+
def spin(tag = nil, orig_caller = Kernel.caller, &block)
|
155
193
|
f = Fiber.new { |v| f.run(v) }
|
156
194
|
f.prepare(tag, block, orig_caller, self)
|
157
195
|
(@children ||= {})[f] = true
|
@@ -160,6 +198,7 @@ module ChildFiberControl
|
|
160
198
|
|
161
199
|
def child_done(child_fiber)
|
162
200
|
@children.delete(child_fiber)
|
201
|
+
@on_child_done&.(child_fiber)
|
163
202
|
end
|
164
203
|
|
165
204
|
def terminate_all_children
|
@@ -174,6 +213,11 @@ module ChildFiberControl
|
|
174
213
|
|
175
214
|
Fiber.await(*@children.keys)
|
176
215
|
end
|
216
|
+
|
217
|
+
def shutdown_all_children
|
218
|
+
terminate_all_children
|
219
|
+
await_all_children
|
220
|
+
end
|
177
221
|
end
|
178
222
|
|
179
223
|
# Fiber extensions
|
@@ -187,13 +231,13 @@ class ::Fiber
|
|
187
231
|
attr_accessor :tag, :thread, :parent
|
188
232
|
|
189
233
|
def prepare(tag, block, caller, parent)
|
190
|
-
__fiber_trace__(:fiber_create, self)
|
191
234
|
@thread = Thread.current
|
192
235
|
@tag = tag
|
193
236
|
@parent = parent
|
194
237
|
@caller = caller
|
195
238
|
@block = block
|
196
239
|
@mailbox = Gyro::Queue.new
|
240
|
+
__fiber_trace__(:fiber_create, self)
|
197
241
|
schedule
|
198
242
|
end
|
199
243
|
|
@@ -207,16 +251,21 @@ class ::Fiber
|
|
207
251
|
end
|
208
252
|
|
209
253
|
def run(first_value)
|
210
|
-
setup
|
211
|
-
uncaught = nil
|
254
|
+
setup first_value
|
212
255
|
result = @block.(first_value)
|
256
|
+
finalize result
|
257
|
+
rescue Exceptions::Restart => e
|
258
|
+
restart_self(e.value)
|
213
259
|
rescue Exceptions::MoveOn, Exceptions::Terminate => e
|
214
|
-
|
260
|
+
finalize e.value
|
215
261
|
rescue Exception => e
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
262
|
+
e.source_fiber = self
|
263
|
+
finalize e, true
|
264
|
+
end
|
265
|
+
|
266
|
+
def restart_self(first_value)
|
267
|
+
@mailbox = Gyro::Queue.new
|
268
|
+
run(first_value)
|
220
269
|
end
|
221
270
|
|
222
271
|
def setup(first_value)
|
@@ -226,8 +275,7 @@ class ::Fiber
|
|
226
275
|
end
|
227
276
|
|
228
277
|
def finalize(result, uncaught_exception = false)
|
229
|
-
|
230
|
-
await_all_children
|
278
|
+
result, uncaught_exception = finalize_children(result, uncaught_exception)
|
231
279
|
__fiber_trace__(:fiber_terminate, self, result)
|
232
280
|
@result = result
|
233
281
|
@running = false
|
@@ -236,6 +284,19 @@ class ::Fiber
|
|
236
284
|
Thread.current.switch_fiber
|
237
285
|
end
|
238
286
|
|
287
|
+
# Shuts down all children of the current fiber. If any exception occurs while
|
288
|
+
# the children are shut down, it is returned along with the uncaught_exception
|
289
|
+
# flag set. Otherwise, it returns the given arguments.
|
290
|
+
def finalize_children(result, uncaught_exception)
|
291
|
+
begin
|
292
|
+
shutdown_all_children
|
293
|
+
rescue Exception => e
|
294
|
+
result = e
|
295
|
+
uncaught_exception = true
|
296
|
+
end
|
297
|
+
[result, uncaught_exception]
|
298
|
+
end
|
299
|
+
|
239
300
|
def inform_dependants(result, uncaught_exception)
|
240
301
|
@parent.child_done(self)
|
241
302
|
@when_done_procs&.each { |p| p.(result) }
|
@@ -4,24 +4,27 @@ Exceptions = import '../core/exceptions'
|
|
4
4
|
|
5
5
|
# Thread extensions
|
6
6
|
class ::Thread
|
7
|
-
attr_reader :main_fiber
|
7
|
+
attr_reader :main_fiber, :result
|
8
8
|
|
9
9
|
alias_method :orig_initialize, :initialize
|
10
10
|
def initialize(*args, &block)
|
11
11
|
@join_wait_queue = Gyro::Queue.new
|
12
12
|
@args = args
|
13
13
|
@block = block
|
14
|
+
@finalization_mutex = Mutex.new
|
14
15
|
orig_initialize { execute }
|
15
16
|
end
|
16
17
|
|
17
18
|
def execute
|
18
19
|
setup
|
20
|
+
@ready = true
|
19
21
|
result = @block.(*@args)
|
20
22
|
rescue Exceptions::MoveOn, Exceptions::Terminate => e
|
21
23
|
result = e.value
|
22
24
|
rescue Exception => e
|
23
25
|
result = e
|
24
26
|
ensure
|
27
|
+
@ready = true
|
25
28
|
finalize(result)
|
26
29
|
end
|
27
30
|
|
@@ -36,7 +39,11 @@ class ::Thread
|
|
36
39
|
Fiber.current.terminate_all_children
|
37
40
|
Fiber.current.await_all_children
|
38
41
|
end
|
39
|
-
|
42
|
+
@finalization_mutex.synchronize do
|
43
|
+
@terminated = true
|
44
|
+
@result = result
|
45
|
+
signal_waiters(result)
|
46
|
+
end
|
40
47
|
stop_event_selector
|
41
48
|
end
|
42
49
|
|
@@ -46,11 +53,15 @@ class ::Thread
|
|
46
53
|
|
47
54
|
alias_method :orig_join, :join
|
48
55
|
def join(timeout = nil)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
56
|
+
async = Gyro::Async.new
|
57
|
+
@finalization_mutex.synchronize do
|
58
|
+
if @terminated
|
59
|
+
@result.is_a?(Exception) ? (raise @result) : (return @result)
|
60
|
+
else
|
61
|
+
@join_wait_queue.push(async)
|
62
|
+
end
|
53
63
|
end
|
64
|
+
timeout ? move_on_after(timeout) { async.await } : async.await
|
54
65
|
end
|
55
66
|
alias_method :await, :join
|
56
67
|
|
@@ -60,7 +71,9 @@ class ::Thread
|
|
60
71
|
error = RuntimeError.new if error.nil?
|
61
72
|
error = RuntimeError.new(error) if error.is_a?(String)
|
62
73
|
error = error.new if error.is_a?(Class)
|
63
|
-
|
74
|
+
|
75
|
+
sleep 0.0001 until @ready
|
76
|
+
main_fiber&.raise(error)
|
64
77
|
end
|
65
78
|
|
66
79
|
alias_method :orig_kill, :kill
|
data/lib/polyphony/version.rb
CHANGED
data/polyphony.gemspec
CHANGED
@@ -4,11 +4,11 @@ Gem::Specification.new do |s|
|
|
4
4
|
s.name = 'polyphony'
|
5
5
|
s.version = Polyphony::VERSION
|
6
6
|
s.licenses = ['MIT']
|
7
|
-
s.summary = '
|
7
|
+
s.summary = 'Fine grained concurrency for Ruby'
|
8
8
|
s.author = 'Sharon Rosner'
|
9
9
|
s.email = 'ciconia@gmail.com'
|
10
10
|
s.files = `git ls-files`.split
|
11
|
-
s.homepage = 'https://
|
11
|
+
s.homepage = 'https://digital-fabric.github.io/polyphony'
|
12
12
|
s.metadata = {
|
13
13
|
"source_code_uri" => "https://github.com/digital-fabric/polyphony",
|
14
14
|
"documentation_uri" => "https://digital-fabric.github.io/polyphony/",
|
data/test/stress.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
@count = 0
|
4
|
+
|
5
|
+
def run_tests
|
6
|
+
@count += 1
|
7
|
+
puts "!(#{@count})"
|
8
|
+
# output = `ruby test/test_thread.rb -n test_thread_inspect`
|
9
|
+
system('ruby test/run.rb')
|
10
|
+
return if $?.exitstatus == 0
|
11
|
+
|
12
|
+
exit!
|
13
|
+
# puts
|
14
|
+
# puts output
|
15
|
+
# exit!
|
16
|
+
end
|
17
|
+
|
18
|
+
loop {
|
19
|
+
run_tests
|
20
|
+
}
|