tipi 0.32 → 0.37

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -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/e +0 -0
  18. data/examples/http_request_ws_server.rb +35 -0
  19. data/examples/http_server.rb +6 -6
  20. data/examples/http_server_forked.rb +4 -5
  21. data/examples/http_server_form.rb +23 -0
  22. data/examples/http_server_throttled_accept.rb +23 -0
  23. data/examples/http_unix_socket_server.rb +17 -0
  24. data/examples/http_ws_server.rb +10 -12
  25. data/examples/routing_server.rb +34 -0
  26. data/examples/ws_page.html +1 -2
  27. data/lib/tipi.rb +7 -5
  28. data/lib/tipi/configuration.rb +1 -1
  29. data/lib/tipi/digital_fabric.rb +7 -0
  30. data/lib/tipi/digital_fabric/agent.rb +225 -0
  31. data/lib/tipi/digital_fabric/agent_proxy.rb +265 -0
  32. data/lib/tipi/digital_fabric/executive.rb +100 -0
  33. data/lib/tipi/digital_fabric/executive/index.html +69 -0
  34. data/lib/tipi/digital_fabric/protocol.rb +90 -0
  35. data/lib/tipi/digital_fabric/request_adapter.rb +48 -0
  36. data/lib/tipi/digital_fabric/service.rb +230 -0
  37. data/lib/tipi/http1_adapter.rb +50 -16
  38. data/lib/tipi/http2_adapter.rb +5 -3
  39. data/lib/tipi/http2_stream.rb +19 -7
  40. data/lib/tipi/rack_adapter.rb +11 -3
  41. data/lib/tipi/version.rb +1 -1
  42. data/lib/tipi/websocket.rb +33 -29
  43. data/test/helper.rb +1 -2
  44. data/test/test_http_server.rb +3 -2
  45. data/test/test_request.rb +108 -0
  46. data/tipi.gemspec +7 -3
  47. metadata +59 -7
  48. data/lib/tipi/request.rb +0 -118
@@ -5,14 +5,14 @@ require 'tipi'
5
5
  require 'tipi/websocket'
6
6
 
7
7
  def ws_handler(conn)
8
- timer = spin do
9
- throttled_loop(1) do
10
- conn << Time.now.to_s
11
- end
8
+ timer = spin_loop(interval: 1) do
9
+ conn << Time.now.to_s
12
10
  end
13
11
  while (msg = conn.recv)
14
12
  conn << "you said: #{msg}"
15
13
  end
14
+ rescue Exception => e
15
+ p e
16
16
  ensure
17
17
  timer.stop
18
18
  end
@@ -21,17 +21,15 @@ opts = {
21
21
  reuse_addr: true,
22
22
  dont_linger: true,
23
23
  upgrade: {
24
- websocket: Polyphony::Websocket.handler(&method(:ws_handler))
24
+ websocket: Tipi::Websocket.handler(&method(:ws_handler))
25
25
  }
26
26
  }
27
27
 
28
28
  HTML = IO.read(File.join(__dir__, 'ws_page.html'))
29
29
 
30
- spin do
31
- Tipi.serve('0.0.0.0', 1234, opts) do |req|
32
- req.respond(HTML, 'Content-Type' => 'text/html')
33
- end
34
- end
35
-
36
30
  puts "pid: #{Process.pid}"
37
- puts 'Listening on port 1234...'
31
+ puts 'Listening on port 4411...'
32
+
33
+ Tipi.serve('0.0.0.0', 4411, opts) do |req|
34
+ req.respond(HTML, 'Content-Type' => 'text/html')
35
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'tipi'
5
+
6
+ opts = {
7
+ reuse_addr: true,
8
+ dont_linger: true
9
+ }
10
+
11
+ puts "pid: #{Process.pid}"
12
+ puts 'Listening on port 4411...'
13
+
14
+ app = Tipi.route do |r|
15
+ r.root do
16
+ r.redirect '/hello'
17
+ end
18
+ r.on 'hello' do
19
+ r.get 'world' do
20
+ r.respond 'Hello world'
21
+ end
22
+ r.get do
23
+ r.respond 'Hello'
24
+ end
25
+ r.post do
26
+ puts 'Someone said Hello'
27
+ r.redirect '/'
28
+ end
29
+ end
30
+ end
31
+
32
+ spin do
33
+ Tipi.serve('0.0.0.0', 4411, opts, &app)
34
+ end.await
@@ -6,14 +6,13 @@
6
6
  <body>
7
7
  <script>
8
8
  var connect = function () {
9
- var exampleSocket = new WebSocket("ws://localhost:1234");
9
+ var exampleSocket = new WebSocket("wss://dev.realiteq.net/");
10
10
 
11
11
  exampleSocket.onopen = function (event) {
12
12
  document.querySelector('#status').innerText = 'connected';
13
13
  exampleSocket.send("Can you hear me?");
14
14
  };
15
15
  exampleSocket.onclose = function (event) {
16
- console.log('onclose');
17
16
  document.querySelector('#status').innerText = 'disconnected';
18
17
  setTimeout(function () {
19
18
  // exampleSocket.removeAllListeners();
data/lib/tipi.rb CHANGED
@@ -28,10 +28,8 @@ module Tipi
28
28
  end
29
29
 
30
30
  def accept_loop(server, opts, &handler)
31
- loop do
32
- client = server.accept
31
+ server.accept_loop do |client|
33
32
  spin { client_loop(client, opts, &handler) }
34
- snooze
35
33
  rescue OpenSSL::SSL::SSLError
36
34
  # disregard
37
35
  end
@@ -42,14 +40,18 @@ module Tipi
42
40
  adapter = protocol_adapter(client, opts)
43
41
  adapter.each(&handler)
44
42
  ensure
45
- client.close
43
+ client.close rescue nil
46
44
  end
47
45
 
48
46
  def protocol_adapter(socket, opts)
49
47
  use_http2 = socket.respond_to?(:alpn_protocol) &&
50
- socket.alpn_protocol == H2_PROTOCOL
48
+ socket.alpn_protocol == H2_PROTOCOL
51
49
  klass = use_http2 ? HTTP2Adapter : HTTP1Adapter
52
50
  klass.new(socket, opts)
53
51
  end
52
+
53
+ def route(&block)
54
+ proc { |req| req.route(&block) }
55
+ end
54
56
  end
55
57
  end
@@ -19,7 +19,7 @@ module Tipi
19
19
  end
20
20
 
21
21
  def start_listeners(config)
22
- puts "listening on port 1234"
22
+ puts "Listening on port 1234"
23
23
  @server = Polyphony::Net.tcp_listen('0.0.0.0', 1234, { reuse_addr: true, dont_linger: true })
24
24
  end
25
25
 
@@ -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