tipi 0.43 → 0.45

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/test.yml +1 -3
  4. data/CHANGELOG.md +12 -0
  5. data/Gemfile +3 -1
  6. data/Gemfile.lock +14 -7
  7. data/README.md +184 -8
  8. data/Rakefile +1 -7
  9. data/benchmarks/bm_http1_parser.rb +1 -1
  10. data/bin/benchmark +0 -0
  11. data/bin/h1pd +0 -0
  12. data/bm.png +0 -0
  13. data/df/agent.rb +1 -1
  14. data/df/sample_agent.rb +2 -2
  15. data/df/server_utils.rb +1 -1
  16. data/examples/hello.rb +5 -0
  17. data/examples/http_server.js +1 -1
  18. data/examples/http_server_graceful.rb +1 -1
  19. data/examples/https_server.rb +41 -18
  20. data/examples/rack_server_forked.rb +26 -0
  21. data/examples/rack_server_https_forked.rb +1 -1
  22. data/examples/websocket_demo.rb +1 -1
  23. data/lib/tipi/acme.rb +46 -39
  24. data/lib/tipi/cli.rb +79 -16
  25. data/lib/tipi/config_dsl.rb +13 -13
  26. data/lib/tipi/configuration.rb +2 -2
  27. data/lib/tipi/controller/bare_polyphony.rb +0 -0
  28. data/lib/tipi/controller/bare_stock.rb +10 -0
  29. data/lib/tipi/controller/stock_http1_adapter.rb +15 -0
  30. data/lib/tipi/controller/web_polyphony.rb +351 -0
  31. data/lib/tipi/controller/web_stock.rb +631 -0
  32. data/lib/tipi/controller.rb +12 -0
  33. data/lib/tipi/digital_fabric/agent.rb +3 -3
  34. data/lib/tipi/digital_fabric/agent_proxy.rb +11 -5
  35. data/lib/tipi/digital_fabric/executive.rb +1 -1
  36. data/lib/tipi/digital_fabric/protocol.rb +1 -1
  37. data/lib/tipi/digital_fabric/service.rb +8 -8
  38. data/lib/tipi/handler.rb +2 -2
  39. data/lib/tipi/http1_adapter.rb +32 -27
  40. data/lib/tipi/http2_adapter.rb +10 -10
  41. data/lib/tipi/http2_stream.rb +14 -14
  42. data/lib/tipi/rack_adapter.rb +2 -2
  43. data/lib/tipi/response_extensions.rb +1 -1
  44. data/lib/tipi/supervisor.rb +75 -0
  45. data/lib/tipi/version.rb +1 -1
  46. data/lib/tipi/websocket.rb +3 -3
  47. data/lib/tipi.rb +4 -83
  48. data/test/coverage.rb +2 -2
  49. data/test/test_http_server.rb +14 -14
  50. data/tipi.gemspec +3 -2
  51. metadata +30 -5
@@ -83,7 +83,7 @@ module DigitalFabric
83
83
  raise 'Invalid output from top (cpu)'
84
84
  end
85
85
  cpu_utilization = 100 - Regexp.last_match(1).to_i
86
-
86
+
87
87
  unless top =~ TOP_MEM_REGEXP && Regexp.last_match(1) =~ TOP_MEM_FREE_REGEXP
88
88
  raise 'Invalid output from top (mem)'
89
89
  end
@@ -141,7 +141,7 @@ module DigitalFabric
141
141
  def ws_data(id, data)
142
142
  [ WS_DATA, id, data ]
143
143
  end
144
-
144
+
145
145
  def ws_close(id)
146
146
  [ WS_CLOSE, id ]
147
147
  end
@@ -90,7 +90,7 @@ module DigitalFabric
90
90
  rescue Exception
91
91
  [nil, nil]
92
92
  end
93
-
93
+
94
94
  def get_stats
95
95
  calculate_stats
96
96
  end
@@ -123,14 +123,14 @@ module DigitalFabric
123
123
 
124
124
  puts format('slow request (%.1f): %p', latency, req.headers)
125
125
  end
126
-
126
+
127
127
  def http_request(req, allow_df_upgrade = false)
128
128
  @current_request_count += 1
129
129
  @counters[:http_requests] += 1
130
130
  @counters[:connections] += 1 if req.headers[':first']
131
131
 
132
132
  return upgrade_request(req, allow_df_upgrade) if req.upgrade_protocol
133
-
133
+
134
134
  inject_request_headers(req)
135
135
  agent = find_agent(req)
136
136
  unless agent
@@ -159,7 +159,7 @@ module DigitalFabric
159
159
  req.headers['x-forwarded-for'] = conn.peeraddr(false)[2]
160
160
  req.headers['x-forwarded-proto'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
161
161
  end
162
-
162
+
163
163
  def upgrade_request(req, allow_df_upgrade)
164
164
  case (protocol = req.upgrade_protocol)
165
165
  when 'df'
@@ -178,7 +178,7 @@ module DigitalFabric
178
178
  agent.http_upgrade(req, protocol)
179
179
  end
180
180
  end
181
-
181
+
182
182
  def df_upgrade(req)
183
183
  # we don't want to count connected agents
184
184
  @current_request_count -= 1
@@ -191,7 +191,7 @@ module DigitalFabric
191
191
  ensure
192
192
  @current_request_count += 1
193
193
  end
194
-
194
+
195
195
  def mount(route, agent)
196
196
  if route[:path]
197
197
  route[:path_regexp] = path_regexp(route[:path])
@@ -200,7 +200,7 @@ module DigitalFabric
200
200
  @agents[agent] = route
201
201
  @routing_changed = true
202
202
  end
203
-
203
+
204
204
  def unmount(agent)
205
205
  route = @agents[agent]
206
206
  return unless route
@@ -211,7 +211,7 @@ module DigitalFabric
211
211
  end
212
212
 
213
213
  INVALID_HOST = 'INVALID_HOST'
214
-
214
+
215
215
  def find_agent(req)
216
216
  compile_agent_routes if @routing_changed
217
217
 
data/lib/tipi/handler.rb CHANGED
@@ -20,14 +20,14 @@ module Tipi
20
20
  ensure
21
21
  socket.close
22
22
  end
23
-
23
+
24
24
  ALPN_PROTOCOLS = %w[h2 http/1.1].freeze
25
25
  H2_PROTOCOL = 'h2'
26
26
 
27
27
  def protocol_adapter(socket, opts)
28
28
  use_http2 = socket.respond_to?(:alpn_protocol) &&
29
29
  socket.alpn_protocol == H2_PROTOCOL
30
-
30
+
31
31
  klass = use_http2 ? HTTP2Adapter : HTTP1Adapter
32
32
  klass.new(socket, opts)
33
33
  end
@@ -17,24 +17,27 @@ module Tipi
17
17
  @first = true
18
18
  @parser = H1P::Parser.new(@conn)
19
19
  end
20
-
20
+
21
21
  def each(&block)
22
22
  while true
23
23
  headers = @parser.parse_headers
24
24
  break unless headers
25
-
25
+
26
26
  # handle_request returns true if connection is not persistent or was
27
27
  # upgraded
28
28
  break if handle_request(headers, &block)
29
29
  end
30
- rescue H1P::Error
30
+ rescue H1P::Error, ArgumentError
31
+ # an ArgumentError might be raised in the parser if an invalid input
32
+ # string is given as the HTTP method (String#upcase will raise on invalid HTTP string)
33
+ #
31
34
  # ignore
32
35
  rescue SystemCallError, IOError
33
36
  # ignore
34
37
  ensure
35
38
  finalize_client_loop
36
39
  end
37
-
40
+
38
41
  def handle_request(headers, &block)
39
42
  scheme = (proto = headers['x-forwarded-proto']) ?
40
43
  proto.downcase : scheme_from_connection
@@ -44,9 +47,9 @@ module Tipi
44
47
  headers[':first'] = true
45
48
  @first = nil
46
49
  end
47
-
50
+
48
51
  return true if upgrade_connection(headers, &block)
49
-
52
+
50
53
  request = Qeweney::Request.new(headers, self)
51
54
  if !@parser.complete?
52
55
  request.buffer_body_chunk(@parser.read_body_chunk(true))
@@ -63,14 +66,14 @@ module Tipi
63
66
  return connection && connection != 'close'
64
67
  end
65
68
  end
66
-
69
+
67
70
  def finalize_client_loop
68
71
  @parser = nil
69
72
  @splicing_pipe = nil
70
73
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
71
74
  @conn.close
72
75
  end
73
-
76
+
74
77
  # Reads a body chunk for the current request. Transfers control to the parse
75
78
  # loop, and resumes once the parse_loop has fired the on_body callback
76
79
  def get_body_chunk(request, buffered_only = false)
@@ -84,11 +87,11 @@ module Tipi
84
87
  def complete?(request)
85
88
  @parser.complete?
86
89
  end
87
-
90
+
88
91
  def protocol
89
92
  @protocol
90
93
  end
91
-
94
+
92
95
  # Upgrades the connection to a different protocol, if the 'Upgrade' header is
93
96
  # given. By default the only supported upgrade protocol is HTTP2. Additional
94
97
  # protocols, notably WebSocket, can be specified by passing a hash to the
@@ -114,28 +117,28 @@ module Tipi
114
117
  def upgrade_connection(headers, &block)
115
118
  upgrade_protocol = headers['upgrade']
116
119
  return nil unless upgrade_protocol
117
-
120
+
118
121
  upgrade_protocol = upgrade_protocol.downcase.to_sym
119
122
  upgrade_handler = @opts[:upgrade] && @opts[:upgrade][upgrade_protocol]
120
123
  return upgrade_with_handler(upgrade_handler, headers) if upgrade_handler
121
124
  return upgrade_to_http2(headers, &block) if upgrade_protocol == :h2c
122
-
125
+
123
126
  nil
124
127
  end
125
-
128
+
126
129
  def upgrade_with_handler(handler, headers)
127
130
  @parser = nil
128
131
  handler.(self, headers)
129
132
  true
130
133
  end
131
-
134
+
132
135
  def upgrade_to_http2(headers, &block)
133
136
  headers = http2_upgraded_headers(headers)
134
137
  body = @parser.read_body
135
138
  HTTP2Adapter.upgrade_each(@conn, @opts, headers, body, &block)
136
139
  true
137
140
  end
138
-
141
+
139
142
  # Returns headers for HTTP2 upgrade
140
143
  # @param headers [Hash] request headers
141
144
  # @return [Hash] headers for HTTP2 upgrade
@@ -153,10 +156,10 @@ module Tipi
153
156
  def scheme_from_connection
154
157
  @conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
155
158
  end
156
-
159
+
157
160
  # response API
158
161
 
159
- CRLF = "\r\n"
162
+ CRLF = "\r\n"
160
163
  CRLF_ZERO_CRLF_CRLF = "\r\n0\r\n\r\n"
161
164
 
162
165
  # Sends response including headers and body. Waits for the request to complete
@@ -174,17 +177,19 @@ module Tipi
174
177
  end
175
178
  end
176
179
 
180
+ CHUNK_LENGTH_PROC = ->(len) { "#{len.to_s(16)}\r\n" }
181
+
177
182
  def respond_from_io(request, io, headers, chunk_size = 2**14)
178
183
  formatted_headers = format_headers(headers, true, true)
179
184
  request.tx_incr(formatted_headers.bytesize)
180
-
185
+
181
186
  # assume chunked encoding
182
187
  Thread.current.backend.splice_chunks(
183
188
  io,
184
189
  @conn,
185
190
  formatted_headers,
186
191
  "0\r\n\r\n",
187
- ->(len) { "#{len.to_s(16)}\r\n" },
192
+ CHUNK_LENGTH_PROC,
188
193
  "\r\n",
189
194
  chunk_size
190
195
  )
@@ -206,7 +211,7 @@ module Tipi
206
211
  def http1_1?(request)
207
212
  request.headers[':protocol'] == 'http/1.1'
208
213
  end
209
-
214
+
210
215
  # Sends a response body chunk. If no headers were sent, default headers are
211
216
  # sent using #send_headers. if the done option is true(thy), an empty chunk
212
217
  # will be sent to signal response completion to the client.
@@ -223,7 +228,7 @@ module Tipi
223
228
  request.tx_incr(data.bytesize)
224
229
  @conn.write(data)
225
230
  end
226
-
231
+
227
232
  def send_chunk_from_io(request, io, r, w, chunk_size)
228
233
  len = w.splice(io, chunk_size)
229
234
  if len > 0
@@ -245,12 +250,12 @@ module Tipi
245
250
  request.tx_incr(5)
246
251
  @conn << "0\r\n\r\n"
247
252
  end
248
-
253
+
249
254
  def close
250
255
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
251
256
  @conn.close
252
257
  end
253
-
258
+
254
259
  private
255
260
 
256
261
  INTERNAL_HEADER_REGEXP = /^:/.freeze
@@ -267,13 +272,13 @@ module Tipi
267
272
  lines = format_status_line(body, status, chunked)
268
273
  headers.each do |k, v|
269
274
  next if k =~ INTERNAL_HEADER_REGEXP
270
-
275
+
271
276
  collect_header_lines(lines, k, v)
272
277
  end
273
278
  lines << CRLF
274
279
  lines
275
280
  end
276
-
281
+
277
282
  def format_status_line(body, status, chunked)
278
283
  if !body
279
284
  empty_status_line(status)
@@ -281,7 +286,7 @@ module Tipi
281
286
  with_body_status_line(status, body, chunked)
282
287
  end
283
288
  end
284
-
289
+
285
290
  def empty_status_line(status)
286
291
  if status == 204
287
292
  +"HTTP/1.1 #{status}\r\n"
@@ -289,7 +294,7 @@ module Tipi
289
294
  +"HTTP/1.1 #{status}\r\nContent-Length: 0\r\n"
290
295
  end
291
296
  end
292
-
297
+
293
298
  def with_body_status_line(status, body, chunked)
294
299
  if chunked
295
300
  +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
@@ -21,7 +21,7 @@ module Tipi
21
21
  adapter = new(socket, opts, headers, body)
22
22
  adapter.each(&block)
23
23
  end
24
-
24
+
25
25
  def initialize(conn, opts, upgrade_headers = nil, upgrade_body = nil)
26
26
  @conn = conn
27
27
  @opts = opts
@@ -36,7 +36,7 @@ module Tipi
36
36
  @interface.on(:frame, &method(:send_frame))
37
37
  @streams = {}
38
38
  end
39
-
39
+
40
40
  def send_frame(data)
41
41
  if @transfer_count_request
42
42
  @transfer_count_request.tx_incr(data.bytesize)
@@ -47,14 +47,14 @@ module Tipi
47
47
  rescue Exception => e
48
48
  @connection_fiber.transfer e
49
49
  end
50
-
50
+
51
51
  UPGRADE_MESSAGE = <<~HTTP.gsub("\n", "\r\n")
52
52
  HTTP/1.1 101 Switching Protocols
53
53
  Connection: Upgrade
54
54
  Upgrade: h2c
55
-
55
+
56
56
  HTTP
57
-
57
+
58
58
  def upgrade
59
59
  @conn << UPGRADE_MESSAGE
60
60
  @tx += UPGRADE_MESSAGE.bytesize
@@ -63,7 +63,7 @@ module Tipi
63
63
  ensure
64
64
  @upgrade_headers = nil
65
65
  end
66
-
66
+
67
67
  # Iterates over incoming requests
68
68
  def each(&block)
69
69
  @interface.on(:stream) { |stream| start_stream(stream, &block) }
@@ -84,26 +84,26 @@ module Tipi
84
84
  @rx = 0
85
85
  count
86
86
  end
87
-
87
+
88
88
  def get_tx_count
89
89
  count = @tx
90
90
  @tx = 0
91
91
  count
92
92
  end
93
-
93
+
94
94
  def start_stream(stream, &block)
95
95
  stream = HTTP2StreamHandler.new(self, stream, @conn, @first, &block)
96
96
  @first = nil if @first
97
97
  @streams[stream] = true
98
98
  end
99
-
99
+
100
100
  def finalize_client_loop
101
101
  @interface = nil
102
102
  @streams.each_key(&:stop)
103
103
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
104
104
  @conn.close
105
105
  end
106
-
106
+
107
107
  def close
108
108
  @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
109
109
  @conn.close
@@ -8,7 +8,7 @@ module Tipi
8
8
  class HTTP2StreamHandler
9
9
  attr_accessor :__next__
10
10
  attr_reader :conn
11
-
11
+
12
12
  def initialize(adapter, stream, conn, first, &block)
13
13
  @adapter = adapter
14
14
  @stream = stream
@@ -32,7 +32,7 @@ module Tipi
32
32
  stream.on(:data, &method(:on_data))
33
33
  stream.on(:half_close, &method(:on_half_close))
34
34
  end
35
-
35
+
36
36
  def run(&block)
37
37
  request = receive
38
38
  error = nil
@@ -45,7 +45,7 @@ module Tipi
45
45
  ensure
46
46
  @connection_fiber.schedule error
47
47
  end
48
-
48
+
49
49
  def on_headers(headers)
50
50
  @request = Qeweney::Request.new(headers.to_h, self)
51
51
  @request.rx_incr(@adapter.get_rx_count)
@@ -59,7 +59,7 @@ module Tipi
59
59
 
60
60
  def on_data(data)
61
61
  data = data.to_s # chunks might be wrapped in a HTTP2::Buffer
62
-
62
+
63
63
  (@buffered_chunks ||= []) << data
64
64
  @get_body_chunk_fiber&.schedule
65
65
  end
@@ -68,7 +68,7 @@ module Tipi
68
68
  @get_body_chunk_fiber&.schedule
69
69
  @complete = true
70
70
  end
71
-
71
+
72
72
  def protocol
73
73
  'h2'
74
74
  end
@@ -84,7 +84,7 @@ module Tipi
84
84
  @buffered_chunks ||= []
85
85
  return @buffered_chunks.shift unless @buffered_chunks.empty?
86
86
  return nil if @complete
87
-
87
+
88
88
  begin
89
89
  @get_body_chunk_fiber = Fiber.current
90
90
  suspend
@@ -112,7 +112,7 @@ module Tipi
112
112
  def complete?(request)
113
113
  @complete
114
114
  end
115
-
115
+
116
116
  # response API
117
117
  def respond(request, chunk, headers)
118
118
  headers[':status'] ||= Qeweney::Status::OK
@@ -149,10 +149,10 @@ module Tipi
149
149
  end
150
150
  end
151
151
  end
152
-
152
+
153
153
  def send_headers(request, headers, empty_response: false)
154
154
  return if @headers_sent
155
-
155
+
156
156
  headers[':status'] ||= (empty_response ? Qeweney::Status::NO_CONTENT : Qeweney::Status::OK).to_s
157
157
  with_transfer_count(request) do
158
158
  @stream.headers(transform_headers(headers), end_stream: false)
@@ -161,10 +161,10 @@ module Tipi
161
161
  rescue HTTP2::Error::StreamClosed
162
162
  # ignore
163
163
  end
164
-
164
+
165
165
  def send_chunk(request, chunk, done: false)
166
166
  send_headers({}, false) unless @headers_sent
167
-
167
+
168
168
  if chunk
169
169
  with_transfer_count(request) do
170
170
  @stream.data(chunk, end_stream: done)
@@ -175,7 +175,7 @@ module Tipi
175
175
  rescue HTTP2::Error::StreamClosed
176
176
  # ignore
177
177
  end
178
-
178
+
179
179
  def finish(request)
180
180
  if @headers_sent
181
181
  @stream.close
@@ -188,10 +188,10 @@ module Tipi
188
188
  rescue HTTP2::Error::StreamClosed
189
189
  # ignore
190
190
  end
191
-
191
+
192
192
  def stop
193
193
  return if @complete
194
-
194
+
195
195
  @stream.close
196
196
  @stream_fiber.schedule(Polyphony::MoveOn.new)
197
197
  end
@@ -3,12 +3,12 @@
3
3
  require 'rack'
4
4
 
5
5
  module Tipi
6
- module RackAdapter
6
+ module RackAdapter
7
7
  class << self
8
8
  def run(app)
9
9
  ->(req) { respond(req, app.(env(req))) }
10
10
  end
11
-
11
+
12
12
  def load(path)
13
13
  src = IO.read(path)
14
14
  instance_eval(src, path, 1)
@@ -9,7 +9,7 @@ module Tipi
9
9
  def serve_io(io, opts)
10
10
  if !opts[:stat] || opts[:stat].size >= SPLICE_CHUNKS_SIZE_THRESHOLD
11
11
  @adapter.respond_from_io(self, io, opts[:headers], opts[:chunk_size] || 2**14)
12
- else
12
+ else
13
13
  respond(io.read, opts[:headers] || {})
14
14
  end
15
15
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'polyphony'
4
+ require 'json'
5
+
6
+ module Tipi
7
+ module Supervisor
8
+ class << self
9
+ def run(opts)
10
+ puts "Start supervisor pid: #{Process.pid}"
11
+ @opts = opts
12
+ @controller_watcher = start_controller_watcher
13
+ supervise_loop
14
+ end
15
+
16
+ def start_controller_watcher
17
+ spin do
18
+ cmd = controller_cmd
19
+ puts "Starting controller..."
20
+ pid = Kernel.spawn(*cmd)
21
+ @controller_pid = pid
22
+ puts "Controller pid: #{pid}"
23
+ _pid, status = Polyphony.backend_waitpid(pid)
24
+ puts "Controller has terminated with status: #{status.inspect}"
25
+ terminated = true
26
+ ensure
27
+ if pid && !terminated
28
+ puts "Terminate controller #{pid.inspect}"
29
+ Polyphony::Process.kill_process(pid)
30
+ end
31
+ Fiber.current.parent << pid
32
+ end
33
+ end
34
+
35
+ def controller_cmd
36
+ [
37
+ 'ruby',
38
+ File.join(__dir__, 'controller.rb'),
39
+ @opts.to_json
40
+ ]
41
+ end
42
+
43
+ def supervise_loop
44
+ this_fiber = Fiber.current
45
+ trap('SIGUSR2') { this_fiber << :replace_controller }
46
+ loop do
47
+ case (msg = receive)
48
+ when :replace_controller
49
+ replace_controller
50
+ when Integer
51
+ pid = msg
52
+ if pid == @controller_pid
53
+ puts 'Detected dead controller. Restarting...'
54
+ exit!
55
+ @controller_watcher.restart
56
+ end
57
+ else
58
+ raise "Invalid message received: #{msg.inspect}"
59
+ end
60
+ end
61
+ end
62
+
63
+ def replace_controller
64
+ puts "Replacing controller"
65
+ old_watcher = @controller_watcher
66
+ @controller_watcher = start_controller_watcher
67
+
68
+ # TODO: we'll want to get some kind of signal from the new controller once it's ready
69
+ sleep 1
70
+
71
+ old_watcher.terminate(true)
72
+ end
73
+ end
74
+ end
75
+ end
data/lib/tipi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tipi
4
- VERSION = '0.43'
4
+ VERSION = '0.45'
5
5
  end
@@ -20,12 +20,12 @@ module Tipi
20
20
  @version = headers['sec-websocket-version'].to_i
21
21
  @reader = ::WebSocket::Frame::Incoming::Server.new(version: @version)
22
22
  end
23
-
23
+
24
24
  def recv
25
25
  if (msg = @reader.next)
26
26
  return msg.to_s
27
27
  end
28
-
28
+
29
29
  @conn.recv_loop do |data|
30
30
  @reader << data
31
31
  if (msg = @reader.next)
@@ -48,7 +48,7 @@ module Tipi
48
48
  end
49
49
  end
50
50
  end
51
-
51
+
52
52
  OutgoingFrame = ::WebSocket::Frame::Outgoing::Server
53
53
 
54
54
  def send(data)