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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/Gemfile.lock +10 -4
- data/LICENSE +1 -1
- data/TODO.md +11 -47
- data/df/agent.rb +63 -0
- data/df/etc_benchmark.rb +15 -0
- data/df/multi_agent_supervisor.rb +87 -0
- data/df/multi_client.rb +84 -0
- data/df/routing_benchmark.rb +60 -0
- data/df/sample_agent.rb +89 -0
- data/df/server.rb +54 -0
- data/df/sse_page.html +29 -0
- data/df/stress.rb +24 -0
- data/df/ws_page.html +38 -0
- data/e +0 -0
- data/examples/http_request_ws_server.rb +35 -0
- data/examples/http_server.rb +6 -6
- data/examples/http_server_form.rb +23 -0
- data/examples/http_unix_socket_server.rb +17 -0
- data/examples/http_ws_server.rb +10 -12
- data/examples/routing_server.rb +34 -0
- data/examples/ws_page.html +1 -2
- data/lib/tipi.rb +5 -1
- data/lib/tipi/digital_fabric.rb +7 -0
- data/lib/tipi/digital_fabric/agent.rb +225 -0
- data/lib/tipi/digital_fabric/agent_proxy.rb +265 -0
- data/lib/tipi/digital_fabric/executive.rb +100 -0
- data/lib/tipi/digital_fabric/executive/index.html +69 -0
- data/lib/tipi/digital_fabric/protocol.rb +90 -0
- data/lib/tipi/digital_fabric/request_adapter.rb +48 -0
- data/lib/tipi/digital_fabric/service.rb +230 -0
- data/lib/tipi/http1_adapter.rb +50 -14
- data/lib/tipi/http2_adapter.rb +4 -2
- data/lib/tipi/http2_stream.rb +20 -8
- data/lib/tipi/rack_adapter.rb +1 -1
- data/lib/tipi/version.rb +1 -1
- data/lib/tipi/websocket.rb +33 -29
- data/test/helper.rb +1 -2
- data/test/test_http_server.rb +10 -12
- data/test/test_request.rb +108 -0
- data/tipi.gemspec +7 -3
- metadata +57 -6
- data/lib/tipi/request.rb +0 -118
@@ -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
|