tipi 0.31 → 0.36
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/Gemfile.lock +10 -4
- data/LICENSE +1 -1
- data/TODO.md +13 -47
- data/bin/tipi +13 -0
- 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/examples/http_request_ws_server.rb +34 -0
- data/examples/http_server.rb +6 -6
- data/examples/http_server_forked.rb +4 -5
- data/examples/http_server_form.rb +23 -0
- data/examples/http_server_throttled_accept.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/websocket_client.rb +1 -2
- data/examples/websocket_demo.rb +4 -2
- data/examples/ws_page.html +1 -2
- data/lib/tipi.rb +7 -5
- data/lib/tipi/config_dsl.rb +153 -0
- data/lib/tipi/configuration.rb +1 -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 +59 -37
- data/lib/tipi/http2_adapter.rb +5 -3
- data/lib/tipi/http2_stream.rb +19 -7
- data/lib/tipi/rack_adapter.rb +11 -3
- data/lib/tipi/version.rb +1 -1
- data/lib/tipi/websocket.rb +33 -13
- data/test/helper.rb +1 -2
- data/test/test_http_server.rb +3 -2
- data/test/test_request.rb +108 -0
- data/tipi.gemspec +7 -3
- metadata +59 -7
- data/lib/tipi/request.rb +0 -118
data/examples/http_ws_server.rb
CHANGED
@@ -5,14 +5,14 @@ require 'tipi'
|
|
5
5
|
require 'tipi/websocket'
|
6
6
|
|
7
7
|
def ws_handler(conn)
|
8
|
-
timer =
|
9
|
-
|
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:
|
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
|
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
|
data/examples/websocket_demo.rb
CHANGED
data/examples/ws_page.html
CHANGED
@@ -6,14 +6,13 @@
|
|
6
6
|
<body>
|
7
7
|
<script>
|
8
8
|
var connect = function () {
|
9
|
-
var exampleSocket = new WebSocket("
|
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
|
-
|
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
|
-
|
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
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tipi
|
4
|
+
module Configuration
|
5
|
+
class Interpreter
|
6
|
+
# make_blank_slate
|
7
|
+
|
8
|
+
def initialize(assembler)
|
9
|
+
@assembler = assembler
|
10
|
+
end
|
11
|
+
|
12
|
+
def gzip_response
|
13
|
+
@assembler.emit 'req = Tipi::GZip.wrap(req)'
|
14
|
+
end
|
15
|
+
|
16
|
+
def log(out)
|
17
|
+
@assembler.wrap_current_frame 'logger.log_request(req) do |req|'
|
18
|
+
end
|
19
|
+
|
20
|
+
def error(&block)
|
21
|
+
assembler.emit_exception_handler &block
|
22
|
+
end
|
23
|
+
|
24
|
+
def match(pattern, &block)
|
25
|
+
@assembler.emit_conditional "if req.path =~ #{pattern.inspect}", &block
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Assembler
|
30
|
+
def self.from_source(code)
|
31
|
+
new.from_source code
|
32
|
+
end
|
33
|
+
|
34
|
+
def from_source(code)
|
35
|
+
@stack = [new_frame]
|
36
|
+
@app_procs = {}
|
37
|
+
@interpreter = Interpreter.new self
|
38
|
+
@interpreter.instance_eval code
|
39
|
+
|
40
|
+
loop do
|
41
|
+
frame = @stack.pop
|
42
|
+
return assemble_app_proc(frame).join("\n") if @stack.empty?
|
43
|
+
|
44
|
+
@stack.last[:body] << assemble_frame(frame)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def new_frame
|
49
|
+
{
|
50
|
+
prelude: [],
|
51
|
+
body: []
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_frame(&block)
|
56
|
+
@stack.push new_frame
|
57
|
+
yield
|
58
|
+
ensure
|
59
|
+
frame = @stack.pop
|
60
|
+
emit assemble(frame)
|
61
|
+
end
|
62
|
+
|
63
|
+
def wrap_current_frame(head)
|
64
|
+
frame = @stack.pop
|
65
|
+
wrapper = new_frame
|
66
|
+
wrapper[:body] << head
|
67
|
+
@stack.push wrapper
|
68
|
+
@stack.push frame
|
69
|
+
end
|
70
|
+
|
71
|
+
def emit(code)
|
72
|
+
@stack.last[:body] << code
|
73
|
+
end
|
74
|
+
|
75
|
+
def emit_prelude(code)
|
76
|
+
@stack.last[:prelude] << code
|
77
|
+
end
|
78
|
+
|
79
|
+
def emit_exception_handler(&block)
|
80
|
+
proc_id = add_app_proc block
|
81
|
+
@stack.last[:rescue_proc_id] = proc_id
|
82
|
+
end
|
83
|
+
|
84
|
+
def emit_block(conditional, &block)
|
85
|
+
proc_id = add_app_proc block
|
86
|
+
@stack.last[:branched] = true
|
87
|
+
emit conditional
|
88
|
+
add_frame &block
|
89
|
+
end
|
90
|
+
|
91
|
+
def add_app_proc(proc)
|
92
|
+
id = :"proc#{@app_procs.size}"
|
93
|
+
@app_procs[id] = proc
|
94
|
+
id
|
95
|
+
end
|
96
|
+
|
97
|
+
def assemble_frame(frame)
|
98
|
+
indent = 0
|
99
|
+
lines = []
|
100
|
+
emit_code lines, frame[:prelude], indent
|
101
|
+
if frame[:rescue_proc_id]
|
102
|
+
emit_code lines, 'begin', indent
|
103
|
+
indent += 1
|
104
|
+
end
|
105
|
+
emit_code lines, frame[:body], indent
|
106
|
+
if frame[:rescue_proc_id]
|
107
|
+
emit_code lines, 'rescue => e', indent
|
108
|
+
emit_code lines, " app_procs[#{frame[:rescue_proc_id].inspect}].call(req, e)", indent
|
109
|
+
emit_code lines, 'end', indent
|
110
|
+
indent -= 1
|
111
|
+
end
|
112
|
+
lines
|
113
|
+
end
|
114
|
+
|
115
|
+
def assemble_app_proc(frame)
|
116
|
+
indent = 0
|
117
|
+
lines = []
|
118
|
+
emit_code lines, frame[:prelude], indent
|
119
|
+
emit_code lines, 'proc do |req|', indent
|
120
|
+
emit_code lines, frame[:body], indent + 1
|
121
|
+
emit_code lines, 'end', indent
|
122
|
+
|
123
|
+
lines
|
124
|
+
end
|
125
|
+
|
126
|
+
def emit_code(lines, code, indent)
|
127
|
+
if code.is_a? Array
|
128
|
+
code.each { |l| emit_code lines, l, indent + 1 }
|
129
|
+
else
|
130
|
+
lines << (indent_line code, indent)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
@@indents = Hash.new { |h, k| h[k] = ' ' * k }
|
135
|
+
|
136
|
+
def indent_line(code, indent)
|
137
|
+
indent == 0 ? code : "#{ @@indents[indent] }#{code}"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
def assemble(code)
|
145
|
+
Tipi::Configuration::Assembler.from_source(code)
|
146
|
+
end
|
147
|
+
|
148
|
+
code = assemble <<~RUBY
|
149
|
+
gzip_response
|
150
|
+
log STDOUT
|
151
|
+
RUBY
|
152
|
+
|
153
|
+
puts code
|
data/lib/tipi/configuration.rb
CHANGED
@@ -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
|