polyphony 0.27 → 0.28

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/CHANGELOG.md +13 -0
  4. data/Gemfile +12 -1
  5. data/Gemfile.lock +83 -5
  6. data/Rakefile +4 -0
  7. data/TODO.md +11 -20
  8. data/docs/_config.yml +15 -0
  9. data/docs/_includes/nav.html +47 -0
  10. data/docs/_sass/custom/custom.scss +5 -0
  11. data/docs/_sass/overrides.scss +45 -0
  12. data/docs/assets/img/echo-fibers.svg +1 -0
  13. data/docs/assets/img/sleeping-fiber.svg +1 -0
  14. data/docs/faq.md +182 -0
  15. data/docs/getting-started/installing.md +10 -2
  16. data/docs/getting-started/tutorial.md +333 -26
  17. data/docs/getting-started.md +10 -0
  18. data/docs/index.md +91 -0
  19. data/docs/technical-overview/concurrency.md +78 -16
  20. data/docs/technical-overview/design-principles.md +7 -0
  21. data/docs/technical-overview/exception-handling.md +57 -9
  22. data/docs/technical-overview/extending.md +7 -0
  23. data/docs/technical-overview/fiber-scheduling.md +128 -18
  24. data/docs/technical-overview.md +10 -0
  25. data/docs/user-guide/web-server.md +7 -0
  26. data/docs/user-guide.md +10 -0
  27. data/examples/core/xx-deadlock.rb +8 -0
  28. data/examples/core/xx-state-machine.rb +51 -0
  29. data/examples/core/xx-trace.rb +80 -0
  30. data/examples/interfaces/pg_notify.rb +35 -0
  31. data/examples/io/xx-httparty.rb +31 -6
  32. data/examples/io/xx-irb.rb +1 -11
  33. data/examples/io/xx-switch.rb +15 -0
  34. data/ext/gyro/gyro.c +77 -38
  35. data/ext/gyro/gyro.h +15 -5
  36. data/ext/gyro/gyro_ext.c +3 -0
  37. data/ext/gyro/thread.c +47 -32
  38. data/ext/gyro/tracing.c +11 -0
  39. data/lib/polyphony/core/global_api.rb +11 -4
  40. data/lib/polyphony/core/supervisor.rb +1 -0
  41. data/lib/polyphony/core/thread_pool.rb +44 -35
  42. data/lib/polyphony/extensions/fiber.rb +19 -9
  43. data/lib/polyphony/extensions/io.rb +14 -14
  44. data/lib/polyphony/extensions/socket.rb +3 -3
  45. data/lib/polyphony/irb.rb +13 -0
  46. data/lib/polyphony/postgres.rb +15 -0
  47. data/lib/polyphony/trace.rb +98 -0
  48. data/lib/polyphony/version.rb +1 -1
  49. data/lib/polyphony.rb +1 -0
  50. data/polyphony.gemspec +21 -12
  51. data/test/helper.rb +3 -2
  52. data/test/test_fiber.rb +53 -3
  53. data/test/test_global_api.rb +12 -0
  54. data/test/test_gyro.rb +2 -2
  55. data/test/test_supervisor.rb +12 -0
  56. data/test/test_thread.rb +12 -0
  57. data/test/test_thread_pool.rb +75 -0
  58. data/test/test_throttler.rb +6 -0
  59. data/test/test_trace.rb +66 -0
  60. metadata +99 -9
  61. data/docs/README.md +0 -36
  62. data/docs/summary.md +0 -60
  63. 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.27'
4
+ VERSION = '0.28'
5
5
  end
data/lib/polyphony.rb CHANGED
@@ -35,6 +35,7 @@ module Polyphony
35
35
  Sync: './polyphony/core/sync',
36
36
  ThreadPool: './polyphony/core/thread_pool',
37
37
  Throttler: './polyphony/core/throttler',
38
+ Trace: './polyphony/trace',
38
39
  Websocket: './polyphony/websocket'
39
40
  )
40
41
 
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://dfab.gitbook.io/polyphony/"
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', '~>1.0'
24
+ s.add_runtime_dependency 'modulation', '~>1.0'
23
25
 
24
- s.add_development_dependency 'httparty', '0.17.0'
25
- s.add_development_dependency 'localhost', '1.1.4'
26
- s.add_development_dependency 'minitest', '5.11.3'
27
- s.add_development_dependency 'minitest-reporters', '1.4.2'
28
- s.add_development_dependency 'simplecov', '0.17.1'
29
- s.add_development_dependency 'pg', '1.1.3'
30
- s.add_development_dependency 'rake-compiler', '1.0.5'
31
- s.add_development_dependency 'redis', '4.1.0'
32
- s.add_development_dependency 'hiredis', '0.6.3'
33
- s.add_development_dependency 'http_parser.rb', '~>0.6.0'
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 :scheduled, f.state
276
+ assert_equal :runnable, f.state
267
277
  assert_equal :running, Fiber.current.state
268
278
  snooze
269
- assert_equal :scheduled, f.state
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\' (scheduled)>',
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
@@ -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 :suspended, f.state
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 :scheduled, f.state
17
+ assert_equal :runnable, f.state
18
18
  snooze
19
19
  assert_equal :dead, f.state
20
20
  end
@@ -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
@@ -38,4 +38,10 @@ class ThrottlerTest < MiniTest::Test
38
38
  ensure
39
39
  t.stop
40
40
  end
41
+
42
+ def test_throttler_with_invalid_argument
43
+ assert_raises RuntimeError do
44
+ Polyphony::Throttler.new(:foobar)
45
+ end
46
+ end
41
47
  end
@@ -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