tipi 0.32 → 0.37
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 +27 -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/e +0 -0
- data/examples/http_request_ws_server.rb +35 -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/ws_page.html +1 -2
- data/lib/tipi.rb +7 -5
- 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 +50 -16
- 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 -29
- 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
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony'
|
5
|
+
require 'tipi/digital_fabric'
|
6
|
+
|
7
|
+
class FakeAgent
|
8
|
+
def initialize(idx)
|
9
|
+
@idx = idx
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def setup_df_service_with_agents(agent_count)
|
14
|
+
server = DigitalFabric::Service.new
|
15
|
+
agent_count.times do |i|
|
16
|
+
server.mount({path: "/#{i}"}, FakeAgent.new(i))
|
17
|
+
end
|
18
|
+
server
|
19
|
+
end
|
20
|
+
|
21
|
+
def benchmark_route_compilation(agent_count, iterations)
|
22
|
+
service = setup_df_service_with_agents(agent_count)
|
23
|
+
t0 = Time.now
|
24
|
+
iterations.times { service.compile_agent_routes }
|
25
|
+
elapsed = Time.now - t0
|
26
|
+
puts "route_compilation: #{agent_count} => #{elapsed / iterations}s (#{1/(elapsed / iterations)} ops/sec)"
|
27
|
+
end
|
28
|
+
|
29
|
+
class FauxRequest
|
30
|
+
def initialize(agent_count)
|
31
|
+
@agent_count = agent_count
|
32
|
+
end
|
33
|
+
|
34
|
+
def headers
|
35
|
+
{ ':path' => "/#{rand(@agent_count)}"}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def benchmark_find_agent(agent_count, iterations)
|
40
|
+
service = setup_df_service_with_agents(agent_count)
|
41
|
+
t0 = Time.now
|
42
|
+
request = FauxRequest.new(agent_count)
|
43
|
+
iterations.times do
|
44
|
+
agent = service.find_agent(request)
|
45
|
+
end
|
46
|
+
elapsed = Time.now - t0
|
47
|
+
puts "routing: #{agent_count} => #{elapsed / iterations}s (#{1/(elapsed / iterations)} ops/sec)"
|
48
|
+
end
|
49
|
+
|
50
|
+
def benchmark
|
51
|
+
benchmark_route_compilation(100, 1000)
|
52
|
+
benchmark_route_compilation(500, 200)
|
53
|
+
benchmark_route_compilation(1000, 100)
|
54
|
+
|
55
|
+
benchmark_find_agent(100, 1000)
|
56
|
+
benchmark_find_agent(500, 200)
|
57
|
+
benchmark_find_agent(1000, 100)
|
58
|
+
end
|
59
|
+
|
60
|
+
benchmark
|
data/df/sample_agent.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony'
|
5
|
+
require 'json'
|
6
|
+
require 'tipi/digital_fabric/protocol'
|
7
|
+
require 'tipi/digital_fabric/agent'
|
8
|
+
|
9
|
+
Protocol = DigitalFabric::Protocol
|
10
|
+
|
11
|
+
class SampleAgent < DigitalFabric::Agent
|
12
|
+
HTML_WS = IO.read(File.join(__dir__, 'ws_page.html'))
|
13
|
+
HTML_SSE = IO.read(File.join(__dir__, 'sse_page.html'))
|
14
|
+
|
15
|
+
def http_request(req)
|
16
|
+
path = req['headers'][':path']
|
17
|
+
case path
|
18
|
+
when '/agent'
|
19
|
+
send_df_message(Protocol.http_response(
|
20
|
+
req['id'],
|
21
|
+
'Hello, world!',
|
22
|
+
{},
|
23
|
+
true
|
24
|
+
))
|
25
|
+
when '/agent/ws'
|
26
|
+
send_df_message(Protocol.http_response(
|
27
|
+
req['id'],
|
28
|
+
HTML_WS,
|
29
|
+
{ 'Content-Type' => 'text/html' },
|
30
|
+
true
|
31
|
+
))
|
32
|
+
when '/agent/sse'
|
33
|
+
send_df_message(Protocol.http_response(
|
34
|
+
req['id'],
|
35
|
+
HTML_SSE,
|
36
|
+
{ 'Content-Type' => 'text/html' },
|
37
|
+
true
|
38
|
+
))
|
39
|
+
when '/agent/sse/events'
|
40
|
+
stream_sse_response(req)
|
41
|
+
else
|
42
|
+
send_df_message(Protocol.http_response(
|
43
|
+
req['id'],
|
44
|
+
nil,
|
45
|
+
{ ':status' => 400 },
|
46
|
+
true
|
47
|
+
))
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
def ws_request(req)
|
53
|
+
send_df_message(Protocol.ws_response(req['id'], {}))
|
54
|
+
|
55
|
+
10.times do
|
56
|
+
sleep 1
|
57
|
+
send_df_message(Protocol.ws_data(req['id'], Time.now.to_s))
|
58
|
+
end
|
59
|
+
send_df_message(Protocol.ws_close(req['id']))
|
60
|
+
end
|
61
|
+
|
62
|
+
def stream_sse_response(req)
|
63
|
+
send_df_message(Protocol.http_response(
|
64
|
+
req['id'],
|
65
|
+
nil,
|
66
|
+
{ 'Content-Type' => 'text/event-stream' },
|
67
|
+
false
|
68
|
+
))
|
69
|
+
10.times do
|
70
|
+
sleep 1
|
71
|
+
send_df_message(Protocol.http_response(
|
72
|
+
req['id'],
|
73
|
+
"data: #{Time.now}\n\n",
|
74
|
+
nil,
|
75
|
+
false
|
76
|
+
))
|
77
|
+
end
|
78
|
+
send_df_message(Protocol.http_response(
|
79
|
+
req['id'],
|
80
|
+
"retry: 0\n\n",
|
81
|
+
nil,
|
82
|
+
true
|
83
|
+
))
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
agent = SampleAgent.new('127.0.0.1', 4411, { path: '/agent' })
|
89
|
+
agent.run
|
data/df/server.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'tipi'
|
5
|
+
require 'tipi/digital_fabric'
|
6
|
+
require 'tipi/digital_fabric/executive'
|
7
|
+
require 'json'
|
8
|
+
require 'fileutils'
|
9
|
+
FileUtils.cd(__dir__)
|
10
|
+
|
11
|
+
service = DigitalFabric::Service.new(token: 'foobar')
|
12
|
+
executive = DigitalFabric::Executive.new(service, { host: 'executive.realiteq.net' })
|
13
|
+
|
14
|
+
spin_loop(interval: 60) { GC.start }
|
15
|
+
|
16
|
+
class Polyphony::BaseException
|
17
|
+
attr_reader :caller_backtrace
|
18
|
+
end
|
19
|
+
|
20
|
+
puts "pid: #{Process.pid}"
|
21
|
+
|
22
|
+
tcp_listener = spin do
|
23
|
+
opts = {
|
24
|
+
reuse_addr: true,
|
25
|
+
dont_linger: true,
|
26
|
+
}
|
27
|
+
puts 'Listening on localhost:4411'
|
28
|
+
server = Polyphony::Net.tcp_listen('0.0.0.0', 4411, opts)
|
29
|
+
server.accept_loop do |client|
|
30
|
+
spin do
|
31
|
+
service.incr_connection_count
|
32
|
+
Tipi.client_loop(client, opts) { |req| service.http_request(req) }
|
33
|
+
ensure
|
34
|
+
service.decr_connection_count
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
UNIX_SOCKET_PATH = '/tmp/df.sock'
|
40
|
+
|
41
|
+
unix_listener = spin do
|
42
|
+
puts "Listening on #{UNIX_SOCKET_PATH}"
|
43
|
+
FileUtils.rm(UNIX_SOCKET_PATH) if File.exists?(UNIX_SOCKET_PATH)
|
44
|
+
socket = UNIXServer.new(UNIX_SOCKET_PATH)
|
45
|
+
Tipi.accept_loop(socket, {}) { |req| service.http_request(req) }
|
46
|
+
end
|
47
|
+
|
48
|
+
begin
|
49
|
+
Fiber.await(tcp_listener, unix_listener)
|
50
|
+
rescue Interrupt
|
51
|
+
puts "Got SIGINT, shutting down gracefully"
|
52
|
+
service.graceful_shutdown
|
53
|
+
puts "post graceful shutdown"
|
54
|
+
end
|
data/df/sse_page.html
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<title>SSE Client</title>
|
5
|
+
</head>
|
6
|
+
<body>
|
7
|
+
<h1>SSE Client</h1>
|
8
|
+
<script>
|
9
|
+
var connect = function () {
|
10
|
+
console.log("connecting...");
|
11
|
+
var eventSource = new EventSource("/agent/sse/events");
|
12
|
+
|
13
|
+
eventSource.addEventListener('open', function(e) {
|
14
|
+
console.log("connected");
|
15
|
+
document.querySelector('#status').innerText = 'connected';
|
16
|
+
return false;
|
17
|
+
}, false);
|
18
|
+
|
19
|
+
eventSource.addEventListener('message', function(e) {
|
20
|
+
document.querySelector('#msg').innerText = e.data;
|
21
|
+
}, false);
|
22
|
+
};
|
23
|
+
|
24
|
+
window.onload = connect;
|
25
|
+
</script>
|
26
|
+
<h1 id="status">disconnected</h1>
|
27
|
+
<h1 id="msg"></h1>
|
28
|
+
</body>
|
29
|
+
</html>
|
data/df/stress.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
FileUtils.cd(__dir__)
|
8
|
+
|
9
|
+
def monitor_process(cmd)
|
10
|
+
while true
|
11
|
+
puts "Starting #{cmd}"
|
12
|
+
Polyphony::Process.watch(cmd)
|
13
|
+
sleep 5
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
puts "pid: #{Process.pid}"
|
18
|
+
puts 'Starting stress test'
|
19
|
+
|
20
|
+
spin { monitor_process('ruby server.rb') }
|
21
|
+
spin { monitor_process('ruby multi_agent_supervisor.rb') }
|
22
|
+
spin { monitor_process('ruby multi_client.rb') }
|
23
|
+
|
24
|
+
sleep
|
data/df/ws_page.html
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<title>Websocket Client</title>
|
5
|
+
</head>
|
6
|
+
<body>
|
7
|
+
<h1>WebSocket Client</h1>
|
8
|
+
<script>
|
9
|
+
var connect = function () {
|
10
|
+
console.log("connecting...")
|
11
|
+
var exampleSocket = new WebSocket("wss://dev.realiteq.net/agent");
|
12
|
+
|
13
|
+
exampleSocket.onopen = function (event) {
|
14
|
+
console.log("connected");
|
15
|
+
document.querySelector('#status').innerText = 'connected';
|
16
|
+
exampleSocket.send("Can you hear me?");
|
17
|
+
};
|
18
|
+
exampleSocket.onclose = function (event) {
|
19
|
+
console.log("disconnected");
|
20
|
+
document.querySelector('#status').innerText = 'disconnected';
|
21
|
+
setTimeout(function () {
|
22
|
+
// exampleSocket.removeAllListeners();
|
23
|
+
connect();
|
24
|
+
}, 1000);
|
25
|
+
}
|
26
|
+
exampleSocket.onmessage = function (event) {
|
27
|
+
console.log("got message", event.data);
|
28
|
+
document.querySelector('#msg').innerText = event.data;
|
29
|
+
console.log(event.data);
|
30
|
+
}
|
31
|
+
};
|
32
|
+
|
33
|
+
window.onload = connect;
|
34
|
+
</script>
|
35
|
+
<h1 id="status">disconnected</h1>
|
36
|
+
<h1 id="msg"></h1>
|
37
|
+
</body>
|
38
|
+
</html>
|
data/e
ADDED
File without changes
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'tipi'
|
5
|
+
require 'tipi/websocket'
|
6
|
+
|
7
|
+
def ws_handler(conn)
|
8
|
+
timer = spin_loop(interval: 1) do
|
9
|
+
conn << Time.now.to_s
|
10
|
+
end
|
11
|
+
while (msg = conn.recv)
|
12
|
+
conn << "you said: #{msg}"
|
13
|
+
end
|
14
|
+
ensure
|
15
|
+
timer.stop
|
16
|
+
end
|
17
|
+
|
18
|
+
opts = {
|
19
|
+
reuse_addr: true,
|
20
|
+
dont_linger: true,
|
21
|
+
}
|
22
|
+
|
23
|
+
HTML = IO.read(File.join(__dir__, 'ws_page.html'))
|
24
|
+
|
25
|
+
puts "pid: #{Process.pid}"
|
26
|
+
puts 'Listening on port 4411...'
|
27
|
+
|
28
|
+
Tipi.serve('0.0.0.0', 4411, opts) do |req|
|
29
|
+
if req.upgrade_protocol == 'websocket'
|
30
|
+
conn = req.upgrade_to_websocket
|
31
|
+
ws_handler(conn)
|
32
|
+
else
|
33
|
+
req.respond(HTML, 'Content-Type' => 'text/html')
|
34
|
+
end
|
35
|
+
end
|
data/examples/http_server.rb
CHANGED
@@ -8,14 +8,14 @@ opts = {
|
|
8
8
|
dont_linger: true
|
9
9
|
}
|
10
10
|
|
11
|
+
puts "pid: #{Process.pid}"
|
12
|
+
puts 'Listening on port 4411...'
|
13
|
+
|
11
14
|
spin do
|
12
|
-
Tipi.serve('0.0.0.0',
|
15
|
+
Tipi.serve('0.0.0.0', 4411, opts) do |req|
|
13
16
|
req.respond("Hello world!\n")
|
14
17
|
rescue Exception => e
|
15
18
|
p e
|
16
19
|
end
|
17
|
-
|
18
|
-
|
19
|
-
puts "pid: #{Process.pid}"
|
20
|
-
puts 'Listening on port 1234...'
|
21
|
-
suspend
|
20
|
+
p 'done...'
|
21
|
+
end.await
|
@@ -7,18 +7,15 @@ require 'tipi'
|
|
7
7
|
|
8
8
|
opts = {
|
9
9
|
reuse_addr: true,
|
10
|
+
reuse_port: true,
|
10
11
|
dont_linger: true
|
11
12
|
}
|
12
13
|
|
13
|
-
server = Polyphony::HTTP::Server.listen('0.0.0.0', 1234, opts)
|
14
|
-
|
15
|
-
puts 'Listening on port 1234'
|
16
|
-
|
17
14
|
child_pids = []
|
18
15
|
8.times do
|
19
16
|
pid = Polyphony.fork do
|
20
17
|
puts "forked pid: #{Process.pid}"
|
21
|
-
|
18
|
+
Tipi.serve('0.0.0.0', 1234, opts) do |req|
|
22
19
|
req.respond("Hello world! from pid: #{Process.pid}\n")
|
23
20
|
end
|
24
21
|
rescue Interrupt
|
@@ -26,4 +23,6 @@ child_pids = []
|
|
26
23
|
child_pids << pid
|
27
24
|
end
|
28
25
|
|
26
|
+
puts 'Listening on port 1234'
|
27
|
+
|
29
28
|
child_pids.each { |pid| Thread.current.backend.waitpid(pid) }
|
@@ -0,0 +1,23 @@
|
|
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
|
+
spin do
|
15
|
+
Tipi.serve('0.0.0.0', 4411, opts) do |req|
|
16
|
+
body = req.read
|
17
|
+
body2 = req.read
|
18
|
+
req.respond("body: #{body} (body2: #{body2.inspect})\n")
|
19
|
+
rescue Exception => e
|
20
|
+
p e
|
21
|
+
end
|
22
|
+
p 'done...'
|
23
|
+
end.await
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'tipi'
|
5
|
+
|
6
|
+
::Exception.__disable_sanitized_backtrace__ = true
|
7
|
+
|
8
|
+
opts = {
|
9
|
+
reuse_addr: true,
|
10
|
+
reuse_port: true,
|
11
|
+
dont_linger: true
|
12
|
+
}
|
13
|
+
|
14
|
+
server = Tipi.listen('0.0.0.0', 1234, opts)
|
15
|
+
|
16
|
+
puts 'Listening on port 1234'
|
17
|
+
|
18
|
+
throttler = Polyphony::Throttler.new(interval: 5)
|
19
|
+
server.accept_loop do |socket|
|
20
|
+
throttler.call do
|
21
|
+
spin { Tipi.client_loop(socket, opts) { |req| req.respond("Hello world!\n") } }
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'tipi'
|
5
|
+
|
6
|
+
path = '/tmp/tipi.sock'
|
7
|
+
|
8
|
+
puts "pid: #{Process.pid}"
|
9
|
+
puts "Listening on #{path}"
|
10
|
+
|
11
|
+
FileUtils.rm(path) rescue nil
|
12
|
+
socket = UNIXServer.new(path)
|
13
|
+
Tipi.accept_loop(socket, {}) do |req|
|
14
|
+
req.respond("Hello world!\n")
|
15
|
+
rescue Exception => e
|
16
|
+
p e
|
17
|
+
end
|