polyphony 0.17 → 0.19

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