polyphony 0.32 → 0.33
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/.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
|
+
}
|