tipi 0.40 → 0.45

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 (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