polyphony 0.39 → 0.43.1

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 (117) 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 +23 -4
  6. data/Gemfile.lock +15 -12
  7. data/README.md +2 -1
  8. data/Rakefile +3 -3
  9. data/TODO.md +27 -97
  10. data/docs/_config.yml +56 -7
  11. data/docs/_sass/custom/custom.scss +6 -26
  12. data/docs/_sass/overrides.scss +0 -46
  13. data/docs/{user-guide → _user-guide}/all-about-timers.md +0 -0
  14. data/docs/_user-guide/index.md +9 -0
  15. data/docs/{user-guide → _user-guide}/web-server.md +0 -0
  16. data/docs/api-reference/fiber.md +2 -2
  17. data/docs/api-reference/index.md +9 -0
  18. data/docs/api-reference/polyphony-process.md +1 -1
  19. data/docs/api-reference/thread.md +1 -1
  20. data/docs/faq.md +21 -11
  21. data/docs/favicon.ico +0 -0
  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 +486 -0
  25. data/docs/getting-started/tutorial.md +27 -19
  26. data/docs/index.md +6 -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/docs/polyphony-logo.png +0 -0
  32. data/examples/core/01-spinning-up-fibers.rb +1 -0
  33. data/examples/core/03-interrupting.rb +4 -1
  34. data/examples/core/04-handling-signals.rb +19 -0
  35. data/examples/core/xx-agent.rb +102 -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 +17 -23
  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/polyphony/polyphony.c +73 -0
  52. data/ext/{gyro/gyro.h → polyphony/polyphony.h} +19 -39
  53. data/ext/polyphony/polyphony_ext.c +21 -0
  54. data/ext/polyphony/thread.c +200 -0
  55. data/ext/{gyro → polyphony}/tracing.c +1 -1
  56. data/lib/polyphony.rb +19 -14
  57. data/lib/polyphony/adapters/irb.rb +1 -1
  58. data/lib/polyphony/adapters/postgres.rb +6 -5
  59. data/lib/polyphony/adapters/process.rb +5 -5
  60. data/lib/polyphony/adapters/trace.rb +28 -28
  61. data/lib/polyphony/core/channel.rb +3 -3
  62. data/lib/polyphony/core/exceptions.rb +1 -1
  63. data/lib/polyphony/core/global_api.rb +13 -11
  64. data/lib/polyphony/core/resource_pool.rb +3 -3
  65. data/lib/polyphony/core/sync.rb +2 -2
  66. data/lib/polyphony/core/thread_pool.rb +6 -6
  67. data/lib/polyphony/core/throttler.rb +13 -6
  68. data/lib/polyphony/event.rb +27 -0
  69. data/lib/polyphony/extensions/core.rb +22 -14
  70. data/lib/polyphony/extensions/fiber.rb +4 -4
  71. data/lib/polyphony/extensions/io.rb +59 -25
  72. data/lib/polyphony/extensions/openssl.rb +36 -16
  73. data/lib/polyphony/extensions/socket.rb +28 -10
  74. data/lib/polyphony/extensions/thread.rb +16 -9
  75. data/lib/polyphony/net.rb +9 -9
  76. data/lib/polyphony/version.rb +1 -1
  77. data/polyphony.gemspec +4 -4
  78. data/test/helper.rb +12 -8
  79. data/test/test_agent.rb +124 -0
  80. data/test/{test_async.rb → test_event.rb} +15 -7
  81. data/test/test_ext.rb +25 -4
  82. data/test/test_fiber.rb +19 -10
  83. data/test/test_global_api.rb +11 -11
  84. data/test/test_io.rb +44 -29
  85. data/test/test_queue.rb +74 -0
  86. data/test/test_signal.rb +3 -40
  87. data/test/test_socket.rb +34 -0
  88. data/test/test_thread.rb +38 -17
  89. data/test/test_thread_pool.rb +2 -2
  90. data/test/test_throttler.rb +5 -3
  91. data/test/test_trace.rb +6 -5
  92. metadata +41 -43
  93. data/docs/_includes/nav.html +0 -51
  94. data/docs/_includes/prevnext.html +0 -17
  95. data/docs/_layouts/default.html +0 -106
  96. data/docs/api-reference.md +0 -11
  97. data/docs/api-reference/gyro-async.md +0 -57
  98. data/docs/api-reference/gyro-child.md +0 -29
  99. data/docs/api-reference/gyro-queue.md +0 -44
  100. data/docs/api-reference/gyro-timer.md +0 -51
  101. data/docs/api-reference/gyro.md +0 -25
  102. data/docs/getting-started.md +0 -10
  103. data/docs/main-concepts.md +0 -10
  104. data/docs/user-guide.md +0 -10
  105. data/examples/core/forever_sleep.rb +0 -19
  106. data/ext/gyro/async.c +0 -162
  107. data/ext/gyro/child.c +0 -141
  108. data/ext/gyro/gyro.c +0 -103
  109. data/ext/gyro/gyro_ext.c +0 -33
  110. data/ext/gyro/io.c +0 -489
  111. data/ext/gyro/queue.c +0 -142
  112. data/ext/gyro/selector.c +0 -228
  113. data/ext/gyro/signal.c +0 -133
  114. data/ext/gyro/socket.c +0 -210
  115. data/ext/gyro/thread.c +0 -308
  116. data/ext/gyro/timer.c +0 -151
  117. data/test/test_timer.rb +0 -32
@@ -6,17 +6,24 @@ module Polyphony
6
6
  def initialize(rate)
7
7
  @rate = rate_from_argument(rate)
8
8
  @min_dt = 1.0 / @rate
9
+ @next_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
9
10
  end
10
11
 
11
- def call(&block)
12
- @timer ||= Gyro::Timer.new(0, @min_dt)
13
- @timer.await
14
- block.call(self)
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
17
+
18
+ loop do
19
+ @next_time += @min_dt
20
+ break if @next_time > now
21
+ end
15
22
  end
16
23
  alias_method :process, :call
17
24
 
18
25
  def stop
19
- @timer&.stop
26
+ @stop = true
20
27
  end
21
28
 
22
29
  private
@@ -31,4 +38,4 @@ module Polyphony
31
38
  raise "Invalid rate argument #{arg.inspect}"
32
39
  end
33
40
  end
34
- end
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
@@ -8,8 +8,6 @@ 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
@@ -24,7 +22,7 @@ class ::Exception
24
22
 
25
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,14 +105,21 @@ 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
@@ -238,7 +238,7 @@ module Polyphony
238
238
  @parent = parent
239
239
  @caller = caller
240
240
  @block = block
241
- @mailbox = Gyro::Queue.new
241
+ @mailbox = Polyphony::Queue.new
242
242
  __fiber_trace__(:fiber_create, self)
243
243
  schedule
244
244
  end
@@ -268,7 +268,7 @@ module Polyphony
268
268
  # allows the fiber to be scheduled and to receive messages.
269
269
  def setup_raw
270
270
  @thread = Thread.current
271
- @mailbox = Gyro::Queue.new
271
+ @mailbox = Polyphony::Queue.new
272
272
  end
273
273
 
274
274
  def setup_main_fiber
@@ -277,11 +277,11 @@ module Polyphony
277
277
  @thread = Thread.current
278
278
  @running = true
279
279
  @children&.clear
280
- @mailbox = Gyro::Queue.new
280
+ @mailbox = Polyphony::Queue.new
281
281
  end
282
282
 
283
283
  def restart_self(first_value)
284
- @mailbox = Gyro::Queue.new
284
+ @mailbox = Polyphony::Queue.new
285
285
  @when_done_procs = nil
286
286
  @waiting_fibers = nil
287
287
  run(first_value)
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'open3'
4
4
 
5
- # IO overrides
5
+ # IO class method patches
6
6
  class ::IO
7
7
  class << self
8
8
  alias_method :orig_binread, :binread
@@ -72,7 +72,10 @@ class ::IO
72
72
  Open3.popen2(cmd) { |_i, o, _t| yield o }
73
73
  end
74
74
  end
75
+ end
75
76
 
77
+ # IO instance method patches
78
+ class ::IO
76
79
  # def each(sep = $/, limit = nil, chomp: nil)
77
80
  # sep, limit = $/, sep if sep.is_a?(Integer)
78
81
  # end
@@ -93,6 +96,39 @@ class ::IO
93
96
  # def getc
94
97
  # end
95
98
 
99
+ alias_method :orig_read, :read
100
+ def read(len = 1 << 30)
101
+ @read_buffer ||= +''
102
+ result = Thread.current.agent.read(self, @read_buffer, len, true)
103
+ return nil unless result
104
+
105
+ already_read = @read_buffer
106
+ @read_buffer = +''
107
+ already_read
108
+ end
109
+
110
+ alias_method :orig_readpartial, :read
111
+ def readpartial(len)
112
+ @read_buffer ||= +''
113
+ result = Thread.current.agent.read(self, @read_buffer, len, false)
114
+ raise EOFError unless result
115
+
116
+ already_read = @read_buffer
117
+ @read_buffer = +''
118
+ already_read
119
+ end
120
+
121
+ alias_method :orig_write, :write
122
+ def write(str)
123
+ Thread.current.agent.write(self, str)
124
+ end
125
+
126
+ alias_method :orig_write_chevron, :<<
127
+ def <<(str)
128
+ Thread.current.agent.write(self, str)
129
+ self
130
+ end
131
+
96
132
  alias_method :orig_gets, :gets
97
133
  def gets(sep = $/, _limit = nil, _chomp: nil)
98
134
  if sep.is_a?(Integer)
@@ -101,23 +137,17 @@ class ::IO
101
137
  end
102
138
  sep_size = sep.bytesize
103
139
 
104
- @gets_buffer ||= +''
140
+ @read_buffer ||= +''
105
141
 
106
142
  loop do
107
- idx = @gets_buffer.index(sep)
108
- return @gets_buffer.slice!(0, idx + sep_size) if idx
109
-
110
- if (data = readpartial(8192))
111
- @gets_buffer << data
112
- else
113
- return nil if @gets_buffer.empty?
143
+ idx = @read_buffer.index(sep)
144
+ return @read_buffer.slice!(0, idx + sep_size) if idx
114
145
 
115
- line = @gets_buffer.freeze
116
- @gets_buffer = +''
117
- return line
118
- end
146
+ data = readpartial(8192)
147
+ @read_buffer << data
148
+ rescue EOFError
149
+ return nil
119
150
  end
120
- # orig_gets(sep, limit, chomp: chomp)
121
151
  end
122
152
 
123
153
  # def print(*args)
@@ -171,16 +201,20 @@ class ::IO
171
201
  buf ? readpartial(maxlen, buf) : readpartial(maxlen)
172
202
  end
173
203
 
174
- alias_method :orig_read, :read
175
- def read(length = nil, outbuf = nil)
176
- if length
177
- return outbuf ? readpartial(length) : readpartial(length, outbuf)
178
- end
179
-
180
- until eof?
181
- outbuf ||= +''
182
- outbuf << readpartial(8192)
183
- end
184
- outbuf
204
+ def read_loop(&block)
205
+ Thread.current.agent.read_loop(self, &block)
185
206
  end
207
+
208
+ # alias_method :orig_read, :read
209
+ # def read(length = nil, outbuf = nil)
210
+ # if length
211
+ # return outbuf ? readpartial(length) : readpartial(length, outbuf)
212
+ # end
213
+
214
+ # until eof?
215
+ # outbuf ||= +''
216
+ # outbuf << readpartial(8192)
217
+ # end
218
+ # outbuf
219
+ # end
186
220
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'openssl'
4
-
5
4
  require_relative './socket'
6
5
 
7
6
  # Open ssl socket helper methods (to make it compatible with Socket API)
@@ -18,14 +17,36 @@ class ::OpenSSL::SSL::SSLSocket
18
17
  io.reuse_addr
19
18
  end
20
19
 
21
- def sysread(maxlen, buf)
22
- read_watcher = nil
23
- write_watcher = nil
20
+ alias_method :orig_accept, :accept
21
+ def accept
22
+ loop do
23
+ result = accept_nonblock(exception: false)
24
+ case result
25
+ when :wait_readable then Thread.current.agent.wait_io(io, false)
26
+ when :wait_writable then Thread.current.agent.wait_io(io, true)
27
+ else
28
+ return result
29
+ end
30
+ end
31
+ end
32
+
33
+ alias_method :orig_sysread, :sysread
34
+ def sysread(maxlen, buf = +'')
24
35
  loop do
25
36
  case (result = read_nonblock(maxlen, buf, exception: false))
26
- when :wait_readable then (read_watcher ||= io.read_watcher).await
27
- when :wait_writable then (write_watcher ||= io.write_watcher).await
28
- else result
37
+ when :wait_readable then Thread.current.agent.wait_io(io, false)
38
+ else return result
39
+ end
40
+ end
41
+ end
42
+
43
+ alias_method :orig_syswrite, :syswrite
44
+ def syswrite(buf)
45
+ loop do
46
+ case (result = write_nonblock(buf, exception: false))
47
+ when :wait_writable then Thread.current.agent.wait_io(io, true)
48
+ else
49
+ return result
29
50
  end
30
51
  end
31
52
  end
@@ -39,15 +60,14 @@ class ::OpenSSL::SSL::SSLSocket
39
60
  # @sync = osync
40
61
  end
41
62
 
42
- def syswrite(buf)
43
- read_watcher = nil
44
- write_watcher = nil
45
- loop do
46
- case (result = write_nonblock(buf, exception: false))
47
- when :wait_readable then (read_watcher ||= io.read_watcher).await
48
- when :wait_writable then (write_watcher ||= io.write_watcher).await
49
- else result
50
- end
63
+ def readpartial(maxlen, buf = +'')
64
+ result = sysread(maxlen, buf)
65
+ result || (raise EOFError)
66
+ end
67
+
68
+ def read_loop
69
+ while (data = sysread(8192))
70
+ yield data
51
71
  end
52
72
  end
53
73
  end
@@ -7,14 +7,21 @@ require_relative '../core/thread_pool'
7
7
 
8
8
  # Socket overrides (eventually rewritten in C)
9
9
  class ::Socket
10
+ def accept
11
+ Thread.current.agent.accept(self)
12
+ end
13
+
10
14
  NO_EXCEPTION = { exception: false }.freeze
11
15
 
12
16
  def connect(remotesockaddr)
13
17
  loop do
14
18
  result = connect_nonblock(remotesockaddr, **NO_EXCEPTION)
15
- return if result == 0
16
-
17
- result == :wait_writable ? write_watcher.await : (raise IOError)
19
+ case result
20
+ when 0 then return
21
+ when :wait_writable then Thread.current.agent.wait_io(self, true)
22
+ else
23
+ raise IOError
24
+ end
18
25
  end
19
26
  end
20
27
 
@@ -22,9 +29,12 @@ class ::Socket
22
29
  outbuf ||= +''
23
30
  loop do
24
31
  result = recv_nonblock(maxlen, flags, outbuf, **NO_EXCEPTION)
25
- raise IOError unless result
26
-
27
- result == :wait_readable ? read_watcher.await : (return result)
32
+ case result
33
+ when nil then raise IOError
34
+ when :wait_readable then Thread.current.agent.wait_io(self, false)
35
+ else
36
+ return result
37
+ end
28
38
  end
29
39
  end
30
40
 
@@ -32,9 +42,12 @@ class ::Socket
32
42
  @read_buffer ||= +''
33
43
  loop do
34
44
  result = recvfrom_nonblock(maxlen, flags, @read_buffer, **NO_EXCEPTION)
35
- raise IOError unless result
36
-
37
- result == :wait_readable ? read_watcher.await : (return result)
45
+ case result
46
+ when nil then raise IOError
47
+ when :wait_readable then Thread.current.agent.wait_io(self, false)
48
+ else
49
+ return result
50
+ end
38
51
  end
39
52
  end
40
53
 
@@ -115,6 +128,11 @@ class ::TCPServer
115
128
 
116
129
  alias_method :orig_accept, :accept
117
130
  def accept
118
- @io ? @io.accept : orig_accept
131
+ @io.accept
132
+ end
133
+
134
+ alias_method :orig_close, :close
135
+ def close
136
+ @io.close
119
137
  end
120
138
  end
@@ -8,26 +8,30 @@ class ::Thread
8
8
 
9
9
  alias_method :orig_initialize, :initialize
10
10
  def initialize(*args, &block)
11
- @join_wait_queue = Gyro::Queue.new
11
+ @join_wait_queue = []
12
+ @finalization_mutex = Mutex.new
12
13
  @args = args
13
14
  @block = block
14
- @finalization_mutex = Mutex.new
15
15
  orig_initialize { execute }
16
16
  end
17
17
 
18
18
  def execute
19
+ # agent must be created in the context of the new thread, therefore it
20
+ # cannot be created in Thread#initialize
21
+ @agent = Polyphony::LibevAgent.new
19
22
  setup
20
23
  @ready = true
21
24
  result = @block.(*@args)
22
25
  rescue Polyphony::MoveOn, Polyphony::Terminate => e
23
26
  result = e.value
24
- rescue Exception => e
25
- result = e
27
+ rescue Exception => result
26
28
  ensure
27
29
  @ready = true
28
30
  finalize(result)
29
31
  end
30
32
 
33
+ attr_accessor :agent
34
+
31
35
  def setup
32
36
  @main_fiber = Fiber.current
33
37
  @main_fiber.setup_main_fiber
@@ -44,24 +48,25 @@ class ::Thread
44
48
  @result = result
45
49
  signal_waiters(result)
46
50
  end
47
- stop_event_selector
51
+ @agent.finalize
48
52
  end
49
53
 
50
54
  def signal_waiters(result)
51
- @join_wait_queue.shift_each { |w| w.signal(result) }
55
+ @join_wait_queue.each { |w| w.signal(result) }
52
56
  end
53
57
 
54
58
  alias_method :orig_join, :join
55
59
  def join(timeout = nil)
56
- async = Fiber.current.auto_async
60
+ watcher = Fiber.current.auto_watcher
61
+
57
62
  @finalization_mutex.synchronize do
58
63
  if @terminated
59
64
  @result.is_a?(Exception) ? (raise @result) : (return @result)
60
65
  else
61
- @join_wait_queue.push(async)
66
+ @join_wait_queue << watcher
62
67
  end
63
68
  end
64
- timeout ? move_on_after(timeout) { async.await } : async.await
69
+ timeout ? move_on_after(timeout) { watcher.await } : watcher.await
65
70
  end
66
71
  alias_method :await, :join
67
72
 
@@ -78,6 +83,8 @@ class ::Thread
78
83
 
79
84
  alias_method :orig_kill, :kill
80
85
  def kill
86
+ return if @terminated
87
+
81
88
  raise Polyphony::Terminate
82
89
  end
83
90