quicsilver 0.2.0 → 0.3.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +3 -4
  3. data/CHANGELOG.md +49 -0
  4. data/Gemfile.lock +8 -4
  5. data/README.md +7 -6
  6. data/Rakefile +29 -2
  7. data/benchmarks/components.rb +191 -0
  8. data/benchmarks/concurrent.rb +110 -0
  9. data/benchmarks/helpers.rb +88 -0
  10. data/benchmarks/quicsilver_server.rb +1 -1
  11. data/benchmarks/rails.rb +170 -0
  12. data/benchmarks/throughput.rb +113 -0
  13. data/ext/quicsilver/quicsilver.c +529 -181
  14. data/lib/quicsilver/client/client.rb +250 -0
  15. data/lib/quicsilver/client/request.rb +98 -0
  16. data/lib/quicsilver/{http3.rb → protocol/frames.rb} +133 -28
  17. data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
  18. data/lib/quicsilver/protocol/qpack/encoder.rb +189 -0
  19. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +125 -0
  20. data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
  21. data/lib/quicsilver/protocol/request_encoder.rb +47 -0
  22. data/lib/quicsilver/protocol/request_parser.rb +387 -0
  23. data/lib/quicsilver/protocol/response_encoder.rb +72 -0
  24. data/lib/quicsilver/protocol/response_parser.rb +249 -0
  25. data/lib/quicsilver/server/listener_data.rb +14 -0
  26. data/lib/quicsilver/server/request_handler.rb +86 -0
  27. data/lib/quicsilver/server/request_registry.rb +50 -0
  28. data/lib/quicsilver/server/server.rb +336 -0
  29. data/lib/quicsilver/transport/configuration.rb +132 -0
  30. data/lib/quicsilver/transport/connection.rb +350 -0
  31. data/lib/quicsilver/transport/event_loop.rb +38 -0
  32. data/lib/quicsilver/transport/inbound_stream.rb +33 -0
  33. data/lib/quicsilver/transport/stream.rb +28 -0
  34. data/lib/quicsilver/transport/stream_event.rb +26 -0
  35. data/lib/quicsilver/version.rb +1 -1
  36. data/lib/quicsilver.rb +31 -13
  37. data/lib/rackup/handler/quicsilver.rb +1 -2
  38. data/quicsilver.gemspec +3 -1
  39. metadata +58 -18
  40. data/benchmarks/benchmark.rb +0 -68
  41. data/lib/quicsilver/client.rb +0 -261
  42. data/lib/quicsilver/connection.rb +0 -42
  43. data/lib/quicsilver/event_loop.rb +0 -38
  44. data/lib/quicsilver/http3/request_encoder.rb +0 -133
  45. data/lib/quicsilver/http3/request_parser.rb +0 -176
  46. data/lib/quicsilver/http3/response_encoder.rb +0 -186
  47. data/lib/quicsilver/http3/response_parser.rb +0 -160
  48. data/lib/quicsilver/listener_data.rb +0 -29
  49. data/lib/quicsilver/quic_stream.rb +0 -36
  50. data/lib/quicsilver/request_registry.rb +0 -48
  51. data/lib/quicsilver/server.rb +0 -355
  52. data/lib/quicsilver/server_configuration.rb +0 -78
@@ -1,355 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Quicsilver
4
- class Server
5
- attr_reader :address, :port, :server_configuration, :running, :connections, :request_registry, :shutting_down
6
-
7
- STREAM_EVENT_RECEIVE = "RECEIVE"
8
- STREAM_EVENT_RECEIVE_FIN = "RECEIVE_FIN"
9
- STREAM_EVENT_CONNECTION_ESTABLISHED = "CONNECTION_ESTABLISHED"
10
- STREAM_EVENT_SEND_COMPLETE = "SEND_COMPLETE"
11
- STREAM_EVENT_CONNECTION_CLOSED = "CONNECTION_CLOSED"
12
-
13
- ServerStopError = Class.new(StandardError)
14
-
15
- class << self
16
- attr_accessor :instance
17
-
18
- # Callback from C extension - delegates to server instance
19
- def handle_stream(connection_data, stream_id, event, data)
20
- instance&.handle_stream_event(connection_data, stream_id, event, data)
21
- end
22
- end
23
-
24
- def initialize(port = 4433, address: "0.0.0.0", app: nil, server_configuration: nil)
25
- @port = port
26
- @address = address
27
- @app = app || default_rack_app
28
- @server_configuration = server_configuration || ServerConfiguration.new
29
- @running = false
30
- @shutting_down = false
31
- @listener_data = nil
32
- @connections = {}
33
- @request_registry = RequestRegistry.new
34
-
35
- self.class.instance = self
36
- end
37
-
38
- def start
39
- raise ServerIsRunningError, "Server is already running" if @running
40
-
41
- Quicsilver.open_connection
42
- config = Quicsilver.create_server_configuration(@server_configuration.to_h)
43
- raise ServerConfigurationError, "Failed to create server configuration" unless config
44
-
45
- # Create and start the listener
46
- result = Quicsilver.create_listener(config)
47
- @listener_data = ListenerData.new(result[0], result[1])
48
- raise ServerListenerError, "Failed to create listener #{@address}:#{@port}" unless @listener_data
49
-
50
- unless Quicsilver.start_listener(@listener_data.listener_handle, @address, @port)
51
- Quicsilver.close_configuration(config)
52
- cleanup_failed_server
53
- raise ServerListenerError, "Failed to start listener on #{@address}:#{@port}"
54
- end
55
-
56
- @running = true
57
-
58
- Quicsilver.event_loop.start
59
- Quicsilver.event_loop.join # Block until shutdown
60
- rescue ServerConfigurationError, ServerListenerError => e
61
- cleanup_failed_server
62
- @running = false
63
- raise e
64
- rescue => e
65
- cleanup_failed_server
66
- @running = false
67
-
68
- error_msg = case e.message
69
- when /0x16/
70
- "Invalid parameter error - check certificate files and network configuration"
71
- when /0x30/
72
- "Address already in use - port #{@port} may be occupied"
73
- else
74
- e.message
75
- end
76
-
77
- raise ServerError, "Server start failed: #{error_msg}"
78
- end
79
-
80
- def stop
81
- return unless @running
82
-
83
- if @listener_data && @listener_data.listener_handle
84
- Quicsilver.stop_listener(@listener_data.listener_handle)
85
- Quicsilver.close_listener([@listener_data.listener_handle, @listener_data.context_handle])
86
- end
87
-
88
- Quicsilver.event_loop.stop # Stop event loop so start unblocks
89
- @running = false
90
- @listener_data = nil
91
- rescue => e
92
- @listener_data = nil
93
- @running = false
94
- raise ServerStopError, "Failed to stop server: #{e.message}"
95
- end
96
-
97
- def running?
98
- @running
99
- end
100
-
101
- # Graceful shutdown: send GOAWAY, wait for in-flight requests, then stop
102
- def shutdown(timeout: 30)
103
- return unless @running
104
- return if @shutting_down
105
-
106
- @shutting_down = true
107
- Quicsilver.logger.info("Initiating graceful shutdown (timeout: #{timeout}s)")
108
-
109
- # Phase 1: Send GOAWAY with max stream ID to all connections
110
- # This tells clients to stop sending new requests
111
- @connections.each_value do |connection|
112
- send_goaway(connection, HTTP3::MAX_STREAM_ID)
113
- end
114
-
115
- # Phase 2: Wait for in-flight requests to drain
116
- deadline = Time.now + timeout
117
- until @request_registry.empty? || Time.now > deadline
118
- sleep 0.1
119
- end
120
-
121
- # Log any requests that didn't complete
122
- unless @request_registry.empty?
123
- @request_registry.active_requests.each do |stream_id, req|
124
- elapsed = Time.now - req[:started_at]
125
- Quicsilver.logger.warn("Force-closing request: #{req[:method]} #{req[:path]} (stream: #{stream_id}, elapsed: #{elapsed.round(2)}s)")
126
- end
127
- end
128
-
129
- # Phase 3: Send final GOAWAY with actual last stream ID and shutdown connections
130
- @connections.each_value do |connection|
131
- last_stream_id = connection.streams.keys.select { |id| (id & 0x02) == 0 }.max || 0
132
- send_goaway(connection, last_stream_id)
133
-
134
- # Graceful QUIC shutdown (sends CONNECTION_CLOSE to peer)
135
- Quicsilver.connection_shutdown(connection.handle, 0, false)
136
- end
137
-
138
- # Give connections a moment to close gracefully
139
- sleep 0.1
140
-
141
- # Phase 4: Hard stop
142
- stop
143
- @shutting_down = false
144
-
145
- Quicsilver.logger.info("Graceful shutdown complete")
146
- end
147
-
148
- def handle_stream_event(connection_data, stream_id, event, data)
149
- connection_handle = connection_data[0]
150
-
151
- case event
152
- when STREAM_EVENT_CONNECTION_ESTABLISHED
153
- connection = Connection.new(connection_handle, connection_data)
154
- @connections[connection_handle] = connection
155
- setup_http3_streams(connection)
156
- when STREAM_EVENT_CONNECTION_CLOSED
157
- @connections.delete(connection_handle)&.streams&.clear
158
- when STREAM_EVENT_SEND_COMPLETE
159
- # Buffer cleanup handled in C extension
160
- when STREAM_EVENT_RECEIVE
161
- return unless connection = @connections[connection_handle]
162
-
163
- stream = connection.get_stream(stream_id) || QuicStream.new(stream_id)
164
- connection.add_stream(stream) unless connection.get_stream(stream_id)
165
- stream.append_data(data)
166
- when STREAM_EVENT_RECEIVE_FIN
167
- return unless connection = @connections[connection_handle]
168
-
169
- # Extract stream handle from data (first 8 bytes)
170
- stream_handle = data[0, 8].unpack1('Q')
171
- actual_data = data[8..-1] || ""
172
-
173
- stream = connection.get_stream(stream_id) || QuicStream.new(stream_id)
174
- stream.stream_handle = stream_handle
175
- stream.append_data(actual_data)
176
-
177
- if stream.bidirectional?
178
- handle_request(connection, stream)
179
- else
180
- handle_unidirectional_stream(connection, stream) # Unidirectional stream (control/QPACK)
181
- end
182
-
183
- connection.remove_stream(stream_id)
184
- end
185
- end
186
-
187
- private
188
-
189
- def default_rack_app
190
- ->(env) {
191
- [200,
192
- {'Content-Type' => 'text/plain'},
193
- ["Hello from Quicsilver!\nMethod: #{env['REQUEST_METHOD']}\nPath: #{env['PATH_INFO']}\n"]]
194
- }
195
- end
196
-
197
- def cleanup_failed_server
198
- if @listener_data
199
- begin
200
- Quicsilver.stop_listener(@listener_data.listener_handle) if @listener_data.listener_handle
201
- Quicsilver.close_listener([@listener_data.listener_handle, @listener_data.context_handle]) if @listener_data.listener_handle
202
- rescue
203
- # Ignore cleanup errors
204
- ensure
205
- @listener_data = nil
206
- end
207
- end
208
- end
209
-
210
- def setup_http3_streams(connection)
211
- connection_data = connection.data
212
-
213
- # Send control stream (required) - store handle for GOAWAY
214
- control_stream = Quicsilver.open_stream(connection_data, true)
215
- control_data = HTTP3.build_control_stream
216
- Quicsilver.send_stream(control_stream, control_data, false)
217
- connection.server_control_stream = control_stream
218
-
219
- # Open QPACK encoder/decoder streams (required)
220
- [0x02, 0x03].each do |type|
221
- stream = Quicsilver.open_stream(connection_data, true)
222
- Quicsilver.send_stream(stream, [type].pack('C'), false)
223
- end
224
- end
225
-
226
-
227
- def handle_control_stream(connection, stream)
228
- return if stream.data.empty?
229
-
230
- case stream.data[0].ord
231
- when 0x00 then connection.set_control_stream(stream.stream_id)
232
- when 0x02 then connection.set_qpack_encoder_stream(stream.stream_id)
233
- when 0x03 then connection.set_qpack_decoder_stream(stream.stream_id)
234
- end
235
- end
236
-
237
- def handle_unidirectional_stream(connection, stream)
238
- data = stream.data
239
- return if data.empty?
240
-
241
- stream_type = data[0].ord
242
- payload = data[1..-1]
243
-
244
- case stream_type
245
- when 0x00 # Control stream
246
- connection.set_control_stream(stream.stream_id)
247
- parse_client_control_stream(payload)
248
- when 0x02 # QPACK encoder stream
249
- # Store encoder stream for sending dynamic table updates
250
- connection.set_qpack_encoder_stream(stream.stream_id)
251
- when 0x03 # QPACK decoder stream
252
- # Store decoder stream for receiving acknowledgments
253
- connection.set_qpack_decoder_stream(stream.stream_id)
254
- else
255
- raise "⚠️ Ruby: Stream #{stream.stream_id}: Unknown stream type: 0x#{stream_type.to_s(16)}"
256
- end
257
- end
258
-
259
- def parse_client_control_stream(data)
260
- offset = 0
261
- while offset < data.bytesize
262
- frame_type, type_len = HTTP3.decode_varint(data.bytes, offset)
263
- frame_length, length_len = HTTP3.decode_varint(data.bytes, offset + type_len)
264
-
265
- if frame_type == HTTP3::FRAME_SETTINGS
266
- # Parse client settings
267
- settings_payload = data[offset + type_len + length_len, frame_length]
268
- parse_settings_frame(settings_payload)
269
- end
270
-
271
- offset += type_len + length_len + frame_length
272
- end
273
- end
274
-
275
- def parse_settings_frame(payload)
276
- offset = 0
277
- settings = {}
278
-
279
- while offset < payload.bytesize
280
- setting_id, id_len = HTTP3.decode_varint(payload.bytes, offset)
281
- setting_value, value_len = HTTP3.decode_varint(payload.bytes, offset + id_len)
282
- settings[setting_id] = setting_value
283
- offset += id_len + value_len
284
- end
285
-
286
- settings
287
- end
288
-
289
- def handle_request(connection, stream)
290
- parser = HTTP3::RequestParser.new(stream.data)
291
- parser.parse
292
- env = parser.to_rack_env
293
-
294
- if env && @app
295
- # Track request
296
- @request_registry.track(
297
- stream.stream_id,
298
- connection.handle,
299
- path: env["PATH_INFO"] || "/",
300
- method: env["REQUEST_METHOD"] || "GET"
301
- )
302
-
303
- # Call Rack app
304
- status, headers, body = @app.call(env)
305
- encoder = HTTP3::ResponseEncoder.new(status, headers, body)
306
-
307
- raise "Stream handle not found for stream #{stream.stream_id}" unless stream.ready_to_send?
308
-
309
- # Rack convention: body.to_ary means bufferable, otherwise stream
310
- if body.respond_to?(:to_ary)
311
- # Buffer mode - small responses (Arrays), send all at once
312
- Quicsilver.send_stream(stream.stream_handle, encoder.encode, true)
313
- else
314
- # Stream mode - lazy bodies (ActionController::Live, SSE), send incrementally
315
- encoder.stream_encode do |frame_data, fin|
316
- Quicsilver.send_stream(stream.stream_handle, frame_data, fin) unless frame_data.empty? && !fin
317
- end
318
- end
319
-
320
- # Mark request complete
321
- @request_registry.complete(stream.stream_id)
322
- else
323
- # failed to parse request
324
- if stream.ready_to_send?
325
- error_response = encode_error_response(400, "Bad Request")
326
- Quicsilver.send_stream(stream.stream_handle, error_response, true)
327
- end
328
- end
329
- rescue => e
330
- Quicsilver.logger.error("Error handling request: #{e.class} - #{e.message}")
331
- Quicsilver.logger.debug(e.backtrace.first(5).join("\n"))
332
- error_response = encode_error_response(500, "Internal Server Error")
333
-
334
- Quicsilver.send_stream(stream.stream_handle, error_response, true) if stream.ready_to_send?
335
- ensure
336
- # Always complete the request, even on error
337
- @request_registry.complete(stream.stream_id) if @request_registry.include?(stream.stream_id)
338
- end
339
-
340
- def encode_error_response(status, message)
341
- body = ["#{status} #{message}"]
342
- encoder = HTTP3::ResponseEncoder.new(status, {"content-type" => "text/plain"}, body)
343
- encoder.encode
344
- end
345
-
346
- def send_goaway(connection, stream_id)
347
- return unless connection.server_control_stream
348
-
349
- goaway_frame = HTTP3.build_goaway_frame(stream_id)
350
- Quicsilver.send_stream(connection.server_control_stream, goaway_frame, false)
351
- rescue => e
352
- Quicsilver.logger.error("Failed to send GOAWAY to connection #{connection.handle}: #{e.message}")
353
- end
354
- end
355
- end
@@ -1,78 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "localhost"
4
-
5
- module Quicsilver
6
- class ServerConfiguration
7
- attr_reader :cert_file, :key_file, :idle_timeout, :server_resumption_level, :peer_bidi_stream_count,
8
- :peer_unidi_stream_count, :stream_recv_window, :stream_recv_buffer, :conn_flow_control_window
9
-
10
- QUIC_SERVER_RESUME_AND_ZERORTT = 1
11
- QUIC_SERVER_RESUME_ONLY = 2
12
- QUIC_SERVER_RESUME_AND_REUSE = 3
13
- QUIC_SERVER_RESUME_AND_REUSE_ZERORTT = 4
14
-
15
- DEFAULT_CERT_FILE = "certificates/server.crt"
16
- DEFAULT_KEY_FILE = "certificates/server.key"
17
- DEFAULT_ALPN = "h3"
18
-
19
- # Flow control defaults (msquic defaults)
20
- # See: https://github.com/microsoft/msquic/blob/main/docs/Settings.md
21
- DEFAULT_STREAM_RECV_WINDOW = 65_536 # 64KB - initial stream receive window
22
- DEFAULT_STREAM_RECV_BUFFER = 4_096 # 4KB - stream buffer size
23
- DEFAULT_CONN_FLOW_CONTROL_WINDOW = 16_777_216 # 16MB - connection-wide flow control
24
-
25
- def initialize(cert_file = nil, key_file = nil, options = {})
26
- @idle_timeout = options[:idle_timeout].nil? ? 10000 : options[:idle_timeout]
27
- @server_resumption_level = options[:server_resumption_level].nil? ? QUIC_SERVER_RESUME_AND_ZERORTT : options[:server_resumption_level]
28
- @peer_bidi_stream_count = options[:peer_bidi_stream_count].nil? ? 10 : options[:peer_bidi_stream_count]
29
- @peer_unidi_stream_count = options[:peer_unidi_stream_count].nil? ? 10 : options[:peer_unidi_stream_count]
30
- @alpn = options[:alpn].nil? ? DEFAULT_ALPN : options[:alpn]
31
-
32
- # Flow control / backpressure settings
33
- @stream_recv_window = options[:stream_recv_window].nil? ? DEFAULT_STREAM_RECV_WINDOW : options[:stream_recv_window]
34
- @stream_recv_buffer = options[:stream_recv_buffer].nil? ? DEFAULT_STREAM_RECV_BUFFER : options[:stream_recv_buffer]
35
- @conn_flow_control_window = options[:conn_flow_control_window].nil? ? DEFAULT_CONN_FLOW_CONTROL_WINDOW : options[:conn_flow_control_window]
36
-
37
- @cert_file = cert_file.nil? ? DEFAULT_CERT_FILE : cert_file
38
- @key_file = key_file.nil? ? DEFAULT_KEY_FILE : key_file
39
-
40
- unless File.exist?(@cert_file)
41
- raise ServerConfigurationError, "Certificate file not found: #{@cert_file}"
42
- end
43
-
44
- unless File.exist?(@key_file)
45
- raise ServerConfigurationError, "Key file not found: #{@key_file}"
46
- end
47
- end
48
-
49
- # Common HTTP/3 ALPN Values:
50
- # "h3" - HTTP/3 (most common)
51
- # "h3-29" - HTTP/3 draft version 29
52
- # "h3-28" - HTTP/3 draft version 28
53
- # "h3-27" - HTTP/3 draft version 27
54
- # Other QUIC ALPN Values:
55
- # "hq-interop" - HTTP/0.9 over QUIC (testing)
56
- # "hq-29" - HTTP/0.9 over QUIC draft 29
57
- # "doq" - DNS over QUIC
58
- # "doq-i03" - DNS over QUIC draft
59
- def alpn
60
- @alpn
61
- end
62
-
63
- def to_h
64
- {
65
- cert_file: @cert_file,
66
- key_file: @key_file,
67
- idle_timeout: @idle_timeout,
68
- server_resumption_level: @server_resumption_level,
69
- peer_bidi_stream_count: @peer_bidi_stream_count,
70
- peer_unidi_stream_count: @peer_unidi_stream_count,
71
- alpn: alpn,
72
- stream_recv_window: @stream_recv_window,
73
- stream_recv_buffer: @stream_recv_buffer,
74
- conn_flow_control_window: @conn_flow_control_window
75
- }
76
- end
77
- end
78
- end