polyphony 0.27 → 0.28
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 +4 -1
- data/CHANGELOG.md +13 -0
- data/Gemfile +12 -1
- data/Gemfile.lock +83 -5
- data/Rakefile +4 -0
- data/TODO.md +11 -20
- data/docs/_config.yml +15 -0
- data/docs/_includes/nav.html +47 -0
- data/docs/_sass/custom/custom.scss +5 -0
- data/docs/_sass/overrides.scss +45 -0
- data/docs/assets/img/echo-fibers.svg +1 -0
- data/docs/assets/img/sleeping-fiber.svg +1 -0
- data/docs/faq.md +182 -0
- data/docs/getting-started/installing.md +10 -2
- data/docs/getting-started/tutorial.md +333 -26
- data/docs/getting-started.md +10 -0
- data/docs/index.md +91 -0
- data/docs/technical-overview/concurrency.md +78 -16
- data/docs/technical-overview/design-principles.md +7 -0
- data/docs/technical-overview/exception-handling.md +57 -9
- data/docs/technical-overview/extending.md +7 -0
- data/docs/technical-overview/fiber-scheduling.md +128 -18
- data/docs/technical-overview.md +10 -0
- data/docs/user-guide/web-server.md +7 -0
- data/docs/user-guide.md +10 -0
- data/examples/core/xx-deadlock.rb +8 -0
- data/examples/core/xx-state-machine.rb +51 -0
- data/examples/core/xx-trace.rb +80 -0
- data/examples/interfaces/pg_notify.rb +35 -0
- data/examples/io/xx-httparty.rb +31 -6
- data/examples/io/xx-irb.rb +1 -11
- data/examples/io/xx-switch.rb +15 -0
- data/ext/gyro/gyro.c +77 -38
- data/ext/gyro/gyro.h +15 -5
- data/ext/gyro/gyro_ext.c +3 -0
- data/ext/gyro/thread.c +47 -32
- data/ext/gyro/tracing.c +11 -0
- data/lib/polyphony/core/global_api.rb +11 -4
- data/lib/polyphony/core/supervisor.rb +1 -0
- data/lib/polyphony/core/thread_pool.rb +44 -35
- data/lib/polyphony/extensions/fiber.rb +19 -9
- data/lib/polyphony/extensions/io.rb +14 -14
- data/lib/polyphony/extensions/socket.rb +3 -3
- data/lib/polyphony/irb.rb +13 -0
- data/lib/polyphony/postgres.rb +15 -0
- data/lib/polyphony/trace.rb +98 -0
- data/lib/polyphony/version.rb +1 -1
- data/lib/polyphony.rb +1 -0
- data/polyphony.gemspec +21 -12
- data/test/helper.rb +3 -2
- data/test/test_fiber.rb +53 -3
- data/test/test_global_api.rb +12 -0
- data/test/test_gyro.rb +2 -2
- data/test/test_supervisor.rb +12 -0
- data/test/test_thread.rb +12 -0
- data/test/test_thread_pool.rb +75 -0
- data/test/test_throttler.rb +6 -0
- data/test/test_trace.rb +66 -0
- metadata +99 -9
- data/docs/README.md +0 -36
- data/docs/summary.md +0 -60
- data/docs/technical-overview/faq.md +0 -97
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :new, :analyze
|
4
|
+
|
5
|
+
require 'polyphony'
|
6
|
+
|
7
|
+
STOCK_EVENTS = %i[line call return c_call c_return b_call b_return].freeze
|
8
|
+
|
9
|
+
def new
|
10
|
+
start_stamp = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
11
|
+
::TracePoint.new(*STOCK_EVENTS) { |tp| yield trace_record(tp, start_stamp) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def trace_record(trp, start_stamp)
|
15
|
+
stamp = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start_stamp
|
16
|
+
{ stamp: stamp, self: trp.self, binding: trp.binding, event: trp.event,
|
17
|
+
fiber: tp_fiber(trp), lineno: trp.lineno, method_id: trp.method_id,
|
18
|
+
file: trp.path, parameters: tp_params(trp),
|
19
|
+
return_value: tp_return_value(trp),
|
20
|
+
exception: tp_raised_exception(trp) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def tp_fiber(trp)
|
24
|
+
trp.is_a?(FiberTracePoint) ? trp.fiber : Fiber.current
|
25
|
+
end
|
26
|
+
|
27
|
+
PARAMS_EVENTS = %i[call c_call b_call].freeze
|
28
|
+
|
29
|
+
def tp_params(trp)
|
30
|
+
PARAMS_EVENTS.include?(trp.event) ? trp.parameters : nil
|
31
|
+
end
|
32
|
+
|
33
|
+
RETURN_VALUE_EVENTS = %i[return c_return b_return].freeze
|
34
|
+
|
35
|
+
def tp_return_value(trp)
|
36
|
+
RETURN_VALUE_EVENTS.include?(trp.event) ? trp.return_value : nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def tp_raised_exception(trp)
|
40
|
+
trp.event == :raise && trp.raised_exception
|
41
|
+
end
|
42
|
+
|
43
|
+
def analyze(records)
|
44
|
+
by_fiber = Hash.new { |h, f| h[f] = [] }
|
45
|
+
records.each_with_object(by_fiber) { |r, h| h[r[:fiber]] << r }
|
46
|
+
{ by_fiber: by_fiber }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Implements fake TracePoint instances for fiber-related events
|
50
|
+
class FiberTracePoint
|
51
|
+
attr_reader :event, :fiber, :value
|
52
|
+
|
53
|
+
def initialize(tpoint)
|
54
|
+
@tp = tpoint
|
55
|
+
@event = tpoint.return_value[0]
|
56
|
+
@fiber = tpoint.return_value[1]
|
57
|
+
@value = tpoint.return_value[2]
|
58
|
+
end
|
59
|
+
|
60
|
+
def lineno
|
61
|
+
@tp.lineno
|
62
|
+
end
|
63
|
+
|
64
|
+
def method_id
|
65
|
+
@tp.method_id
|
66
|
+
end
|
67
|
+
|
68
|
+
def path
|
69
|
+
@tp.path
|
70
|
+
end
|
71
|
+
|
72
|
+
def self
|
73
|
+
@tp.self
|
74
|
+
end
|
75
|
+
|
76
|
+
def binding
|
77
|
+
@tp.binding
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class << ::TracePoint
|
82
|
+
alias_method :orig_new, :new
|
83
|
+
def new(*args, &block)
|
84
|
+
polyphony_file_regexp = /^#{::Exception::POLYPHONY_DIR}/
|
85
|
+
|
86
|
+
orig_new(*args) do |tp|
|
87
|
+
# next unless !$watched_fiber || Fiber.current == $watched_fiber
|
88
|
+
|
89
|
+
if tp.method_id == :__fiber_trace__
|
90
|
+
block.(FiberTracePoint.new(tp)) if tp.event == :c_return
|
91
|
+
else
|
92
|
+
next if tp.path =~ polyphony_file_regexp
|
93
|
+
|
94
|
+
block.(tp)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/lib/polyphony/version.rb
CHANGED
data/lib/polyphony.rb
CHANGED
data/polyphony.gemspec
CHANGED
@@ -11,7 +11,9 @@ Gem::Specification.new do |s|
|
|
11
11
|
s.homepage = 'https://dfab.gitbook.io/polyphony/'
|
12
12
|
s.metadata = {
|
13
13
|
"source_code_uri" => "https://github.com/digital-fabric/polyphony",
|
14
|
-
"documentation_uri" => "https://
|
14
|
+
"documentation_uri" => "https://digital-fabric.github.io/polyphony/",
|
15
|
+
"homepage_uri" => "https://digital-fabric.github.io/polyphony/",
|
16
|
+
"changelog_uri" => "https://github.com/digital-fabric/polyphony/blob/master/CHANGELOG.md"
|
15
17
|
}
|
16
18
|
s.rdoc_options = ["--title", "polyphony", "--main", "README.md"]
|
17
19
|
s.extra_rdoc_files = ["README.md"]
|
@@ -19,16 +21,23 @@ Gem::Specification.new do |s|
|
|
19
21
|
s.require_paths = ["lib"]
|
20
22
|
s.required_ruby_version = '>= 2.6'
|
21
23
|
|
22
|
-
s.add_runtime_dependency 'modulation',
|
24
|
+
s.add_runtime_dependency 'modulation', '~>1.0'
|
23
25
|
|
24
|
-
s.add_development_dependency 'httparty',
|
25
|
-
s.add_development_dependency 'localhost',
|
26
|
-
s.add_development_dependency 'minitest',
|
27
|
-
s.add_development_dependency 'minitest-reporters',
|
28
|
-
s.add_development_dependency 'simplecov',
|
29
|
-
s.add_development_dependency '
|
30
|
-
s.add_development_dependency '
|
31
|
-
s.add_development_dependency '
|
32
|
-
s.add_development_dependency '
|
33
|
-
s.add_development_dependency '
|
26
|
+
s.add_development_dependency 'httparty', '0.17.0'
|
27
|
+
s.add_development_dependency 'localhost', '1.1.4'
|
28
|
+
s.add_development_dependency 'minitest', '5.13.0'
|
29
|
+
s.add_development_dependency 'minitest-reporters', '1.4.2'
|
30
|
+
s.add_development_dependency 'simplecov', '0.17.1'
|
31
|
+
s.add_development_dependency 'rubocop', '0.79.0'
|
32
|
+
s.add_development_dependency 'pg', '1.1.3'
|
33
|
+
s.add_development_dependency 'rake-compiler', '1.0.5'
|
34
|
+
s.add_development_dependency 'redis', '4.1.0'
|
35
|
+
s.add_development_dependency 'hiredis', '0.6.3'
|
36
|
+
s.add_development_dependency 'http_parser.rb', '~>0.6.0'
|
37
|
+
|
38
|
+
s.add_development_dependency 'jekyll', '~>3.8.6'
|
39
|
+
s.add_development_dependency 'jekyll-remote-theme', '~>0.4.1'
|
40
|
+
s.add_development_dependency 'jekyll-seo-tag', '~>2.6.1'
|
41
|
+
s.add_development_dependency 'just-the-docs', '~>0.2.7'
|
42
|
+
|
34
43
|
end
|
data/test/helper.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'bundler/setup'
|
4
|
+
|
5
|
+
require_relative './coverage' if ENV['COVERAGE']
|
6
|
+
|
4
7
|
require 'polyphony'
|
5
8
|
|
6
9
|
require 'fileutils'
|
7
10
|
require_relative './eg'
|
8
11
|
|
9
|
-
require_relative './coverage' if ENV['COVERAGE']
|
10
|
-
|
11
12
|
require 'minitest/autorun'
|
12
13
|
require 'minitest/reporters'
|
13
14
|
|
data/test/test_fiber.rb
CHANGED
@@ -25,6 +25,15 @@ class FiberTest < MiniTest::Test
|
|
25
25
|
f&.stop
|
26
26
|
end
|
27
27
|
|
28
|
+
def test_tag
|
29
|
+
assert_equal :main, Fiber.current.tag
|
30
|
+
Fiber.current.tag = :foo
|
31
|
+
assert_equal :foo, Fiber.current.tag
|
32
|
+
|
33
|
+
f = Fiber.spin(:bar) { }
|
34
|
+
assert_equal :bar, f.tag
|
35
|
+
end
|
36
|
+
|
28
37
|
def test_await_return_value
|
29
38
|
f = Fiber.spin { %i[foo bar] }
|
30
39
|
assert_equal %i[foo bar], f.await
|
@@ -261,13 +270,16 @@ class FiberTest < MiniTest::Test
|
|
261
270
|
snooze
|
262
271
|
counter += 1
|
263
272
|
end
|
273
|
+
suspend
|
264
274
|
end
|
265
275
|
|
266
|
-
assert_equal :
|
276
|
+
assert_equal :runnable, f.state
|
267
277
|
assert_equal :running, Fiber.current.state
|
268
278
|
snooze
|
269
|
-
assert_equal :
|
279
|
+
assert_equal :runnable, f.state
|
270
280
|
snooze while counter < 3
|
281
|
+
assert_equal :waiting, f.state
|
282
|
+
f.stop
|
271
283
|
assert_equal :dead, f.state
|
272
284
|
ensure
|
273
285
|
f&.stop
|
@@ -444,7 +456,7 @@ class MailboxTest < MiniTest::Test
|
|
444
456
|
f = spin { :foo }
|
445
457
|
|
446
458
|
expected = format(
|
447
|
-
'#<Fiber:%s %s:%d:in `test_inspect\' (
|
459
|
+
'#<Fiber:%s %s:%d:in `test_inspect\' (runnable)>',
|
448
460
|
f.object_id,
|
449
461
|
__FILE__,
|
450
462
|
spin_line_no
|
@@ -460,4 +472,42 @@ class MailboxTest < MiniTest::Test
|
|
460
472
|
)
|
461
473
|
assert_equal expected, f.inspect
|
462
474
|
end
|
475
|
+
|
476
|
+
def test_system_exit_in_fiber
|
477
|
+
parent_error = nil
|
478
|
+
main_fiber_error = nil
|
479
|
+
f2 = nil
|
480
|
+
f1 = spin do
|
481
|
+
f2 = spin { raise SystemExit }
|
482
|
+
suspend
|
483
|
+
rescue Exception => parent_error
|
484
|
+
end
|
485
|
+
|
486
|
+
begin
|
487
|
+
suspend
|
488
|
+
rescue Exception => main_fiber_error
|
489
|
+
end
|
490
|
+
|
491
|
+
assert_nil parent_error
|
492
|
+
assert_kind_of SystemExit, main_fiber_error
|
493
|
+
end
|
494
|
+
|
495
|
+
def test_interrupt_in_fiber
|
496
|
+
parent_error = nil
|
497
|
+
main_fiber_error = nil
|
498
|
+
f2 = nil
|
499
|
+
f1 = spin do
|
500
|
+
f2 = spin { raise Interrupt }
|
501
|
+
suspend
|
502
|
+
rescue Exception => parent_error
|
503
|
+
end
|
504
|
+
|
505
|
+
begin
|
506
|
+
suspend
|
507
|
+
rescue Exception => main_fiber_error
|
508
|
+
end
|
509
|
+
|
510
|
+
assert_nil parent_error
|
511
|
+
assert_kind_of Interrupt, main_fiber_error
|
512
|
+
end
|
463
513
|
end
|
data/test/test_global_api.rb
CHANGED
@@ -242,6 +242,18 @@ class MoveOnAfterTest < MiniTest::Test
|
|
242
242
|
assert_equal :bar, v
|
243
243
|
end
|
244
244
|
|
245
|
+
def test_spin_without_tag
|
246
|
+
f = spin { }
|
247
|
+
assert_kind_of Fiber, f
|
248
|
+
assert_nil f.tag
|
249
|
+
end
|
250
|
+
|
251
|
+
def test_spin_with_tag
|
252
|
+
f = spin(:foo) { }
|
253
|
+
assert_kind_of Fiber, f
|
254
|
+
assert_equal :foo, f.tag
|
255
|
+
end
|
256
|
+
|
245
257
|
def test_spin_loop
|
246
258
|
buffer = []
|
247
259
|
counter = 0
|
data/test/test_gyro.rb
CHANGED
@@ -8,13 +8,13 @@ class GyroTest < MiniTest::Test
|
|
8
8
|
|
9
9
|
f = Fiber.new {}
|
10
10
|
|
11
|
-
assert_equal :
|
11
|
+
assert_equal :waiting, f.state
|
12
12
|
f.resume
|
13
13
|
assert_equal :dead, f.state
|
14
14
|
|
15
15
|
f = Fiber.new { }
|
16
16
|
f.schedule
|
17
|
-
assert_equal :
|
17
|
+
assert_equal :runnable, f.state
|
18
18
|
snooze
|
19
19
|
assert_equal :dead, f.state
|
20
20
|
end
|
data/test/test_supervisor.rb
CHANGED
@@ -25,6 +25,18 @@ class SupervisorTest < MiniTest::Test
|
|
25
25
|
assert_equal [10, 20, 30], result
|
26
26
|
end
|
27
27
|
|
28
|
+
def test_new_with_block
|
29
|
+
supervisor = Polyphony::Supervisor.new { |s|
|
30
|
+
(1..3).each { |i|
|
31
|
+
s.spin {
|
32
|
+
snooze
|
33
|
+
i * 10
|
34
|
+
}
|
35
|
+
}
|
36
|
+
}
|
37
|
+
assert_equal [10, 20, 30], supervisor.await
|
38
|
+
end
|
39
|
+
|
28
40
|
def test_join_multiple_fibers
|
29
41
|
result = Polyphony::Supervisor.new.join { |s|
|
30
42
|
(1..3).each { |i|
|
data/test/test_thread.rb
CHANGED
@@ -41,4 +41,16 @@ class ThreadTest < MiniTest::Test
|
|
41
41
|
# thread's event selector, leading to a memory leak.
|
42
42
|
t.kill if t.alive?
|
43
43
|
end
|
44
|
+
|
45
|
+
def test_thread_inspect
|
46
|
+
lineno = __LINE__ + 1
|
47
|
+
t = Thread.new {}
|
48
|
+
str = format(
|
49
|
+
"#<Thread:%d %s:%d (run)>",
|
50
|
+
t.object_id,
|
51
|
+
__FILE__,
|
52
|
+
lineno,
|
53
|
+
)
|
54
|
+
assert_equal str, t.inspect
|
55
|
+
end
|
44
56
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
class ThreadPoolTest < MiniTest::Test
|
6
|
+
def setup
|
7
|
+
super
|
8
|
+
@pool = Polyphony::ThreadPool.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_process
|
12
|
+
current_thread = Thread.current
|
13
|
+
|
14
|
+
processing_thread = nil
|
15
|
+
result = @pool.process do
|
16
|
+
processing_thread = Thread.current
|
17
|
+
+'foo' + 'bar'
|
18
|
+
end
|
19
|
+
assert_equal 'foobar', result
|
20
|
+
assert processing_thread != current_thread
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_multi_process
|
24
|
+
current_thread = Thread.current
|
25
|
+
threads = []
|
26
|
+
results = []
|
27
|
+
|
28
|
+
10.times do |i|
|
29
|
+
spin do
|
30
|
+
results << @pool.process do
|
31
|
+
threads << Thread.current
|
32
|
+
sleep 0.01
|
33
|
+
i * 10
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
suspend
|
39
|
+
|
40
|
+
assert_equal @pool.size, threads.uniq.size
|
41
|
+
assert_equal (0..9).map { |i| i * 10}, results.sort
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_process_with_exception
|
45
|
+
result = nil
|
46
|
+
begin
|
47
|
+
result = @pool.process { raise 'foo' }
|
48
|
+
rescue => result
|
49
|
+
end
|
50
|
+
|
51
|
+
assert_kind_of RuntimeError, result
|
52
|
+
assert_equal 'foo', result.message
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_cast
|
56
|
+
t0 = Time.now
|
57
|
+
threads = []
|
58
|
+
buffer = []
|
59
|
+
10.times do |i|
|
60
|
+
@pool.cast do
|
61
|
+
sleep 0.01
|
62
|
+
threads << Thread.current
|
63
|
+
buffer << i
|
64
|
+
end
|
65
|
+
end
|
66
|
+
elapsed = Time.now - t0
|
67
|
+
|
68
|
+
assert elapsed < 0.005
|
69
|
+
assert buffer.size < 2
|
70
|
+
|
71
|
+
sleep 0.04
|
72
|
+
assert_equal @pool.size, threads.uniq.size
|
73
|
+
assert_equal (0..9).to_a, buffer.sort
|
74
|
+
end
|
75
|
+
end
|
data/test/test_throttler.rb
CHANGED
data/test/test_trace.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
class TraceTest < MiniTest::Test
|
6
|
+
def test_tracing_disabled
|
7
|
+
records = []
|
8
|
+
t = Polyphony::Trace.new { |r| records << r if r[:event] =~ /^fiber_/ }
|
9
|
+
t.enable
|
10
|
+
snooze
|
11
|
+
assert_equal 0, records.size
|
12
|
+
ensure
|
13
|
+
t.disable
|
14
|
+
Gyro.trace(nil)
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_tracing_enabled
|
18
|
+
records = []
|
19
|
+
t = Polyphony::Trace.new { |r| records << r if r[:event] =~ /^fiber_/ }
|
20
|
+
t.enable
|
21
|
+
Gyro.trace(true)
|
22
|
+
snooze
|
23
|
+
t.disable
|
24
|
+
|
25
|
+
assert_equal 3, records.size
|
26
|
+
events = records.map { |r| r[:event] }
|
27
|
+
assert_equal [:fiber_schedule, :fiber_switchpoint, :fiber_run], events
|
28
|
+
assert_equal [Fiber.current], records.map { |r| r[:fiber] }.uniq
|
29
|
+
ensure
|
30
|
+
t.disable
|
31
|
+
Gyro.trace(nil)
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_2_fiber_trace
|
35
|
+
records = []
|
36
|
+
t = Polyphony::Trace.new { |r| records << r if r[:event] =~ /^fiber_/ }
|
37
|
+
t.enable
|
38
|
+
Gyro.trace(true)
|
39
|
+
|
40
|
+
f = spin { sleep 0 }
|
41
|
+
suspend
|
42
|
+
sleep 0
|
43
|
+
|
44
|
+
events = records.map { |r| [r[:fiber], r[:event]] }
|
45
|
+
assert_equal [
|
46
|
+
[f, :fiber_create],
|
47
|
+
[f, :fiber_schedule],
|
48
|
+
[Fiber.current, :fiber_switchpoint],
|
49
|
+
[f, :fiber_run],
|
50
|
+
[f, :fiber_switchpoint],
|
51
|
+
[f, :fiber_ev_loop_enter],
|
52
|
+
[f, :fiber_schedule],
|
53
|
+
[f, :fiber_ev_loop_leave],
|
54
|
+
[f, :fiber_run],
|
55
|
+
[f, :fiber_terminate],
|
56
|
+
[Fiber.current, :fiber_switchpoint],
|
57
|
+
[Fiber.current, :fiber_ev_loop_enter],
|
58
|
+
[Fiber.current, :fiber_schedule],
|
59
|
+
[Fiber.current, :fiber_ev_loop_leave],
|
60
|
+
[Fiber.current, :fiber_run]
|
61
|
+
], events
|
62
|
+
ensure
|
63
|
+
t.disable
|
64
|
+
Gyro.trace(nil)
|
65
|
+
end
|
66
|
+
end
|