polyphony 0.43.8 → 0.45.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -1
  3. data/CHANGELOG.md +38 -0
  4. data/Gemfile.lock +13 -11
  5. data/README.md +20 -5
  6. data/Rakefile +1 -1
  7. data/TODO.md +16 -14
  8. data/bin/stress.rb +28 -0
  9. data/docs/_posts/2020-07-26-polyphony-0.44.md +77 -0
  10. data/docs/api-reference/thread.md +1 -1
  11. data/docs/getting-started/overview.md +14 -14
  12. data/docs/getting-started/tutorial.md +1 -1
  13. data/examples/adapters/sequel_mysql.rb +23 -0
  14. data/examples/adapters/sequel_mysql_pool.rb +33 -0
  15. data/examples/core/{xx-agent.rb → xx-backend.rb} +5 -5
  16. data/examples/core/xx-channels.rb +4 -2
  17. data/examples/core/xx-using-a-mutex.rb +2 -1
  18. data/examples/io/xx-pry.rb +18 -0
  19. data/examples/io/xx-rack_server.rb +71 -0
  20. data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +1 -1
  21. data/ext/polyphony/backend.h +41 -0
  22. data/ext/polyphony/event.c +86 -0
  23. data/ext/polyphony/extconf.rb +1 -1
  24. data/ext/polyphony/fiber.c +0 -5
  25. data/ext/polyphony/{libev_agent.c → libev_backend.c} +234 -228
  26. data/ext/polyphony/polyphony.c +4 -0
  27. data/ext/polyphony/polyphony.h +16 -16
  28. data/ext/polyphony/polyphony_ext.c +4 -2
  29. data/ext/polyphony/queue.c +52 -12
  30. data/ext/polyphony/thread.c +55 -42
  31. data/lib/polyphony.rb +25 -39
  32. data/lib/polyphony/adapters/irb.rb +2 -17
  33. data/lib/polyphony/adapters/mysql2.rb +19 -0
  34. data/lib/polyphony/adapters/postgres.rb +5 -5
  35. data/lib/polyphony/adapters/process.rb +2 -2
  36. data/lib/polyphony/adapters/readline.rb +17 -0
  37. data/lib/polyphony/adapters/sequel.rb +45 -0
  38. data/lib/polyphony/core/channel.rb +3 -34
  39. data/lib/polyphony/core/exceptions.rb +11 -0
  40. data/lib/polyphony/core/global_api.rb +11 -6
  41. data/lib/polyphony/core/resource_pool.rb +22 -71
  42. data/lib/polyphony/core/sync.rb +48 -9
  43. data/lib/polyphony/core/throttler.rb +1 -1
  44. data/lib/polyphony/extensions/core.rb +37 -19
  45. data/lib/polyphony/extensions/fiber.rb +5 -1
  46. data/lib/polyphony/extensions/io.rb +7 -8
  47. data/lib/polyphony/extensions/openssl.rb +6 -6
  48. data/lib/polyphony/extensions/socket.rb +12 -22
  49. data/lib/polyphony/extensions/thread.rb +6 -5
  50. data/lib/polyphony/net.rb +2 -1
  51. data/lib/polyphony/version.rb +1 -1
  52. data/polyphony.gemspec +6 -3
  53. data/test/helper.rb +1 -1
  54. data/test/{test_agent.rb → test_backend.rb} +22 -22
  55. data/test/test_event.rb +1 -0
  56. data/test/test_fiber.rb +21 -5
  57. data/test/test_io.rb +1 -1
  58. data/test/test_kernel.rb +5 -0
  59. data/test/test_queue.rb +20 -0
  60. data/test/test_resource_pool.rb +34 -43
  61. data/test/test_signal.rb +5 -29
  62. data/test/test_sync.rb +52 -0
  63. metadata +74 -30
  64. data/.gitbook.yaml +0 -4
  65. data/lib/polyphony/event.rb +0 -17
@@ -12,7 +12,7 @@ module Polyphony
12
12
  def call
13
13
  now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
14
14
  delta = @next_time - now
15
- Thread.current.agent.sleep(delta) if delta > 0
15
+ Thread.current.backend.sleep(delta) if delta > 0
16
16
  yield self
17
17
 
18
18
  loop do
@@ -46,6 +46,10 @@ class ::Exception
46
46
 
47
47
  backtrace.reject { |l| l[POLYPHONY_DIR] }
48
48
  end
49
+
50
+ def invoke
51
+ Kernel.raise(self)
52
+ end
49
53
  end
50
54
 
51
55
  # Overrides for Process
@@ -53,7 +57,7 @@ module ::Process
53
57
  class << self
54
58
  alias_method :orig_detach, :detach
55
59
  def detach(pid)
56
- fiber = spin { Thread.current.agent.waitpid(pid) }
60
+ fiber = spin { Thread.current.backend.waitpid(pid) }
57
61
  fiber.define_singleton_method(:pid) { pid }
58
62
  fiber
59
63
  end
@@ -112,19 +116,28 @@ module ::Kernel
112
116
  strs = args.inject([]) do |m, a|
113
117
  m << a.inspect << "\n"
114
118
  end
115
- STDOUT.write *strs
119
+ STDOUT.write(*strs)
116
120
  args.size == 1 ? args.first : args
117
121
  end
118
122
 
119
123
  alias_method :orig_system, :system
120
124
  def system(*args)
121
- Open3.popen2(*args) do |i, o, _t|
122
- i.close
123
- pipe_to_eof(o, $stdout)
125
+ Kernel.system(*args)
126
+ end
127
+
128
+ class << self
129
+ alias_method :orig_system, :system
130
+ def system(*args)
131
+ waiter = nil
132
+ Open3.popen2(*args) do |i, o, t|
133
+ waiter = t
134
+ i.close
135
+ pipe_to_eof(o, $stdout)
136
+ end
137
+ waiter.await.last == 0
138
+ rescue SystemCallError
139
+ nil
124
140
  end
125
- true
126
- rescue SystemCallError
127
- nil
128
141
  end
129
142
 
130
143
  def pipe_to_eof(src, dest)
@@ -139,29 +152,34 @@ module ::Kernel
139
152
  alias_method :orig_trap, :trap
140
153
  def trap(sig, command = nil, &block)
141
154
  return orig_trap(sig, command) if command.is_a? String
142
-
143
- block = command if command.respond_to?(:call) && !block
144
- exception = command.is_a?(Class) && command.new
155
+
156
+ block = command if !block && command.respond_to?(:call)
157
+ exception = signal_exception(block, command)
145
158
 
146
159
  # The signal trap can be invoked at any time, including while the system
147
- # agent is blocking while polling for events. In order to deal with this
160
+ # backend is blocking while polling for events. In order to deal with this
148
161
  # correctly, we spin a fiber that will run the signal handler code, then
149
162
  # call break_out_of_ev_loop, which will put the fiber at the front of the
150
- # run queue, then wake up the system agent.
163
+ # run queue, then wake up the backend.
151
164
  #
152
165
  # If the command argument is an exception class however, it will be raised
153
166
  # directly in the context of the main fiber.
154
167
  orig_trap(sig) do
155
- if exception
156
- Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, exception)
157
- else
158
- fiber = spin { snooze; block.call }
159
- Thread.current.break_out_of_ev_loop(fiber, nil)
160
- end
168
+ Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, exception)
161
169
  end
162
170
  end
163
171
  end
164
172
 
173
+ def signal_exception(block, command)
174
+ if block
175
+ Polyphony::Interjection.new(block)
176
+ elsif command.is_a?(Class)
177
+ command.new
178
+ else
179
+ raise ArgumentError, 'Must supply block or exception or callable object'
180
+ end
181
+ end
182
+
165
183
  # Override Timeout to use cancel scope
166
184
  module ::Timeout
167
185
  def self.timeout(sec, klass = nil, message = nil, &block)
@@ -67,6 +67,10 @@ module Polyphony
67
67
  else RuntimeError.new
68
68
  end
69
69
  end
70
+
71
+ def interject(&block)
72
+ raise Polyphony::Interjection.new(block)
73
+ end
70
74
  end
71
75
 
72
76
  # Fiber supervision
@@ -224,7 +228,7 @@ module Polyphony
224
228
  @results = @children.dup
225
229
  @on_child_done = proc do |c, r|
226
230
  @results[c] = r
227
- self.schedule if @children.empty?
231
+ schedule if @children.empty?
228
232
  end
229
233
  suspend
230
234
  @on_child_done = nil
@@ -99,7 +99,7 @@ class ::IO
99
99
  alias_method :orig_read, :read
100
100
  def read(len = nil)
101
101
  @read_buffer ||= +''
102
- result = Thread.current.agent.read(self, @read_buffer, len, true)
102
+ result = Thread.current.backend.read(self, @read_buffer, len, true)
103
103
  return nil unless result
104
104
 
105
105
  already_read = @read_buffer
@@ -110,7 +110,7 @@ class ::IO
110
110
  alias_method :orig_readpartial, :read
111
111
  def readpartial(len, str = nil)
112
112
  @read_buffer ||= +''
113
- result = Thread.current.agent.read(self, @read_buffer, len, false)
113
+ result = Thread.current.backend.read(self, @read_buffer, len, false)
114
114
  raise EOFError unless result
115
115
 
116
116
  if str
@@ -124,12 +124,12 @@ class ::IO
124
124
 
125
125
  alias_method :orig_write, :write
126
126
  def write(str, *args)
127
- Thread.current.agent.write(self, str, *args)
127
+ Thread.current.backend.write(self, str, *args)
128
128
  end
129
129
 
130
130
  alias_method :orig_write_chevron, :<<
131
131
  def <<(str)
132
- Thread.current.agent.write(self, str)
132
+ Thread.current.backend.write(self, str)
133
133
  self
134
134
  end
135
135
 
@@ -170,13 +170,12 @@ class ::IO
170
170
  return
171
171
  end
172
172
 
173
- strs = args.inject([]) do |m, a|
173
+ strs = args.each_with_object([]) do |a, m|
174
174
  a = a.to_s
175
175
  m << a
176
176
  m << "\n" unless a =~ /\n$/
177
- m
178
177
  end
179
- write *strs
178
+ write(*strs)
180
179
  nil
181
180
  end
182
181
 
@@ -203,7 +202,7 @@ class ::IO
203
202
  end
204
203
 
205
204
  def read_loop(&block)
206
- Thread.current.agent.read_loop(self, &block)
205
+ Thread.current.backend.read_loop(self, &block)
207
206
  end
208
207
 
209
208
  # alias_method :orig_read, :read
@@ -28,8 +28,8 @@ class ::OpenSSL::SSL::SSLSocket
28
28
  loop do
29
29
  result = accept_nonblock(exception: false)
30
30
  case result
31
- when :wait_readable then Thread.current.agent.wait_io(io, false)
32
- when :wait_writable then Thread.current.agent.wait_io(io, true)
31
+ when :wait_readable then Thread.current.backend.wait_io(io, false)
32
+ when :wait_writable then Thread.current.backend.wait_io(io, true)
33
33
  else
34
34
  return result
35
35
  end
@@ -40,8 +40,8 @@ class ::OpenSSL::SSL::SSLSocket
40
40
  def sysread(maxlen, buf = +'')
41
41
  loop do
42
42
  case (result = read_nonblock(maxlen, buf, exception: false))
43
- when :wait_readable then Thread.current.agent.wait_io(io, false)
44
- when :wait_writable then Thread.current.agent.wait_io(io, true)
43
+ when :wait_readable then Thread.current.backend.wait_io(io, false)
44
+ when :wait_writable then Thread.current.backend.wait_io(io, true)
45
45
  else return result
46
46
  end
47
47
  end
@@ -51,8 +51,8 @@ class ::OpenSSL::SSL::SSLSocket
51
51
  def syswrite(buf)
52
52
  loop do
53
53
  case (result = write_nonblock(buf, exception: false))
54
- when :wait_readable then Thread.current.agent.wait_io(io, false)
55
- when :wait_writable then Thread.current.agent.wait_io(io, true)
54
+ when :wait_readable then Thread.current.backend.wait_io(io, false)
55
+ when :wait_writable then Thread.current.backend.wait_io(io, true)
56
56
  else
57
57
  return result
58
58
  end
@@ -5,34 +5,16 @@ require 'socket'
5
5
  require_relative './io'
6
6
  require_relative '../core/thread_pool'
7
7
 
8
- class ::BasicSocket
9
- def write_nonblock(string, _options = {})
10
- write(string)
11
- end
12
-
13
- def read_nonblock(maxlen, str = nil, _options = {})
14
- readpartial(maxlen, str)
15
- end
16
- end
17
-
18
8
  # Socket overrides (eventually rewritten in C)
19
9
  class ::Socket
20
10
  def accept
21
- Thread.current.agent.accept(self)
11
+ Thread.current.backend.accept(self)
22
12
  end
23
13
 
24
14
  NO_EXCEPTION = { exception: false }.freeze
25
15
 
26
16
  def connect(remotesockaddr)
27
- loop do
28
- result = connect_nonblock(remotesockaddr, **NO_EXCEPTION)
29
- case result
30
- when 0 then return
31
- when :wait_writable then Thread.current.agent.wait_io(self, true)
32
- else
33
- raise IOError
34
- end
35
- end
17
+ Thread.current.backend.connect(self, remotesockaddr.ip_address, remotesockaddr.ip_port)
36
18
  end
37
19
 
38
20
  def recv(maxlen, flags = 0, outbuf = nil)
@@ -41,7 +23,7 @@ class ::Socket
41
23
  result = recv_nonblock(maxlen, flags, outbuf, **NO_EXCEPTION)
42
24
  case result
43
25
  when nil then raise IOError
44
- when :wait_readable then Thread.current.agent.wait_io(self, false)
26
+ when :wait_readable then Thread.current.backend.wait_io(self, false)
45
27
  else
46
28
  return result
47
29
  end
@@ -54,7 +36,7 @@ class ::Socket
54
36
  result = recvfrom_nonblock(maxlen, flags, @read_buffer, **NO_EXCEPTION)
55
37
  case result
56
38
  when nil then raise IOError
57
- when :wait_readable then Thread.current.agent.wait_io(self, false)
39
+ when :wait_readable then Thread.current.backend.wait_io(self, false)
58
40
  else
59
41
  return result
60
42
  end
@@ -75,6 +57,10 @@ class ::Socket
75
57
  setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
76
58
  end
77
59
 
60
+ def reuse_port
61
+ setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
62
+ end
63
+
78
64
  class << self
79
65
  alias_method :orig_getaddrinfo, :getaddrinfo
80
66
  def getaddrinfo(*args)
@@ -128,6 +114,10 @@ class ::TCPSocket
128
114
  def reuse_addr
129
115
  setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, 1)
130
116
  end
117
+
118
+ def reuse_port
119
+ setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
120
+ end
131
121
  end
132
122
 
133
123
  # Override stock TCPServer code by encapsulating a Socket instance.
@@ -16,21 +16,22 @@ class ::Thread
16
16
  end
17
17
 
18
18
  def execute
19
- # agent must be created in the context of the new thread, therefore it
19
+ # backend must be created in the context of the new thread, therefore it
20
20
  # cannot be created in Thread#initialize
21
- @agent = Polyphony::LibevAgent.new
21
+ @backend = Polyphony::Backend.new
22
22
  setup
23
23
  @ready = true
24
24
  result = @block.(*@args)
25
25
  rescue Polyphony::MoveOn, Polyphony::Terminate => e
26
26
  result = e.value
27
- rescue Exception => result
27
+ rescue Exception => e
28
+ result = e
28
29
  ensure
29
30
  @ready = true
30
31
  finalize(result)
31
32
  end
32
33
 
33
- attr_accessor :agent
34
+ attr_accessor :backend
34
35
 
35
36
  def setup
36
37
  @main_fiber = Fiber.current
@@ -48,7 +49,7 @@ class ::Thread
48
49
  @result = result
49
50
  signal_waiters(result)
50
51
  end
51
- @agent.finalize
52
+ @backend.finalize
52
53
  end
53
54
 
54
55
  def signal_waiters(result)
@@ -35,9 +35,10 @@ module Polyphony
35
35
  ::Socket.new(:INET, :STREAM).tap do |s|
36
36
  s.reuse_addr if opts[:reuse_addr]
37
37
  s.dont_linger if opts[:dont_linger]
38
+ s.reuse_port if opts[:reuse_port]
38
39
  addr = ::Socket.sockaddr_in(port, host)
39
40
  s.bind(addr)
40
- s.listen(0)
41
+ s.listen(opts[:backlog] || Socket::SOMAXCONN)
41
42
  end
42
43
  end
43
44
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.43.8'
4
+ VERSION = '0.45.0'
5
5
  end
@@ -21,17 +21,20 @@ Gem::Specification.new do |s|
21
21
  s.require_paths = ["lib"]
22
22
  s.required_ruby_version = '>= 2.6'
23
23
 
24
- s.add_development_dependency 'httparty', '0.17.0'
25
- s.add_development_dependency 'localhost', '1.1.4'
24
+ s.add_development_dependency 'rake-compiler', '1.0.5'
26
25
  s.add_development_dependency 'minitest', '5.13.0'
27
26
  s.add_development_dependency 'minitest-reporters', '1.4.2'
28
27
  s.add_development_dependency 'simplecov', '0.17.1'
29
28
  s.add_development_dependency 'rubocop', '0.85.1'
29
+ s.add_development_dependency 'pry', '0.13.1'
30
+
30
31
  s.add_development_dependency 'pg', '1.1.4'
31
- s.add_development_dependency 'rake-compiler', '1.0.5'
32
32
  s.add_development_dependency 'redis', '4.1.0'
33
33
  s.add_development_dependency 'hiredis', '0.6.3'
34
34
  s.add_development_dependency 'http_parser.rb', '~>0.6.0'
35
+ s.add_development_dependency 'rack', '>=2.0.8', '<2.3.0'
36
+ s.add_development_dependency 'mysql2', '0.5.3'
37
+ s.add_development_dependency 'sequel', '5.34.0'
35
38
 
36
39
  s.add_development_dependency 'jekyll', '~>3.8.6'
37
40
  s.add_development_dependency 'jekyll-remote-theme', '~>0.4.1'
@@ -31,7 +31,7 @@ class MiniTest::Test
31
31
  end
32
32
  Fiber.current.setup_main_fiber
33
33
  Fiber.current.instance_variable_set(:@auto_watcher, nil)
34
- Thread.current.agent = Polyphony::LibevAgent.new
34
+ Thread.current.backend = Polyphony::Backend.new
35
35
  sleep 0 # apparently this helps with timer accuracy
36
36
  end
37
37
 
@@ -2,39 +2,39 @@
2
2
 
3
3
  require_relative 'helper'
4
4
 
5
- class AgentTest < MiniTest::Test
5
+ class BackendTest < MiniTest::Test
6
6
  def setup
7
7
  super
8
- @prev_agent = Thread.current.agent
9
- @agent = Polyphony::LibevAgent.new
10
- Thread.current.agent = @agent
8
+ @prev_backend = Thread.current.backend
9
+ @backend = Polyphony::Backend.new
10
+ Thread.current.backend = @backend
11
11
  end
12
12
 
13
13
  def teardown
14
- @agent.finalize
15
- Thread.current.agent = @prev_agent
14
+ @backend.finalize
15
+ Thread.current.backend = @prev_backend
16
16
  end
17
17
 
18
18
  def test_sleep
19
19
  count = 0
20
20
  t0 = Time.now
21
21
  spin {
22
- @agent.sleep 0.01
22
+ @backend.sleep 0.01
23
23
  count += 1
24
- @agent.sleep 0.01
24
+ @backend.sleep 0.01
25
25
  count += 1
26
- @agent.sleep 0.01
26
+ @backend.sleep 0.01
27
27
  count += 1
28
28
  }.await
29
- assert Time.now - t0 >= 0.03
29
+ assert_in_delta 0.03, Time.now - t0, 0.005
30
30
  assert_equal 3, count
31
31
  end
32
32
 
33
33
  def test_write_read_partial
34
34
  i, o = IO.pipe
35
35
  buf = +''
36
- f = spin { @agent.read(i, buf, 5, false) }
37
- @agent.write(o, 'Hello world')
36
+ f = spin { @backend.read(i, buf, 5, false) }
37
+ @backend.write(o, 'Hello world')
38
38
  return_value = f.await
39
39
 
40
40
  assert_equal 'Hello', buf
@@ -44,10 +44,10 @@ class AgentTest < MiniTest::Test
44
44
  def test_write_read_to_eof_limited_buffer
45
45
  i, o = IO.pipe
46
46
  buf = +''
47
- f = spin { @agent.read(i, buf, 5, true) }
48
- @agent.write(o, 'Hello')
47
+ f = spin { @backend.read(i, buf, 5, true) }
48
+ @backend.write(o, 'Hello')
49
49
  snooze
50
- @agent.write(o, ' world')
50
+ @backend.write(o, ' world')
51
51
  snooze
52
52
  o.close
53
53
  return_value = f.await
@@ -59,10 +59,10 @@ class AgentTest < MiniTest::Test
59
59
  def test_write_read_to_eof
60
60
  i, o = IO.pipe
61
61
  buf = +''
62
- f = spin { @agent.read(i, buf, 10**6, true) }
63
- @agent.write(o, 'Hello')
62
+ f = spin { @backend.read(i, buf, 10**6, true) }
63
+ @backend.write(o, 'Hello')
64
64
  snooze
65
- @agent.write(o, ' world')
65
+ @backend.write(o, ' world')
66
66
  snooze
67
67
  o.close
68
68
  return_value = f.await
@@ -73,11 +73,11 @@ class AgentTest < MiniTest::Test
73
73
 
74
74
  def test_waitpid
75
75
  pid = fork do
76
- @agent.post_fork
76
+ @backend.post_fork
77
77
  exit(42)
78
78
  end
79
79
 
80
- result = @agent.waitpid(pid)
80
+ result = @backend.waitpid(pid)
81
81
  assert_equal [pid, 42], result
82
82
  end
83
83
 
@@ -87,7 +87,7 @@ class AgentTest < MiniTest::Test
87
87
  buf = []
88
88
  spin do
89
89
  buf << :ready
90
- @agent.read_loop(i) { |d| buf << d }
90
+ @backend.read_loop(i) { |d| buf << d }
91
91
  buf << :done
92
92
  end
93
93
 
@@ -107,7 +107,7 @@ class AgentTest < MiniTest::Test
107
107
 
108
108
  clients = []
109
109
  server_fiber = spin do
110
- @agent.accept_loop(server) { |c| clients << c }
110
+ @backend.accept_loop(server) { |c| clients << c }
111
111
  end
112
112
 
113
113
  c1 = TCPSocket.new('127.0.0.1', 1234)