polyphony 0.44.0 → 0.45.0

Sign up to get free protection for your applications and to get access to all the features.
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)