tipi 0.40 → 0.45

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/test.yml +3 -1
  4. data/.gitignore +5 -1
  5. data/CHANGELOG.md +35 -0
  6. data/Gemfile +7 -1
  7. data/Gemfile.lock +55 -29
  8. data/README.md +184 -8
  9. data/Rakefile +1 -3
  10. data/benchmarks/bm_http1_parser.rb +85 -0
  11. data/bin/benchmark +37 -0
  12. data/bin/h1pd +6 -0
  13. data/bin/tipi +3 -21
  14. data/bm.png +0 -0
  15. data/df/agent.rb +1 -1
  16. data/df/sample_agent.rb +2 -2
  17. data/df/server.rb +16 -102
  18. data/df/server_utils.rb +175 -0
  19. data/examples/full_service.rb +13 -0
  20. data/examples/hello.rb +5 -0
  21. data/examples/http1_parser.rb +55 -0
  22. data/examples/http_server.js +1 -1
  23. data/examples/http_server.rb +15 -3
  24. data/examples/http_server_graceful.rb +1 -1
  25. data/examples/http_server_static.rb +6 -18
  26. data/examples/https_server.rb +41 -15
  27. data/examples/rack_server_forked.rb +26 -0
  28. data/examples/rack_server_https_forked.rb +1 -1
  29. data/examples/servername_cb.rb +37 -0
  30. data/examples/websocket_demo.rb +1 -1
  31. data/lib/tipi/acme.rb +315 -0
  32. data/lib/tipi/cli.rb +93 -0
  33. data/lib/tipi/config_dsl.rb +13 -13
  34. data/lib/tipi/configuration.rb +2 -2
  35. data/{e → lib/tipi/controller/bare_polyphony.rb} +0 -0
  36. data/lib/tipi/controller/bare_stock.rb +10 -0
  37. data/lib/tipi/controller/stock_http1_adapter.rb +15 -0
  38. data/lib/tipi/controller/web_polyphony.rb +351 -0
  39. data/lib/tipi/controller/web_stock.rb +631 -0
  40. data/lib/tipi/controller.rb +12 -0
  41. data/lib/tipi/digital_fabric/agent.rb +10 -8
  42. data/lib/tipi/digital_fabric/agent_proxy.rb +26 -12
  43. data/lib/tipi/digital_fabric/executive.rb +7 -3
  44. data/lib/tipi/digital_fabric/protocol.rb +19 -4
  45. data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
  46. data/lib/tipi/digital_fabric/service.rb +84 -56
  47. data/lib/tipi/handler.rb +2 -2
  48. data/lib/tipi/http1_adapter.rb +86 -125
  49. data/lib/tipi/http2_adapter.rb +29 -16
  50. data/lib/tipi/http2_stream.rb +52 -56
  51. data/lib/tipi/rack_adapter.rb +2 -53
  52. data/lib/tipi/response_extensions.rb +2 -2
  53. data/lib/tipi/supervisor.rb +75 -0
  54. data/lib/tipi/version.rb +1 -1
  55. data/lib/tipi/websocket.rb +3 -3
  56. data/lib/tipi.rb +8 -5
  57. data/test/coverage.rb +2 -2
  58. data/test/helper.rb +60 -12
  59. data/test/test_http_server.rb +14 -41
  60. data/test/test_request.rb +2 -29
  61. data/tipi.gemspec +12 -8
  62. metadata +88 -28
  63. data/examples/automatic_certificate.rb +0 -193
@@ -32,13 +32,13 @@ module DigitalFabric
32
32
  @fiber = Fiber.current
33
33
  @service.mount(route, self)
34
34
  @mounted = true
35
- keep_alive_timer = spin_loop(interval: 5) { keep_alive }
35
+ # keep_alive_timer = spin_loop("#{@fiber.tag}-keep_alive", interval: 5) { keep_alive }
36
36
  process_incoming_messages(false)
37
37
  rescue GracefulShutdown
38
38
  puts "Proxy got graceful shutdown, left: #{@requests.size} requests" if @requests.size > 0
39
- process_incoming_messages(true)
39
+ move_on_after(15) { process_incoming_messages(true) }
40
40
  ensure
41
- keep_alive_timer&.stop
41
+ # keep_alive_timer&.stop
42
42
  unmount
43
43
  end
44
44
 
@@ -56,11 +56,11 @@ module DigitalFabric
56
56
  def unmount
57
57
  return unless @mounted
58
58
 
59
- @service.unmount(self)
59
+ @service.unmount(self)
60
60
  @mounted = nil
61
61
  end
62
62
 
63
- def shutdown
63
+ def send_shutdown
64
64
  send_df_message(Protocol.shutdown)
65
65
  @fiber.raise GracefulShutdown.new
66
66
  end
@@ -98,6 +98,8 @@ module DigitalFabric
98
98
  return
99
99
  when Protocol::UNMOUNT
100
100
  return unmount
101
+ when Protocol::STATS_REQUEST
102
+ return handle_stats_request(message[Protocol::Attribute::ID])
101
103
  end
102
104
 
103
105
  handler = @requests[message[Protocol::Attribute::ID]]
@@ -142,13 +144,20 @@ module DigitalFabric
142
144
  t0 = Time.now
143
145
  t1 = nil
144
146
  with_request do |id|
145
- send_df_message(Protocol.http_request(id, req))
147
+ msg = Protocol.http_request(id, req.headers, req.next_chunk(true), req.complete?)
148
+ send_df_message(msg)
146
149
  while (message = receive)
150
+ kind = message[Protocol::Attribute::KIND]
147
151
  unless t1
148
152
  t1 = Time.now
149
- @service.record_latency_measurement(t1 - t0)
153
+ if kind == Protocol::HTTP_RESPONSE
154
+ headers = message[Protocol::Attribute::HttpResponse::HEADERS]
155
+ status = (headers && headers[':status']) || 200
156
+ if status < Qeweney::Status::BAD_REQUEST
157
+ @service.record_latency_measurement(t1 - t0, req)
158
+ end
159
+ end
150
160
  end
151
- kind = message[Protocol::Attribute::KIND]
152
161
  attributes = message[Protocol::Attribute::HttpRequest::HEADERS..-1]
153
162
  return if http_request_message(id, req, kind, attributes)
154
163
  end
@@ -187,6 +196,11 @@ module DigitalFabric
187
196
  send_df_message(Protocol.transfer_count(key, rx, tx))
188
197
  end
189
198
 
199
+ def handle_stats_request(id)
200
+ stats = @service.get_stats
201
+ send_df_message(Protocol.stats_response(id, stats))
202
+ end
203
+
190
204
  HTTP_RESPONSE_UPGRADE_HEADERS = { ':status' => Qeweney::Status::SWITCHING_PROTOCOLS }
191
205
 
192
206
  def http_custom_upgrade(id, req, headers)
@@ -197,7 +211,7 @@ module DigitalFabric
197
211
  req.send_headers(upgrade_headers, true)
198
212
 
199
213
  conn = req.adapter.conn
200
- reader = spin do
214
+ reader = spin("#{Fiber.current.tag}.#{id}") do
201
215
  conn.recv_loop do |data|
202
216
  send_df_message(Protocol.conn_data(id, data))
203
217
  end
@@ -233,7 +247,7 @@ module DigitalFabric
233
247
  else
234
248
  req.send_headers(headers) if headers && !req.headers_sent?
235
249
  req.send_chunk(body, done: complete) if body or complete
236
-
250
+
237
251
  if complete && transfer_count_key
238
252
  rx, tx = req.transfer_counts
239
253
  send_transfer_count(transfer_count_key, rx, tx)
@@ -277,7 +291,7 @@ module DigitalFabric
277
291
  response = receive
278
292
  case response[0]
279
293
  when Protocol::WS_RESPONSE
280
- headers = response[2] || {}
294
+ headers = response[2] || {}
281
295
  status = headers[':status'] || Qeweney::Status::SWITCHING_PROTOCOLS
282
296
  if status != Qeweney::Status::SWITCHING_PROTOCOLS
283
297
  req.respond(nil, headers)
@@ -294,7 +308,7 @@ module DigitalFabric
294
308
  end
295
309
 
296
310
  def run_websocket_connection(id, websocket)
297
- reader = spin do
311
+ reader = spin("#{Fiber.current}.#{id}-ws") do
298
312
  websocket.recv_loop do |data|
299
313
  send_df_message(Protocol.ws_data(id, data))
300
314
  end
@@ -15,7 +15,7 @@ module DigitalFabric
15
15
  route[:executive] = true
16
16
  @service.mount(route, self)
17
17
  @current_request_count = 0
18
- @updater = spin_loop(interval: 10) { update_service_stats }
18
+ # @updater = spin_loop(:executive_updater, interval: 10) { update_service_stats }
19
19
  update_service_stats
20
20
  end
21
21
 
@@ -33,9 +33,13 @@ module DigitalFabric
33
33
  req.respond(message.to_json, { 'Content-Type' => 'text.json' })
34
34
  when '/stream/stats'
35
35
  stream_stats(req)
36
+ when '/upload'
37
+ req.respond("body: #{req.read.inspect}")
36
38
  else
37
39
  req.respond('Invalid path', { ':status' => Qeweney::Status::NOT_FOUND })
38
40
  end
41
+ rescue => e
42
+ puts "Error: #{e.inspect}"
39
43
  ensure
40
44
  @current_request_count -= 1
41
45
  end
@@ -43,7 +47,7 @@ module DigitalFabric
43
47
  def stream_stats(req)
44
48
  req.send_headers({ 'Content-Type' => 'text/event-stream' })
45
49
 
46
- @service.timer.every(10) do
50
+ every(10) do
47
51
  message = last_service_stats
48
52
  req.send_chunk(format_sse_event(message.to_json))
49
53
  end
@@ -79,7 +83,7 @@ module DigitalFabric
79
83
  raise 'Invalid output from top (cpu)'
80
84
  end
81
85
  cpu_utilization = 100 - Regexp.last_match(1).to_i
82
-
86
+
83
87
  unless top =~ TOP_MEM_REGEXP && Regexp.last_match(1) =~ TOP_MEM_FREE_REGEXP
84
88
  raise 'Invalid output from top (mem)'
85
89
  end
@@ -22,6 +22,9 @@ module DigitalFabric
22
22
 
23
23
  TRANSFER_COUNT = 'transfer_count'
24
24
 
25
+ STATS_REQUEST = 'stats_request'
26
+ STATS_RESPONSE = 'stats_response'
27
+
25
28
  SEND_TIMEOUT = 15
26
29
  RECV_TIMEOUT = SEND_TIMEOUT + 5
27
30
 
@@ -69,6 +72,10 @@ module DigitalFabric
69
72
  RX = 2
70
73
  TX = 3
71
74
  end
75
+
76
+ module Stats
77
+ STATS = 2
78
+ end
72
79
  end
73
80
 
74
81
  class << self
@@ -95,8 +102,8 @@ module DigitalFabric
95
102
  DF_UPGRADE_RESPONSE
96
103
  end
97
104
 
98
- def http_request(id, req)
99
- [ HTTP_REQUEST, id, req.headers, req.next_chunk, req.complete? ]
105
+ def http_request(id, headers, buffered_chunk, complete)
106
+ [ HTTP_REQUEST, id, headers, buffered_chunk, complete ]
100
107
  end
101
108
 
102
109
  def http_response(id, body, headers, complete, transfer_count_key = nil)
@@ -134,14 +141,22 @@ module DigitalFabric
134
141
  def ws_data(id, data)
135
142
  [ WS_DATA, id, data ]
136
143
  end
137
-
144
+
138
145
  def ws_close(id)
139
- [WS_CLOSE, id ]
146
+ [ WS_CLOSE, id ]
140
147
  end
141
148
 
142
149
  def transfer_count(key, rx, tx)
143
150
  [ TRANSFER_COUNT, key, rx, tx ]
144
151
  end
152
+
153
+ def stats_request(id)
154
+ [ STATS_REQUEST, id ]
155
+ end
156
+
157
+ def stats_response(id, stats)
158
+ [ STATS_RESPONSE, id, stats ]
159
+ end
145
160
  end
146
161
  end
147
162
  end
@@ -17,10 +17,6 @@ module DigitalFabric
17
17
  @agent.get_http_request_body(@id, 1)
18
18
  end
19
19
 
20
- def consume_request(request)
21
- @agent.get_http_request_body(@id, nil)
22
- end
23
-
24
20
  def respond(request, body, headers)
25
21
  @agent.send_df_message(
26
22
  Protocol.http_response(@id, body, headers, true)
@@ -13,26 +13,22 @@ module DigitalFabric
13
13
  @token = token
14
14
  @agents = {}
15
15
  @routes = {}
16
- @waiting_lists = {} # hash mapping routes to arrays of requests waiting for an agent to mount
17
16
  @counters = {
18
17
  connections: 0,
19
18
  http_requests: 0,
20
19
  errors: 0
21
20
  }
22
21
  @connection_count = 0
22
+ @current_request_count = 0
23
23
  @http_latency_accumulator = 0
24
24
  @http_latency_counter = 0
25
+ @http_latency_max = 0
25
26
  @last_counters = @counters.merge(stamp: Time.now.to_f - 1)
26
27
  @fiber = Fiber.current
27
- @timer = Polyphony::Timer.new(resolution: 1)
28
-
29
- stats_updater = spin { @timer.every(10) { update_stats } }
30
- @stats = {}
31
-
32
- @current_request_count = 0
28
+ # @timer = Polyphony::Timer.new('service_timer', resolution: 5)
33
29
  end
34
30
 
35
- def update_stats
31
+ def calculate_stats
36
32
  now = Time.now.to_f
37
33
  elapsed = now - @last_counters[:stamp]
38
34
  connections = @counters[:connections] - @last_counters[:connections]
@@ -40,23 +36,65 @@ module DigitalFabric
40
36
  errors = @counters[:errors] - @last_counters[:errors]
41
37
  @last_counters = @counters.merge(stamp: now)
42
38
 
43
- average_latency = @http_latency_counter > 0 ?
44
- @http_latency_accumulator / @http_latency_counter :
45
- 0
39
+ average_latency = @http_latency_counter == 0 ? 0 :
40
+ @http_latency_accumulator / @http_latency_counter
46
41
  @http_latency_accumulator = 0
47
42
  @http_latency_counter = 0
48
-
49
- @stats = {
50
- connection_rate: connections / elapsed,
51
- http_request_rate: http_requests / elapsed,
52
- error_rate: errors / elapsed,
53
- average_latency: average_latency,
54
- agent_count: @agents.size,
55
- connection_count: @connection_count,
56
- concurrent_requests: @current_request_count
43
+ max_latency = @http_latency_max
44
+ @http_latency_max = 0
45
+
46
+ cpu, rss = pid_cpu_and_rss(Process.pid)
47
+
48
+ backend_stats = Thread.backend.stats
49
+ op_rate = backend_stats[:op_count] / elapsed
50
+ switch_rate = backend_stats[:switch_count] / elapsed
51
+ poll_rate = backend_stats[:poll_count] / elapsed
52
+
53
+ object_space_stats = ObjectSpace.count_objects
54
+
55
+ {
56
+ service: {
57
+ agent_count: @agents.size,
58
+ connection_count: @connection_count,
59
+ connection_rate: connections / elapsed,
60
+ error_rate: errors / elapsed,
61
+ http_request_rate: http_requests / elapsed,
62
+ latency_avg: average_latency,
63
+ latency_max: max_latency,
64
+ pending_requests: @current_request_count,
65
+ },
66
+ backend: {
67
+ op_rate: op_rate,
68
+ pending_ops: backend_stats[:pending_ops],
69
+ poll_rate: poll_rate,
70
+ runqueue_size: backend_stats[:runqueue_size],
71
+ runqueue_high_watermark: backend_stats[:runqueue_max_length],
72
+ switch_rate: switch_rate,
73
+
74
+ },
75
+ process: {
76
+ cpu_usage: cpu,
77
+ rss: rss.to_f / 1024,
78
+ objects_total: object_space_stats[:TOTAL],
79
+ objects_free: object_space_stats[:FREE]
80
+ }
57
81
  }
58
82
  end
59
83
 
84
+ def pid_cpu_and_rss(pid)
85
+ s = `ps -p #{pid} -o %cpu,rss`
86
+ cpu, rss = s.lines[1].chomp.strip.split(' ')
87
+ [cpu.to_f, rss.to_i]
88
+ rescue Polyphony::BaseException
89
+ raise
90
+ rescue Exception
91
+ [nil, nil]
92
+ end
93
+
94
+ def get_stats
95
+ calculate_stats
96
+ end
97
+
60
98
  def incr_connection_count
61
99
  @connection_count += 1
62
100
  end
@@ -77,23 +115,25 @@ module DigitalFabric
77
115
  count
78
116
  end
79
117
 
80
- def record_latency_measurement(latency)
118
+ def record_latency_measurement(latency, req)
81
119
  @http_latency_accumulator += latency
82
120
  @http_latency_counter += 1
121
+ @http_latency_max = latency if latency > @http_latency_max
122
+ return if latency < 1.0
123
+
124
+ puts format('slow request (%.1f): %p', latency, req.headers)
83
125
  end
84
-
85
- def http_request(req)
126
+
127
+ def http_request(req, allow_df_upgrade = false)
86
128
  @current_request_count += 1
87
129
  @counters[:http_requests] += 1
88
130
  @counters[:connections] += 1 if req.headers[':first']
89
131
 
90
- return upgrade_request(req) if req.upgrade_protocol
91
-
132
+ return upgrade_request(req, allow_df_upgrade) if req.upgrade_protocol
133
+
92
134
  inject_request_headers(req)
93
135
  agent = find_agent(req)
94
136
  unless agent
95
- return req.respond('pong') if req.query[:q] == 'ping'
96
-
97
137
  @counters[:errors] += 1
98
138
  return req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)
99
139
  end
@@ -119,11 +159,15 @@ module DigitalFabric
119
159
  req.headers['x-forwarded-for'] = conn.peeraddr(false)[2]
120
160
  req.headers['x-forwarded-proto'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
121
161
  end
122
-
123
- def upgrade_request(req)
162
+
163
+ def upgrade_request(req, allow_df_upgrade)
124
164
  case (protocol = req.upgrade_protocol)
125
165
  when 'df'
126
- df_upgrade(req)
166
+ if allow_df_upgrade
167
+ df_upgrade(req)
168
+ else
169
+ req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)
170
+ end
127
171
  else
128
172
  agent = find_agent(req)
129
173
  unless agent
@@ -134,16 +178,20 @@ module DigitalFabric
134
178
  agent.http_upgrade(req, protocol)
135
179
  end
136
180
  end
137
-
181
+
138
182
  def df_upgrade(req)
183
+ # we don't want to count connected agents
184
+ @current_request_count -= 1
139
185
  if req.headers['df-token'] != @token
140
186
  return req.respond(nil, ':status' => Qeweney::Status::FORBIDDEN)
141
187
  end
142
188
 
143
189
  req.adapter.conn << Protocol.df_upgrade_response
144
190
  AgentProxy.new(self, req)
191
+ ensure
192
+ @current_request_count += 1
145
193
  end
146
-
194
+
147
195
  def mount(route, agent)
148
196
  if route[:path]
149
197
  route[:path_regexp] = path_regexp(route[:path])
@@ -151,13 +199,8 @@ module DigitalFabric
151
199
  @executive = agent if route[:executive]
152
200
  @agents[agent] = route
153
201
  @routing_changed = true
154
-
155
- if (waiting = @waiting_lists[route])
156
- waiting.each { |f| f.schedule(agent) }
157
- @waiting_lists.delete(route)
158
- end
159
202
  end
160
-
203
+
161
204
  def unmount(agent)
162
205
  route = @agents[agent]
163
206
  return unless route
@@ -165,12 +208,10 @@ module DigitalFabric
165
208
  @executive = nil if route[:executive]
166
209
  @agents.delete(agent)
167
210
  @routing_changed = true
168
-
169
- @waiting_lists[route] ||= []
170
211
  end
171
212
 
172
213
  INVALID_HOST = 'INVALID_HOST'
173
-
214
+
174
215
  def find_agent(req)
175
216
  compile_agent_routes if @routing_changed
176
217
 
@@ -182,12 +223,6 @@ module DigitalFabric
182
223
  end
183
224
  return @routes[route] if route
184
225
 
185
- # # search for a known route for an agent that recently unmounted
186
- # route, wait_list = @waiting_lists.find do |route, _|
187
- # (host == route[:host]) || (path =~ route[:path_regexp])
188
- # end
189
- # return wait_for_agent(wait_list) if route
190
-
191
226
  nil
192
227
  end
193
228
 
@@ -202,13 +237,6 @@ module DigitalFabric
202
237
  @route_keys = @routes.keys
203
238
  end
204
239
 
205
- def wait_for_agent(wait_list)
206
- wait_list << Fiber.current
207
- @timer.move_on_after(10) { suspend }
208
- ensure
209
- wait_list.delete(self)
210
- end
211
-
212
240
  def path_regexp(path)
213
241
  /^#{path}/
214
242
  end
@@ -216,8 +244,8 @@ module DigitalFabric
216
244
  def graceful_shutdown
217
245
  @shutdown = true
218
246
  @agents.keys.each do |agent|
219
- if agent.respond_to?(:shutdown)
220
- agent.shutdown
247
+ if agent.respond_to?(:send_shutdown)
248
+ agent.send_shutdown
221
249
  else
222
250
  @agents.delete(agent)
223
251
  end
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