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