tipi 0.33 → 0.37.1
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 +22 -0
- data/Gemfile.lock +10 -4
- data/LICENSE +1 -1
- data/TODO.md +11 -47
- 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_form.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 +5 -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 -14
- data/lib/tipi/http2_adapter.rb +4 -2
- data/lib/tipi/http2_stream.rb +20 -8
- data/lib/tipi/rack_adapter.rb +1 -1
- 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 +10 -12
- data/test/test_request.rb +108 -0
- data/tipi.gemspec +7 -3
- metadata +57 -6
- data/lib/tipi/request.rb +0 -118
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 46b961262c4d31ce6e479864bced5b698f77e757f60e93a9dff8ba7401f2fa0d
|
4
|
+
data.tar.gz: 23881ff338c0b7aeaea31a7700ed08271eca7349322fb1868246c1541386c2b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ccb684d646130d74292809c1c3080f5c4280d260ae38da119813d9137e84278dfe61cf7471512353b95ea36ced2bb2913fd23695e1b3ce549a17a66d243c1bd6
|
7
|
+
data.tar.gz: fe7e6ed4c6b10ae4d409edbf9e3a105d49763cccc6c8ed81f42af5b7a903b45f53548bff830a63051c5626cd107fe169538bba08b83f8a26adf5b36f51a4f070
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,25 @@
|
|
1
|
+
## 0.37 2021-02-15
|
2
|
+
|
3
|
+
* Update upgrade mechanism to work with updated Qeweney API
|
4
|
+
|
5
|
+
## 0.36 2021-02-12
|
6
|
+
|
7
|
+
* Use `Qeweney::Status` constants
|
8
|
+
|
9
|
+
## 0.35 2021-02-10
|
10
|
+
|
11
|
+
* Extract Request class into separate [qeweney](https://github.com/digital-fabric/qeweney) gem
|
12
|
+
|
13
|
+
## 0.34 2021-02-07
|
14
|
+
|
15
|
+
* Implement digital fabric service and agents
|
16
|
+
* Add multipart and urlencoded form data parsing
|
17
|
+
* Improve request body reading behaviour
|
18
|
+
* Add more `Request` information methods
|
19
|
+
* Add access to connection for HTTP2 requests
|
20
|
+
* Allow calling `Request#send_chunk` with empty chunk
|
21
|
+
* Add support for handling protocol upgrades from within request handler
|
22
|
+
|
1
23
|
## 0.33 2020-11-20
|
2
24
|
|
3
25
|
* Update code for Polyphony 0.47.5
|
data/Gemfile.lock
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
tipi (0.
|
4
|
+
tipi (0.37.1)
|
5
5
|
http-2 (~> 0.10.0)
|
6
6
|
http_parser.rb (~> 0.6.0)
|
7
|
-
|
7
|
+
msgpack (~> 1.4.2)
|
8
|
+
polyphony (~> 0.52.0)
|
9
|
+
qeweney (~> 0.6)
|
8
10
|
rack (>= 2.0.8, < 2.3.0)
|
9
11
|
websocket (~> 1.2.8)
|
10
12
|
|
@@ -14,6 +16,7 @@ GEM
|
|
14
16
|
ansi (1.5.0)
|
15
17
|
builder (3.2.4)
|
16
18
|
docile (1.3.2)
|
19
|
+
escape_utils (1.2.1)
|
17
20
|
http-2 (0.10.2)
|
18
21
|
http_parser.rb (0.6.0)
|
19
22
|
json (2.3.1)
|
@@ -24,7 +27,10 @@ GEM
|
|
24
27
|
builder
|
25
28
|
minitest (>= 5.0)
|
26
29
|
ruby-progressbar
|
27
|
-
|
30
|
+
msgpack (1.4.2)
|
31
|
+
polyphony (0.52.0)
|
32
|
+
qeweney (0.6)
|
33
|
+
escape_utils (~> 1.2.1)
|
28
34
|
rack (2.2.3)
|
29
35
|
rake (12.3.3)
|
30
36
|
ruby-progressbar (1.10.1)
|
@@ -33,7 +39,7 @@ GEM
|
|
33
39
|
json (>= 1.8, < 3)
|
34
40
|
simplecov-html (~> 0.10.0)
|
35
41
|
simplecov-html (0.10.2)
|
36
|
-
websocket (1.2.
|
42
|
+
websocket (1.2.9)
|
37
43
|
|
38
44
|
PLATFORMS
|
39
45
|
ruby
|
data/LICENSE
CHANGED
data/TODO.md
CHANGED
@@ -1,10 +1,18 @@
|
|
1
|
+
For immediate execution:
|
2
|
+
|
3
|
+
|
4
|
+
|
1
5
|
# Roadmap
|
2
6
|
|
3
7
|
- Update README (get rid of non-http stuff)
|
4
8
|
- Improve Rack spec compliance, add tests
|
5
|
-
- Homogenize HTTP 1 and HTTP 2 headers -
|
9
|
+
- Homogenize HTTP 1 and HTTP 2 headers - downcase symbols
|
6
10
|
|
7
|
-
|
11
|
+
- Use `http-2-next` instead of `http-2` for http/2
|
12
|
+
- https://gitlab.com/honeyryderchuck/http-2-next
|
13
|
+
- Open an issue there, ask what's the difference between the two gems?
|
14
|
+
|
15
|
+
## 0.38
|
8
16
|
|
9
17
|
- Add more poly CLI commands and options:
|
10
18
|
|
@@ -16,51 +24,7 @@
|
|
16
24
|
- set port to bind to
|
17
25
|
- set forking process count
|
18
26
|
|
19
|
-
## 0.
|
27
|
+
## 0.39 Working Sinatra application
|
20
28
|
|
21
29
|
- app with database access (postgresql)
|
22
30
|
- benchmarks!
|
23
|
-
|
24
|
-
# HTTP Client Agent
|
25
|
-
|
26
|
-
The concurrency model and the fact that we want to serve the response object on
|
27
|
-
receiving headers and let the user lazily read the response body, means we'll
|
28
|
-
need to change the API to accept a block:
|
29
|
-
|
30
|
-
```ruby
|
31
|
-
# current API
|
32
|
-
resp = Agent.get('http://acme.org')
|
33
|
-
puts resp.body
|
34
|
-
|
35
|
-
# proposed API
|
36
|
-
Agent.get('http://acme.org') do |resp|
|
37
|
-
puts resp.body
|
38
|
-
end
|
39
|
-
```
|
40
|
-
|
41
|
-
While the block is running, the connection adapter is acquired. Once the block
|
42
|
-
is done running, the request (and response) can be discarded. The problem with
|
43
|
-
that if we spin up a coprocess from that block we risk all kinds of race
|
44
|
-
conditions and weird behaviours.
|
45
|
-
|
46
|
-
A compromise might be to allow the two: doing a `get` without providing a block
|
47
|
-
will return a response object that already has the body (i.e. the entire
|
48
|
-
response has already been received). Doing a `get` with a block will invoke the
|
49
|
-
block once headers are received, letting the user's code stream the body:
|
50
|
-
|
51
|
-
```ruby
|
52
|
-
def request(ctx, &block)
|
53
|
-
...
|
54
|
-
connection_manager.acquire do |adapter|
|
55
|
-
response = adapter.request(ctx)
|
56
|
-
if block
|
57
|
-
block.(response)
|
58
|
-
else
|
59
|
-
# wait for body
|
60
|
-
response.body
|
61
|
-
end
|
62
|
-
response
|
63
|
-
end
|
64
|
-
end
|
65
|
-
```
|
66
|
-
|
data/df/agent.rb
ADDED
@@ -0,0 +1,63 @@
|
|
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
|
+
def initialize(id, server_url)
|
13
|
+
@id = id
|
14
|
+
super(server_url, { host: "#{id}.realiteq.net" }, 'foobar')
|
15
|
+
@name = "agent-#{@id}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def http_request(req)
|
19
|
+
return streaming_http_request(req) if req.path == '/streaming'
|
20
|
+
return form_http_request(req) if req.path == '/form'
|
21
|
+
|
22
|
+
req.respond({ id: @id, time: Time.now.to_i }.to_json)
|
23
|
+
end
|
24
|
+
|
25
|
+
def streaming_http_request(req)
|
26
|
+
req.send_headers({ 'Content-Type': 'text/json' })
|
27
|
+
|
28
|
+
60.times do
|
29
|
+
sleep 1
|
30
|
+
do_some_activity
|
31
|
+
req.send_chunk({ id: @id, time: Time.now.to_i }.to_json)
|
32
|
+
end
|
33
|
+
|
34
|
+
req.finish
|
35
|
+
rescue Polyphony::Terminate
|
36
|
+
req.respond(' * shutting down *') if Fiber.current.graceful_shutdown?
|
37
|
+
rescue Exception => e
|
38
|
+
p e
|
39
|
+
puts e.backtrace.join("\n")
|
40
|
+
end
|
41
|
+
|
42
|
+
def form_http_request(req)
|
43
|
+
body = req.read
|
44
|
+
form_data = Tipi::Request.parse_form_data(body, req.headers)
|
45
|
+
req.respond({ form_data: form_data, headers: req.headers }.to_json, { 'Content-Type': 'text/json' })
|
46
|
+
end
|
47
|
+
|
48
|
+
def do_some_activity
|
49
|
+
File.open('/tmp/df-test.log', 'a+') { |f| sleep rand; f.puts "#{Time.now} #{@name} #{generate_data(2**8)}" }
|
50
|
+
end
|
51
|
+
|
52
|
+
def generate_data(length)
|
53
|
+
charset = Array('A'..'Z') + Array('a'..'z') + Array('0'..'9')
|
54
|
+
Array.new(length) { charset.sample }.join
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# id = ARGV[0]
|
59
|
+
# puts "Starting agent #{id} pid: #{Process.pid}"
|
60
|
+
|
61
|
+
# spin_loop(interval: 60) { GC.start }
|
62
|
+
# SampleAgent.new(id, '/tmp/df.sock').run
|
63
|
+
# SampleAgent.new(id, 'localhost:4411').run
|
data/df/etc_benchmark.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
require 'fileutils'
|
8
|
+
FileUtils.cd(__dir__)
|
9
|
+
|
10
|
+
require_relative 'agent'
|
11
|
+
|
12
|
+
class AgentManager
|
13
|
+
def initialize
|
14
|
+
@running_agents = {}
|
15
|
+
@pending_actions = Queue.new
|
16
|
+
@processor = spin_loop { process_pending_action }
|
17
|
+
end
|
18
|
+
|
19
|
+
def process_pending_action
|
20
|
+
action = @pending_actions.shift
|
21
|
+
case action[:kind]
|
22
|
+
when :start
|
23
|
+
start_agent(action[:spec])
|
24
|
+
when :stop
|
25
|
+
stop_agent(action[:spec])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def start_agent(spec)
|
30
|
+
return if @running_agents[spec]
|
31
|
+
|
32
|
+
@running_agents[spec] = spin do
|
33
|
+
while true
|
34
|
+
launch_agent_from_spec(spec)
|
35
|
+
sleep 1
|
36
|
+
end
|
37
|
+
ensure
|
38
|
+
@running_agents.delete(spec)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def stop_agent(spec)
|
43
|
+
fiber = @running_agents[spec]
|
44
|
+
return unless fiber
|
45
|
+
|
46
|
+
fiber.terminate
|
47
|
+
fiber.await
|
48
|
+
end
|
49
|
+
|
50
|
+
def update
|
51
|
+
return unless @pending_actions.empty?
|
52
|
+
|
53
|
+
current_specs = @running_agents.keys
|
54
|
+
updated_specs = agent_specs
|
55
|
+
|
56
|
+
to_start = updated_specs - current_specs
|
57
|
+
to_stop = current_specs - current_specs
|
58
|
+
|
59
|
+
to_start.each { |s| @pending_actions << { kind: :start, spec: s } }
|
60
|
+
to_stop.each { |s| @pending_actions << { kind: :stop, spec: s } }
|
61
|
+
end
|
62
|
+
|
63
|
+
def run
|
64
|
+
every(2) { update }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class RealityAgentManager < AgentManager
|
69
|
+
def agent_specs
|
70
|
+
(1..400).map { |i| { id: i } }
|
71
|
+
end
|
72
|
+
|
73
|
+
def launch_agent_from_spec(spec)
|
74
|
+
# Polyphony::Process.watch("ruby agent.rb #{spec[:id]}")
|
75
|
+
Polyphony::Process.watch do
|
76
|
+
spin_loop(interval: 60) { GC.start }
|
77
|
+
agent = SampleAgent.new(spec[:id], '/tmp/df.sock')
|
78
|
+
puts "Starting agent #{spec[:id]} pid: #{Process.pid}"
|
79
|
+
agent.run
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
puts "Agent manager pid: #{Process.pid}"
|
85
|
+
|
86
|
+
manager = RealityAgentManager.new
|
87
|
+
manager.run
|
data/df/multi_client.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony'
|
5
|
+
require 'http/parser'
|
6
|
+
|
7
|
+
class Client
|
8
|
+
def initialize(id, host, port, http_host, interval)
|
9
|
+
@id = id
|
10
|
+
@host = host
|
11
|
+
@port = port
|
12
|
+
@http_host = http_host
|
13
|
+
@interval = interval.to_f
|
14
|
+
@interval_delta = @interval / 2
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
while true
|
19
|
+
connect && issue_requests
|
20
|
+
sleep 5
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def connect
|
25
|
+
@socket = Polyphony::Net.tcp_connect(@host, @port)
|
26
|
+
rescue SystemCallError
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
REQUEST = <<~HTTP
|
31
|
+
GET / HTTP/1.1
|
32
|
+
Host: %s
|
33
|
+
|
34
|
+
HTTP
|
35
|
+
|
36
|
+
def issue_requests
|
37
|
+
@parser = Http::Parser.new
|
38
|
+
@parser.on_message_complete = proc { @got_reply = true }
|
39
|
+
@parser.on_body = proc { |chunk| @response = chunk }
|
40
|
+
|
41
|
+
while true
|
42
|
+
do_request
|
43
|
+
sleep rand((@interval - @interval_delta)..(@interval + @interval_delta))
|
44
|
+
end
|
45
|
+
rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNREFUSED => e
|
46
|
+
# fail quitely
|
47
|
+
ensure
|
48
|
+
@parser = nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def do_request
|
52
|
+
@got_reply = nil
|
53
|
+
@response = nil
|
54
|
+
@socket << format(REQUEST, @http_host)
|
55
|
+
wait_for_response
|
56
|
+
# if @parser.status_code != 200
|
57
|
+
# puts "Got status code #{@parser.status_code} from #{@http_host} => #{@parser.headers && @parser.headers['X-Request-ID']}"
|
58
|
+
# end
|
59
|
+
# puts "#{Time.now} [client-#{@id}] #{@http_host} => #{@response || '<error>'}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def wait_for_response
|
63
|
+
@socket.recv_loop do |data|
|
64
|
+
@parser << data
|
65
|
+
return @response if @got_reply
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def spin_client(id, host)
|
71
|
+
spin do
|
72
|
+
client = Client.new(id, 'localhost', 4411, host, 30)
|
73
|
+
client.run
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
spin_loop(interval: 60) { GC.start }
|
78
|
+
|
79
|
+
10000.times { |id| spin_client(id, "#{rand(1..400)}.realiteq.net") }
|
80
|
+
|
81
|
+
trap('SIGINT') { exit! }
|
82
|
+
|
83
|
+
puts "Multi client pid: #{Process.pid}"
|
84
|
+
sleep
|
@@ -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
|