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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4129b10a7f6b5fb92e4e4784bf8cfd12ea2659840b616b7627bf28d1dc423e87
|
4
|
+
data.tar.gz: 7f5f71c4a870e33c7de84fd292568b3aca582dcc1adc9496d99e636328b3df4b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c1bfcbf1cdf4fa2868554e690a71543d89a37bf2d8cfa27cf604e453fb411d5b1dd2e4bc10678cbe79db6a94238ffef1a09e53771f7e8d8e71f2512b87f7265a
|
7
|
+
data.tar.gz: 4f476a65800ffb6cbcdfde6e10ed9e1dd7b9fcc4ddd69ad2babed1a747cba4f94e05659cbd235c1f7e71e1595851d77896262d45d1036d216d866b7bdc3f914a
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,30 @@
|
|
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
|
+
|
23
|
+
## 0.33 2020-11-20
|
24
|
+
|
25
|
+
* Update code for Polyphony 0.47.5
|
26
|
+
* Add support for Rack::File body to Tipi::RackAdapter
|
27
|
+
|
1
28
|
## 0.32 2020-08-14
|
2
29
|
|
3
30
|
* Respond with array of strings instead of concatenating for HTTP 1
|
data/Gemfile.lock
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
tipi (0.
|
4
|
+
tipi (0.37)
|
5
5
|
http-2 (~> 0.10.0)
|
6
6
|
http_parser.rb (~> 0.6.0)
|
7
|
-
|
7
|
+
msgpack (~> 1.4.2)
|
8
|
+
polyphony (~> 0.51.0)
|
9
|
+
qeweney (~> 0.5)
|
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.51.0)
|
32
|
+
qeweney (0.5)
|
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,20 @@
|
|
1
|
+
# Digital Fabric
|
2
|
+
|
3
|
+
Problems to fix:
|
4
|
+
|
5
|
+
- Memory leak (in server? multi agent? multi client?)
|
6
|
+
|
1
7
|
# Roadmap
|
2
8
|
|
3
9
|
- Update README (get rid of non-http stuff)
|
4
10
|
- Improve Rack spec compliance, add tests
|
5
|
-
- Homogenize HTTP 1 and HTTP 2 headers -
|
11
|
+
- Homogenize HTTP 1 and HTTP 2 headers - downcase symbols
|
6
12
|
|
7
|
-
|
13
|
+
- Use `http-2-next` instead of `http-2` for http/2
|
14
|
+
- https://gitlab.com/honeyryderchuck/http-2-next
|
15
|
+
- Open an issue there, ask what's the difference between the two gems?
|
16
|
+
|
17
|
+
## 0.35
|
8
18
|
|
9
19
|
- Add more poly CLI commands and options:
|
10
20
|
|
@@ -16,51 +26,7 @@
|
|
16
26
|
- set port to bind to
|
17
27
|
- set forking process count
|
18
28
|
|
19
|
-
## 0.
|
29
|
+
## 0.36 Working Sinatra application
|
20
30
|
|
21
31
|
- app with database access (postgresql)
|
22
32
|
- 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/bin/tipi
CHANGED
@@ -7,7 +7,20 @@ require File.expand_path('../lib/tipi/configuration', __dir__)
|
|
7
7
|
config = {}
|
8
8
|
#config[:forked] = 4
|
9
9
|
|
10
|
+
puts DATA.read
|
11
|
+
puts
|
12
|
+
|
10
13
|
configuration_manager = spin { Tipi::Configuration.supervise_config }
|
11
14
|
|
12
15
|
configuration_manager << config
|
13
16
|
configuration_manager.await
|
17
|
+
|
18
|
+
__END__
|
19
|
+
|
20
|
+
ooo
|
21
|
+
oo
|
22
|
+
o
|
23
|
+
\|/
|
24
|
+
/ \ Tipi - A better web server for a better world
|
25
|
+
/___\
|
26
|
+
|
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
|