polyphony 0.44.0 → 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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -1
  3. data/CHANGELOG.md +7 -0
  4. data/Gemfile.lock +9 -11
  5. data/Rakefile +1 -1
  6. data/TODO.md +12 -7
  7. data/docs/_posts/2020-07-26-polyphony-0.44.md +77 -0
  8. data/docs/api-reference/thread.md +1 -1
  9. data/docs/getting-started/overview.md +14 -14
  10. data/docs/getting-started/tutorial.md +1 -1
  11. data/examples/core/{xx-agent.rb → xx-backend.rb} +5 -5
  12. data/examples/io/xx-pry.rb +18 -0
  13. data/examples/io/xx-rack_server.rb +71 -0
  14. data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +1 -1
  15. data/ext/polyphony/backend.h +41 -0
  16. data/ext/polyphony/event.c +3 -3
  17. data/ext/polyphony/extconf.rb +1 -1
  18. data/ext/polyphony/{libev_agent.c → libev_backend.c} +175 -175
  19. data/ext/polyphony/polyphony.c +1 -1
  20. data/ext/polyphony/polyphony.h +4 -4
  21. data/ext/polyphony/polyphony_ext.c +2 -2
  22. data/ext/polyphony/queue.c +2 -2
  23. data/ext/polyphony/thread.c +21 -21
  24. data/lib/polyphony.rb +13 -12
  25. data/lib/polyphony/adapters/irb.rb +2 -17
  26. data/lib/polyphony/adapters/mysql2.rb +1 -1
  27. data/lib/polyphony/adapters/postgres.rb +5 -5
  28. data/lib/polyphony/adapters/process.rb +2 -2
  29. data/lib/polyphony/adapters/readline.rb +17 -0
  30. data/lib/polyphony/adapters/sequel.rb +1 -1
  31. data/lib/polyphony/core/global_api.rb +11 -6
  32. data/lib/polyphony/core/resource_pool.rb +2 -2
  33. data/lib/polyphony/core/sync.rb +38 -2
  34. data/lib/polyphony/core/throttler.rb +1 -1
  35. data/lib/polyphony/extensions/core.rb +31 -20
  36. data/lib/polyphony/extensions/fiber.rb +1 -1
  37. data/lib/polyphony/extensions/io.rb +7 -8
  38. data/lib/polyphony/extensions/openssl.rb +6 -6
  39. data/lib/polyphony/extensions/socket.rb +4 -14
  40. data/lib/polyphony/extensions/thread.rb +6 -5
  41. data/lib/polyphony/version.rb +1 -1
  42. data/polyphony.gemspec +4 -3
  43. data/test/helper.rb +1 -1
  44. data/test/{test_agent.rb → test_backend.rb} +22 -22
  45. data/test/test_fiber.rb +4 -4
  46. data/test/test_io.rb +1 -1
  47. data/test/test_kernel.rb +5 -0
  48. data/test/test_signal.rb +3 -3
  49. data/test/test_sync.rb +52 -0
  50. metadata +40 -30
  51. data/.gitbook.yaml +0 -4
  52. data/ext/polyphony/agent.h +0 -41
@@ -12,12 +12,48 @@ module Polyphony
12
12
  return yield if @holding_fiber == Fiber.current
13
13
 
14
14
  begin
15
- token = @store.shift
15
+ @token = @store.shift
16
16
  @holding_fiber = Fiber.current
17
17
  yield
18
18
  ensure
19
19
  @holding_fiber = nil
20
- @store << token if token
20
+ @store << @token if @token
21
+ end
22
+ end
23
+
24
+ def conditional_release
25
+ @store << @token
26
+ @token = nil
27
+ @holding_fiber = nil
28
+ end
29
+
30
+ def conditional_reacquire
31
+ @token = @store.shift
32
+ @holding_fiber = Fiber.current
33
+ end
34
+ end
35
+
36
+ # Implements a fiber-aware ConditionVariable
37
+ class ConditionVariable
38
+ def initialize
39
+ @queue = Polyphony::Queue.new
40
+ end
41
+
42
+ def wait(mutex, _timeout = nil)
43
+ mutex.conditional_release
44
+ @queue << Fiber.current
45
+ Thread.current.backend.wait_event(true)
46
+ mutex.conditional_reacquire
47
+ end
48
+
49
+ def signal
50
+ fiber = @queue.shift
51
+ fiber.schedule
52
+ end
53
+
54
+ def broadcast
55
+ while (fiber = @queue.shift)
56
+ fiber.schedule
21
57
  end
22
58
  end
23
59
  end
@@ -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
@@ -57,7 +57,7 @@ module ::Process
57
57
  class << self
58
58
  alias_method :orig_detach, :detach
59
59
  def detach(pid)
60
- fiber = spin { Thread.current.agent.waitpid(pid) }
60
+ fiber = spin { Thread.current.backend.waitpid(pid) }
61
61
  fiber.define_singleton_method(:pid) { pid }
62
62
  fiber
63
63
  end
@@ -116,19 +116,28 @@ module ::Kernel
116
116
  strs = args.inject([]) do |m, a|
117
117
  m << a.inspect << "\n"
118
118
  end
119
- STDOUT.write *strs
119
+ STDOUT.write(*strs)
120
120
  args.size == 1 ? args.first : args
121
121
  end
122
122
 
123
123
  alias_method :orig_system, :system
124
124
  def system(*args)
125
- Open3.popen2(*args) do |i, o, _t|
126
- i.close
127
- 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
128
140
  end
129
- true
130
- rescue SystemCallError
131
- nil
132
141
  end
133
142
 
134
143
  def pipe_to_eof(src, dest)
@@ -143,23 +152,15 @@ module ::Kernel
143
152
  alias_method :orig_trap, :trap
144
153
  def trap(sig, command = nil, &block)
145
154
  return orig_trap(sig, command) if command.is_a? String
146
-
147
- block = command if !block && command.respond_to?(:call)
148
- if block
149
- exception = Polyphony::Interjection.new(block)
150
- else
151
- exception = command.is_a?(Class) && command.new
152
- end
153
155
 
154
- unless exception
155
- raise ArgumentError, "Must supply block or exception or callable object"
156
- end
156
+ block = command if !block && command.respond_to?(:call)
157
+ exception = signal_exception(block, command)
157
158
 
158
159
  # The signal trap can be invoked at any time, including while the system
159
- # 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
160
161
  # correctly, we spin a fiber that will run the signal handler code, then
161
162
  # call break_out_of_ev_loop, which will put the fiber at the front of the
162
- # run queue, then wake up the system agent.
163
+ # run queue, then wake up the backend.
163
164
  #
164
165
  # If the command argument is an exception class however, it will be raised
165
166
  # directly in the context of the main fiber.
@@ -169,6 +170,16 @@ module ::Kernel
169
170
  end
170
171
  end
171
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
+
172
183
  # Override Timeout to use cancel scope
173
184
  module ::Timeout
174
185
  def self.timeout(sec, klass = nil, message = nil, &block)
@@ -228,7 +228,7 @@ module Polyphony
228
228
  @results = @children.dup
229
229
  @on_child_done = proc do |c, r|
230
230
  @results[c] = r
231
- self.schedule if @children.empty?
231
+ schedule if @children.empty?
232
232
  end
233
233
  suspend
234
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,26 +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
- Thread.current.agent.connect(self, remotesockaddr.ip_address, remotesockaddr.ip_port)
17
+ Thread.current.backend.connect(self, remotesockaddr.ip_address, remotesockaddr.ip_port)
28
18
  end
29
19
 
30
20
  def recv(maxlen, flags = 0, outbuf = nil)
@@ -33,7 +23,7 @@ class ::Socket
33
23
  result = recv_nonblock(maxlen, flags, outbuf, **NO_EXCEPTION)
34
24
  case result
35
25
  when nil then raise IOError
36
- when :wait_readable then Thread.current.agent.wait_io(self, false)
26
+ when :wait_readable then Thread.current.backend.wait_io(self, false)
37
27
  else
38
28
  return result
39
29
  end
@@ -46,7 +36,7 @@ class ::Socket
46
36
  result = recvfrom_nonblock(maxlen, flags, @read_buffer, **NO_EXCEPTION)
47
37
  case result
48
38
  when nil then raise IOError
49
- when :wait_readable then Thread.current.agent.wait_io(self, false)
39
+ when :wait_readable then Thread.current.backend.wait_io(self, false)
50
40
  else
51
41
  return result
52
42
  end
@@ -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::Agent.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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.44.0'
4
+ VERSION = '0.45.0'
5
5
  end
@@ -21,17 +21,18 @@ 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'
35
36
  s.add_development_dependency 'mysql2', '0.5.3'
36
37
  s.add_development_dependency 'sequel', '5.34.0'
37
38
 
@@ -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::Agent.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::Agent.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)