polyphony 0.40 → 0.41

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) 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 +6 -2
  6. data/Gemfile.lock +9 -6
  7. data/Rakefile +2 -2
  8. data/TODO.md +18 -97
  9. data/docs/_includes/head.html +40 -0
  10. data/docs/_includes/nav.html +5 -5
  11. data/docs/api-reference/fiber.md +2 -2
  12. data/docs/main-concepts/design-principles.md +67 -9
  13. data/docs/main-concepts/extending.md +1 -1
  14. data/examples/core/xx-agent.rb +102 -0
  15. data/examples/core/xx-sleeping.rb +14 -6
  16. data/examples/io/xx-irb.rb +1 -1
  17. data/examples/performance/thread-vs-fiber/polyphony_mt_server.rb +7 -6
  18. data/examples/performance/thread-vs-fiber/polyphony_server.rb +14 -25
  19. data/ext/{gyro → polyphony}/extconf.rb +2 -2
  20. data/ext/{gyro → polyphony}/fiber.c +15 -19
  21. data/ext/{gyro → polyphony}/libev.c +0 -0
  22. data/ext/{gyro → polyphony}/libev.h +0 -0
  23. data/ext/polyphony/libev_agent.c +503 -0
  24. data/ext/polyphony/libev_queue.c +214 -0
  25. data/ext/{gyro/gyro.c → polyphony/polyphony.c} +16 -25
  26. data/ext/polyphony/polyphony.h +90 -0
  27. data/ext/polyphony/polyphony_ext.c +23 -0
  28. data/ext/{gyro → polyphony}/socket.c +14 -14
  29. data/ext/{gyro → polyphony}/thread.c +32 -115
  30. data/ext/{gyro → polyphony}/tracing.c +1 -1
  31. data/lib/polyphony.rb +16 -12
  32. data/lib/polyphony/adapters/irb.rb +1 -1
  33. data/lib/polyphony/adapters/postgres.rb +6 -5
  34. data/lib/polyphony/adapters/process.rb +5 -5
  35. data/lib/polyphony/adapters/trace.rb +28 -28
  36. data/lib/polyphony/core/channel.rb +3 -3
  37. data/lib/polyphony/core/exceptions.rb +1 -1
  38. data/lib/polyphony/core/global_api.rb +11 -9
  39. data/lib/polyphony/core/resource_pool.rb +3 -3
  40. data/lib/polyphony/core/sync.rb +2 -2
  41. data/lib/polyphony/core/thread_pool.rb +6 -6
  42. data/lib/polyphony/core/throttler.rb +13 -6
  43. data/lib/polyphony/event.rb +27 -0
  44. data/lib/polyphony/extensions/core.rb +20 -11
  45. data/lib/polyphony/extensions/fiber.rb +4 -4
  46. data/lib/polyphony/extensions/io.rb +56 -26
  47. data/lib/polyphony/extensions/openssl.rb +4 -8
  48. data/lib/polyphony/extensions/socket.rb +27 -9
  49. data/lib/polyphony/extensions/thread.rb +16 -9
  50. data/lib/polyphony/net.rb +9 -9
  51. data/lib/polyphony/version.rb +1 -1
  52. data/polyphony.gemspec +2 -2
  53. data/test/helper.rb +12 -1
  54. data/test/test_agent.rb +77 -0
  55. data/test/{test_async.rb → test_event.rb} +13 -7
  56. data/test/test_ext.rb +25 -4
  57. data/test/test_fiber.rb +19 -10
  58. data/test/test_global_api.rb +4 -4
  59. data/test/test_io.rb +46 -24
  60. data/test/test_queue.rb +74 -0
  61. data/test/test_signal.rb +3 -40
  62. data/test/test_socket.rb +33 -0
  63. data/test/test_thread.rb +37 -16
  64. data/test/test_trace.rb +6 -5
  65. metadata +24 -24
  66. data/ext/gyro/async.c +0 -132
  67. data/ext/gyro/child.c +0 -108
  68. data/ext/gyro/gyro.h +0 -158
  69. data/ext/gyro/gyro_ext.c +0 -33
  70. data/ext/gyro/io.c +0 -457
  71. data/ext/gyro/queue.c +0 -146
  72. data/ext/gyro/selector.c +0 -205
  73. data/ext/gyro/signal.c +0 -99
  74. data/ext/gyro/timer.c +0 -115
  75. data/test/test_timer.rb +0 -56
@@ -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
 
@@ -117,4 +130,9 @@ class ::TCPServer
117
130
  def accept
118
131
  @io ? @io.accept : orig_accept
119
132
  end
133
+
134
+ alias_method :orig_close, :close
135
+ def close
136
+ @io ? @io.close : orig_close
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
 
@@ -4,6 +4,7 @@ require_relative './extensions/socket'
4
4
  require_relative './extensions/openssl'
5
5
 
6
6
  module Polyphony
7
+ # A more elegant networking API
7
8
  module Net
8
9
  class << self
9
10
  def tcp_connect(host, port, opts = {})
@@ -17,11 +18,11 @@ module Polyphony
17
18
  socket
18
19
  end
19
20
  end
20
-
21
+
21
22
  def tcp_listen(host = nil, port = nil, opts = {})
22
23
  host ||= '0.0.0.0'
23
24
  raise 'Port number not specified' unless port
24
-
25
+
25
26
  socket = socket_from_options(host, port, opts)
26
27
  if opts[:secure_context] || opts[:secure]
27
28
  secure_server(socket, opts[:secure_context], opts)
@@ -29,7 +30,7 @@ module Polyphony
29
30
  socket
30
31
  end
31
32
  end
32
-
33
+
33
34
  def socket_from_options(host, port, opts)
34
35
  ::Socket.new(:INET, :STREAM).tap do |s|
35
36
  s.reuse_addr if opts[:reuse_addr]
@@ -39,19 +40,19 @@ module Polyphony
39
40
  s.listen(0)
40
41
  end
41
42
  end
42
-
43
+
43
44
  def secure_socket(socket, context, opts)
44
45
  context ||= OpenSSL::SSL::SSLContext.new
45
46
  setup_alpn(context, opts[:alpn_protocols]) if opts[:alpn_protocols]
46
47
  socket = secure_socket_wrapper(socket, context)
47
-
48
+
48
49
  socket.tap do |s|
49
50
  s.hostname = opts[:host] if opts[:host]
50
51
  s.connect
51
52
  s.post_connection_check(opts[:host]) if opts[:host]
52
53
  end
53
54
  end
54
-
55
+
55
56
  def secure_socket_wrapper(socket, context)
56
57
  if context
57
58
  OpenSSL::SSL::SSLSocket.new(socket, context)
@@ -59,12 +60,12 @@ module Polyphony
59
60
  OpenSSL::SSL::SSLSocket.new(socket)
60
61
  end
61
62
  end
62
-
63
+
63
64
  def secure_server(socket, context, opts)
64
65
  setup_alpn(context, opts[:alpn_protocols]) if opts[:alpn_protocols]
65
66
  OpenSSL::SSL::SSLServer.new(socket, context)
66
67
  end
67
-
68
+
68
69
  def setup_alpn(context, protocols)
69
70
  context.alpn_protocols = protocols
70
71
  context.alpn_select_cb = lambda do |peer_protocols|
@@ -74,4 +75,3 @@ module Polyphony
74
75
  end
75
76
  end
76
77
  end
77
-
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.40'
4
+ VERSION = '0.41'
5
5
  end
@@ -17,7 +17,7 @@ Gem::Specification.new do |s|
17
17
  }
18
18
  s.rdoc_options = ["--title", "polyphony", "--main", "README.md"]
19
19
  s.extra_rdoc_files = ["README.md"]
20
- s.extensions = ["ext/gyro/extconf.rb"]
20
+ s.extensions = ["ext/polyphony/extconf.rb"]
21
21
  s.require_paths = ["lib"]
22
22
  s.required_ruby_version = '>= 2.6'
23
23
 
@@ -26,7 +26,7 @@ Gem::Specification.new do |s|
26
26
  s.add_development_dependency 'minitest', '5.13.0'
27
27
  s.add_development_dependency 'minitest-reporters', '1.4.2'
28
28
  s.add_development_dependency 'simplecov', '0.17.1'
29
- s.add_development_dependency 'rubocop', '0.80.0'
29
+ s.add_development_dependency 'rubocop', '0.85.1'
30
30
  s.add_development_dependency 'pg', '1.1.4'
31
31
  s.add_development_dependency 'rake-compiler', '1.0.5'
32
32
  s.add_development_dependency 'redis', '4.1.0'
@@ -18,6 +18,10 @@ Minitest::Reporters.use! [
18
18
  Minitest::Reporters::SpecReporter.new
19
19
  ]
20
20
 
21
+ class ::Fiber
22
+ attr_writer :auto_watcher
23
+ end
24
+
21
25
  class MiniTest::Test
22
26
  def setup
23
27
  # puts "* setup #{self.name}"
@@ -26,13 +30,20 @@ class MiniTest::Test
26
30
  exit!
27
31
  end
28
32
  Fiber.current.setup_main_fiber
33
+ Fiber.current.instance_variable_set(:@auto_watcher, nil)
34
+ Thread.current.agent = Polyphony::LibevAgent.new
29
35
  sleep 0
30
36
  end
31
37
 
32
38
  def teardown
33
- #puts "* teardown #{self.name}"
39
+ # puts "* teardown #{self.name.inspect} Fiber.current: #{Fiber.current.inspect}"
34
40
  Fiber.current.terminate_all_children
35
41
  Fiber.current.await_all_children
42
+ Fiber.current.auto_watcher = nil
43
+ rescue => e
44
+ puts e
45
+ puts e.backtrace.join("\n")
46
+ exit!
36
47
  end
37
48
  end
38
49
 
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class AgentTest < MiniTest::Test
6
+ def setup
7
+ super
8
+ @agent = Polyphony::LibevAgent.new
9
+ end
10
+
11
+ def teardown
12
+ @agent.finalize
13
+ end
14
+
15
+ def test_sleep
16
+ count = 0
17
+ t0 = Time.now
18
+ spin {
19
+ @agent.sleep 0.01
20
+ count += 1
21
+ }
22
+ suspend
23
+ assert Time.now - t0 >= 0.01
24
+ assert_equal 1, count
25
+ end
26
+
27
+ def test_write_read_partial
28
+ i, o = IO.pipe
29
+ buf = +''
30
+ f = spin { @agent.read(i, buf, 5, false) }
31
+ @agent.write(o, 'Hello world')
32
+ return_value = f.await
33
+
34
+ assert_equal 'Hello', buf
35
+ assert_equal return_value, buf
36
+ end
37
+
38
+ def test_write_read_to_eof_limited_buffer
39
+ i, o = IO.pipe
40
+ buf = +''
41
+ f = spin { @agent.read(i, buf, 5, true) }
42
+ @agent.write(o, 'Hello')
43
+ snooze
44
+ @agent.write(o, ' world')
45
+ snooze
46
+ o.close
47
+ return_value = f.await
48
+
49
+ assert_equal 'Hello', buf
50
+ assert_equal return_value, buf
51
+ end
52
+
53
+ def test_write_read_to_eof
54
+ i, o = IO.pipe
55
+ buf = +''
56
+ f = spin { @agent.read(i, buf, 10**6, true) }
57
+ @agent.write(o, 'Hello')
58
+ snooze
59
+ @agent.write(o, ' world')
60
+ snooze
61
+ o.close
62
+ return_value = f.await
63
+
64
+ assert_equal 'Hello world', buf
65
+ assert_equal return_value, buf
66
+ end
67
+
68
+ def test_waitpid
69
+ pid = fork do
70
+ Thread.current.agent.post_fork
71
+ exit(42)
72
+ end
73
+
74
+ result = Thread.current.agent.waitpid(pid)
75
+ assert_equal [pid, 42], result
76
+ end
77
+ end
@@ -2,26 +2,29 @@
2
2
 
3
3
  require_relative 'helper'
4
4
 
5
- class AsyncTest < MiniTest::Test
6
- def test_that_async_watcher_receives_signal_across_threads
5
+ class EventTest < MiniTest::Test
6
+ def test_that_event_receives_signal_across_threads
7
7
  count = 0
8
- a = Gyro::Async.new
8
+ a = Polyphony::Event.new
9
9
  spin {
10
10
  a.await
11
11
  count += 1
12
12
  }
13
13
  snooze
14
- Thread.new do
14
+ t = Thread.new do
15
15
  orig_sleep 0.001
16
16
  a.signal
17
17
  end
18
18
  suspend
19
19
  assert_equal 1, count
20
+ ensure
21
+ t&.kill
22
+ t&.join
20
23
  end
21
24
 
22
- def test_that_async_watcher_coalesces_signals
25
+ def test_that_event_coalesces_signals
23
26
  count = 0
24
- a = Gyro::Async.new
27
+ a = Polyphony::Event.new
25
28
 
26
29
  coproc = spin {
27
30
  loop {
@@ -31,12 +34,15 @@ class AsyncTest < MiniTest::Test
31
34
  }
32
35
  }
33
36
  snooze
34
- Thread.new do
37
+ t = Thread.new do
35
38
  orig_sleep 0.001
36
39
  3.times { a.signal }
37
40
  end
38
41
 
39
42
  coproc.await
40
43
  assert_equal 1, count
44
+ ensure
45
+ t&.kill
46
+ t&.join
41
47
  end
42
48
  end
@@ -74,7 +74,7 @@ class KernelTest < MiniTest::Test
74
74
  $stderr.rewind
75
75
  $stderr = prev_stderr
76
76
 
77
- assert_nil data
77
+ assert_equal '', data
78
78
  assert_equal "error\n", err_io.read
79
79
  ensure
80
80
  $stderr = prev_stderr
@@ -93,6 +93,28 @@ class KernelTest < MiniTest::Test
93
93
  $stdin = prev_stdin
94
94
  end
95
95
 
96
+ def test_multiline_gets
97
+ prev_stdin = $stdin
98
+ i, o = IO.pipe
99
+ $stdin = i
100
+
101
+ spin do
102
+ o << "hello\n"
103
+ o << "world\n"
104
+ o << "nice\n"
105
+ o << "to\n"
106
+ o << "meet\n"
107
+ o << "you\n"
108
+ end
109
+
110
+ s = +''
111
+ 6.times { s << gets }
112
+
113
+ assert_equal "hello\nworld\nnice\nto\nmeet\nyou\n", s
114
+ ensure
115
+ $stdin = prev_stdin
116
+ end
117
+
96
118
  def test_gets_from_argv
97
119
  prev_stdin = $stdin
98
120
 
@@ -103,8 +125,7 @@ class KernelTest < MiniTest::Test
103
125
  count = contents.size
104
126
 
105
127
  buffer = []
106
-
107
- (count * 2).times { buffer << gets }
128
+ (count * 2).times { |i| s = gets; buffer << s }
108
129
  assert_equal contents * 2, buffer
109
130
 
110
131
  i, o = IO.pipe
@@ -172,4 +193,4 @@ class TimeoutTest < MiniTest::Test
172
193
  assert_kind_of MyTimeout, e
173
194
  assert_equal 'foo', e.message
174
195
  end
175
- end
196
+ end