polyphony 0.49.1 → 0.52.0

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +1 -1
  3. data/CHANGELOG.md +35 -0
  4. data/Gemfile.lock +7 -68
  5. data/TODO.md +6 -0
  6. data/examples/core/forking.rb +2 -2
  7. data/examples/core/nested.rb +21 -0
  8. data/examples/core/suspend.rb +13 -0
  9. data/examples/core/terminate_main_fiber.rb +12 -0
  10. data/examples/performance/thread-vs-fiber/polyphony_server.rb +2 -4
  11. data/ext/polyphony/backend_common.h +58 -8
  12. data/ext/polyphony/backend_io_uring.c +158 -35
  13. data/ext/polyphony/backend_libev.c +192 -25
  14. data/ext/polyphony/event.c +1 -1
  15. data/ext/polyphony/extconf.rb +7 -2
  16. data/ext/polyphony/fiber.c +2 -1
  17. data/ext/polyphony/polyphony.c +94 -0
  18. data/ext/polyphony/polyphony.h +29 -2
  19. data/ext/polyphony/queue.c +1 -1
  20. data/ext/polyphony/runqueue.c +7 -1
  21. data/ext/polyphony/runqueue_ring_buffer.c +9 -0
  22. data/ext/polyphony/runqueue_ring_buffer.h +1 -0
  23. data/ext/polyphony/thread.c +14 -0
  24. data/lib/polyphony/adapters/irb.rb +1 -1
  25. data/lib/polyphony/adapters/mysql2.rb +1 -1
  26. data/lib/polyphony/adapters/postgres.rb +5 -5
  27. data/lib/polyphony/adapters/process.rb +4 -4
  28. data/lib/polyphony/core/exceptions.rb +1 -0
  29. data/lib/polyphony/core/global_api.rb +6 -6
  30. data/lib/polyphony/core/sync.rb +1 -1
  31. data/lib/polyphony/core/throttler.rb +1 -1
  32. data/lib/polyphony/core/timer.rb +63 -20
  33. data/lib/polyphony/extensions/core.rb +5 -5
  34. data/lib/polyphony/extensions/fiber.rb +11 -8
  35. data/lib/polyphony/extensions/io.rb +13 -22
  36. data/lib/polyphony/extensions/openssl.rb +6 -6
  37. data/lib/polyphony/extensions/socket.rb +41 -41
  38. data/lib/polyphony/extensions/thread.rb +1 -2
  39. data/lib/polyphony/version.rb +1 -1
  40. data/polyphony.gemspec +6 -5
  41. data/test/helper.rb +2 -3
  42. data/test/stress.rb +2 -0
  43. data/test/test_backend.rb +58 -5
  44. data/test/test_fiber.rb +31 -0
  45. data/test/test_global_api.rb +2 -2
  46. data/test/test_io.rb +84 -1
  47. data/test/test_kernel.rb +1 -1
  48. data/test/test_signal.rb +2 -3
  49. data/test/test_socket.rb +61 -0
  50. data/test/test_timer.rb +41 -8
  51. metadata +21 -60
@@ -245,8 +245,12 @@ module Polyphony
245
245
  end
246
246
 
247
247
  def shutdown_all_children(graceful = false)
248
- terminate_all_children(graceful)
249
- await_all_children
248
+ return unless @children
249
+
250
+ @children.keys.each do |c|
251
+ c.terminate(graceful)
252
+ c.await
253
+ end
250
254
  end
251
255
  end
252
256
 
@@ -312,6 +316,8 @@ module Polyphony
312
316
  @running = false
313
317
  inform_dependants(result, uncaught_exception)
314
318
  ensure
319
+ # Prevent fiber from being resumed after terminating
320
+ @thread.fiber_unschedule(self)
315
321
  Thread.current.switch_fiber
316
322
  end
317
323
 
@@ -319,13 +325,10 @@ module Polyphony
319
325
  # the children are shut down, it is returned along with the uncaught_exception
320
326
  # flag set. Otherwise, it returns the given arguments.
321
327
  def finalize_children(result, uncaught_exception)
322
- begin
323
- shutdown_all_children
324
- rescue Exception => e
325
- result = e
326
- uncaught_exception = true
327
- end
328
+ shutdown_all_children
328
329
  [result, uncaught_exception]
330
+ rescue Exception => e
331
+ [e, true]
329
332
  end
330
333
 
331
334
  def inform_dependants(result, uncaught_exception)
@@ -101,7 +101,7 @@ class ::IO
101
101
  return @read_buffer.slice!(0) if @read_buffer && !@read_buffer.empty?
102
102
 
103
103
  @read_buffer ||= +''
104
- Thread.current.backend.read(self, @read_buffer, 8192, false)
104
+ Polyphony.backend_read(self, @read_buffer, 8192, false)
105
105
  return @read_buffer.slice!(0) if !@read_buffer.empty?
106
106
 
107
107
  nil
@@ -110,7 +110,7 @@ class ::IO
110
110
  alias_method :orig_read, :read
111
111
  def read(len = nil)
112
112
  @read_buffer ||= +''
113
- result = Thread.current.backend.read(self, @read_buffer, len, true)
113
+ result = Polyphony.backend_read(self, @read_buffer, len, true)
114
114
  return nil unless result
115
115
 
116
116
  already_read = @read_buffer
@@ -120,7 +120,7 @@ class ::IO
120
120
 
121
121
  alias_method :orig_readpartial, :read
122
122
  def readpartial(len, str = +'')
123
- result = Thread.current.backend.read(self, str, len, false)
123
+ result = Polyphony.backend_read(self, str, len, false)
124
124
  raise EOFError unless result
125
125
 
126
126
  result
@@ -128,12 +128,12 @@ class ::IO
128
128
 
129
129
  alias_method :orig_write, :write
130
130
  def write(str, *args)
131
- Thread.current.backend.write(self, str, *args)
131
+ Polyphony.backend_write(self, str, *args)
132
132
  end
133
133
 
134
134
  alias_method :orig_write_chevron, :<<
135
135
  def <<(str)
136
- Thread.current.backend.write(self, str)
136
+ Polyphony.backend_write(self, str)
137
137
  self
138
138
  end
139
139
 
@@ -217,30 +217,21 @@ class ::IO
217
217
  end
218
218
 
219
219
  def read_loop(&block)
220
- Thread.current.backend.read_loop(self, &block)
220
+ Polyphony.backend_read_loop(self, &block)
221
221
  end
222
222
 
223
- # alias_method :orig_read, :read
224
- # def read(length = nil, outbuf = nil)
225
- # if length
226
- # return outbuf ? readpartial(length) : readpartial(length, outbuf)
227
- # end
228
-
229
- # until eof?
230
- # outbuf ||= +''
231
- # outbuf << readpartial(8192)
232
- # end
233
- # outbuf
234
- # end
223
+ def feed_loop(receiver, method = :call, &block)
224
+ Polyphony.backend_feed_loop(self, receiver, method, &block)
225
+ end
235
226
 
236
227
  def wait_readable(timeout = nil)
237
228
  if timeout
238
229
  move_on_after(timeout) do
239
- Thread.current.backend.wait_io(self, false)
230
+ Polyphony.backend_wait_io(self, false)
240
231
  self
241
232
  end
242
233
  else
243
- Thread.current.backend.wait_io(self, false)
234
+ Polyphony.backend_wait_io(self, false)
244
235
  self
245
236
  end
246
237
  end
@@ -248,11 +239,11 @@ class ::IO
248
239
  def wait_writable(timeout = nil)
249
240
  if timeout
250
241
  move_on_after(timeout) do
251
- Thread.current.backend.wait_io(self, true)
242
+ Polyphony.backend_wait_io(self, true)
252
243
  self
253
244
  end
254
245
  else
255
- Thread.current.backend.wait_io(self, true)
246
+ Polyphony.backend_wait_io(self, true)
256
247
  self
257
248
  end
258
249
  end
@@ -28,8 +28,8 @@ class ::OpenSSL::SSL::SSLSocket
28
28
  while true
29
29
  result = accept_nonblock(exception: false)
30
30
  case result
31
- when :wait_readable then Thread.current.backend.wait_io(io, false)
32
- when :wait_writable then Thread.current.backend.wait_io(io, true)
31
+ when :wait_readable then Polyphony.backend_wait_io(io, false)
32
+ when :wait_writable then Polyphony.backend_wait_io(io, true)
33
33
  else
34
34
  return result
35
35
  end
@@ -46,8 +46,8 @@ class ::OpenSSL::SSL::SSLSocket
46
46
  def sysread(maxlen, buf = +'')
47
47
  while true
48
48
  case (result = read_nonblock(maxlen, buf, exception: false))
49
- when :wait_readable then Thread.current.backend.wait_io(io, false)
50
- when :wait_writable then Thread.current.backend.wait_io(io, true)
49
+ when :wait_readable then Polyphony.backend_wait_io(io, false)
50
+ when :wait_writable then Polyphony.backend_wait_io(io, true)
51
51
  else return result
52
52
  end
53
53
  end
@@ -57,8 +57,8 @@ class ::OpenSSL::SSL::SSLSocket
57
57
  def syswrite(buf)
58
58
  while true
59
59
  case (result = write_nonblock(buf, exception: false))
60
- when :wait_readable then Thread.current.backend.wait_io(io, false)
61
- when :wait_writable then Thread.current.backend.wait_io(io, true)
60
+ when :wait_readable then Polyphony.backend_wait_io(io, false)
61
+ when :wait_writable then Polyphony.backend_wait_io(io, true)
62
62
  else
63
63
  return result
64
64
  end
@@ -8,57 +8,57 @@ require_relative '../core/thread_pool'
8
8
  # Socket overrides (eventually rewritten in C)
9
9
  class ::Socket
10
10
  def accept
11
- Thread.current.backend.accept(self, TCPSocket)
11
+ Polyphony.backend_accept(self, TCPSocket)
12
12
  end
13
13
 
14
14
  def accept_loop(&block)
15
- Thread.current.backend.accept_loop(self, TCPSocket, &block)
15
+ Polyphony.backend_accept_loop(self, TCPSocket, &block)
16
16
  end
17
17
 
18
18
  NO_EXCEPTION = { exception: false }.freeze
19
19
 
20
20
  def connect(addr)
21
21
  addr = Addrinfo.new(addr) if addr.is_a?(String)
22
- Thread.current.backend.connect(self, addr.ip_address, addr.ip_port)
22
+ Polyphony.backend_connect(self, addr.ip_address, addr.ip_port)
23
23
  end
24
24
 
25
25
  def recv(maxlen, flags = 0, outbuf = nil)
26
- Thread.current.backend.recv(self, outbuf || +'', maxlen)
26
+ Polyphony.backend_recv(self, outbuf || +'', maxlen)
27
27
  end
28
28
 
29
29
  def recv_loop(&block)
30
- Thread.current.backend.recv_loop(self, &block)
30
+ Polyphony.backend_recv_loop(self, &block)
31
31
  end
32
32
  alias_method :read_loop, :recv_loop
33
33
 
34
+ def feed_loop(receiver, method = :call, &block)
35
+ Polyphony.backend_recv_feed_loop(self, receiver, method, &block)
36
+ end
37
+
34
38
  def recvfrom(maxlen, flags = 0)
35
39
  @read_buffer ||= +''
36
40
  while true
37
41
  result = recvfrom_nonblock(maxlen, flags, @read_buffer, **NO_EXCEPTION)
38
42
  case result
39
43
  when nil then raise IOError
40
- when :wait_readable then Thread.current.backend.wait_io(self, false)
44
+ when :wait_readable then Polyphony.backend_wait_io(self, false)
41
45
  else
42
46
  return result
43
47
  end
44
48
  end
45
49
  end
46
50
 
47
- def send(mesg, flags = 0)
48
- Thread.current.backend.send(self, mesg)
51
+ def send(mesg, flags)
52
+ Polyphony.backend_send(self, mesg, flags)
49
53
  end
50
54
 
51
- def write(str, *args)
52
- if args.empty?
53
- Thread.current.backend.send(self, str)
54
- else
55
- Thread.current.backend.send(self, str + args.join)
56
- end
55
+ def write(*args)
56
+ Polyphony.backend_sendv(self, args, 0)
57
57
  end
58
58
  alias_method :<<, :write
59
59
 
60
60
  def readpartial(maxlen, str = +'')
61
- Thread.current.backend.recv(self, str, maxlen)
61
+ Polyphony.backend_recv(self, str, maxlen)
62
62
  end
63
63
 
64
64
  ZERO_LINGER = [0, 0].pack('ii').freeze
@@ -138,30 +138,30 @@ class ::TCPSocket
138
138
  end
139
139
 
140
140
  def recv(maxlen, flags = 0, outbuf = nil)
141
- Thread.current.backend.recv(self, outbuf || +'', maxlen)
141
+ Polyphony.backend_recv(self, outbuf || +'', maxlen)
142
142
  end
143
143
 
144
144
  def recv_loop(&block)
145
- Thread.current.backend.recv_loop(self, &block)
145
+ Polyphony.backend_recv_loop(self, &block)
146
146
  end
147
147
  alias_method :read_loop, :recv_loop
148
148
 
149
- def send(mesg, flags = 0)
150
- Thread.current.backend.send(self, mesg)
149
+ def feed_loop(receiver, method = :call, &block)
150
+ Polyphony.backend_recv_feed_loop(self, receiver, method, &block)
151
151
  end
152
152
 
153
- def write(str, *args)
154
- if args.empty?
155
- Thread.current.backend.send(self, str)
156
- else
157
- Thread.current.backend.send(self, str + args.join)
158
- end
153
+ def send(mesg, flags)
154
+ Polyphony.backend_send(self, mesg, flags)
155
+ end
156
+
157
+ def write(*args)
158
+ Polyphony.backend_sendv(self, args, 0)
159
159
  end
160
160
  alias_method :<<, :write
161
161
 
162
162
  def readpartial(maxlen, str = nil)
163
163
  @read_buffer ||= +''
164
- result = Thread.current.backend.recv(self, @read_buffer, maxlen)
164
+ result = Polyphony.backend_recv(self, @read_buffer, maxlen)
165
165
  raise EOFError unless result
166
166
 
167
167
  if str
@@ -192,12 +192,12 @@ class ::TCPServer
192
192
 
193
193
  alias_method :orig_accept, :accept
194
194
  def accept
195
- Thread.current.backend.accept(@io, TCPSocket)
195
+ Polyphony.backend_accept(@io, TCPSocket)
196
196
  # @io.accept
197
197
  end
198
198
 
199
199
  def accept_loop(&block)
200
- Thread.current.backend.accept_loop(@io, TCPSocket, &block)
200
+ Polyphony.backend_accept_loop(@io, TCPSocket, &block)
201
201
  end
202
202
 
203
203
  alias_method :orig_close, :close
@@ -209,40 +209,40 @@ end
209
209
  class ::UNIXServer
210
210
  alias_method :orig_accept, :accept
211
211
  def accept
212
- Thread.current.backend.accept(self, UNIXSocket)
212
+ Polyphony.backend_accept(self, UNIXSocket)
213
213
  end
214
214
 
215
215
  def accept_loop(&block)
216
- Thread.current.backend.accept_loop(self, UNIXSocket, &block)
216
+ Polyphony.backend_accept_loop(self, UNIXSocket, &block)
217
217
  end
218
218
  end
219
219
 
220
220
  class ::UNIXSocket
221
221
  def recv(maxlen, flags = 0, outbuf = nil)
222
- Thread.current.backend.recv(self, outbuf || +'', maxlen)
222
+ Polyphony.backend_recv(self, outbuf || +'', maxlen)
223
223
  end
224
224
 
225
225
  def recv_loop(&block)
226
- Thread.current.backend.recv_loop(self, &block)
226
+ Polyphony.backend_recv_loop(self, &block)
227
227
  end
228
228
  alias_method :read_loop, :recv_loop
229
229
 
230
- def send(mesg, flags = 0)
231
- Thread.current.backend.send(self, mesg)
230
+ def feed_loop(receiver, method = :call, &block)
231
+ Polyphony.backend_recv_feed_loop(self, receiver, method, &block)
232
232
  end
233
233
 
234
- def write(str, *args)
235
- if args.empty?
236
- Thread.current.backend.send(self, str)
237
- else
238
- Thread.current.backend.send(self, str + args.join)
239
- end
234
+ def send(mesg, flags)
235
+ Polyphony.backend_send(self, mesg, flags)
236
+ end
237
+
238
+ def write(*args)
239
+ Polyphony.backend_sendv(self, args, 0)
240
240
  end
241
241
  alias_method :<<, :write
242
242
 
243
243
  def readpartial(maxlen, str = nil)
244
244
  @read_buffer ||= +''
245
- result = Thread.current.backend.recv(self, @read_buffer, maxlen)
245
+ result = Polyphony.backend_recv(self, @read_buffer, maxlen)
246
246
  raise EOFError unless result
247
247
 
248
248
  if str
@@ -41,8 +41,7 @@ class ::Thread
41
41
 
42
42
  def finalize(result)
43
43
  unless Fiber.current.children.empty?
44
- Fiber.current.terminate_all_children
45
- Fiber.current.await_all_children
44
+ Fiber.current.shutdown_all_children
46
45
  end
47
46
  @finalization_mutex.synchronize do
48
47
  @terminated = true
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.49.1'
4
+ VERSION = '0.52.0'
5
5
  end
data/polyphony.gemspec CHANGED
@@ -22,12 +22,13 @@ Gem::Specification.new do |s|
22
22
  s.required_ruby_version = '>= 2.6'
23
23
 
24
24
  s.add_development_dependency 'rake-compiler', '1.1.1'
25
- s.add_development_dependency 'minitest', '5.13.0'
25
+ s.add_development_dependency 'minitest', '5.14.4'
26
26
  s.add_development_dependency 'minitest-reporters', '1.4.2'
27
27
  s.add_development_dependency 'simplecov', '0.17.1'
28
28
  s.add_development_dependency 'rubocop', '0.85.1'
29
29
  s.add_development_dependency 'pry', '0.13.1'
30
30
 
31
+ s.add_development_dependency 'msgpack', '1.4.2'
31
32
  s.add_development_dependency 'pg', '1.1.4'
32
33
  s.add_development_dependency 'redis', '4.1.0'
33
34
  s.add_development_dependency 'hiredis', '0.6.3'
@@ -37,8 +38,8 @@ Gem::Specification.new do |s|
37
38
  s.add_development_dependency 'sequel', '5.34.0'
38
39
  s.add_development_dependency 'httparty', '0.17.1'
39
40
 
40
- s.add_development_dependency 'jekyll', '~>3.8.6'
41
- s.add_development_dependency 'jekyll-remote-theme', '~>0.4.1'
42
- s.add_development_dependency 'jekyll-seo-tag', '~>2.6.1'
43
- s.add_development_dependency 'just-the-docs', '~>0.3.0'
41
+ # s.add_development_dependency 'jekyll', '~>3.8.6'
42
+ # s.add_development_dependency 'jekyll-remote-theme', '~>0.4.1'
43
+ # s.add_development_dependency 'jekyll-seo-tag', '~>2.6.1'
44
+ # s.add_development_dependency 'just-the-docs', '~>0.3.0'
44
45
  end
data/test/helper.rb CHANGED
@@ -52,13 +52,12 @@ class MiniTest::Test
52
52
  Fiber.current.instance_variable_set(:@auto_watcher, nil)
53
53
  Thread.current.backend.finalize
54
54
  Thread.current.backend = Polyphony::Backend.new
55
- sleep 0
55
+ sleep 0.001
56
56
  end
57
57
 
58
58
  def teardown
59
59
  # trace "* teardown #{self.name}"
60
- Fiber.current.terminate_all_children
61
- Fiber.current.await_all_children
60
+ Fiber.current.shutdown_all_children
62
61
  Fiber.current.instance_variable_set(:@auto_watcher, nil)
63
62
  rescue => e
64
63
  puts e
data/test/stress.rb CHANGED
@@ -8,6 +8,8 @@ def run_test(count)
8
8
  puts "#{count}: running tests..."
9
9
  # sleep 1
10
10
  system(TEST_CMD)
11
+ puts
12
+
11
13
  return if $?.exitstatus == 0
12
14
 
13
15
  puts "Failure after #{count} tests"
data/test/test_backend.rb CHANGED
@@ -100,12 +100,40 @@ class BackendTest < MiniTest::Test
100
100
  assert_equal [:ready, 'foo', 'bar', :done], buf
101
101
  end
102
102
 
103
- def test_accept_loop
104
- server = TCPServer.new('127.0.0.1', 1234)
103
+ def test_read_loop_terminate
104
+ i, o = IO.pipe
105
+
106
+ buf = []
107
+ parent = spin do
108
+ f = spin do
109
+ buf << :ready
110
+ @backend.read_loop(i) { |d| buf << d }
111
+ buf << :done
112
+ end
113
+ suspend
114
+ end
115
+
116
+ # writing always causes snoozing
117
+ o << 'foo'
118
+ sleep 0.01
119
+ o << 'bar'
120
+ sleep 0.01
121
+
122
+ parent.stop
123
+
124
+ parent.await
125
+ assert_equal [:ready, 'foo', 'bar'], buf
126
+ end
127
+
128
+ Net = Polyphony::Net
129
+
130
+ def test_accept
131
+ server = Net.listening_socket_from_options('127.0.0.1', 1234, reuse_addr: true)
105
132
 
106
133
  clients = []
107
- server_fiber = spin do
108
- @backend.accept_loop(server, TCPSocket) { |c| clients << c }
134
+ server_fiber = spin_loop do
135
+ c = @backend.accept(server, TCPSocket)
136
+ clients << c
109
137
  end
110
138
 
111
139
  c1 = TCPSocket.new('127.0.0.1', 1234)
@@ -121,7 +149,32 @@ class BackendTest < MiniTest::Test
121
149
  ensure
122
150
  c1&.close
123
151
  c2&.close
124
- server_fiber.stop
152
+ server_fiber&.stop
153
+ snooze
154
+ server&.close
155
+ end
156
+
157
+ def test_accept_loop
158
+ server = Net.listening_socket_from_options('127.0.0.1', 1235, reuse_addr: true)
159
+
160
+ clients = []
161
+ server_fiber = spin do
162
+ @backend.accept_loop(server, TCPSocket) { |c| clients << c }
163
+ end
164
+
165
+ c1 = TCPSocket.new('127.0.0.1', 1235)
166
+ sleep 0.01
167
+
168
+ assert_equal 1, clients.size
169
+
170
+ c2 = TCPSocket.new('127.0.0.1', 1235)
171
+ sleep 0.01
172
+
173
+ assert_equal 2, clients.size
174
+ ensure
175
+ c1&.close
176
+ c2&.close
177
+ server_fiber&.stop
125
178
  snooze
126
179
  server&.close
127
180
  end