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.
- 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
|