tipi 0.33 → 0.37.1

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/Gemfile.lock +10 -4
  4. data/LICENSE +1 -1
  5. data/TODO.md +11 -47
  6. data/df/agent.rb +63 -0
  7. data/df/etc_benchmark.rb +15 -0
  8. data/df/multi_agent_supervisor.rb +87 -0
  9. data/df/multi_client.rb +84 -0
  10. data/df/routing_benchmark.rb +60 -0
  11. data/df/sample_agent.rb +89 -0
  12. data/df/server.rb +54 -0
  13. data/df/sse_page.html +29 -0
  14. data/df/stress.rb +24 -0
  15. data/df/ws_page.html +38 -0
  16. data/e +0 -0
  17. data/examples/http_request_ws_server.rb +35 -0
  18. data/examples/http_server.rb +6 -6
  19. data/examples/http_server_form.rb +23 -0
  20. data/examples/http_unix_socket_server.rb +17 -0
  21. data/examples/http_ws_server.rb +10 -12
  22. data/examples/routing_server.rb +34 -0
  23. data/examples/ws_page.html +1 -2
  24. data/lib/tipi.rb +5 -1
  25. data/lib/tipi/digital_fabric.rb +7 -0
  26. data/lib/tipi/digital_fabric/agent.rb +225 -0
  27. data/lib/tipi/digital_fabric/agent_proxy.rb +265 -0
  28. data/lib/tipi/digital_fabric/executive.rb +100 -0
  29. data/lib/tipi/digital_fabric/executive/index.html +69 -0
  30. data/lib/tipi/digital_fabric/protocol.rb +90 -0
  31. data/lib/tipi/digital_fabric/request_adapter.rb +48 -0
  32. data/lib/tipi/digital_fabric/service.rb +230 -0
  33. data/lib/tipi/http1_adapter.rb +50 -14
  34. data/lib/tipi/http2_adapter.rb +4 -2
  35. data/lib/tipi/http2_stream.rb +20 -8
  36. data/lib/tipi/rack_adapter.rb +1 -1
  37. data/lib/tipi/version.rb +1 -1
  38. data/lib/tipi/websocket.rb +33 -29
  39. data/test/helper.rb +1 -2
  40. data/test/test_http_server.rb +10 -12
  41. data/test/test_request.rb +108 -0
  42. data/tipi.gemspec +7 -3
  43. metadata +57 -6
  44. data/lib/tipi/request.rb +0 -118
@@ -0,0 +1,7 @@
1
+ module DigitalFabric
2
+ end
3
+
4
+ ::DF = DigitalFabric
5
+
6
+ require_relative 'digital_fabric/service'
7
+ require_relative 'digital_fabric/agent_proxy'
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './protocol'
4
+ require_relative './request_adapter'
5
+
6
+ require 'msgpack'
7
+ require 'tipi/websocket'
8
+ require 'tipi/request'
9
+
10
+ module DigitalFabric
11
+ class Agent
12
+ def initialize(server_url, route, token)
13
+ @server_url = server_url
14
+ @route = route
15
+ @token = token
16
+ @requests = {}
17
+ @long_running_requests = {}
18
+ @name = '<unknown>'
19
+ end
20
+
21
+ class TimeoutError < RuntimeError
22
+ end
23
+
24
+ class GracefulShutdown < RuntimeError
25
+ end
26
+
27
+ def run
28
+ @fiber = Fiber.current
29
+ @keep_alive_timer = spin_loop(interval: 5) { keep_alive }
30
+ while true
31
+ connect_and_process_incoming_requests
32
+ return if @shutdown
33
+ sleep 5
34
+ end
35
+ ensure
36
+ @keep_alive_timer.stop
37
+ end
38
+
39
+ def connect_and_process_incoming_requests
40
+ # log 'Connecting...'
41
+ @socket = connect_to_server
42
+ @last_recv = @last_send = Time.now
43
+
44
+ df_upgrade
45
+ @connected = true
46
+ @msgpack_reader = MessagePack::Unpacker.new
47
+
48
+ process_incoming_requests
49
+ rescue IOError, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE, TimeoutError
50
+ log 'Disconnected' if @connected
51
+ @connected = nil
52
+ end
53
+
54
+ def connect_to_server
55
+ if @server_url =~ /^([^\:]+)\:(\d+)$/
56
+ host = Regexp.last_match(1)
57
+ port = Regexp.last_match(2)
58
+ Polyphony::Net.tcp_connect(host, port)
59
+ else
60
+ UNIXSocket.new(@server_url)
61
+ end
62
+ end
63
+
64
+ UPGRADE_REQUEST = <<~HTTP
65
+ GET / HTTP/1.1
66
+ Host: localhost
67
+ Connection: upgrade
68
+ Upgrade: df
69
+ DF-Token: %s
70
+ DF-Mount: %s
71
+
72
+ HTTP
73
+
74
+ def df_upgrade
75
+ @socket << format(UPGRADE_REQUEST, @token, mount_point)
76
+ while (line = @socket.gets)
77
+ break if line.chomp.empty?
78
+ end
79
+ # log 'Connection upgraded'
80
+ end
81
+
82
+ def mount_point
83
+ if @route[:host]
84
+ "host=#{@route[:host]}"
85
+ elsif @route[:path]
86
+ "path=#{@route[:path]}"
87
+ else
88
+ nil
89
+ end
90
+ end
91
+
92
+ def log(msg)
93
+ puts "#{Time.now} (#{@name}) #{msg}"
94
+ end
95
+
96
+ def process_incoming_requests
97
+ @socket.feed_loop(@msgpack_reader, :feed_each) do |msg|
98
+ recv_df_message(msg)
99
+ return if @shutdown && @requests.empty?
100
+ end
101
+ rescue IOError, SystemCallError, TimeoutError
102
+ # ignore
103
+ end
104
+
105
+ def keep_alive
106
+ return unless @connected
107
+
108
+ now = Time.now
109
+ if now - @last_send >= Protocol::SEND_TIMEOUT
110
+ send_df_message(Protocol.ping)
111
+ end
112
+ # if now - @last_recv >= Protocol::RECV_TIMEOUT
113
+ # raise TimeoutError
114
+ # end
115
+ rescue IOError, SystemCallError => e
116
+ # transmit exception to fiber running the agent
117
+ @fiber.raise(e)
118
+ end
119
+
120
+ def recv_df_message(msg)
121
+ @last_recv = Time.now
122
+ case msg['kind']
123
+ when Protocol::SHUTDOWN
124
+ recv_shutdown
125
+ when Protocol::HTTP_REQUEST
126
+ recv_http_request(msg)
127
+ when Protocol::HTTP_REQUEST_BODY
128
+ recv_http_request_body(msg)
129
+ when Protocol::WS_REQUEST
130
+ recv_ws_request(msg)
131
+ when Protocol::CONN_DATA, Protocol::CONN_CLOSE,
132
+ Protocol::WS_DATA, Protocol::WS_CLOSE
133
+ fiber = @requests[msg['id']]
134
+ fiber << msg if fiber
135
+ end
136
+ end
137
+
138
+ def send_df_message(msg)
139
+ # we mark long-running requests by applying simple heuristics to sent DF
140
+ # messages. This is so we can correctly stop long-running requests
141
+ # upon graceful shutdown
142
+ if is_long_running_request_response?(msg)
143
+ id = msg[:id]
144
+ @long_running_requests[id] = @requests[id]
145
+ end
146
+ @last_send = Time.now
147
+ @socket << msg.to_msgpack
148
+ end
149
+
150
+ def is_long_running_request_response?(msg)
151
+ case msg[:kind]
152
+ when Protocol::HTTP_UPGRADE
153
+ true
154
+ when Protocol::HTTP_RESPONSE
155
+ msg[:body] && !msg[:complete]
156
+ end
157
+ end
158
+
159
+ def recv_shutdown
160
+ # puts "Received shutdown message (#{@requests.size} pending requests)"
161
+ # puts " (Long running requests: #{@long_running_requests.size})"
162
+ @shutdown = true
163
+ @long_running_requests.values.each { |f| f.terminate(true) }
164
+ end
165
+
166
+ def recv_http_request(msg)
167
+ req = prepare_http_request(msg)
168
+ id = msg['id']
169
+ @requests[id] = spin do
170
+ http_request(req)
171
+ rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE
172
+ # ignore
173
+ rescue Polyphony::Terminate
174
+ req.respond(nil, { ':status' => Qeweney::Status::SERVICE_UNAVAILABLE }) if Fiber.current.graceful_shutdown?
175
+ ensure
176
+ @requests.delete(id)
177
+ @long_running_requests.delete(id)
178
+ @fiber.terminate if @shutdown && @requests.empty?
179
+ end
180
+ end
181
+
182
+ def prepare_http_request(msg)
183
+ req = Qeweney::Request.new(msg['headers'], RequestAdapter.new(self, msg))
184
+ req.buffer_body_chunk(msg['body']) if msg['body']
185
+ req.complete! if msg['complete']
186
+ req
187
+ end
188
+
189
+ def recv_http_request_body(msg)
190
+ fiber = @requests[msg['id']]
191
+ return unless fiber
192
+
193
+ fiber << msg['body']
194
+ end
195
+
196
+ def get_http_request_body(id, limit)
197
+ send_df_message(Protocol.http_get_request_body(id, limit))
198
+ receive
199
+ end
200
+
201
+ def recv_ws_request(msg)
202
+ req = Qeweney::Request.new(msg['headers'], RequestAdapter.new(self, msg))
203
+ id = msg['id']
204
+ @requests[id] = @long_running_requests[id] = spin do
205
+ ws_request(req)
206
+ rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE
207
+ # ignore
208
+ ensure
209
+ @requests.delete(id)
210
+ @long_running_requests.delete(id)
211
+ @fiber.terminate if @shutdown && @requests.empty?
212
+ end
213
+ end
214
+
215
+ # default handler for HTTP request
216
+ def http_request(req)
217
+ req.respond(nil, { ':status': Qeweney::Status::SERVICE_UNAVAILABLE })
218
+ end
219
+
220
+ # default handler for WS request
221
+ def ws_request(req)
222
+ req.respond(nil, { ':status': Qeweney::Status::SERVICE_UNAVAILABLE })
223
+ end
224
+ end
225
+ end
@@ -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