polyphony 0.36 → 0.42

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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +11 -2
  3. data/.gitignore +2 -2
  4. data/.rubocop.yml +30 -0
  5. data/CHANGELOG.md +28 -2
  6. data/Gemfile +0 -11
  7. data/Gemfile.lock +15 -14
  8. data/README.md +2 -1
  9. data/Rakefile +7 -3
  10. data/TODO.md +28 -95
  11. data/docs/_config.yml +56 -7
  12. data/docs/_sass/custom/custom.scss +0 -30
  13. data/docs/_sass/overrides.scss +0 -46
  14. data/docs/{user-guide → _user-guide}/all-about-timers.md +0 -0
  15. data/docs/_user-guide/index.md +9 -0
  16. data/docs/{user-guide → _user-guide}/web-server.md +0 -0
  17. data/docs/api-reference/fiber.md +2 -2
  18. data/docs/api-reference/index.md +9 -0
  19. data/docs/api-reference/polyphony-process.md +1 -1
  20. data/docs/api-reference/thread.md +1 -1
  21. data/docs/faq.md +21 -11
  22. data/docs/getting-started/index.md +10 -0
  23. data/docs/getting-started/installing.md +2 -6
  24. data/docs/getting-started/overview.md +507 -0
  25. data/docs/getting-started/tutorial.md +27 -19
  26. data/docs/index.md +3 -2
  27. data/docs/main-concepts/concurrency.md +0 -5
  28. data/docs/main-concepts/design-principles.md +69 -21
  29. data/docs/main-concepts/extending.md +1 -1
  30. data/docs/main-concepts/index.md +9 -0
  31. data/examples/core/01-spinning-up-fibers.rb +1 -0
  32. data/examples/core/03-interrupting.rb +4 -1
  33. data/examples/core/04-handling-signals.rb +19 -0
  34. data/examples/core/xx-agent.rb +102 -0
  35. data/examples/core/xx-fork-cleanup.rb +22 -0
  36. data/examples/core/xx-sleeping.rb +14 -6
  37. data/examples/io/tunnel.rb +48 -0
  38. data/examples/io/xx-irb.rb +1 -1
  39. data/examples/performance/thread-vs-fiber/polyphony_mt_server.rb +7 -6
  40. data/examples/performance/thread-vs-fiber/polyphony_server.rb +13 -36
  41. data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +58 -0
  42. data/examples/performance/xx-array.rb +11 -0
  43. data/examples/performance/xx-fiber-switch.rb +9 -0
  44. data/examples/performance/xx-snooze.rb +15 -0
  45. data/ext/{gyro → polyphony}/extconf.rb +2 -2
  46. data/ext/{gyro → polyphony}/fiber.c +18 -22
  47. data/ext/{gyro → polyphony}/libev.c +0 -0
  48. data/ext/{gyro → polyphony}/libev.h +0 -0
  49. data/ext/polyphony/libev_agent.c +718 -0
  50. data/ext/polyphony/libev_queue.c +216 -0
  51. data/ext/{gyro/gyro.c → polyphony/polyphony.c} +16 -46
  52. data/ext/{gyro/gyro.h → polyphony/polyphony.h} +25 -39
  53. data/ext/polyphony/polyphony_ext.c +23 -0
  54. data/ext/{gyro → polyphony}/socket.c +21 -18
  55. data/ext/polyphony/thread.c +206 -0
  56. data/ext/{gyro → polyphony}/tracing.c +1 -1
  57. data/lib/polyphony.rb +40 -44
  58. data/lib/polyphony/adapters/fs.rb +1 -4
  59. data/lib/polyphony/adapters/irb.rb +1 -1
  60. data/lib/polyphony/adapters/postgres.rb +6 -5
  61. data/lib/polyphony/adapters/process.rb +27 -23
  62. data/lib/polyphony/adapters/trace.rb +110 -105
  63. data/lib/polyphony/core/channel.rb +35 -35
  64. data/lib/polyphony/core/exceptions.rb +29 -29
  65. data/lib/polyphony/core/global_api.rb +94 -91
  66. data/lib/polyphony/core/resource_pool.rb +83 -83
  67. data/lib/polyphony/core/sync.rb +16 -16
  68. data/lib/polyphony/core/thread_pool.rb +49 -37
  69. data/lib/polyphony/core/throttler.rb +30 -23
  70. data/lib/polyphony/event.rb +27 -0
  71. data/lib/polyphony/extensions/core.rb +25 -17
  72. data/lib/polyphony/extensions/fiber.rb +269 -267
  73. data/lib/polyphony/extensions/io.rb +56 -26
  74. data/lib/polyphony/extensions/openssl.rb +5 -9
  75. data/lib/polyphony/extensions/socket.rb +29 -10
  76. data/lib/polyphony/extensions/thread.rb +19 -12
  77. data/lib/polyphony/net.rb +64 -60
  78. data/lib/polyphony/version.rb +1 -1
  79. data/polyphony.gemspec +4 -7
  80. data/test/helper.rb +14 -1
  81. data/test/stress.rb +17 -12
  82. data/test/test_agent.rb +124 -0
  83. data/test/{test_async.rb → test_event.rb} +15 -7
  84. data/test/test_ext.rb +25 -4
  85. data/test/test_fiber.rb +19 -10
  86. data/test/test_global_api.rb +4 -4
  87. data/test/test_io.rb +46 -24
  88. data/test/test_queue.rb +74 -0
  89. data/test/test_signal.rb +3 -40
  90. data/test/test_socket.rb +33 -0
  91. data/test/test_thread.rb +38 -16
  92. data/test/test_thread_pool.rb +2 -2
  93. data/test/test_throttler.rb +0 -1
  94. data/test/test_trace.rb +6 -5
  95. metadata +41 -57
  96. data/docs/_includes/nav.html +0 -51
  97. data/docs/_includes/prevnext.html +0 -17
  98. data/docs/_layouts/default.html +0 -106
  99. data/docs/api-reference.md +0 -11
  100. data/docs/api-reference/gyro-async.md +0 -57
  101. data/docs/api-reference/gyro-child.md +0 -29
  102. data/docs/api-reference/gyro-queue.md +0 -44
  103. data/docs/api-reference/gyro-timer.md +0 -51
  104. data/docs/api-reference/gyro.md +0 -25
  105. data/docs/getting-started.md +0 -10
  106. data/docs/main-concepts.md +0 -10
  107. data/docs/user-guide.md +0 -10
  108. data/examples/core/forever_sleep.rb +0 -19
  109. data/ext/gyro/async.c +0 -148
  110. data/ext/gyro/child.c +0 -127
  111. data/ext/gyro/gyro_ext.c +0 -33
  112. data/ext/gyro/io.c +0 -474
  113. data/ext/gyro/queue.c +0 -142
  114. data/ext/gyro/selector.c +0 -205
  115. data/ext/gyro/signal.c +0 -118
  116. data/ext/gyro/thread.c +0 -298
  117. data/ext/gyro/timer.c +0 -134
  118. data/test/test_timer.rb +0 -56
@@ -1,107 +1,107 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- export_default :ResourcePool
4
-
5
- # Implements a limited resource pool
6
- class ResourcePool
7
- attr_reader :limit, :size
8
-
9
- # Initializes a new resource pool
10
- # @param opts [Hash] options
11
- # @param &block [Proc] allocator block
12
- def initialize(opts, &block)
13
- @allocator = block
3
+ module Polyphony
4
+ # Implements a limited resource pool
5
+ class ResourcePool
6
+ attr_reader :limit, :size
7
+
8
+ # Initializes a new resource pool
9
+ # @param opts [Hash] options
10
+ # @param &block [Proc] allocator block
11
+ def initialize(opts, &block)
12
+ @allocator = block
13
+
14
+ @stock = []
15
+ @queue = []
16
+
17
+ @limit = opts[:limit] || 4
18
+ @size = 0
19
+ end
14
20
 
15
- @stock = []
16
- @queue = []
21
+ def available
22
+ @stock.size
23
+ end
17
24
 
18
- @limit = opts[:limit] || 4
19
- @size = 0
20
- end
25
+ def acquire
26
+ Thread.current.agent.ref
27
+ resource = wait_for_resource
28
+ return unless resource
21
29
 
22
- def available
23
- @stock.size
24
- end
30
+ yield resource
31
+ ensure
32
+ Thread.current.agent.unref
33
+ release(resource) if resource
34
+ end
25
35
 
26
- def acquire
27
- Gyro.ref
28
- resource = wait_for_resource
29
- return unless resource
36
+ def wait_for_resource
37
+ fiber = Fiber.current
38
+ @queue << fiber
39
+ ready_resource = from_stock
40
+ return ready_resource if ready_resource
30
41
 
31
- yield resource
32
- ensure
33
- Gyro.unref
34
- release(resource) if resource
35
- end
42
+ suspend
43
+ ensure
44
+ @queue.delete(fiber)
45
+ end
36
46
 
37
- def wait_for_resource
38
- fiber = Fiber.current
39
- @queue << fiber
40
- ready_resource = from_stock
41
- return ready_resource if ready_resource
47
+ def release(resource)
48
+ if resource.__discarded__
49
+ @size -= 1
50
+ elsif resource
51
+ return_to_stock(resource)
52
+ dequeue
53
+ end
54
+ end
42
55
 
43
- suspend
44
- ensure
45
- @queue.delete(fiber)
46
- end
56
+ def dequeue
57
+ return if @queue.empty? || @stock.empty?
47
58
 
48
- def release(resource)
49
- if resource.__discarded__
50
- @size -= 1
51
- elsif resource
52
- return_to_stock(resource)
53
- dequeue
59
+ @queue.shift.schedule(@stock.shift)
54
60
  end
55
- end
56
-
57
- def dequeue
58
- return if @queue.empty? || @stock.empty?
59
61
 
60
- @queue.shift.schedule(@stock.shift)
61
- end
62
+ def return_to_stock(resource)
63
+ @stock << resource
64
+ end
62
65
 
63
- def return_to_stock(resource)
64
- @stock << resource
65
- end
66
+ def from_stock
67
+ @stock.shift || (@size < @limit && allocate)
68
+ end
66
69
 
67
- def from_stock
68
- @stock.shift || (@size < @limit && allocate)
69
- end
70
+ def method_missing(sym, *args, &block)
71
+ acquire { |r| r.send(sym, *args, &block) }
72
+ end
70
73
 
71
- def method_missing(sym, *args, &block)
72
- acquire { |r| r.send(sym, *args, &block) }
73
- end
74
+ def respond_to_missing?(*_args)
75
+ true
76
+ end
74
77
 
75
- def respond_to_missing?(*_args)
76
- true
77
- end
78
+ # Extension to allow discarding of resources
79
+ module ResourceExtensions
80
+ def __discarded__
81
+ @__discarded__
82
+ end
78
83
 
79
- # Extension to allow discarding of resources
80
- module ResourceExtensions
81
- def __discarded__
82
- @__discarded__
84
+ def __discard__
85
+ @__discarded__ = true
86
+ end
83
87
  end
84
88
 
85
- def __discard__
86
- @__discarded__ = true
89
+ # Allocates a resource
90
+ # @return [any] allocated resource
91
+ def allocate
92
+ @size += 1
93
+ @allocator.().tap { |r| r.extend ResourceExtensions }
87
94
  end
88
- end
89
-
90
- # Allocates a resource
91
- # @return [any] allocated resource
92
- def allocate
93
- @size += 1
94
- @allocator.().tap { |r| r.extend ResourceExtensions }
95
- end
96
95
 
97
- def <<(resource)
98
- @size += 1
99
- resource.extend ResourceExtensions
100
- @stock << resource
101
- dequeue
102
- end
96
+ def <<(resource)
97
+ @size += 1
98
+ resource.extend ResourceExtensions
99
+ @stock << resource
100
+ dequeue
101
+ end
103
102
 
104
- def preheat!
105
- (@limit - @size).times { @stock << allocate }
103
+ def preheat!
104
+ (@limit - @size).times { @stock << allocate }
105
+ end
106
106
  end
107
107
  end
@@ -1,21 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- export :Mutex
3
+ module Polyphony
4
+ # Implements mutex lock for synchronizing access to a shared resource
5
+ class Mutex
6
+ def initialize
7
+ @waiting_fibers = Polyphony::Queue.new
8
+ end
4
9
 
5
- # Implements mutex lock for synchronizing access to a shared resource
6
- class Mutex
7
- def initialize
8
- @waiting_fibers = []
9
- end
10
-
11
- def synchronize
12
- fiber = Fiber.current
13
- @waiting_fibers << fiber
14
- suspend if @waiting_fibers.size > 1
15
- yield
16
- ensure
17
- @waiting_fibers.delete(fiber)
18
- @waiting_fibers.first&.schedule
19
- snooze
10
+ def synchronize
11
+ fiber = Fiber.current
12
+ @waiting_fibers << fiber
13
+ suspend if @waiting_fibers.size > 1
14
+ yield
15
+ ensure
16
+ @waiting_fibers.delete(fiber)
17
+ @waiting_fibers.first&.schedule
18
+ snooze
19
+ end
20
20
  end
21
21
  end
@@ -1,52 +1,64 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- export_default :ThreadPool
4
-
5
3
  require 'etc'
6
4
 
7
- # Implements a pool of threads
8
- class ThreadPool
9
- attr_reader :size
5
+ module Polyphony
6
+ # Implements a pool of threads
7
+ class ThreadPool
8
+ attr_reader :size
10
9
 
11
- def self.process(&block)
12
- @default_pool ||= new
13
- @default_pool.process(&block)
14
- end
10
+ def self.process(&block)
11
+ @default_pool ||= new
12
+ @default_pool.process(&block)
13
+ end
15
14
 
16
- def initialize(size = Etc.nprocessors)
17
- @size = size
18
- @task_queue = ::Queue.new
19
- @threads = (1..@size).map { Thread.new { thread_loop } }
20
- end
15
+ def self.reset
16
+ return unless @default_pool
21
17
 
22
- def process(&block)
23
- setup unless @task_queue
18
+ @default_pool.stop
19
+ @default_pool = nil
20
+ end
24
21
 
25
- async = Fiber.current.auto_async
26
- @task_queue << [block, async]
27
- async.await
28
- end
22
+ def initialize(size = Etc.nprocessors)
23
+ @size = size
24
+ @task_queue = Polyphony::Queue.new
25
+ @threads = (1..@size).map { Thread.new { thread_loop } }
26
+ end
29
27
 
30
- def cast(&block)
31
- setup unless @task_queue
28
+ def process(&block)
29
+ setup unless @task_queue
32
30
 
33
- @task_queue << [block, nil]
34
- self
35
- end
31
+ watcher = Fiber.current.auto_watcher
32
+ @task_queue << [block, watcher]
33
+ watcher.await
34
+ end
36
35
 
37
- def busy?
38
- !@task_queue.empty?
39
- end
36
+ def cast(&block)
37
+ setup unless @task_queue
40
38
 
41
- def thread_loop
42
- loop { run_queued_task }
43
- end
39
+ @task_queue << [block, nil]
40
+ self
41
+ end
42
+
43
+ def busy?
44
+ !@task_queue.empty?
45
+ end
46
+
47
+ def thread_loop
48
+ loop { run_queued_task }
49
+ end
50
+
51
+ def run_queued_task
52
+ (block, watcher) = @task_queue.pop
53
+ result = block.()
54
+ watcher&.signal(result)
55
+ rescue Exception => e
56
+ watcher ? watcher.signal(e) : raise(e)
57
+ end
44
58
 
45
- def run_queued_task
46
- (block, watcher) = @task_queue.pop
47
- result = block.()
48
- watcher&.signal(result)
49
- rescue Exception => e
50
- watcher ? watcher.signal(e) : raise(e)
59
+ def stop
60
+ @threads.each(&:kill)
61
+ @threads.each(&:join)
62
+ end
51
63
  end
52
64
  end
@@ -1,34 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- export_default :Throttler
3
+ module Polyphony
4
+ # Implements general-purpose throttling
5
+ class Throttler
6
+ def initialize(rate)
7
+ @rate = rate_from_argument(rate)
8
+ @min_dt = 1.0 / @rate
9
+ @next_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
10
+ end
4
11
 
5
- # Implements general-purpose throttling
6
- class Throttler
7
- def initialize(rate)
8
- @rate = rate_from_argument(rate)
9
- @min_dt = 1.0 / @rate
10
- end
12
+ def call
13
+ now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
14
+ delta = @next_time - now
15
+ Thread.current.agent.sleep(delta) if delta > 0
16
+ yield self
11
17
 
12
- def call(&block)
13
- @timer ||= Gyro::Timer.new(0, @min_dt)
14
- @timer.await
15
- block.call(self)
16
- end
17
- alias_method :process, :call
18
+ loop do
19
+ @next_time += @min_dt
20
+ break if @next_time > now
21
+ end
22
+ end
23
+ alias_method :process, :call
18
24
 
19
- def stop
20
- @timer&.stop
21
- end
25
+ def stop
26
+ @stop = true
27
+ end
22
28
 
23
- private
29
+ private
24
30
 
25
- def rate_from_argument(arg)
26
- return arg if arg.is_a?(Numeric)
31
+ def rate_from_argument(arg)
32
+ return arg if arg.is_a?(Numeric)
27
33
 
28
- if arg.is_a?(Hash)
29
- return 1.0 / arg[:interval] if arg[:interval]
30
- return arg[:rate] if arg[:rate]
34
+ if arg.is_a?(Hash)
35
+ return 1.0 / arg[:interval] if arg[:interval]
36
+ return arg[:rate] if arg[:rate]
37
+ end
38
+ raise "Invalid rate argument #{arg.inspect}"
31
39
  end
32
- raise "Invalid rate argument #{arg.inspect}"
33
40
  end
34
41
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polyphony
4
+ # Event watcher for thread-safe synchronisation
5
+ class Event
6
+ def initialize
7
+ @i, @o = IO.pipe
8
+ end
9
+
10
+ def await
11
+ Thread.current.agent.read(@i, +'', 8192, false)
12
+ raise @value if @value.is_a?(Exception)
13
+
14
+ @value
15
+ end
16
+
17
+ def await_no_raise
18
+ Thread.current.agent.read(@i, +'', 8192, false)
19
+ @value
20
+ end
21
+
22
+ def signal(value = nil)
23
+ @value = value
24
+ Thread.current.agent.write(@o, '1')
25
+ end
26
+ end
27
+ end
@@ -4,12 +4,10 @@ require 'fiber'
4
4
  require 'timeout'
5
5
  require 'open3'
6
6
 
7
- Exceptions = import('../core/exceptions')
7
+ require_relative '../core/exceptions'
8
8
 
9
9
  # Exeption overrides
10
10
  class ::Exception
11
- EXIT_EXCEPTION_CLASSES = [::Interrupt, ::SystemExit].freeze
12
-
13
11
  class << self
14
12
  attr_accessor :__disable_sanitized_backtrace__
15
13
  end
@@ -22,9 +20,9 @@ class ::Exception
22
20
  orig_initialize(*args)
23
21
  end
24
22
 
25
- alias_method_once :orig_backtrace, :backtrace
23
+ alias_method :orig_backtrace, :backtrace
26
24
  def backtrace
27
- unless @first_backtrace_call || EXIT_EXCEPTION_CLASSES.include?(self.class)
25
+ unless @first_backtrace_call
28
26
  @first_backtrace_call = true
29
27
  return orig_backtrace
30
28
  end
@@ -52,10 +50,13 @@ end
52
50
 
53
51
  # Overrides for Process
54
52
  module ::Process
55
- def self.detach(pid)
56
- fiber = spin { Gyro::Child.new(pid).await }
57
- fiber.define_singleton_method(:pid) { pid }
58
- fiber
53
+ class << self
54
+ alias_method :orig_detach, :detach
55
+ def detach(pid)
56
+ fiber = spin { Thread.current.agent.waitpid(pid) }
57
+ fiber.define_singleton_method(:pid) { pid }
58
+ fiber
59
+ end
59
60
  end
60
61
  end
61
62
 
@@ -67,10 +68,9 @@ module ::Kernel
67
68
  def `(cmd)
68
69
  Open3.popen3(cmd) do |i, o, e, _t|
69
70
  i.close
70
- while (l = e.readpartial(8192))
71
- $stderr << l
72
- end
73
- o.read
71
+ err = e.read
72
+ $stderr << err if err
73
+ o.read || ''
74
74
  end
75
75
  end
76
76
 
@@ -91,6 +91,7 @@ module ::Kernel
91
91
  def gets(*_args)
92
92
  if !ARGV.empty? || @gets_fiber
93
93
  @gets_fiber ||= Fiber.new(&ARGV_GETS_LOOP)
94
+ @gets_fiber.thread = Thread.current
94
95
  result = @gets_fiber.alive? && @gets_fiber.safe_transfer(Fiber.current)
95
96
  return result if result
96
97
 
@@ -104,21 +105,28 @@ module ::Kernel
104
105
  def system(*args)
105
106
  Open3.popen2(*args) do |i, o, _t|
106
107
  i.close
107
- while (l = o.readpartial(8192))
108
- $stdout << l
109
- end
108
+ pipe_to_eof(o, $stdout)
110
109
  end
111
110
  true
112
111
  rescue SystemCallError
113
112
  nil
114
113
  end
114
+
115
+ def pipe_to_eof(src, dest)
116
+ loop do
117
+ data = src.readpartial(8192)
118
+ dest << data
119
+ rescue EOFError
120
+ break
121
+ end
122
+ end
115
123
  end
116
124
 
117
125
  # Override Timeout to use cancel scope
118
126
  module ::Timeout
119
127
  def self.timeout(sec, klass = nil, message = nil, &block)
120
128
  cancel_after(sec, &block)
121
- rescue Exceptions::Cancel => e
129
+ rescue Polyphony::Cancel => e
122
130
  error = klass ? klass.new(message) : ::Timeout::Error.new
123
131
  error.set_backtrace(e.backtrace)
124
132
  raise error