polyphony 0.17 → 0.19

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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/Gemfile.lock +11 -3
  4. data/README.md +18 -18
  5. data/TODO.md +5 -21
  6. data/examples/core/channel_echo.rb +3 -3
  7. data/examples/core/enumerator.rb +1 -1
  8. data/examples/core/fork.rb +1 -1
  9. data/examples/core/genserver.rb +1 -1
  10. data/examples/core/lock.rb +3 -3
  11. data/examples/core/multiple_spawn.rb +2 -2
  12. data/examples/core/nested_async.rb +1 -1
  13. data/examples/core/nested_multiple_spawn.rb +3 -3
  14. data/examples/core/resource_cancel.rb +1 -1
  15. data/examples/core/sleep_spawn.rb +2 -2
  16. data/examples/core/spawn.rb +1 -1
  17. data/examples/core/spawn_cancel.rb +1 -1
  18. data/examples/core/spawn_error.rb +4 -4
  19. data/examples/core/supervisor.rb +1 -1
  20. data/examples/core/supervisor_with_error.rb +1 -1
  21. data/examples/core/supervisor_with_manual_move_on.rb +1 -1
  22. data/examples/core/thread.rb +2 -2
  23. data/examples/core/thread_cancel.rb +2 -2
  24. data/examples/core/thread_pool.rb +1 -1
  25. data/examples/core/throttle.rb +3 -3
  26. data/examples/core/timeout.rb +10 -0
  27. data/examples/fs/read.rb +1 -1
  28. data/examples/http/http_client.rb +1 -1
  29. data/examples/http/http_get.rb +7 -0
  30. data/examples/http/http_parse_experiment.rb +118 -0
  31. data/examples/http/http_proxy.rb +81 -0
  32. data/examples/http/http_server.rb +15 -4
  33. data/examples/http/http_server_forked.rb +2 -2
  34. data/examples/http/http_server_throttled.rb +1 -1
  35. data/examples/http/http_ws_server.rb +2 -2
  36. data/examples/http/https_server.rb +5 -1
  37. data/examples/http/https_wss_server.rb +1 -1
  38. data/examples/http/rack_server_https_forked.rb +1 -1
  39. data/examples/interfaces/pg_client.rb +1 -1
  40. data/examples/interfaces/pg_pool.rb +1 -1
  41. data/examples/interfaces/redis_channels.rb +5 -5
  42. data/examples/interfaces/redis_pubsub.rb +2 -2
  43. data/examples/interfaces/redis_pubsub_perf.rb +3 -3
  44. data/examples/io/echo_client.rb +2 -2
  45. data/examples/io/echo_pipe.rb +17 -0
  46. data/examples/io/echo_server.rb +1 -1
  47. data/examples/io/echo_server_with_timeout.rb +1 -1
  48. data/examples/io/httparty.rb +10 -0
  49. data/examples/io/httparty_multi.rb +29 -0
  50. data/examples/io/httparty_threaded.rb +25 -0
  51. data/examples/io/irb.rb +15 -0
  52. data/examples/io/net-http.rb +15 -0
  53. data/examples/io/system.rb +1 -1
  54. data/examples/io/tcpsocket.rb +18 -0
  55. data/examples/performance/perf_multi_snooze.rb +2 -2
  56. data/examples/performance/perf_snooze.rb +17 -20
  57. data/examples/performance/thread-vs-fiber/polyphony_server.rb +1 -1
  58. data/ext/ev/ev.h +9 -1
  59. data/ext/ev/ev_ext.c +4 -1
  60. data/ext/ev/ev_module.c +36 -22
  61. data/ext/ev/extconf.rb +1 -1
  62. data/ext/ev/io.c +23 -23
  63. data/ext/ev/signal.c +1 -1
  64. data/ext/ev/socket.c +161 -0
  65. data/lib/polyphony/core/coprocess.rb +1 -1
  66. data/lib/polyphony/core/fiber_pool.rb +2 -2
  67. data/lib/polyphony/core/supervisor.rb +2 -18
  68. data/lib/polyphony/extensions/io.rb +19 -6
  69. data/lib/polyphony/extensions/kernel.rb +17 -5
  70. data/lib/polyphony/extensions/socket.rb +40 -1
  71. data/lib/polyphony/http/agent.rb +56 -25
  72. data/lib/polyphony/http/http1_adapter.rb +254 -0
  73. data/lib/polyphony/http/http2_adapter.rb +157 -0
  74. data/lib/polyphony/http/{http2_request.rb → request.rb} +25 -22
  75. data/lib/polyphony/http/server.rb +19 -11
  76. data/lib/polyphony/net.rb +10 -6
  77. data/lib/polyphony/version.rb +1 -1
  78. data/polyphony.gemspec +6 -5
  79. data/test/test_coprocess.rb +9 -9
  80. data/test/test_core.rb +14 -14
  81. data/test/test_io.rb +4 -4
  82. data/test/test_kernel.rb +1 -1
  83. metadata +48 -23
  84. data/lib/polyphony/http/http1.rb +0 -124
  85. data/lib/polyphony/http/http1_request.rb +0 -83
  86. data/lib/polyphony/http/http2.rb +0 -65
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ export_default :Protocol
4
+
5
+ require 'http/2'
6
+
7
+ Request = import('./request')
8
+
9
+ class StreamWrapper
10
+ def initialize(stream, parse_fiber)
11
+ @stream = stream
12
+ @parse_fiber = parse_fiber
13
+ @parsing = true
14
+
15
+ stream.on(:data) { |data| on_body(data) }
16
+ stream.on(:half_close) { on_message_complete }
17
+ end
18
+
19
+ # Reads body chunk from connection
20
+ def get_body_chunk
21
+ @calling_fiber = Fiber.current
22
+ @read_body = true
23
+ @parse_fiber.safe_transfer
24
+ end
25
+
26
+ # Wait for request to finish
27
+ def consume_request
28
+ return unless @parsing
29
+
30
+ @calling_fiber = Fiber.current
31
+ @read_body = false
32
+ @parse_fiber.safe_transfer while @parsing
33
+ end
34
+
35
+ def on_body(data)
36
+ @calling_fiber.transfer(data) if @read_body
37
+ end
38
+
39
+ def on_message_complete
40
+ @parsing = false
41
+ @calling_fiber.transfer nil
42
+ end
43
+
44
+ # response API
45
+ def respond(chunk, headers)
46
+ consume_request if @parsing
47
+
48
+ headers[':status'] ||= '200'
49
+ @stream.headers(headers, end_stream: false)
50
+ @stream.data(chunk, end_stream: true)
51
+ @headers_sent = true
52
+ end
53
+
54
+ def send_headers(headers, empty_response = false)
55
+ return if @headers_sent
56
+
57
+ consume_request if @parsing
58
+
59
+ headers[':status'] ||= (empty_response ? 204 : 200).to_s
60
+ @stream.headers(headers, end_stream: false)
61
+ @headers_sent = true
62
+ end
63
+
64
+ def send_body_chunk(chunk, done: false)
65
+ send_headers({}, false) unless @headers_sent
66
+ @stream.data(chunk, end_stream: done)
67
+ end
68
+
69
+ def finish
70
+ consume_request if @parsing
71
+
72
+ unless @headers_sent
73
+ headers[':status'] ||= '204'
74
+ @stream.headers(headers, end_stream: true)
75
+ else
76
+ @stream.close
77
+ end
78
+ end
79
+ end
80
+
81
+ class Protocol
82
+ def self.upgrade_each(socket, opts, headers, &block)
83
+ adapter = new(socket, opts, headers)
84
+ adapter.each(&block)
85
+ end
86
+
87
+ def initialize(conn, opts, upgrade_headers = nil)
88
+ @conn = conn
89
+ @opts = opts
90
+
91
+ @interface = ::HTTP2::Server.new
92
+ @interface.on(:frame) { |bytes| conn << bytes }
93
+ @interface.on(:stream) { |stream| start_stream(stream) }
94
+ @parse_fiber = Fiber.new { parse_loop(upgrade_headers) }
95
+ end
96
+
97
+ def start_stream(stream)
98
+ stream.on(:headers) do |headers|
99
+ @calling_fiber.transfer([stream, headers.to_h])
100
+ end
101
+ end
102
+
103
+ def protocol
104
+ 'h2'
105
+ end
106
+
107
+ def parse_loop(upgrade_headers)
108
+ upgrade(upgrade_headers) if upgrade_headers
109
+
110
+ while (data = @conn.readpartial(8192))
111
+ @interface << data
112
+ snooze
113
+ end
114
+ @calling_fiber.transfer nil
115
+ rescue SystemCallError, IOError
116
+ # ignore
117
+ @calling_fiber.transfer nil
118
+ rescue => error
119
+ # an error return value will be raised by the receiving fiber
120
+ @calling_fiber.transfer error
121
+ end
122
+
123
+ # request API
124
+
125
+ UPGRADE_MESSAGE = <<~HTTP.gsub("\n", "\r\n")
126
+ HTTP/1.1 101 Switching Protocols
127
+ Connection: Upgrade
128
+ Upgrade: h2c
129
+
130
+ HTTP
131
+
132
+ def upgrade(headers)
133
+ settings = headers['HTTP2-Settings']
134
+ @conn << UPGRADE_MESSAGE
135
+ @interface.upgrade(settings, headers, '')
136
+ end
137
+
138
+ # Iterates over incoming requests
139
+ def each(&block)
140
+ can_upgrade = true
141
+ while @parse_fiber.alive?
142
+ stream, headers = get_headers
143
+ break unless stream
144
+ wrapper = StreamWrapper.new(stream, @parse_fiber)
145
+ request = Request.new(headers, wrapper)
146
+ Fiber.new { block.(request) }.resume
147
+ end
148
+ ensure
149
+ @conn.close rescue nil
150
+ end
151
+
152
+ # Reads headers from connection
153
+ def get_headers
154
+ @calling_fiber = Fiber.current
155
+ @parse_fiber.safe_transfer
156
+ end
157
+ end
@@ -5,24 +5,15 @@ export_default :Request
5
5
  require 'uri'
6
6
 
7
7
  class Request
8
- def initialize(stream)
9
- @stream = stream
10
- end
8
+ attr_reader :headers, :adapter
11
9
 
12
- def http_version
13
- 2
10
+ def initialize(headers, adapter)
11
+ @headers = headers
12
+ @adapter = adapter
14
13
  end
15
14
 
16
- def set_headers(headers)
17
- @headers = Hash[*headers.flatten]
18
- end
19
-
20
- def add_body_chunk(chunk)
21
- if @body
22
- @body << chunk
23
- else
24
- @body = +chunk
25
- end
15
+ def protocol
16
+ @adapter.protocol
26
17
  end
27
18
 
28
19
  def method
@@ -33,16 +24,19 @@ class Request
33
24
  @scheme ||= @headers[':scheme']
34
25
  end
35
26
 
36
- def path
27
+ def uri
37
28
  @uri ||= URI.parse(@headers[':path'] || '')
38
- @path ||= @uri.path
29
+ end
30
+
31
+ def path
32
+ @path ||= uri.path
39
33
  end
40
34
 
41
35
  def query
42
36
  @uri ||= URI.parse(@headers[':path'] || '')
43
37
  return @query if @query
44
38
 
45
- if (q = u.query)
39
+ if (q = uri.query)
46
40
  @query = q.split('&').each_with_object({}) do |kv, h|
47
41
  k, v = kv.split('=')
48
42
  h[k.to_sym] = URI.decode_www_form_component(v)
@@ -54,10 +48,19 @@ class Request
54
48
 
55
49
  EMPTY_HASH = {}
56
50
 
57
- def respond(body, headers = EMPTY_HASH)
58
- headers[':status'] ||= '200'
51
+ def respond(chunk, headers = EMPTY_HASH)
52
+ @adapter.respond(chunk, headers)
53
+ end
54
+
55
+ def send_headers(headers = EMPTY_HASH, empty_response = false)
56
+ @adapter.send_headers(headers, empty_response)
57
+ end
58
+
59
+ def send_body_chunk(body, done: false)
60
+ @adapter.send_body_chunk(body, done: done)
61
+ end
59
62
 
60
- @stream.headers(headers, end_stream: false)
61
- @stream.data(body, end_stream: true)
63
+ def finish
64
+ @adapter.finish
62
65
  end
63
66
  end
@@ -3,8 +3,8 @@
3
3
  export :serve, :listen, :accept_loop
4
4
 
5
5
  Net = import('../net')
6
- HTTP1 = import('./http1')
7
- HTTP2 = import('./http2')
6
+ HTTP1 = import('./http1_adapter')
7
+ HTTP2 = import('./http2_adapter')
8
8
 
9
9
  ALPN_PROTOCOLS = %w[h2 http/1.1].freeze
10
10
  H2_PROTOCOL = 'h2'
@@ -17,25 +17,33 @@ end
17
17
 
18
18
  def listen(host, port, opts = {})
19
19
  opts[:alpn_protocols] = ALPN_PROTOCOLS
20
- Net.tcp_listen(host, port, opts)
20
+ Net.tcp_listen(host, port, opts).tap do |socket|
21
+ socket.define_singleton_method(:each) do |&block|
22
+ MODULE.accept_loop(socket, opts, &block)
23
+ end
24
+ end
21
25
  end
22
26
 
23
27
  def accept_loop(server, opts, &handler)
24
- while true
28
+ loop do
25
29
  client = server.accept
26
- coproc { client_task(client, opts, &handler) }
30
+ spin { client_loop(client, opts, &handler) }
31
+ rescue OpenSSL::SSL::SSLError
32
+ # disregard
27
33
  end
28
- rescue OpenSSL::SSL::SSLError
29
- retry # disregard
30
34
  end
31
35
 
32
- def client_task(client, opts, &handler)
36
+ def client_loop(client, opts, &handler)
33
37
  client.no_delay rescue nil
34
- protocol_module(client).(client, opts, &handler)
38
+ adapter = protocol_adapter(client, opts)
39
+ adapter.each(&handler)
40
+ ensure
41
+ client.close rescue nil
35
42
  end
36
43
 
37
- def protocol_module(socket)
44
+ def protocol_adapter(socket, opts)
38
45
  use_http2 = socket.respond_to?(:alpn_protocol) &&
39
46
  socket.alpn_protocol == H2_PROTOCOL
40
- use_http2 ? HTTP2 : HTTP1
47
+ klass = use_http2 ? HTTP2 : HTTP1
48
+ klass.new(socket, opts)
41
49
  end
@@ -12,7 +12,7 @@ def tcp_connect(host, port, opts = {})
12
12
  s.connect(addr)
13
13
  }
14
14
  if opts[:secure_context] || opts[:secure]
15
- secure_socket(socket, opts[:secure_context], opts)
15
+ secure_socket(socket, opts[:secure_context], opts.merge(host: host))
16
16
  else
17
17
  socket
18
18
  end
@@ -36,11 +36,15 @@ def tcp_listen(host = nil, port = nil, opts = {})
36
36
  end
37
37
 
38
38
  def secure_socket(socket, context, opts)
39
- if context
40
- setup_alpn(context, opts[:alpn_protocols]) if opts[:alpn_protocols]
41
- OpenSSL::SSL::SSLSocket.new(socket, context).tap { |s| s.connect }
42
- else
43
- OpenSSL::SSL::SSLSocket.new(socket).tap { |s| s.connect }
39
+ setup_alpn(context, opts[:alpn_protocols]) if context && opts[:alpn_protocols]
40
+ socket = context ?
41
+ OpenSSL::SSL::SSLSocket.new(socket, context) :
42
+ OpenSSL::SSL::SSLSocket.new(socket)
43
+
44
+ socket.tap do |s|
45
+ s.hostname = opts[:host] if opts[:host]
46
+ s.connect
47
+ s.post_connection_check(opts[:host]) if opts[:host]
44
48
  end
45
49
  end
46
50
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.17'
4
+ VERSION = '0.19'
5
5
  end
@@ -17,16 +17,17 @@ Gem::Specification.new do |s|
17
17
  s.extensions = ["ext/ev/extconf.rb"]
18
18
  s.require_paths = ["lib"]
19
19
 
20
- s.add_runtime_dependency 'modulation', '0.24'
20
+ s.add_runtime_dependency 'modulation', '~>0.25'
21
21
 
22
22
  s.add_runtime_dependency 'http_parser.rb', '0.6.0'
23
23
  s.add_runtime_dependency 'http-2', '0.10.0'
24
24
 
25
- s.add_development_dependency 'rake-compiler', '1.0.5'
26
- s.add_development_dependency 'minitest', '5.11.3'
25
+ s.add_development_dependency 'hiredis', '0.6.3'
26
+ s.add_development_dependency 'httparty', '0.17.0'
27
27
  s.add_development_dependency 'localhost', '1.1.4'
28
- s.add_development_dependency 'websocket', '1.2.8'
28
+ s.add_development_dependency 'minitest', '5.11.3'
29
29
  s.add_development_dependency 'pg', '1.1.3'
30
+ s.add_development_dependency 'rake-compiler', '1.0.5'
30
31
  s.add_development_dependency 'redis', '4.1.0'
31
- s.add_development_dependency 'hiredis', '0.6.3'
32
+ s.add_development_dependency 'websocket', '1.2.8'
32
33
  end
@@ -86,7 +86,7 @@ class CoprocessTest < MiniTest::Test
86
86
 
87
87
  def test_that_coprocess_can_be_awaited
88
88
  result = nil
89
- coproc do
89
+ spin do
90
90
  coprocess = Polyphony::Coprocess.new { sleep(0.001); 42 }
91
91
  result = coprocess.await
92
92
  end
@@ -96,7 +96,7 @@ class CoprocessTest < MiniTest::Test
96
96
 
97
97
  def test_that_coprocess_can_be_stopped
98
98
  result = nil
99
- coprocess = coproc do
99
+ coprocess = spin do
100
100
  sleep(0.001)
101
101
  result = 42
102
102
  end
@@ -107,7 +107,7 @@ class CoprocessTest < MiniTest::Test
107
107
 
108
108
  def test_that_coprocess_can_be_cancelled
109
109
  result = nil
110
- coprocess = coproc do
110
+ coprocess = spin do
111
111
  sleep(0.001)
112
112
  result = 42
113
113
  rescue Polyphony::Cancel => e
@@ -125,8 +125,8 @@ class CoprocessTest < MiniTest::Test
125
125
  def test_that_inner_coprocess_can_be_interrupted
126
126
  result = nil
127
127
  coprocess2 = nil
128
- coprocess = coproc do
129
- coprocess2 = coproc do
128
+ coprocess = spin do
129
+ coprocess2 = spin do
130
130
  sleep(0.001)
131
131
  result = 42
132
132
  end
@@ -143,8 +143,8 @@ class CoprocessTest < MiniTest::Test
143
143
  def test_that_inner_coprocess_can_interrupt_outer_coprocess
144
144
  result, coprocess2 = nil
145
145
 
146
- coprocess = coproc do
147
- coprocess2 = coproc do
146
+ coprocess = spin do
147
+ coprocess2 = spin do
148
148
  EV.next_tick { coprocess.interrupt }
149
149
  sleep(0.001)
150
150
  result = 42
@@ -168,7 +168,7 @@ class MailboxTest < MiniTest::Test
168
168
 
169
169
  def test_that_coprocess_can_receive_messages
170
170
  msgs = []
171
- coprocess = coproc {
171
+ coprocess = spin {
172
172
  loop {
173
173
  msgs << receive
174
174
  }
@@ -185,7 +185,7 @@ class MailboxTest < MiniTest::Test
185
185
 
186
186
  def test_that_multiple_messages_sent_at_once_arrive
187
187
  msgs = []
188
- coprocess = coproc {
188
+ coprocess = spin {
189
189
  loop {
190
190
  msgs << receive
191
191
  }
@@ -9,7 +9,7 @@ class SpawnTest < MiniTest::Test
9
9
 
10
10
  def test_that_spawn_returns_a_coprocess
11
11
  result = nil
12
- coprocess = coproc { result = 42 }
12
+ coprocess = spin { result = 42 }
13
13
 
14
14
  assert_kind_of(Polyphony::Coprocess, coprocess)
15
15
  assert_nil(result)
@@ -20,7 +20,7 @@ class SpawnTest < MiniTest::Test
20
20
  def test_that_spawn_accepts_coprocess_argument
21
21
  result = nil
22
22
  coprocess = Polyphony::Coprocess.new { result = 42 }
23
- coproc coprocess
23
+ spin coprocess
24
24
 
25
25
  assert_nil(result)
26
26
  suspend
@@ -28,7 +28,7 @@ class SpawnTest < MiniTest::Test
28
28
  end
29
29
 
30
30
  def test_that_spawned_coprocess_saves_result
31
- coprocess = coproc { 42 }
31
+ coprocess = spin { 42 }
32
32
 
33
33
  assert_kind_of(Polyphony::Coprocess, coprocess)
34
34
  assert_nil(coprocess.result)
@@ -38,7 +38,7 @@ class SpawnTest < MiniTest::Test
38
38
 
39
39
  def test_that_spawned_coprocess_can_be_interrupted
40
40
  result = nil
41
- coprocess = coproc { sleep(1); 42 }
41
+ coprocess = spin { sleep(1); 42 }
42
42
  EV.next_tick { coprocess.interrupt }
43
43
  suspend
44
44
  assert_nil(coprocess.result)
@@ -59,7 +59,7 @@ class CancelScopeTest < Minitest::Test
59
59
 
60
60
  def test_that_cancel_scope_cancels_coprocess
61
61
  ctx = {}
62
- coproc do
62
+ spin do
63
63
  EV::Timer.new(0.005, 0).start { ctx[:cancel_scope]&.cancel! }
64
64
  sleep_with_cancel(ctx, :cancel)
65
65
  rescue Exception => e
@@ -76,7 +76,7 @@ class CancelScopeTest < Minitest::Test
76
76
 
77
77
  # def test_that_cancel_scope_cancels_async_op_with_stop
78
78
  # ctx = {}
79
- # coproc do
79
+ # spin do
80
80
  # EV::Timer.new(0, 0).start { ctx[:cancel_scope].cancel! }
81
81
  # sleep_with_cancel(ctx, :stop)
82
82
  # end
@@ -88,7 +88,7 @@ class CancelScopeTest < Minitest::Test
88
88
 
89
89
  def test_that_cancel_after_raises_cancelled_exception
90
90
  result = nil
91
- coproc do
91
+ spin do
92
92
  cancel_after(0.01) do
93
93
  sleep(1000)
94
94
  end
@@ -103,7 +103,7 @@ class CancelScopeTest < Minitest::Test
103
103
  def test_that_cancel_scopes_can_be_nested
104
104
  inner_result = nil
105
105
  outer_result = nil
106
- coproc do
106
+ spin do
107
107
  move_on_after(0.01) do
108
108
  move_on_after(0.02) do
109
109
  sleep(1000)
@@ -119,7 +119,7 @@ class CancelScopeTest < Minitest::Test
119
119
  EV.rerun
120
120
 
121
121
  outer_result = nil
122
- coproc do
122
+ spin do
123
123
  move_on_after(0.02) do
124
124
  move_on_after(0.01) do
125
125
  sleep(1000)
@@ -148,13 +148,13 @@ class SupervisorTest < MiniTest::Test
148
148
 
149
149
  def parallel_sleep(ctx)
150
150
  supervise do |s|
151
- (1..3).each { |idx| s.spawn sleep_and_set(ctx, idx) }
151
+ (1..3).each { |idx| s.spin sleep_and_set(ctx, idx) }
152
152
  end
153
153
  end
154
154
 
155
155
  def test_that_supervisor_waits_for_all_nested_coprocesses_to_complete
156
156
  ctx = {}
157
- coproc do
157
+ spin do
158
158
  parallel_sleep(ctx)
159
159
  end
160
160
  suspend
@@ -165,12 +165,12 @@ class SupervisorTest < MiniTest::Test
165
165
 
166
166
  def test_that_supervisor_can_add_coprocesses_after_having_started
167
167
  result = []
168
- coproc {
168
+ spin {
169
169
  supervisor = Polyphony::Supervisor.new
170
170
  3.times do |i|
171
- coproc do
171
+ spin do
172
172
  sleep(0.001)
173
- supervisor.spawn do
173
+ supervisor.spin do
174
174
  sleep(0.001)
175
175
  result << i
176
176
  end