tipi 0.32 → 0.37
Sign up to get free protection for your applications and to get access to all the features.
- 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
|