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
@@ -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
@@ -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
@@ -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', 1234, opts) do |req|
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
- end
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
- server.each do |req|
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