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