quicsilver 0.2.0 → 0.4.0

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +4 -5
  3. data/.github/workflows/cibuildgem.yaml +93 -0
  4. data/.gitignore +3 -1
  5. data/CHANGELOG.md +81 -0
  6. data/Gemfile.lock +26 -4
  7. data/README.md +95 -31
  8. data/Rakefile +95 -3
  9. data/benchmarks/components.rb +191 -0
  10. data/benchmarks/concurrent.rb +110 -0
  11. data/benchmarks/helpers.rb +88 -0
  12. data/benchmarks/quicsilver_server.rb +1 -1
  13. data/benchmarks/rails.rb +170 -0
  14. data/benchmarks/throughput.rb +113 -0
  15. data/examples/README.md +44 -91
  16. data/examples/benchmark.rb +111 -0
  17. data/examples/connection_pool_demo.rb +47 -0
  18. data/examples/example_helper.rb +18 -0
  19. data/examples/falcon_middleware.rb +44 -0
  20. data/examples/feature_demo.rb +125 -0
  21. data/examples/grpc_style.rb +97 -0
  22. data/examples/minimal_http3_server.rb +6 -18
  23. data/examples/priorities.rb +60 -0
  24. data/examples/protocol_http_server.rb +31 -0
  25. data/examples/rack_http3_server.rb +8 -20
  26. data/examples/rails_feature_test.rb +260 -0
  27. data/examples/simple_client_test.rb +2 -2
  28. data/examples/streaming_sse.rb +33 -0
  29. data/examples/trailers.rb +69 -0
  30. data/ext/quicsilver/extconf.rb +14 -0
  31. data/ext/quicsilver/quicsilver.c +568 -181
  32. data/lib/quicsilver/client/client.rb +349 -0
  33. data/lib/quicsilver/client/connection_pool.rb +106 -0
  34. data/lib/quicsilver/client/request.rb +98 -0
  35. data/lib/quicsilver/libmsquic.2.dylib +0 -0
  36. data/lib/quicsilver/protocol/adapter.rb +176 -0
  37. data/lib/quicsilver/protocol/control_stream_parser.rb +106 -0
  38. data/lib/quicsilver/protocol/frame_parser.rb +142 -0
  39. data/lib/quicsilver/protocol/frame_reader.rb +55 -0
  40. data/lib/quicsilver/{http3.rb → protocol/frames.rb} +146 -30
  41. data/lib/quicsilver/protocol/priority.rb +56 -0
  42. data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
  43. data/lib/quicsilver/protocol/qpack/encoder.rb +227 -0
  44. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +140 -0
  45. data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
  46. data/lib/quicsilver/protocol/request_encoder.rb +47 -0
  47. data/lib/quicsilver/protocol/request_parser.rb +275 -0
  48. data/lib/quicsilver/protocol/response_encoder.rb +97 -0
  49. data/lib/quicsilver/protocol/response_parser.rb +141 -0
  50. data/lib/quicsilver/protocol/stream_input.rb +98 -0
  51. data/lib/quicsilver/protocol/stream_output.rb +59 -0
  52. data/lib/quicsilver/quicsilver.bundle +0 -0
  53. data/lib/quicsilver/server/listener_data.rb +14 -0
  54. data/lib/quicsilver/server/request_handler.rb +138 -0
  55. data/lib/quicsilver/server/request_registry.rb +50 -0
  56. data/lib/quicsilver/server/server.rb +610 -0
  57. data/lib/quicsilver/transport/configuration.rb +141 -0
  58. data/lib/quicsilver/transport/connection.rb +379 -0
  59. data/lib/quicsilver/transport/event_loop.rb +38 -0
  60. data/lib/quicsilver/transport/inbound_stream.rb +33 -0
  61. data/lib/quicsilver/transport/stream.rb +28 -0
  62. data/lib/quicsilver/transport/stream_event.rb +26 -0
  63. data/lib/quicsilver/version.rb +1 -1
  64. data/lib/quicsilver.rb +55 -14
  65. data/lib/rackup/handler/quicsilver.rb +1 -2
  66. data/quicsilver.gemspec +13 -3
  67. metadata +125 -21
  68. data/benchmarks/benchmark.rb +0 -68
  69. data/examples/setup_certs.sh +0 -57
  70. data/lib/quicsilver/client.rb +0 -261
  71. data/lib/quicsilver/connection.rb +0 -42
  72. data/lib/quicsilver/event_loop.rb +0 -38
  73. data/lib/quicsilver/http3/request_encoder.rb +0 -133
  74. data/lib/quicsilver/http3/request_parser.rb +0 -176
  75. data/lib/quicsilver/http3/response_encoder.rb +0 -186
  76. data/lib/quicsilver/http3/response_parser.rb +0 -160
  77. data/lib/quicsilver/listener_data.rb +0 -29
  78. data/lib/quicsilver/quic_stream.rb +0 -36
  79. data/lib/quicsilver/request_registry.rb +0 -48
  80. data/lib/quicsilver/server.rb +0 -355
  81. data/lib/quicsilver/server_configuration.rb +0 -78
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quicsilver
4
+ class Server
5
+ class RequestHandler
6
+ SAFE_METHODS = %w[GET HEAD OPTIONS].freeze
7
+
8
+ attr_reader :adapter
9
+
10
+ def initialize(app:, configuration:, request_registry:, cancelled_streams:, cancelled_mutex:)
11
+ @configuration = configuration
12
+ @request_registry = request_registry
13
+ @cancelled_streams = cancelled_streams
14
+ @cancelled_mutex = cancelled_mutex
15
+ @adapter = Protocol::Adapter.new(app)
16
+ end
17
+
18
+ def call(connection, stream, early_data: false)
19
+ request = parse_request(connection, stream, early_data: early_data)
20
+ return unless request
21
+
22
+ response = @adapter.call(request)
23
+
24
+ send_response(connection, stream, request, response)
25
+ rescue Server::DrainTimeoutError
26
+ Quicsilver.logger.debug("Request interrupted by drain: stream #{stream.stream_id}")
27
+ rescue Protocol::FrameError => e
28
+ Quicsilver.logger.error("Frame error: #{e.message} (0x#{e.error_code.to_s(16)})")
29
+ Quicsilver.connection_shutdown(connection.handle, e.error_code, false) rescue nil
30
+ rescue Protocol::MessageError => e
31
+ Quicsilver.logger.error("Message error: #{e.message} (0x#{e.error_code.to_s(16)})")
32
+ Quicsilver.stream_reset(stream.stream_handle, e.error_code) if stream.writable?
33
+ rescue => e
34
+ Quicsilver.logger.error("Error handling request: #{e.class} - #{e.message}")
35
+ Quicsilver.logger.debug(e.backtrace.first(5).join("\n"))
36
+ connection.send_error(stream, 500, "Internal Server Error") if stream.writable?
37
+ ensure
38
+ @request_registry.complete(stream.stream_id) if @request_registry.include?(stream.stream_id)
39
+ @cancelled_mutex.synchronize { @cancelled_streams.delete(stream.stream_id) }
40
+ end
41
+
42
+ private
43
+
44
+ def parse_request(connection, stream, early_data: false)
45
+ parser = Protocol::RequestParser.new(
46
+ stream.data,
47
+ max_body_size: @configuration.max_body_size,
48
+ max_header_size: @configuration.max_header_size,
49
+ max_header_count: @configuration.max_header_count,
50
+ max_frame_payload_size: @configuration.max_frame_payload_size
51
+ )
52
+ parser.parse
53
+ parser.validate_headers!
54
+
55
+ headers = parser.headers
56
+ unless headers && !headers.empty?
57
+ connection.send_error(stream, 400, "Bad Request") if stream.writable?
58
+ return
59
+ end
60
+
61
+ method = headers[":method"]
62
+
63
+ if @configuration.early_data_policy == :reject &&
64
+ early_data && !SAFE_METHODS.include?(method)
65
+ connection.send_error(stream, 425, "Too Early") if stream.writable?
66
+ return
67
+ end
68
+
69
+ request, body = @adapter.build_request(headers)
70
+ request.headers.add("quicsilver-early-data", early_data.to_s)
71
+
72
+ # Wire interim_response so apps can send 103 Early Hints.
73
+ # Falcon mode: app calls request.send_interim_response(103, headers)
74
+ # Rack mode: bridged to rack.early_hints via EarlyHintsMiddleware
75
+ request.interim_response = ->(status, headers) {
76
+ connection.send_informational(stream, status, headers)
77
+ }
78
+
79
+ if body && parser.body && parser.body.size > 0
80
+ parser.body.rewind
81
+ body_data = parser.body.read
82
+ body.write(body_data) unless body_data.empty?
83
+ end
84
+ body&.close_write
85
+
86
+ connection.apply_stream_priority(stream, parser.priority)
87
+
88
+ @request_registry.track(
89
+ stream.stream_id, connection.handle,
90
+ path: headers[":path"] || "/", method: method || "GET"
91
+ )
92
+
93
+ request
94
+ end
95
+
96
+ def send_response(connection, stream, request, response)
97
+ if cancelled_stream?(stream.stream_id)
98
+ Quicsilver.logger.debug("Skipping response for cancelled stream #{stream.stream_id}")
99
+ return
100
+ end
101
+
102
+ raise "Stream handle not found for stream #{stream.stream_id}" unless stream.writable?
103
+
104
+ headers = response.headers
105
+
106
+ # Extract trailers before flattening (Protocol::HTTP::Headers tracks
107
+ # trailer! state that a plain Hash would lose — needed for gRPC).
108
+ trailers = if headers.respond_to?(:trailer?) && headers.trailer?
109
+ trailer_hash = {}
110
+ headers.trailer.each { |name, value| trailer_hash[name] = value }
111
+ trailer_hash
112
+ end
113
+
114
+ response_headers = {}
115
+ if headers.respond_to?(:header)
116
+ headers.header.each { |name, value| response_headers[name] = value }
117
+ else
118
+ headers&.each { |name, value| response_headers[name] = value }
119
+ end
120
+
121
+ # Protocol-rack moves content-length from headers to body.length —
122
+ # re-add it so the HTTP/3 response includes the header.
123
+ if !response_headers.key?("content-length") && response.body&.length
124
+ response_headers["content-length"] = response.body.length.to_s
125
+ end
126
+
127
+ body = response.body || []
128
+ connection.send_response(stream, response.status, response_headers, body,
129
+ head_request: request.head?, trailers: trailers)
130
+ @request_registry.complete(stream.stream_id)
131
+ end
132
+
133
+ def cancelled_stream?(stream_id)
134
+ @cancelled_mutex.synchronize { @cancelled_streams.include?(stream_id) }
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quicsilver
4
+ class Server
5
+ class RequestRegistry
6
+ def initialize
7
+ @requests = {}
8
+ @mutex = Mutex.new
9
+ end
10
+
11
+ def track(stream_id, connection_handle, path:, method:, started_at: Time.now)
12
+ @mutex.synchronize do
13
+ @requests[stream_id] = {
14
+ connection_handle: connection_handle,
15
+ path: path,
16
+ method: method,
17
+ started_at: started_at
18
+ }
19
+ end
20
+ end
21
+
22
+ def complete(stream_id)
23
+ @mutex.synchronize { @requests.delete(stream_id) }
24
+ end
25
+
26
+ def active_count
27
+ @mutex.synchronize { @requests.size }
28
+ end
29
+
30
+ def active_requests
31
+ @mutex.synchronize { @requests.dup }
32
+ end
33
+
34
+ def requests_older_than(seconds)
35
+ cutoff = Time.now - seconds
36
+ @mutex.synchronize do
37
+ @requests.select { |_, r| r[:started_at] < cutoff }
38
+ end
39
+ end
40
+
41
+ def empty?
42
+ @mutex.synchronize { @requests.empty? }
43
+ end
44
+
45
+ def include?(stream_id)
46
+ @mutex.synchronize { @requests.key?(stream_id) }
47
+ end
48
+ end
49
+ end
50
+ end