tipi 0.31 → 0.36

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/Gemfile.lock +10 -4
  4. data/LICENSE +1 -1
  5. data/TODO.md +13 -47
  6. data/bin/tipi +13 -0
  7. data/df/agent.rb +63 -0
  8. data/df/etc_benchmark.rb +15 -0
  9. data/df/multi_agent_supervisor.rb +87 -0
  10. data/df/multi_client.rb +84 -0
  11. data/df/routing_benchmark.rb +60 -0
  12. data/df/sample_agent.rb +89 -0
  13. data/df/server.rb +54 -0
  14. data/df/sse_page.html +29 -0
  15. data/df/stress.rb +24 -0
  16. data/df/ws_page.html +38 -0
  17. data/examples/http_request_ws_server.rb +34 -0
  18. data/examples/http_server.rb +6 -6
  19. data/examples/http_server_forked.rb +4 -5
  20. data/examples/http_server_form.rb +23 -0
  21. data/examples/http_server_throttled_accept.rb +23 -0
  22. data/examples/http_unix_socket_server.rb +17 -0
  23. data/examples/http_ws_server.rb +10 -12
  24. data/examples/routing_server.rb +34 -0
  25. data/examples/websocket_client.rb +1 -2
  26. data/examples/websocket_demo.rb +4 -2
  27. data/examples/ws_page.html +1 -2
  28. data/lib/tipi.rb +7 -5
  29. data/lib/tipi/config_dsl.rb +153 -0
  30. data/lib/tipi/configuration.rb +1 -1
  31. data/lib/tipi/digital_fabric.rb +7 -0
  32. data/lib/tipi/digital_fabric/agent.rb +225 -0
  33. data/lib/tipi/digital_fabric/agent_proxy.rb +265 -0
  34. data/lib/tipi/digital_fabric/executive.rb +100 -0
  35. data/lib/tipi/digital_fabric/executive/index.html +69 -0
  36. data/lib/tipi/digital_fabric/protocol.rb +90 -0
  37. data/lib/tipi/digital_fabric/request_adapter.rb +48 -0
  38. data/lib/tipi/digital_fabric/service.rb +230 -0
  39. data/lib/tipi/http1_adapter.rb +59 -37
  40. data/lib/tipi/http2_adapter.rb +5 -3
  41. data/lib/tipi/http2_stream.rb +19 -7
  42. data/lib/tipi/rack_adapter.rb +11 -3
  43. data/lib/tipi/version.rb +1 -1
  44. data/lib/tipi/websocket.rb +33 -13
  45. data/test/helper.rb +1 -2
  46. data/test/test_http_server.rb +3 -2
  47. data/test/test_request.rb +108 -0
  48. data/tipi.gemspec +7 -3
  49. metadata +59 -7
  50. data/lib/tipi/request.rb +0 -118
@@ -0,0 +1,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './protocol'
4
+ require 'msgpack'
5
+ require 'tipi/websocket'
6
+
7
+ module DigitalFabric
8
+ class AgentProxy
9
+ def initialize(service, req)
10
+ @service = service
11
+ @req = req
12
+ @conn = req.adapter.conn
13
+ @msgpack_reader = MessagePack::Unpacker.new
14
+ @requests = {}
15
+ @current_request_count = 0
16
+ @last_request_id = 0
17
+ @last_recv = @last_send = Time.now
18
+ run
19
+ end
20
+
21
+ def current_request_count
22
+ @current_request_count
23
+ end
24
+
25
+ class TimeoutError < RuntimeError
26
+ end
27
+
28
+ class GracefulShutdown < RuntimeError
29
+ end
30
+
31
+ def run
32
+ @fiber = Fiber.current
33
+ @service.mount(route, self)
34
+ keep_alive_timer = spin_loop(interval: 5) { keep_alive }
35
+ process_incoming_messages(false)
36
+ rescue GracefulShutdown
37
+ puts "Proxy got graceful shutdown, left: #{@requests.size} requests" if @requests.size > 0
38
+ process_incoming_messages(true)
39
+ ensure
40
+ keep_alive_timer&.stop
41
+ @service.unmount(self)
42
+ end
43
+
44
+ def process_incoming_messages(shutdown = false)
45
+ return if shutdown && @requests.empty?
46
+
47
+ @conn.feed_loop(@msgpack_reader, :feed_each) do |msg|
48
+ recv_df_message(msg)
49
+ return if shutdown && @requests.empty?
50
+ end
51
+ rescue TimeoutError, IOError
52
+ end
53
+
54
+ def shutdown
55
+ send_df_message(Protocol.shutdown)
56
+ @fiber.raise GracefulShutdown.new
57
+ end
58
+
59
+ def keep_alive
60
+ now = Time.now
61
+ if now - @last_send >= Protocol::SEND_TIMEOUT
62
+ send_df_message(Protocol.ping)
63
+ end
64
+ # if now - @last_recv >= Protocol::RECV_TIMEOUT
65
+ # raise TimeoutError
66
+ # end
67
+ rescue TimeoutError, IOError
68
+ end
69
+
70
+ def route
71
+ case @req.headers['df-mount']
72
+ when /^\s*host\s*=\s*([^\s]+)/
73
+ { host: Regexp.last_match(1) }
74
+ when /^\s*path\s*=\s*([^\s]+)/
75
+ { path: Regexp.last_match(1) }
76
+ when /catch_all/
77
+ { catch_all: true }
78
+ else
79
+ nil
80
+ end
81
+ end
82
+
83
+ def recv_df_message(message)
84
+ @last_recv = Time.now
85
+ return if message['kind'] == Protocol::PING
86
+
87
+ handler = @requests[message['id']]
88
+ if !handler
89
+ # puts "Unknown request id in #{message}"
90
+ return
91
+ end
92
+
93
+ handler << message
94
+ end
95
+
96
+ def send_df_message(message)
97
+ @last_send = Time.now
98
+ @conn << message.to_msgpack
99
+ end
100
+
101
+ # HTTP / WebSocket
102
+
103
+ def register_request_fiber
104
+ id = (@last_request_id += 1)
105
+ @requests[id] = Fiber.current
106
+ id
107
+ end
108
+
109
+ def unregister_request_fiber(id)
110
+ @requests.delete(id)
111
+ end
112
+
113
+ def with_request
114
+ @current_request_count += 1
115
+ id = (@last_request_id += 1)
116
+ @requests[id] = Fiber.current
117
+ yield id
118
+ ensure
119
+ @current_request_count -= 1
120
+ @requests.delete(id)
121
+ end
122
+
123
+ def http_request(req)
124
+ t0 = Time.now
125
+ t1 = nil
126
+ with_request do |id|
127
+ send_df_message(Protocol.http_request(id, req))
128
+ while (message = receive)
129
+ unless t1
130
+ t1 = Time.now
131
+ @service.record_latency_measurement(t1 - t0)
132
+ end
133
+ return if http_request_message(id, req, message)
134
+ end
135
+ end
136
+ rescue => e
137
+ req.respond("Error: #{e.inspect}", ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
138
+ end
139
+
140
+ # @return [Boolean] true if response is complete
141
+ def http_request_message(id, req, message)
142
+ case message['kind']
143
+ when Protocol::HTTP_UPGRADE
144
+ http_custom_upgrade(id, req, message)
145
+ true
146
+ when Protocol::HTTP_GET_REQUEST_BODY
147
+ http_get_request_body(id, req, message)
148
+ false
149
+ when Protocol::HTTP_RESPONSE
150
+ headers = message['headers']
151
+ body = message['body']
152
+ done = message['complete']
153
+ req.send_headers(headers) if headers && !req.headers_sent?
154
+ req.send_chunk(body, done: done) if body or done
155
+ done
156
+ else
157
+ # invalid message
158
+ true
159
+ end
160
+ end
161
+
162
+ HTTP_RESPONSE_UPGRADE_HEADERS = { ':status' => Qeweney::Status::SWITCHING_PROTOCOLS }
163
+
164
+ def http_custom_upgrade(id, req, message)
165
+ # send upgrade response
166
+ upgrade_headers = message['headers'] ?
167
+ message['headers'].merge(HTTP_RESPONSE_UPGRADE_HEADERS) :
168
+ HTTP_RESPONSE_UPGRADE_HEADERS
169
+ req.send_headers(upgrade_headers, true)
170
+
171
+ conn = req.adapter.conn
172
+ reader = spin do
173
+ conn.recv_loop do |data|
174
+ send_df_message(Protocol.conn_data(id, data))
175
+ end
176
+ end
177
+ while (message = receive)
178
+ return if http_custom_upgrade_message(conn, message)
179
+ end
180
+ ensure
181
+ reader.stop
182
+ end
183
+
184
+ def http_custom_upgrade_message(conn, message)
185
+ case message['kind']
186
+ when Protocol::CONN_DATA
187
+ conn << message['data']
188
+ false
189
+ when Protocol::CONN_CLOSE
190
+ true
191
+ else
192
+ # invalid message
193
+ true
194
+ end
195
+ end
196
+
197
+ def http_get_request_body(id, req, message)
198
+ case (limit = message['limit'])
199
+ when nil
200
+ body = req.read
201
+ else
202
+ limit = limit.to_i
203
+ body = nil
204
+ req.each_chunk do |chunk|
205
+ (body ||= +'') << chunk
206
+ break if body.bytesize >= limit
207
+ end
208
+ end
209
+ send_df_message(Protocol.http_request_body(id, body, req.complete?))
210
+ end
211
+
212
+ def http_upgrade(req, protocol)
213
+ if protocol == 'websocket'
214
+ handle_websocket_upgrade(req)
215
+ else
216
+ # other protocol upgrades should be handled by the agent, so we just run
217
+ # the request normally. The agent is expected to upgrade the connection
218
+ # using a http_upgrade message. From that moment on, two-way
219
+ # communication is handled using conn_data and conn_close messages.
220
+ http_request(req)
221
+ end
222
+ end
223
+
224
+ def handle_websocket_upgrade(req)
225
+ with_request do |id|
226
+ send_df_message(Protocol.ws_request(id, req.headers))
227
+ response = receive
228
+ case response['kind']
229
+ when Protocol::WS_RESPONSE
230
+ headers = response['headers'] || {}
231
+ status = headers[':status'] || Qeweney::Status::SWITCHING_PROTOCOLS
232
+ if status != Qeweney::Status::SWITCHING_PROTOCOLS
233
+ req.respond(nil, headers)
234
+ return
235
+ end
236
+ ws = Tipi::Websocket.new(req.adapter.conn, req.headers)
237
+ run_websocket_connection(id, ws)
238
+ else
239
+ req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)
240
+ end
241
+ end
242
+ end
243
+
244
+ def run_websocket_connection(id, websocket)
245
+ reader = spin do
246
+ websocket.recv_loop do |data|
247
+ send_df_message(Protocol.ws_data(id, data))
248
+ end
249
+ end
250
+ while (message = receive)
251
+ case message['kind']
252
+ when Protocol::WS_DATA
253
+ websocket << message['data']
254
+ when Protocol::WS_CLOSE
255
+ return
256
+ else
257
+ raise "Unexpected websocket message #{message.inspect}"
258
+ end
259
+ end
260
+ ensure
261
+ reader.stop
262
+ websocket.close
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tipi/digital_fabric'
4
+ require 'json'
5
+
6
+ module DigitalFabric
7
+ # agent for managing DF service
8
+ class Executive
9
+ INDEX_HTML = IO.read(File.join(__dir__, 'executive/index.html'))
10
+
11
+ attr_reader :last_service_stats
12
+
13
+ def initialize(service, route = { path: '/executive' })
14
+ @service = service
15
+ route[:executive] = true
16
+ @service.mount(route, self)
17
+ @current_request_count = 0
18
+ @updater = spin_loop(interval: 10) { update_service_stats }
19
+ update_service_stats
20
+ end
21
+
22
+ def current_request_count
23
+ @current_request_count
24
+ end
25
+
26
+ def http_request(req)
27
+ @current_request_count += 1
28
+ case req.path
29
+ when '/'
30
+ req.respond(INDEX_HTML, 'Content-Type' => 'text/html')
31
+ when '/stats'
32
+ message = last_service_stats
33
+ req.respond(message.to_json, { 'Content-Type' => 'text.json' })
34
+ when '/stream/stats'
35
+ stream_stats(req)
36
+ else
37
+ req.respond('Invalid path', { ':status' => Qeweney::Status::NOT_FOUND })
38
+ end
39
+ ensure
40
+ @current_request_count -= 1
41
+ end
42
+
43
+ def stream_stats(req)
44
+ req.send_headers({ 'Content-Type' => 'text/event-stream' })
45
+
46
+ @service.timer.every(10) do
47
+ message = last_service_stats
48
+ req.send_chunk(format_sse_event(message.to_json))
49
+ end
50
+ rescue IOError, SystemCallError
51
+ # ignore
52
+ ensure
53
+ req.send_chunk("retry: 0\n\n", true) rescue nil
54
+ end
55
+
56
+ def format_sse_event(data)
57
+ "data: #{data}\n\n"
58
+ end
59
+
60
+ def update_service_stats
61
+ @last_service_stats = {
62
+ service: @service.stats,
63
+ machine: machine_stats
64
+ }
65
+ end
66
+
67
+ TOP_CPU_REGEXP = /%Cpu(.+)/.freeze
68
+ TOP_CPU_IDLE_REGEXP = /([\d\.]+) id/.freeze
69
+ TOP_MEM_REGEXP = /MiB Mem(.+)/.freeze
70
+ TOP_MEM_FREE_REGEXP = /([\d\.]+) free/.freeze
71
+ LOADAVG_REGEXP = /^([\d\.]+)/.freeze
72
+
73
+ def machine_stats
74
+ top = `top -bn1 | head -n4`
75
+ unless top =~ TOP_CPU_REGEXP && Regexp.last_match(1) =~ TOP_CPU_IDLE_REGEXP
76
+ p top =~ TOP_CPU_REGEXP
77
+ p Regexp.last_match(1)
78
+ p Regexp.last_match(1) =~ TOP_CPU_IDLE_REGEXP
79
+ raise 'Invalid output from top (cpu)'
80
+ end
81
+ cpu_utilization = 100 - Regexp.last_match(1).to_i
82
+
83
+ unless top =~ TOP_MEM_REGEXP && Regexp.last_match(1) =~ TOP_MEM_FREE_REGEXP
84
+ raise 'Invalid output from top (mem)'
85
+ end
86
+
87
+ mem_free = Regexp.last_match(1).to_f
88
+
89
+ stats = `cat /proc/loadavg`
90
+ raise 'Invalid output from /proc/loadavg' unless stats =~ LOADAVG_REGEXP
91
+ load_avg = Regexp.last_match(1).to_f
92
+
93
+ {
94
+ mem_free: mem_free,
95
+ cpu_utilization: cpu_utilization,
96
+ load_avg: load_avg
97
+ }
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,69 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Digital Fabric Executive</title>
5
+ <style>
6
+ .hidden { display: none }
7
+ </style>
8
+ </head>
9
+ <body>
10
+ <h1>Digital Fabric Executive</h1>
11
+ <script>
12
+ function updateStats(update) {
13
+ for (let k in update.service) {
14
+ let v = update.service[k];
15
+ let e = document.querySelector('#' + k);
16
+ if (e) e.innerText = v;
17
+ }
18
+ for (let k in update.machine) {
19
+ let v = update.machine[k];
20
+ let e = document.querySelector('#' + k);
21
+ if (e) e.innerText = v;
22
+ }
23
+ }
24
+
25
+ function connect() {
26
+ console.log("connecting...");
27
+ window.eventSource = new EventSource("/stream/stats");
28
+
29
+ window.eventSource.onopen = function(e) {
30
+ console.log("connected");
31
+ document.querySelector('#status').innerText = 'connected';
32
+ document.querySelector('#stats').className = '';
33
+ return false;
34
+ }
35
+
36
+ window.eventSource.onmessage = function(e) {
37
+ console.log("message", e.data);
38
+ updateStats(JSON.parse(e.data));
39
+ }
40
+
41
+ window.eventSource.onerror = function(e) {
42
+ console.log("error", e);
43
+ document.querySelector('#status').innerText = 'disconnected';
44
+ document.querySelector('#stats').className = 'hidden';
45
+ window.eventSource.close();
46
+ window.eventSource = null;
47
+ setTimeout(connect, 5000);
48
+ }
49
+ };
50
+
51
+ window.onload = connect;
52
+ </script>
53
+ <h2 id="status"></h2>
54
+ <div id="stats" class="hidden">
55
+ <h2>Service</h2>
56
+ <p>Request rate: <span id="http_request_rate"></span></p>
57
+ <p>Error rate: <span id="error_rate"></span></p>
58
+ <p>Average Latency: <span id="average_latency"></span>s</p>
59
+ <p>Connected agents: <span id="agent_count"></span></p>
60
+ <p>Connected clients: <span id="connection_count"></span></p>
61
+ <p>Concurrent requests: <span id="concurrent_requests"></span></p>
62
+
63
+ <h2>Machine</h2>
64
+ <p>CPU utilization: <span id="cpu_utilization"></span>%</p>
65
+ <p>Free memory: <span id="mem_free"></span>MB</p>
66
+ <p>Load average: <span id="load_avg"></span></p>
67
+ </div>
68
+ </body>
69
+ </html>
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DigitalFabric
4
+ module Protocol
5
+ PING = 'ping'
6
+ SHUTDOWN = 'shutdown'
7
+
8
+ HTTP_REQUEST = 'http_request'
9
+ HTTP_RESPONSE = 'http_response'
10
+ HTTP_UPGRADE = 'http_upgrade'
11
+ HTTP_GET_REQUEST_BODY = 'http_get_request_body'
12
+ HTTP_REQUEST_BODY = 'http_request_body'
13
+
14
+ CONN_DATA = 'conn_data'
15
+ CONN_CLOSE = 'conn_close'
16
+
17
+ WS_REQUEST = 'ws_request'
18
+ WS_RESPONSE = 'ws_response'
19
+ WS_DATA = 'ws_data'
20
+ WS_CLOSE = 'ws_close'
21
+
22
+ SEND_TIMEOUT = 15
23
+ RECV_TIMEOUT = SEND_TIMEOUT + 5
24
+
25
+ class << self
26
+ def ping
27
+ { kind: PING }
28
+ end
29
+
30
+ def shutdown
31
+ { kind: SHUTDOWN }
32
+ end
33
+
34
+ DF_UPGRADE_RESPONSE = <<~HTTP.gsub("\n", "\r\n")
35
+ HTTP/1.1 101 Switching Protocols
36
+ Upgrade: df
37
+ Connection: Upgrade
38
+
39
+ HTTP
40
+
41
+ def df_upgrade_response
42
+ DF_UPGRADE_RESPONSE
43
+ end
44
+
45
+ def http_request(id, req)
46
+ { kind: HTTP_REQUEST, id: id, headers: req.headers, body: req.next_chunk, complete: req.complete? }
47
+ end
48
+
49
+ def http_response(id, body, headers, complete)
50
+ { kind: HTTP_RESPONSE, id: id, body: body, headers: headers, complete: complete }
51
+ end
52
+
53
+ def http_upgrade(id, headers)
54
+ { kind: HTTP_UPGRADE, id: id }
55
+ end
56
+
57
+ def http_get_request_body(id, limit = nil)
58
+ { kind: HTTP_GET_REQUEST_BODY, id: id, limit: limit }
59
+ end
60
+
61
+ def http_request_body(id, body, complete)
62
+ { kind: HTTP_REQUEST_BODY, id: id, body: body, complete: complete }
63
+ end
64
+
65
+ def connection_data(id, data)
66
+ { kind: CONN_DATA, id: id, data: data }
67
+ end
68
+
69
+ def connection_close(id)
70
+ { kind: CONN_CLOSE, id: id }
71
+ end
72
+
73
+ def ws_request(id, headers)
74
+ { kind: WS_REQUEST, id: id, headers: headers }
75
+ end
76
+
77
+ def ws_response(id, headers)
78
+ { kind: WS_RESPONSE, id: id, headers: headers }
79
+ end
80
+
81
+ def ws_data(id, data)
82
+ { id: id, kind: WS_DATA, data: data }
83
+ end
84
+
85
+ def ws_close(id)
86
+ { id: id, kind: WS_CLOSE }
87
+ end
88
+ end
89
+ end
90
+ end