quicsilver 0.3.0 → 0.5.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -1
  3. data/.github/workflows/cibuildgem.yaml +117 -0
  4. data/.gitignore +8 -1
  5. data/CHANGELOG.md +82 -0
  6. data/Gemfile.lock +27 -2
  7. data/README.md +205 -29
  8. data/Rakefile +75 -3
  9. data/benchmarks/baseline.rb +175 -0
  10. data/benchmarks/compare.rb +398 -0
  11. data/benchmarks/helpers.rb +138 -64
  12. data/benchmarks/latency.rb +128 -0
  13. data/benchmarks/loss.rb +193 -0
  14. data/benchmarks/mobile.rb +125 -0
  15. data/docs/connection_identity.md +141 -0
  16. data/examples/README.md +44 -91
  17. data/examples/benchmark.rb +191 -0
  18. data/examples/connection_pool_demo.rb +47 -0
  19. data/examples/example_helper.rb +69 -0
  20. data/examples/falcon_middleware.rb +44 -0
  21. data/examples/feature_demo.rb +125 -0
  22. data/examples/grpc_style.rb +97 -0
  23. data/examples/latency_aware_routing.rb +382 -0
  24. data/examples/lifecycle_drain.rb +264 -0
  25. data/examples/minimal_http3_server.rb +6 -18
  26. data/examples/priorities.rb +60 -0
  27. data/examples/protocol_http_server.rb +31 -0
  28. data/examples/rack_http3_server.rb +8 -20
  29. data/examples/rails_feature_test.rb +280 -0
  30. data/examples/server_stats.rb +117 -0
  31. data/examples/simple_client_test.rb +5 -5
  32. data/examples/streaming_sse.rb +33 -0
  33. data/examples/trailers.rb +69 -0
  34. data/examples/webtransport_smoke.rb +546 -0
  35. data/examples/worker_load_balancing.rb +425 -0
  36. data/ext/quicsilver/extconf.rb +111 -12
  37. data/ext/quicsilver/include/msquic.h +2226 -0
  38. data/ext/quicsilver/include/msquic_posix.h +696 -0
  39. data/ext/quicsilver/include/quic_sal_stub.h +314 -0
  40. data/ext/quicsilver/msquic_patch.rb +68 -0
  41. data/ext/quicsilver/patches/reliable-reset.patch +723 -0
  42. data/ext/quicsilver/patches/stream-final-size.patch +47 -0
  43. data/ext/quicsilver/quicsilver.c +1646 -189
  44. data/lib/quicsilver/client/client.rb +546 -88
  45. data/lib/quicsilver/client/connection_pool.rb +202 -0
  46. data/lib/quicsilver/client/request.rb +106 -14
  47. data/lib/quicsilver/errors.rb +39 -0
  48. data/lib/quicsilver/libmsquic.2.dylib +0 -0
  49. data/lib/quicsilver/protocol/adapter.rb +196 -0
  50. data/lib/quicsilver/protocol/capsule.rb +38 -0
  51. data/lib/quicsilver/protocol/connect_stream_decoder.rb +75 -0
  52. data/lib/quicsilver/protocol/control_stream_parser.rb +117 -0
  53. data/lib/quicsilver/protocol/datagram.rb +40 -0
  54. data/lib/quicsilver/protocol/frame_parser.rb +141 -0
  55. data/lib/quicsilver/protocol/frame_reader.rb +84 -0
  56. data/lib/quicsilver/protocol/frames.rb +137 -10
  57. data/lib/quicsilver/protocol/priority.rb +65 -0
  58. data/lib/quicsilver/protocol/qpack/decoder.rb +4 -42
  59. data/lib/quicsilver/protocol/qpack/encoder.rb +49 -21
  60. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +16 -1
  61. data/lib/quicsilver/protocol/qpack/huffman.rb +4 -4
  62. data/lib/quicsilver/protocol/request_encoder.rb +6 -3
  63. data/lib/quicsilver/protocol/request_parser.rb +43 -141
  64. data/lib/quicsilver/protocol/response_encoder.rb +40 -5
  65. data/lib/quicsilver/protocol/response_parser.rb +26 -128
  66. data/lib/quicsilver/protocol/stream_input.rb +98 -0
  67. data/lib/quicsilver/protocol/stream_output.rb +59 -0
  68. data/lib/quicsilver/protocol/unidirectional_stream.rb +12 -0
  69. data/lib/quicsilver/protocol/web_transport.rb +214 -0
  70. data/lib/quicsilver/rack/context.rb +33 -0
  71. data/lib/quicsilver/response.rb +40 -0
  72. data/lib/quicsilver/server/rack_adapter.rb +77 -0
  73. data/lib/quicsilver/server/receive_queue.rb +106 -0
  74. data/lib/quicsilver/server/request_handler.rb +111 -48
  75. data/lib/quicsilver/server/request_registry.rb +18 -5
  76. data/lib/quicsilver/server/scheduler.rb +50 -0
  77. data/lib/quicsilver/server/schedulers/fiber_scheduler.rb +79 -0
  78. data/lib/quicsilver/server/schedulers/thread_scheduler.rb +76 -0
  79. data/lib/quicsilver/server/server.rb +848 -104
  80. data/lib/quicsilver/server/web_transport_flow_control.rb +270 -0
  81. data/lib/quicsilver/server/web_transport_manager.rb +281 -0
  82. data/lib/quicsilver/server/web_transport_registry.rb +40 -0
  83. data/lib/quicsilver/server/web_transport_session.rb +812 -0
  84. data/lib/quicsilver/server/web_transport_stream.rb +326 -0
  85. data/lib/quicsilver/transport/configuration.rb +113 -11
  86. data/lib/quicsilver/transport/connection.rb +376 -132
  87. data/lib/quicsilver/transport/connection_stats.rb +53 -0
  88. data/lib/quicsilver/transport/event_loop.rb +36 -0
  89. data/lib/quicsilver/transport/inbound_stream.rb +21 -1
  90. data/lib/quicsilver/transport/stream.rb +18 -3
  91. data/lib/quicsilver/transport/stream_event.rb +25 -2
  92. data/lib/quicsilver/transport/stream_id.rb +26 -0
  93. data/lib/quicsilver/version.rb +1 -1
  94. data/lib/quicsilver.rb +45 -11
  95. data/lib/rackup/handler/quicsilver.rb +57 -38
  96. data/quicsilver.gemspec +17 -2
  97. metadata +120 -10
  98. data/benchmarks/concurrent.rb +0 -110
  99. data/benchmarks/quicsilver_server.rb +0 -46
  100. data/benchmarks/rails.rb +0 -170
  101. data/benchmarks/throughput.rb +0 -113
  102. data/examples/setup_certs.sh +0 -57
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 1: graceful drain as an HTTP/3 lifecycle control loop.
5
+ #
6
+ # bundle exec ruby examples/lifecycle_drain.rb
7
+ #
8
+ # This keeps the policy deliberately small and in one file:
9
+ #
10
+ # Quicsilver::Server#stats -> LifecycleSignal -> readiness / drain / safe-to-exit
11
+ #
12
+ # A real quicsilver-health/quicsilver-lifecycle package could extract the
13
+ # LifecycleSignal, but the core idea should stay this boring:
14
+ #
15
+ # 1. fail readiness
16
+ # 2. send HTTP/3 GOAWAY via Server#shutdown
17
+ # 3. refuse/avoid new work
18
+ # 4. finish in-flight streams
19
+ # 5. exit when active work reaches zero or the deadline expires
20
+
21
+ require_relative "example_helper"
22
+
23
+ $stdout.sync = true
24
+
25
+ HOST = ENV.fetch("HOST", "127.0.0.1")
26
+ PORT = Integer(ENV.fetch("PORT", "0"))
27
+ SLOW_REQUESTS = Integer(ENV.fetch("SLOW_REQUESTS", "3"))
28
+ SLOW_MS = Integer(ENV.fetch("SLOW_MS", "2_000"))
29
+ SHUTDOWN_TIMEOUT = Integer(ENV.fetch("SHUTDOWN_TIMEOUT", "10"))
30
+
31
+ class LifecycleSignal
32
+ def self.for(server)
33
+ new(server&.stats || {})
34
+ end
35
+
36
+ def initialize(stats)
37
+ @stats = stats || {}
38
+ end
39
+
40
+ def ready?
41
+ running? && !draining? && !queue_full?
42
+ end
43
+
44
+ def accepting_requests?
45
+ ready?
46
+ end
47
+
48
+ def safe_to_exit?
49
+ draining? && active_requests.zero? && pending_requests.zero?
50
+ end
51
+
52
+ def state
53
+ if !running?
54
+ "stopped"
55
+ elsif draining?
56
+ safe_to_exit? ? "drained" : "draining"
57
+ elsif queue_full?
58
+ "overloaded"
59
+ else
60
+ "ready"
61
+ end
62
+ end
63
+
64
+ def to_h
65
+ {
66
+ "state" => state,
67
+ "ready" => ready?,
68
+ "accepting_requests" => accepting_requests?,
69
+ "safe_to_exit" => safe_to_exit?,
70
+ "active_connections" => active_connections,
71
+ "active_requests" => active_requests,
72
+ "pending_requests" => pending_requests,
73
+ "queue_max" => @stats.dig("scheduler", "max_queue_size"),
74
+ "goaway_or_drain" => draining?
75
+ }
76
+ end
77
+
78
+ private
79
+ def running?
80
+ @stats["running"] == true
81
+ end
82
+
83
+ def draining?
84
+ @stats["shutting_down"] == true
85
+ end
86
+
87
+ def queue_full?
88
+ @stats.dig("scheduler", "full") == true
89
+ end
90
+
91
+ def active_connections
92
+ @stats.dig("connections", "active").to_i
93
+ end
94
+
95
+ def active_requests
96
+ @stats.dig("requests", "active").to_i
97
+ end
98
+
99
+ def pending_requests
100
+ if running? || draining?
101
+ @stats.dig("scheduler", "pending").to_i
102
+ else
103
+ 0
104
+ end
105
+ end
106
+ end
107
+
108
+ def banner(title)
109
+ Example.heading(title, level: 1)
110
+ end
111
+
112
+ def section(number, title)
113
+ Example.heading("#{number}. #{title}")
114
+ end
115
+
116
+ def yes_no(value)
117
+ value ? "yes" : "no"
118
+ end
119
+
120
+ def lifecycle_header
121
+ Example.say
122
+ Example.say format(
123
+ " %-22s %-10s %-5s %-7s %-5s %5s %6s %5s",
124
+ "sample", "state", "ready", "accept", "safe", "conns", "active", "queue"
125
+ )
126
+ Example.say " #{'-' * 75}"
127
+ end
128
+
129
+ def lifecycle_row(label, server)
130
+ data = LifecycleSignal.for(server).to_h
131
+
132
+ Example.say format(
133
+ " %-22s %-10s %-5s %-7s %-5s %5d %6d %5d",
134
+ label,
135
+ data["state"],
136
+ yes_no(data["ready"]),
137
+ yes_no(data["accepting_requests"]),
138
+ yes_no(data["safe_to_exit"]),
139
+ data["active_connections"],
140
+ data["active_requests"],
141
+ data["pending_requests"]
142
+ )
143
+ end
144
+
145
+ def elapsed_label(started_at)
146
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
147
+ "t+#{format('%.1f', elapsed)}s"
148
+ end
149
+
150
+ def attempt_new_request(client)
151
+ response = client.get("/hello", timeout: 2)
152
+ "#{response.status} #{response.body.inspect}"
153
+ rescue => error
154
+ "#{error.class}: #{error.message}"
155
+ end
156
+
157
+ Quicsilver.logger = Logger.new(File::NULL)
158
+
159
+ port = PORT.zero? ? Example.available_udp_port(HOST) : PORT
160
+ server = nil
161
+
162
+ app = ->(env) {
163
+ signal = LifecycleSignal.for(server)
164
+
165
+ case env["PATH_INFO"]
166
+ when "/_quicsilver/ready"
167
+ Example.json_response(signal.ready? ? 200 : 503, signal.to_h)
168
+ when "/_quicsilver/lifecycle"
169
+ Example.json_response(200, signal.to_h)
170
+ when %r{\A/slow/(\d+)}
171
+ request_id = Regexp.last_match(1)
172
+ sleep SLOW_MS / 1000.0
173
+ Example.text_response(200, "slow request #{request_id} completed while #{signal.state}\n")
174
+ when "/hello"
175
+ if signal.accepting_requests?
176
+ Example.text_response(200, "hello before drain\n")
177
+ else
178
+ Example.json_response(503, "error" => "server draining", "lifecycle" => signal.to_h)
179
+ end
180
+ else
181
+ Example.text_response(404, "not found\n")
182
+ end
183
+ }
184
+
185
+ server = Quicsilver::Server.new(
186
+ port,
187
+ app: app,
188
+ server_configuration: EXAMPLE_TLS_CONFIG,
189
+ threads: SLOW_REQUESTS + 2
190
+ )
191
+ server_thread = Thread.new { server.start }
192
+ client = Quicsilver::Client.new(HOST, port, unsecure: true)
193
+ shutdown_thread = nil
194
+ monitor = nil
195
+ slow_threads = []
196
+ monitor_done = false
197
+
198
+ begin
199
+ Example.wait_until { server.running? }
200
+
201
+ banner "HTTP/3 Lifecycle Drain Demo"
202
+ Example.detail "server", "https://#{HOST}:#{port}"
203
+ Example.detail "slow streams", "#{SLOW_REQUESTS} × #{SLOW_MS}ms"
204
+ Example.detail "shutdown timeout", "#{SHUTDOWN_TIMEOUT}s"
205
+ Example.detail "control loop", "readiness=false -> GOAWAY/drain -> active=0 -> safe exit"
206
+
207
+ section 1, "Warm up the connection"
208
+ Example.detail "GET /hello", attempt_new_request(client)
209
+ lifecycle_header
210
+ lifecycle_row "before drain", server
211
+
212
+ section 2, "Start long-running streams"
213
+ slow_threads = SLOW_REQUESTS.times.map do |index|
214
+ Thread.new do
215
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
216
+ response = client.get("/slow/#{index + 1}", timeout: SHUTDOWN_TIMEOUT + 5)
217
+ elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(1)
218
+ Example.say format(" stream #%-2d completed status=%d elapsed=%7.1fms", index + 1, response.status, elapsed_ms)
219
+ rescue => error
220
+ Example.say format(" stream #%-2d failed %s: %s", index + 1, error.class, error.message)
221
+ end
222
+ end
223
+
224
+ Example.wait_until { server.stats.dig("requests", "active") >= SLOW_REQUESTS }
225
+ lifecycle_header
226
+ lifecycle_row "streams in flight", server
227
+
228
+ section 3, "Begin drain"
229
+ Example.detail "action", "Server#shutdown(timeout: #{SHUTDOWN_TIMEOUT})"
230
+ shutdown_thread = Thread.new { server.shutdown(timeout: SHUTDOWN_TIMEOUT) }
231
+ Example.wait_until { server.shutting_down }
232
+ sleep 0.05 # give GOAWAY/readiness state a moment to propagate, but keep streams in flight
233
+
234
+ Example.detail "readiness", "not ready"
235
+ Example.detail "routing", "new work should move to other endpoints"
236
+ Example.detail "in-flight", "existing streams continue until complete"
237
+ lifecycle_header
238
+ lifecycle_row "after GOAWAY", server
239
+
240
+ monitor_started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
241
+ monitor = Thread.new do
242
+ until monitor_done
243
+ lifecycle_row elapsed_label(monitor_started_at), server
244
+ sleep 0.5
245
+ end
246
+ end
247
+
248
+ slow_threads.each(&:join)
249
+ shutdown_thread.join
250
+ monitor_done = true
251
+ monitor.join
252
+
253
+ section 4, "Shutdown complete"
254
+ lifecycle_header
255
+ lifecycle_row "after shutdown", server
256
+ Example.detail "takeaway", "routing changes immediately; in-flight HTTP/3 streams drain cleanly"
257
+ ensure
258
+ monitor_done = true
259
+ monitor&.join(1)
260
+ client&.disconnect rescue nil
261
+ Quicsilver::Client.close_pool rescue nil
262
+ server&.stop if server&.running? rescue nil
263
+ server_thread&.join(2)
264
+ end
@@ -1,26 +1,14 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "quicsilver"
3
+ require_relative "example_helper"
5
4
 
6
5
  puts "🚀 Minimal HTTP/3 Server Example"
7
6
  puts "=" * 40
8
7
 
9
- # Create and start the server
10
- server = Quicsilver::Server.new(4433)
8
+ server = Quicsilver::Server.new(4433, server_configuration: EXAMPLE_TLS_CONFIG)
11
9
 
12
- puts "🔧 Starting server..."
13
- server.start
14
-
15
- puts "✅ Server is running on port 4433"
16
- puts "📋 Server info: #{server.server_info}"
10
+ puts "✅ Listening on https://localhost:4433"
11
+ puts "⏳ Press Ctrl+C to stop."
12
+ puts
17
13
 
18
- # Keep the server running
19
- puts "⏳ Server is running. Press Ctrl+C to stop..."
20
- begin
21
- server.wait_for_connections
22
- rescue Interrupt
23
- puts "\n🛑 Stopping server..."
24
- server.stop
25
- puts "👋 Server stopped"
26
- end
14
+ server.start
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # HTTP/3 Extensible Priorities (RFC 9218).
4
+ #
5
+ # Browsers send priority hints: CSS is urgency 0 (highest),
6
+ # images are urgency 5 (low). Quicsilver parses these and tells
7
+ # MsQuic to send high-priority data first.
8
+ #
9
+ # ruby examples/priorities.rb
10
+ #
11
+ # Then from another terminal, the client fires CSS and image
12
+ # requests and shows the priority was parsed.
13
+
14
+ require_relative "example_helper"
15
+
16
+ PORT = 4433
17
+ HOST = "localhost"
18
+
19
+ app = ->(env) {
20
+ path = env["PATH_INFO"]
21
+
22
+ case path
23
+ when "/style.css"
24
+ # Browsers send: priority: u=0 (highest urgency)
25
+ [200, { "content-type" => "text/css" }, ["body { margin: 0; }\n" * 50]]
26
+ when "/image.png"
27
+ # Browsers send: priority: u=5 (low urgency)
28
+ [200, { "content-type" => "image/png" }, ["x" * 10_000]]
29
+ else
30
+ [200, { "content-type" => "text/html" }, [
31
+ "<link rel='stylesheet' href='/style.css'>\n<img src='/image.png'>\n"
32
+ ]]
33
+ end
34
+ }
35
+
36
+ server = Quicsilver::Server.new(PORT, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
37
+ server_thread = Thread.new { server.start }
38
+ sleep 0.3
39
+
40
+ puts "🎯 HTTP/3 Priorities Demo"
41
+ puts "=" * 50
42
+
43
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
44
+
45
+ # Request with priority header (what a browser would send)
46
+ puts "\n CSS request (high priority):"
47
+ response = client.get("/style.css", headers: { "priority" => "u=0, i" })
48
+ puts " Status: #{response.status}, Size: #{response.body.bytesize} bytes"
49
+
50
+ puts "\n Image request (low priority):"
51
+ response = client.get("/image.png", headers: { "priority" => "u=5" })
52
+ puts " Status: #{response.status}, Size: #{response.body.bytesize} bytes"
53
+
54
+ puts "\n MsQuic schedules CSS data before image data"
55
+ puts " when both are in flight on the same connection."
56
+
57
+ client.disconnect
58
+ server.stop
59
+ server_thread.join(2)
60
+ puts "\n✅ Done"
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example: Running a Rack app with protocol-http as the internal layer
5
+ #
6
+ # quicsilver uses protocol-http Request/Response internally.
7
+ # Rack apps are automatically wrapped — no code changes needed.
8
+ #
9
+ # Modes:
10
+ # :rack (default) — Rack app, wrapped with Protocol::Rack::Adapter
11
+ # :falcon — native protocol-http app, no wrapping
12
+
13
+ require_relative "example_helper"
14
+
15
+ # Any standard Rack app works
16
+ app = ->(env) {
17
+ puts "#{env['REQUEST_METHOD']} #{env['PATH_INFO']} #{env['SERVER_PROTOCOL']}"
18
+
19
+ body = "Hello from Quicsilver!\n" \
20
+ "Method: #{env['REQUEST_METHOD']}\n" \
21
+ "Path: #{env['PATH_INFO']}\n" \
22
+ "Protocol: #{env['SERVER_PROTOCOL']}\n"
23
+
24
+ [200, { "content-type" => "text/plain" }, [body]]
25
+ }
26
+
27
+ server = Quicsilver::Server.new(4433, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
28
+
29
+ puts "Starting Quicsilver on https://localhost:4433"
30
+ puts "Test with: curl --http3 -k https://localhost:4433/"
31
+ server.start
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "quicsilver"
3
+ require_relative "example_helper"
5
4
 
6
5
  puts "🚀 Rack HTTP/3 Server Example"
7
6
  puts "=" * 40
@@ -31,24 +30,13 @@ app = ->(env) {
31
30
  end
32
31
  }
33
32
 
34
- # Create and start the server with the Rack app
35
- server = Quicsilver::Server.new(4433, app: app)
33
+ server = Quicsilver::Server.new(4433, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
36
34
 
37
- puts "🔧 Starting server..."
38
- server.start
39
-
40
- puts "✅ Server is running on port 4433"
41
35
  puts "📋 Try these requests:"
42
- puts " curl --http3 -k https://127.0.0.1:4433/"
43
- puts " curl --http3 -k https://127.0.0.1:4433/api/users"
44
- puts " curl --http3 -k https://127.0.0.1:4433/api/status"
36
+ puts " curl --http3 -k https://localhost:4433/"
37
+ puts " curl --http3 -k https://localhost:4433/api/users"
38
+ puts " curl --http3 -k https://localhost:4433/api/status"
39
+ puts "⏳ Press Ctrl+C to stop."
40
+ puts
45
41
 
46
- # Keep the server running
47
- puts "⏳ Server is running. Press Ctrl+C to stop..."
48
- begin
49
- server.wait_for_connections
50
- rescue Interrupt
51
- puts "\n🛑 Stopping server..."
52
- server.stop
53
- puts "👋 Server stopped"
54
- end
42
+ server.start