tipi 0.33 → 0.37.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/Gemfile.lock +10 -4
  4. data/LICENSE +1 -1
  5. data/TODO.md +11 -47
  6. data/df/agent.rb +63 -0
  7. data/df/etc_benchmark.rb +15 -0
  8. data/df/multi_agent_supervisor.rb +87 -0
  9. data/df/multi_client.rb +84 -0
  10. data/df/routing_benchmark.rb +60 -0
  11. data/df/sample_agent.rb +89 -0
  12. data/df/server.rb +54 -0
  13. data/df/sse_page.html +29 -0
  14. data/df/stress.rb +24 -0
  15. data/df/ws_page.html +38 -0
  16. data/e +0 -0
  17. data/examples/http_request_ws_server.rb +35 -0
  18. data/examples/http_server.rb +6 -6
  19. data/examples/http_server_form.rb +23 -0
  20. data/examples/http_unix_socket_server.rb +17 -0
  21. data/examples/http_ws_server.rb +10 -12
  22. data/examples/routing_server.rb +34 -0
  23. data/examples/ws_page.html +1 -2
  24. data/lib/tipi.rb +5 -1
  25. data/lib/tipi/digital_fabric.rb +7 -0
  26. data/lib/tipi/digital_fabric/agent.rb +225 -0
  27. data/lib/tipi/digital_fabric/agent_proxy.rb +265 -0
  28. data/lib/tipi/digital_fabric/executive.rb +100 -0
  29. data/lib/tipi/digital_fabric/executive/index.html +69 -0
  30. data/lib/tipi/digital_fabric/protocol.rb +90 -0
  31. data/lib/tipi/digital_fabric/request_adapter.rb +48 -0
  32. data/lib/tipi/digital_fabric/service.rb +230 -0
  33. data/lib/tipi/http1_adapter.rb +50 -14
  34. data/lib/tipi/http2_adapter.rb +4 -2
  35. data/lib/tipi/http2_stream.rb +20 -8
  36. data/lib/tipi/rack_adapter.rb +1 -1
  37. data/lib/tipi/version.rb +1 -1
  38. data/lib/tipi/websocket.rb +33 -29
  39. data/test/helper.rb +1 -2
  40. data/test/test_http_server.rb +10 -12
  41. data/test/test_request.rb +108 -0
  42. data/tipi.gemspec +7 -3
  43. metadata +57 -6
  44. data/lib/tipi/request.rb +0 -118
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: adbbb6098994749bc96df90a6c73820ca54245c6026141768c83d883677fd6d3
4
- data.tar.gz: daf182efaff0561bb6ff85706ae14ab70de68f840290eb5bb88e870fc0a29199
3
+ metadata.gz: 46b961262c4d31ce6e479864bced5b698f77e757f60e93a9dff8ba7401f2fa0d
4
+ data.tar.gz: 23881ff338c0b7aeaea31a7700ed08271eca7349322fb1868246c1541386c2b8
5
5
  SHA512:
6
- metadata.gz: 78c8fb0c465779673563fb87857fdc935c4906830086f664657c00b2b80686f4fa35c3673a554976d26a41af072686ad2c197dd1fbd6a14d875480831e8455ff
7
- data.tar.gz: bbb9c98e7f167aae1a006fd30ac7557ddf9671d3951e00012582b748f3afea363e53b166c40ec1af3be0f5c6d96d1991f45d9e549f5fd23e614345508719ef5f
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.33)
4
+ tipi (0.37.1)
5
5
  http-2 (~> 0.10.0)
6
6
  http_parser.rb (~> 0.6.0)
7
- polyphony (~> 0.47.5.1)
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
- polyphony (0.47.5.1)
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.8)
42
+ websocket (1.2.9)
37
43
 
38
44
  PLATFORMS
39
45
  ruby
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2018 Sharon Rosner
3
+ Copyright (c) 2021 Sharon Rosner
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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 - upcase ? downcase ?
9
+ - Homogenize HTTP 1 and HTTP 2 headers - downcase symbols
6
10
 
7
- ## 0.30
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.31 Working Sinatra application
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
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ def generate
6
+ SecureRandom.uuid
7
+ end
8
+
9
+ count = 100000
10
+
11
+ GC.disable
12
+ t0 = Time.now
13
+ count.times { generate }
14
+ elapsed = Time.now - t0
15
+ puts "rate: #{count / elapsed}/s"
@@ -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
@@ -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