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