tipi 0.39 → 0.43

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/.github/workflows/test.yml +4 -0
  3. data/.gitignore +5 -1
  4. data/CHANGELOG.md +30 -0
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +62 -25
  7. data/Rakefile +7 -3
  8. data/benchmarks/bm_http1_parser.rb +85 -0
  9. data/bin/benchmark +37 -0
  10. data/bin/h1pd +6 -0
  11. data/bin/tipi +3 -21
  12. data/df/server.rb +16 -87
  13. data/df/server_utils.rb +175 -0
  14. data/examples/full_service.rb +13 -0
  15. data/examples/http1_parser.rb +55 -0
  16. data/examples/http_server.rb +15 -3
  17. data/examples/http_server_forked.rb +3 -1
  18. data/examples/http_server_routes.rb +29 -0
  19. data/examples/http_server_static.rb +26 -0
  20. data/examples/https_server.rb +3 -0
  21. data/examples/servername_cb.rb +37 -0
  22. data/examples/websocket_demo.rb +2 -8
  23. data/examples/ws_page.html +2 -2
  24. data/lib/tipi.rb +89 -1
  25. data/lib/tipi/acme.rb +308 -0
  26. data/lib/tipi/cli.rb +30 -0
  27. data/lib/tipi/digital_fabric/agent.rb +7 -5
  28. data/lib/tipi/digital_fabric/agent_proxy.rb +16 -8
  29. data/lib/tipi/digital_fabric/executive.rb +6 -2
  30. data/lib/tipi/digital_fabric/protocol.rb +18 -3
  31. data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
  32. data/lib/tipi/digital_fabric/service.rb +77 -49
  33. data/lib/tipi/http1_adapter.rb +91 -100
  34. data/lib/tipi/http2_adapter.rb +21 -6
  35. data/lib/tipi/http2_stream.rb +54 -44
  36. data/lib/tipi/rack_adapter.rb +2 -53
  37. data/lib/tipi/response_extensions.rb +17 -0
  38. data/lib/tipi/version.rb +1 -1
  39. data/test/helper.rb +60 -12
  40. data/test/test_http_server.rb +0 -27
  41. data/test/test_request.rb +2 -29
  42. data/tipi.gemspec +11 -7
  43. metadata +79 -26
  44. data/e +0 -0
@@ -3,18 +3,30 @@
3
3
  require 'http/2'
4
4
  require_relative './http2_stream'
5
5
 
6
+ # patch to fix bug in HTTP2::Stream
7
+ class HTTP2::Stream
8
+ def end_stream?(frame)
9
+ case frame[:type]
10
+ when :data, :headers, :continuation
11
+ frame[:flags]&.include?(:end_stream)
12
+ else false
13
+ end
14
+ end
15
+ end
16
+
6
17
  module Tipi
7
18
  # HTTP2 server adapter
8
19
  class HTTP2Adapter
9
- def self.upgrade_each(socket, opts, headers, &block)
10
- adapter = new(socket, opts, headers)
20
+ def self.upgrade_each(socket, opts, headers, body, &block)
21
+ adapter = new(socket, opts, headers, body)
11
22
  adapter.each(&block)
12
23
  end
13
24
 
14
- def initialize(conn, opts, upgrade_headers = nil)
25
+ def initialize(conn, opts, upgrade_headers = nil, upgrade_body = nil)
15
26
  @conn = conn
16
27
  @opts = opts
17
28
  @upgrade_headers = upgrade_headers
29
+ @upgrade_body = upgrade_body
18
30
  @first = true
19
31
  @rx = (upgrade_headers && upgrade_headers[':rx']) || 0
20
32
  @tx = (upgrade_headers && upgrade_headers[':tx']) || 0
@@ -30,6 +42,8 @@ module Tipi
30
42
  @transfer_count_request.tx_incr(data.bytesize)
31
43
  end
32
44
  @conn << data
45
+ rescue Polyphony::BaseException
46
+ raise
33
47
  rescue Exception => e
34
48
  @connection_fiber.transfer e
35
49
  end
@@ -45,8 +59,7 @@ module Tipi
45
59
  @conn << UPGRADE_MESSAGE
46
60
  @tx += UPGRADE_MESSAGE.bytesize
47
61
  settings = @upgrade_headers['http2-settings']
48
- Fiber.current.schedule(nil)
49
- @interface.upgrade(settings, @upgrade_headers, '')
62
+ @interface.upgrade(settings, @upgrade_headers, @upgrade_body || '')
50
63
  ensure
51
64
  @upgrade_headers = nil
52
65
  end
@@ -60,7 +73,7 @@ module Tipi
60
73
  @rx += data.bytesize
61
74
  @interface << data
62
75
  end
63
- rescue SystemCallError, IOError
76
+ rescue SystemCallError, IOError, HTTP2::Error::Error
64
77
  # ignore
65
78
  ensure
66
79
  finalize_client_loop
@@ -87,10 +100,12 @@ module Tipi
87
100
  def finalize_client_loop
88
101
  @interface = nil
89
102
  @streams.each_key(&:stop)
103
+ @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
90
104
  @conn.close
91
105
  end
92
106
 
93
107
  def close
108
+ @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil
94
109
  @conn.close
95
110
  end
96
111
 
@@ -15,8 +15,7 @@ module Tipi
15
15
  @conn = conn
16
16
  @first = first
17
17
  @connection_fiber = Fiber.current
18
- @stream_fiber = spin { |req| handle_request(req, &block) }
19
- Thread.current.fiber_unschedule(@stream_fiber)
18
+ @stream_fiber = spin { run(&block) }
20
19
 
21
20
  # Stream callbacks occur on the connection fiber (see HTTP2Adapter#each).
22
21
  # The request handler is run on a separate fiber for each stream, allowing
@@ -34,16 +33,16 @@ module Tipi
34
33
  stream.on(:half_close, &method(:on_half_close))
35
34
  end
36
35
 
37
- def handle_request(request, &block)
36
+ def run(&block)
37
+ request = receive
38
38
  error = nil
39
39
  block.(request)
40
40
  @connection_fiber.schedule
41
- rescue Polyphony::MoveOn
42
- # ignore
41
+ rescue Polyphony::BaseException
42
+ raise
43
43
  rescue Exception => e
44
44
  error = e
45
45
  ensure
46
- @done = true
47
46
  @connection_fiber.schedule error
48
47
  end
49
48
 
@@ -55,29 +54,19 @@ module Tipi
55
54
  @request.headers[':first'] = true
56
55
  @first = false
57
56
  end
58
- @stream_fiber.schedule @request
57
+ @stream_fiber << @request
59
58
  end
60
59
 
61
60
  def on_data(data)
62
61
  data = data.to_s # chunks might be wrapped in a HTTP2::Buffer
63
- if @waiting_for_body_chunk
64
- @waiting_for_body_chunk = nil
65
- @stream_fiber.schedule data
66
- else
67
- @request.buffer_body_chunk(data)
68
- end
62
+
63
+ (@buffered_chunks ||= []) << data
64
+ @get_body_chunk_fiber&.schedule
69
65
  end
70
66
 
71
67
  def on_half_close
72
- if @waiting_for_body_chunk
73
- @waiting_for_body_chunk = nil
74
- @stream_fiber.schedule
75
- elsif @waiting_for_half_close
76
- @waiting_for_half_close = nil
77
- @stream_fiber.schedule
78
- else
79
- @request.complete!
80
- end
68
+ @get_body_chunk_fiber&.schedule
69
+ @complete = true
81
70
  end
82
71
 
83
72
  def protocol
@@ -90,31 +79,38 @@ module Tipi
90
79
  ensure
91
80
  @adapter.unset_request_for_transfer_count(request)
92
81
  end
93
-
94
- def get_body_chunk(request)
95
- # called in the context of the stream fiber
96
- return nil if @request.complete?
82
+
83
+ def get_body_chunk(request, buffered_only = false)
84
+ @buffered_chunks ||= []
85
+ return @buffered_chunks.shift unless @buffered_chunks.empty?
86
+ return nil if @complete
97
87
 
98
- with_transfer_count(request) do
99
- @waiting_for_body_chunk = true
100
- # the chunk (or an exception) will be returned once the stream fiber is
101
- # resumed
88
+ begin
89
+ @get_body_chunk_fiber = Fiber.current
102
90
  suspend
91
+ ensure
92
+ @get_body_chunk_fiber = nil
103
93
  end
104
- ensure
105
- @waiting_for_body_chunk = nil
94
+ @buffered_chunks.shift
106
95
  end
107
-
108
- # Wait for request to finish
109
- def consume_request(request)
110
- return if @request.complete?
111
-
112
- with_transfer_count(request) do
113
- @waiting_for_half_close = true
114
- suspend
96
+
97
+ def get_body(request)
98
+ @buffered_chunks ||= []
99
+ return @buffered_chunks.join if @complete
100
+
101
+ while !@complete
102
+ begin
103
+ @get_body_chunk_fiber = Fiber.current
104
+ suspend
105
+ ensure
106
+ @get_body_chunk_fiber = nil
107
+ end
115
108
  end
116
- ensure
117
- @waiting_for_half_close = nil
109
+ @buffered_chunks.join
110
+ end
111
+
112
+ def complete?(request)
113
+ @complete
118
114
  end
119
115
 
120
116
  # response API
@@ -123,9 +119,23 @@ module Tipi
123
119
  headers[':status'] = headers[':status'].to_s
124
120
  with_transfer_count(request) do
125
121
  @stream.headers(transform_headers(headers))
122
+ @headers_sent = true
126
123
  @stream.data(chunk || '')
127
124
  end
128
- @headers_sent = true
125
+ rescue HTTP2::Error::StreamClosed
126
+ # ignore
127
+ end
128
+
129
+ def respond_from_io(request, io, headers, chunk_size = 2**16)
130
+ headers[':status'] ||= Qeweney::Status::OK
131
+ headers[':status'] = headers[':status'].to_s
132
+ with_transfer_count(request) do
133
+ @stream.headers(transform_headers(headers))
134
+ @headers_sent = true
135
+ while (chunk = io.read(chunk_size))
136
+ @stream.data(chunk)
137
+ end
138
+ end
129
139
  rescue HTTP2::Error::StreamClosed
130
140
  # ignore
131
141
  end
@@ -180,7 +190,7 @@ module Tipi
180
190
  end
181
191
 
182
192
  def stop
183
- return if @done
193
+ return if @complete
184
194
 
185
195
  @stream.close
186
196
  @stream_fiber.schedule(Polyphony::MoveOn.new)
@@ -3,25 +3,7 @@
3
3
  require 'rack'
4
4
 
5
5
  module Tipi
6
- module RackAdapter
7
- # Implements a rack input stream:
8
- # https://www.rubydoc.info/github/rack/rack/master/file/SPEC#label-The+Input+Stream
9
- class InputStream
10
- def initialize(request)
11
- @request = request
12
- end
13
-
14
- def gets; end
15
-
16
- def read(length = nil, outbuf = nil); end
17
-
18
- def each(&block)
19
- @request.each_chunk(&block)
20
- end
21
-
22
- def rewind; end
23
- end
24
-
6
+ module RackAdapter
25
7
  class << self
26
8
  def run(app)
27
9
  ->(req) { respond(req, app.(env(req))) }
@@ -32,41 +14,8 @@ module Tipi
32
14
  instance_eval(src, path, 1)
33
15
  end
34
16
 
35
- RACK_ENV = {
36
- 'SCRIPT_NAME' => '',
37
- 'rack.version' => Rack::VERSION,
38
- 'SERVER_PORT' => '80', # ?
39
- 'rack.url_scheme' => 'http', # ?
40
- 'rack.errors' => STDERR, # ?
41
- 'rack.multithread' => false,
42
- 'rack.run_once' => false,
43
- 'rack.hijack?' => false,
44
- 'rack.hijack' => nil,
45
- 'rack.hijack_io' => nil,
46
- 'rack.session' => nil,
47
- 'rack.logger' => nil,
48
- 'rack.multipart.buffer_size' => nil,
49
- 'rack.multipar.tempfile_factory' => nil
50
- }
51
-
52
17
  def env(request)
53
- Hash.new do |h, k|
54
- h[k] = env_value_from_request(request, k)
55
- end
56
- end
57
-
58
- HTTP_HEADER_RE = /^HTTP_(.+)$/.freeze
59
-
60
- def env_value_from_request(request, key)
61
- case key
62
- when 'REQUEST_METHOD' then request.method
63
- when 'PATH_INFO' then request.path
64
- when 'QUERY_STRING' then request.query_string || ''
65
- when 'SERVER_NAME' then request.headers['host']
66
- when 'rack.input' then InputStream.new(request)
67
- when HTTP_HEADER_RE then request.headers[$1.downcase]
68
- else RACK_ENV[key]
69
- end
18
+ Qeweney.rack_env_from_request(request)
70
19
  end
71
20
 
72
21
  def respond(request, (status_code, headers, body))
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'qeweney/request'
4
+
5
+ module Tipi
6
+ module ResponseExtensions
7
+ SPLICE_CHUNKS_SIZE_THRESHOLD = 2**20
8
+
9
+ def serve_io(io, opts)
10
+ if !opts[:stat] || opts[:stat].size >= SPLICE_CHUNKS_SIZE_THRESHOLD
11
+ @adapter.respond_from_io(self, io, opts[:headers], opts[:chunk_size] || 2**14)
12
+ else
13
+ respond(io.read, opts[:headers] || {})
14
+ end
15
+ end
16
+ end
17
+ end
data/lib/tipi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tipi
4
- VERSION = '0.39'
4
+ VERSION = '0.43'
5
5
  end
data/test/helper.rb CHANGED
@@ -8,32 +8,29 @@ require_relative './eg'
8
8
  require_relative './coverage' if ENV['COVERAGE']
9
9
 
10
10
  require 'minitest/autorun'
11
- require 'minitest/reporters'
12
11
 
13
12
  require 'polyphony'
14
13
 
15
14
  ::Exception.__disable_sanitized_backtrace__ = true
16
15
 
17
- Minitest::Reporters.use! [
18
- Minitest::Reporters::SpecReporter.new
19
- ]
20
-
21
16
  class MiniTest::Test
22
17
  def setup
23
- # puts "* setup #{self.name}"
24
- if Fiber.current.children.size > 0
25
- puts "Children left: #{Fiber.current.children.inspect}"
26
- exit!
27
- end
18
+ # trace "* setup #{self.name}"
28
19
  Fiber.current.setup_main_fiber
29
20
  Fiber.current.instance_variable_set(:@auto_watcher, nil)
21
+ Thread.current.backend.finalize
30
22
  Thread.current.backend = Polyphony::Backend.new
31
23
  sleep 0
32
24
  end
33
25
 
34
26
  def teardown
35
- # puts "* teardown #{self.name.inspect} Fiber.current: #{Fiber.current.inspect}"
27
+ # trace "* teardown #{self.name}"
36
28
  Fiber.current.shutdown_all_children
29
+ if Fiber.current.children.size > 0
30
+ puts "Children left after #{self.name}: #{Fiber.current.children.inspect}"
31
+ exit!
32
+ end
33
+ Fiber.current.instance_variable_set(:@auto_watcher, nil)
37
34
  rescue => e
38
35
  puts e
39
36
  puts e.backtrace.join("\n")
@@ -47,4 +44,55 @@ module Kernel
47
44
  rescue Exception => e
48
45
  e
49
46
  end
50
- end
47
+
48
+ def trace(*args)
49
+ STDOUT.orig_write(format_trace(args))
50
+ end
51
+
52
+ def format_trace(args)
53
+ if args.first.is_a?(String)
54
+ if args.size > 1
55
+ format("%s: %p\n", args.shift, args)
56
+ else
57
+ format("%s\n", args.first)
58
+ end
59
+ else
60
+ format("%p\n", args.size == 1 ? args.first : args)
61
+ end
62
+ end
63
+ end
64
+
65
+ class IO
66
+ # Creates two mockup sockets for simulating server-client communication
67
+ def self.server_client_mockup
68
+ server_in, client_out = IO.pipe
69
+ client_in, server_out = IO.pipe
70
+
71
+ server_connection = mockup_connection(server_in, server_out, client_out)
72
+ client_connection = mockup_connection(client_in, client_out, server_out)
73
+
74
+ [server_connection, client_connection]
75
+ end
76
+
77
+ def self.mockup_connection(input, output, output2)
78
+ eg(
79
+ __parser_read_method__: ->() { :readpartial },
80
+ read: ->(*args) { input.read(*args) },
81
+ read_loop: ->(*args, &block) { input.read_loop(*args, &block) },
82
+ recv_loop: ->(*args, &block) { input.read_loop(*args, &block) },
83
+ readpartial: ->(*args) { input.readpartial(*args) },
84
+ recv: ->(*args) { input.readpartial(*args) },
85
+ '<<': ->(*args) { output.write(*args) },
86
+ write: ->(*args) { output.write(*args) },
87
+ close: -> { output.close },
88
+ eof?: -> { output2.closed? }
89
+ )
90
+ end
91
+ end
92
+
93
+ module Minitest::Assertions
94
+ def assert_in_range exp_range, act
95
+ msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
96
+ assert exp_range.include?(act), msg
97
+ end
98
+ end
@@ -9,33 +9,6 @@ class String
9
9
  end
10
10
  end
11
11
 
12
- class IO
13
- # Creates two mockup sockets for simulating server-client communication
14
- def self.server_client_mockup
15
- server_in, client_out = IO.pipe
16
- client_in, server_out = IO.pipe
17
-
18
- server_connection = mockup_connection(server_in, server_out, client_out)
19
- client_connection = mockup_connection(client_in, client_out, server_out)
20
-
21
- [server_connection, client_connection]
22
- end
23
-
24
- def self.mockup_connection(input, output, output2)
25
- eg(
26
- :read => ->(*args) { input.read(*args) },
27
- :read_loop => ->(*args, &block) { input.read_loop(*args, &block) },
28
- :recv_loop => ->(*args, &block) { input.read_loop(*args, &block) },
29
- :readpartial => ->(*args) { input.readpartial(*args) },
30
- :recv => ->(*args) { input.readpartial(*args) },
31
- :<< => ->(*args) { output.write(*args) },
32
- :write => ->(*args) { output.write(*args) },
33
- :close => -> { output.close },
34
- :eof? => -> { output2.closed? }
35
- )
36
- end
37
- end
38
-
39
12
  class HTTP1ServerTest < MiniTest::Test
40
13
  def teardown
41
14
  @server&.interrupt if @server&.alive?