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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +9 -1
- data/.github/workflows/cibuildgem.yaml +117 -0
- data/.gitignore +8 -1
- data/CHANGELOG.md +82 -0
- data/Gemfile.lock +27 -2
- data/README.md +205 -29
- data/Rakefile +75 -3
- data/benchmarks/baseline.rb +175 -0
- data/benchmarks/compare.rb +398 -0
- data/benchmarks/helpers.rb +138 -64
- data/benchmarks/latency.rb +128 -0
- data/benchmarks/loss.rb +193 -0
- data/benchmarks/mobile.rb +125 -0
- data/docs/connection_identity.md +141 -0
- data/examples/README.md +44 -91
- data/examples/benchmark.rb +191 -0
- data/examples/connection_pool_demo.rb +47 -0
- data/examples/example_helper.rb +69 -0
- data/examples/falcon_middleware.rb +44 -0
- data/examples/feature_demo.rb +125 -0
- data/examples/grpc_style.rb +97 -0
- data/examples/latency_aware_routing.rb +382 -0
- data/examples/lifecycle_drain.rb +264 -0
- data/examples/minimal_http3_server.rb +6 -18
- data/examples/priorities.rb +60 -0
- data/examples/protocol_http_server.rb +31 -0
- data/examples/rack_http3_server.rb +8 -20
- data/examples/rails_feature_test.rb +280 -0
- data/examples/server_stats.rb +117 -0
- data/examples/simple_client_test.rb +5 -5
- data/examples/streaming_sse.rb +33 -0
- data/examples/trailers.rb +69 -0
- data/examples/webtransport_smoke.rb +546 -0
- data/examples/worker_load_balancing.rb +425 -0
- data/ext/quicsilver/extconf.rb +111 -12
- data/ext/quicsilver/include/msquic.h +2226 -0
- data/ext/quicsilver/include/msquic_posix.h +696 -0
- data/ext/quicsilver/include/quic_sal_stub.h +314 -0
- data/ext/quicsilver/msquic_patch.rb +68 -0
- data/ext/quicsilver/patches/reliable-reset.patch +723 -0
- data/ext/quicsilver/patches/stream-final-size.patch +47 -0
- data/ext/quicsilver/quicsilver.c +1646 -189
- data/lib/quicsilver/client/client.rb +546 -88
- data/lib/quicsilver/client/connection_pool.rb +202 -0
- data/lib/quicsilver/client/request.rb +106 -14
- data/lib/quicsilver/errors.rb +39 -0
- data/lib/quicsilver/libmsquic.2.dylib +0 -0
- data/lib/quicsilver/protocol/adapter.rb +196 -0
- data/lib/quicsilver/protocol/capsule.rb +38 -0
- data/lib/quicsilver/protocol/connect_stream_decoder.rb +75 -0
- data/lib/quicsilver/protocol/control_stream_parser.rb +117 -0
- data/lib/quicsilver/protocol/datagram.rb +40 -0
- data/lib/quicsilver/protocol/frame_parser.rb +141 -0
- data/lib/quicsilver/protocol/frame_reader.rb +84 -0
- data/lib/quicsilver/protocol/frames.rb +137 -10
- data/lib/quicsilver/protocol/priority.rb +65 -0
- data/lib/quicsilver/protocol/qpack/decoder.rb +4 -42
- data/lib/quicsilver/protocol/qpack/encoder.rb +49 -21
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +16 -1
- data/lib/quicsilver/protocol/qpack/huffman.rb +4 -4
- data/lib/quicsilver/protocol/request_encoder.rb +6 -3
- data/lib/quicsilver/protocol/request_parser.rb +43 -141
- data/lib/quicsilver/protocol/response_encoder.rb +40 -5
- data/lib/quicsilver/protocol/response_parser.rb +26 -128
- data/lib/quicsilver/protocol/stream_input.rb +98 -0
- data/lib/quicsilver/protocol/stream_output.rb +59 -0
- data/lib/quicsilver/protocol/unidirectional_stream.rb +12 -0
- data/lib/quicsilver/protocol/web_transport.rb +214 -0
- data/lib/quicsilver/rack/context.rb +33 -0
- data/lib/quicsilver/response.rb +40 -0
- data/lib/quicsilver/server/rack_adapter.rb +77 -0
- data/lib/quicsilver/server/receive_queue.rb +106 -0
- data/lib/quicsilver/server/request_handler.rb +111 -48
- data/lib/quicsilver/server/request_registry.rb +18 -5
- data/lib/quicsilver/server/scheduler.rb +50 -0
- data/lib/quicsilver/server/schedulers/fiber_scheduler.rb +79 -0
- data/lib/quicsilver/server/schedulers/thread_scheduler.rb +76 -0
- data/lib/quicsilver/server/server.rb +848 -104
- data/lib/quicsilver/server/web_transport_flow_control.rb +270 -0
- data/lib/quicsilver/server/web_transport_manager.rb +281 -0
- data/lib/quicsilver/server/web_transport_registry.rb +40 -0
- data/lib/quicsilver/server/web_transport_session.rb +812 -0
- data/lib/quicsilver/server/web_transport_stream.rb +326 -0
- data/lib/quicsilver/transport/configuration.rb +113 -11
- data/lib/quicsilver/transport/connection.rb +376 -132
- data/lib/quicsilver/transport/connection_stats.rb +53 -0
- data/lib/quicsilver/transport/event_loop.rb +36 -0
- data/lib/quicsilver/transport/inbound_stream.rb +21 -1
- data/lib/quicsilver/transport/stream.rb +18 -3
- data/lib/quicsilver/transport/stream_event.rb +25 -2
- data/lib/quicsilver/transport/stream_id.rb +26 -0
- data/lib/quicsilver/version.rb +1 -1
- data/lib/quicsilver.rb +45 -11
- data/lib/rackup/handler/quicsilver.rb +57 -38
- data/quicsilver.gemspec +17 -2
- metadata +120 -10
- data/benchmarks/concurrent.rb +0 -110
- data/benchmarks/quicsilver_server.rb +0 -46
- data/benchmarks/rails.rb +0 -170
- data/benchmarks/throughput.rb +0 -113
- data/examples/setup_certs.sh +0 -57
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Tests all major quicsilver features against a running Rails app.
|
|
4
|
+
#
|
|
5
|
+
# Prerequisites:
|
|
6
|
+
# 1. Start the Rails app: cd blogz && bundle exec rackup -s quicsilver -p 4433
|
|
7
|
+
# 2. Run this script: cd quicsilver && bundle exec ruby examples/rails_feature_test.rb
|
|
8
|
+
#
|
|
9
|
+
# Tests: ping, streaming, priorities, echo, HEAD, multiplexing, connection pooling
|
|
10
|
+
|
|
11
|
+
require "bundler/setup"
|
|
12
|
+
require "quicsilver"
|
|
13
|
+
|
|
14
|
+
HOST = "localhost"
|
|
15
|
+
PORT = 4433
|
|
16
|
+
PASS = "✅"
|
|
17
|
+
FAIL = "❌"
|
|
18
|
+
|
|
19
|
+
def test(name)
|
|
20
|
+
result = yield
|
|
21
|
+
puts " #{PASS} #{name}"
|
|
22
|
+
result
|
|
23
|
+
rescue => e
|
|
24
|
+
puts " #{FAIL} #{name}: #{e.message}"
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
puts "Quicsilver Rails Feature Test"
|
|
29
|
+
puts " Target: https://#{HOST}:#{PORT}"
|
|
30
|
+
puts "=" * 60
|
|
31
|
+
|
|
32
|
+
# === 1. Basic connectivity ===
|
|
33
|
+
puts "\n1. Basic Connectivity"
|
|
34
|
+
puts "-" * 60
|
|
35
|
+
|
|
36
|
+
test("GET /h3/ping returns JSON over HTTP/3") do
|
|
37
|
+
response = Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true)
|
|
38
|
+
raise "Expected 200, got #{response.status}" unless response.status == 200
|
|
39
|
+
raise "Expected JSON" unless response.body.include?('"status":"ok"')
|
|
40
|
+
raise "Expected HTTP/3" unless response.body.include?("HTTP/3")
|
|
41
|
+
puts " Response: #{response.body}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# === 2. Connection Pooling ===
|
|
45
|
+
puts "\n2. Connection Pooling"
|
|
46
|
+
puts "-" * 60
|
|
47
|
+
|
|
48
|
+
test("5 requests reuse one connection (no repeat handshakes)") do
|
|
49
|
+
Quicsilver::Client.close_pool
|
|
50
|
+
times = 5.times.map do |i|
|
|
51
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
52
|
+
Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true)
|
|
53
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
|
|
54
|
+
label = i == 0 ? "new connection + handshake" : "reused"
|
|
55
|
+
puts " Request #{i}: #{elapsed}ms (#{label})"
|
|
56
|
+
elapsed
|
|
57
|
+
end
|
|
58
|
+
puts " Pool: #{Quicsilver::Client.pool.size} connection(s)"
|
|
59
|
+
raise "Pool should have 1 connection" unless Quicsilver::Client.pool.size == 1
|
|
60
|
+
# First request includes handshake, subsequent should be faster on average
|
|
61
|
+
avg_reused = times[1..].sum / times[1..].size
|
|
62
|
+
puts " First request: #{times[0]}ms, avg reused: #{avg_reused.round(1)}ms"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# === 3. Streaming ===
|
|
66
|
+
puts "\n3. Streaming Response"
|
|
67
|
+
puts "-" * 60
|
|
68
|
+
|
|
69
|
+
test("GET /h3/stream returns chunked SSE data") do
|
|
70
|
+
response = Quicsilver::Client.get(HOST, PORT, "/h3/stream", unsecure: true)
|
|
71
|
+
raise "Expected 200" unless response.status == 200
|
|
72
|
+
chunks = response.body.scan(/data: chunk \d+/)
|
|
73
|
+
raise "Expected 5 chunks, got #{chunks.size}" unless chunks.size == 5
|
|
74
|
+
puts " Received #{chunks.size} streamed chunks"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# === 4. POST with Body (Echo) ===
|
|
78
|
+
puts "\n4. POST with Body"
|
|
79
|
+
puts "-" * 60
|
|
80
|
+
|
|
81
|
+
test("POST /h3/echo returns the request body") do
|
|
82
|
+
body = '{"message": "Hello HTTP/3!"}'
|
|
83
|
+
response = Quicsilver::Client.post(HOST, PORT, "/h3/echo",
|
|
84
|
+
body: body,
|
|
85
|
+
headers: { "content-type" => "application/json" },
|
|
86
|
+
unsecure: true)
|
|
87
|
+
raise "Expected 200" unless response.status == 200
|
|
88
|
+
raise "Body not echoed" unless response.body.include?("Hello HTTP/3!")
|
|
89
|
+
puts " Echoed: #{response.body[0..80]}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# === 5. HEAD Request ===
|
|
93
|
+
puts "\n5. HEAD Request"
|
|
94
|
+
puts "-" * 60
|
|
95
|
+
|
|
96
|
+
test("HEAD /h3/head returns headers but no body") do
|
|
97
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
98
|
+
response = client.head("/h3/head")
|
|
99
|
+
raise "Expected 200" unless response.status == 200
|
|
100
|
+
raise "HEAD should have empty body" unless response.body.nil? || response.body.empty?
|
|
101
|
+
puts " Status: #{response.status}, body size: #{response.body&.bytesize || 0}"
|
|
102
|
+
client.disconnect
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# === 6. True Multiplexing (concurrent streams, single connection) ===
|
|
106
|
+
puts "\n6. True Multiplexing (concurrent streams, single connection)"
|
|
107
|
+
puts "-" * 60
|
|
108
|
+
|
|
109
|
+
test("10 concurrent requests on one connection") do
|
|
110
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
111
|
+
client.get("/h3/ping") # warmup + handshake
|
|
112
|
+
|
|
113
|
+
t_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
114
|
+
requests = 10.times.map { client.build_request("GET", "/h3/ping") }
|
|
115
|
+
responses = requests.map { |r| r.response(timeout: 10) }
|
|
116
|
+
total = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t_start) * 1000).round(1)
|
|
117
|
+
|
|
118
|
+
ok = responses.count { |r| r.status == 200 }
|
|
119
|
+
rps = (10 / (total / 1000.0)).round(0)
|
|
120
|
+
puts " 10 concurrent requests in #{total}ms (#{ok}/10 ok, #{rps} req/s)"
|
|
121
|
+
raise "Not all 200" unless ok == 10
|
|
122
|
+
client.disconnect
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# === 7. Priority Endpoints (CSS vs Image) ===
|
|
126
|
+
puts "\n7. Priority Endpoints"
|
|
127
|
+
puts "-" * 60
|
|
128
|
+
|
|
129
|
+
test("GET /h3/css returns CSS content") do
|
|
130
|
+
response = Quicsilver::Client.get(HOST, PORT, "/h3/css", unsecure: true)
|
|
131
|
+
raise "Expected 200" unless response.status == 200
|
|
132
|
+
puts " CSS: #{response.body.bytesize} bytes"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
test("GET /h3/image returns large binary payload") do
|
|
136
|
+
response = Quicsilver::Client.get(HOST, PORT, "/h3/image", unsecure: true)
|
|
137
|
+
raise "Expected 200" unless response.status == 200
|
|
138
|
+
raise "Expected 50KB" unless response.body.bytesize == 50_000
|
|
139
|
+
puts " Image: #{response.body.bytesize} bytes"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# === 8. Slow Response (Delay) ===
|
|
143
|
+
puts "\n8. Slow Response"
|
|
144
|
+
puts "-" * 60
|
|
145
|
+
|
|
146
|
+
test("GET /h3/slow?delay=0.2 respects delay") do
|
|
147
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
148
|
+
response = Quicsilver::Client.get(HOST, PORT, "/h3/slow?delay=0.2", unsecure: true)
|
|
149
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
|
|
150
|
+
raise "Expected 200" unless response.status == 200
|
|
151
|
+
raise "Should take ~200ms, took #{elapsed}ms" unless elapsed > 150
|
|
152
|
+
puts " Delayed response in #{elapsed}ms"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# === 9. Concurrent Connections ===
|
|
156
|
+
puts "\n9. Concurrent Connections (4 threads, 4 connections)"
|
|
157
|
+
puts "-" * 60
|
|
158
|
+
|
|
159
|
+
test("4 concurrent requests on separate connections complete in parallel") do
|
|
160
|
+
Quicsilver::Client.close_pool
|
|
161
|
+
threads = 4.times.map do |i|
|
|
162
|
+
Thread.new do
|
|
163
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
164
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
165
|
+
response = client.get("/h3/slow?delay=0.2")
|
|
166
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
|
|
167
|
+
client.disconnect
|
|
168
|
+
[i, response.status, elapsed]
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
results = threads.map(&:value).sort_by(&:first)
|
|
172
|
+
results.each { |i, status, elapsed| puts " Connection #{i}: #{status} -- #{elapsed}ms" }
|
|
173
|
+
total = results.map { |_, _, e| e }.max
|
|
174
|
+
sequential = 200 * 4
|
|
175
|
+
puts " Total: #{total}ms (sequential would be ~#{sequential}ms)"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# === 10. Large Upload ===
|
|
179
|
+
puts "\n10. Large Upload"
|
|
180
|
+
puts "-" * 60
|
|
181
|
+
|
|
182
|
+
test("POST 100KB body") do
|
|
183
|
+
big_body = "x" * 100_000
|
|
184
|
+
response = Quicsilver::Client.post(HOST, PORT, "/h3/upload",
|
|
185
|
+
body: big_body,
|
|
186
|
+
headers: { "content-type" => "application/octet-stream" },
|
|
187
|
+
unsecure: true)
|
|
188
|
+
raise "Expected 200" unless response.status == 200
|
|
189
|
+
raise "Size mismatch" unless response.body.include?('"received_bytes":100000')
|
|
190
|
+
puts " Uploaded 100KB, server received 100,000 bytes"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# === 11. Content-Length Validation ===
|
|
194
|
+
puts "\n11. Content-Length Validation"
|
|
195
|
+
puts "-" * 60
|
|
196
|
+
|
|
197
|
+
test("POST with content-length header") do
|
|
198
|
+
body = "hello"
|
|
199
|
+
response = Quicsilver::Client.post(HOST, PORT, "/h3/upload",
|
|
200
|
+
body: body,
|
|
201
|
+
headers: { "content-type" => "text/plain" },
|
|
202
|
+
unsecure: true)
|
|
203
|
+
raise "Expected 200, got #{response.status}" unless response.status == 200
|
|
204
|
+
raise "Size mismatch" unless response.body.include?('"received_bytes":5')
|
|
205
|
+
puts " Sent 5 bytes, server received 5 bytes"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# === 12. Handshake Cost (connection pooling benefit) ===
|
|
209
|
+
puts "\n12. Handshake Cost"
|
|
210
|
+
puts "-" * 60
|
|
211
|
+
|
|
212
|
+
test("Fresh connections pay handshake cost -- pooling avoids this") do
|
|
213
|
+
Quicsilver::Client.close_pool
|
|
214
|
+
|
|
215
|
+
# First connection -- full handshake
|
|
216
|
+
client1 = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
217
|
+
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
218
|
+
client1.get("/h3/ping")
|
|
219
|
+
first = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t1) * 1000).round(1)
|
|
220
|
+
client1.disconnect
|
|
221
|
+
|
|
222
|
+
# Second connection -- another full handshake
|
|
223
|
+
client2 = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
224
|
+
t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
225
|
+
client2.get("/h3/ping")
|
|
226
|
+
second = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t2) * 1000).round(1)
|
|
227
|
+
client2.disconnect
|
|
228
|
+
|
|
229
|
+
# Pooled -- reuses existing connection, no handshake
|
|
230
|
+
Quicsilver::Client.close_pool
|
|
231
|
+
Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true) # establish
|
|
232
|
+
t3 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
233
|
+
Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true)
|
|
234
|
+
pooled = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t3) * 1000).round(1)
|
|
235
|
+
|
|
236
|
+
puts " Fresh connection 1: #{first}ms"
|
|
237
|
+
puts " Fresh connection 2: #{second}ms"
|
|
238
|
+
puts " Pooled (no handshake): #{pooled}ms"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# === 13. Server Health ===
|
|
242
|
+
puts "\n13. Server Health"
|
|
243
|
+
puts "-" * 60
|
|
244
|
+
|
|
245
|
+
test("Server still responding after all tests") do
|
|
246
|
+
response = Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true)
|
|
247
|
+
raise "Expected 200" unless response.status == 200
|
|
248
|
+
puts " Server healthy"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# === 14. Rails CRUD ===
|
|
252
|
+
puts "\n14. Rails CRUD"
|
|
253
|
+
puts "-" * 60
|
|
254
|
+
|
|
255
|
+
test("GET /posts.json returns posts array") do
|
|
256
|
+
response = Quicsilver::Client.get(HOST, PORT, "/posts.json", unsecure: true)
|
|
257
|
+
raise "Expected 200" unless response.status == 200
|
|
258
|
+
puts " Posts: #{response.body[0..40]}"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# === 15. HTTP Methods ===
|
|
262
|
+
puts "\n15. HTTP Methods"
|
|
263
|
+
puts "-" * 60
|
|
264
|
+
|
|
265
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
266
|
+
%i[get post put patch delete head].each do |method|
|
|
267
|
+
test("#{method.to_s.upcase} /h3/ping") do
|
|
268
|
+
response = client.public_send(method, "/h3/ping")
|
|
269
|
+
body_size = response.body&.bytesize || 0
|
|
270
|
+
puts " #{response.status} (#{body_size} bytes)"
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
client.disconnect
|
|
274
|
+
|
|
275
|
+
# === Summary ===
|
|
276
|
+
puts "\n" + "=" * 60
|
|
277
|
+
puts "Feature test complete!"
|
|
278
|
+
puts " Pool: #{Quicsilver::Client.pool.size} connection(s)"
|
|
279
|
+
Quicsilver::Client.close_pool
|
|
280
|
+
puts "Done"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Demonstrates Quicsilver::Server#stats.
|
|
5
|
+
#
|
|
6
|
+
# ruby examples/server_stats.rb
|
|
7
|
+
# PORT=4444 ruby examples/server_stats.rb
|
|
8
|
+
#
|
|
9
|
+
# The stats snapshot combines:
|
|
10
|
+
# - Quicsilver app-server state: running, connections, active requests, queue.
|
|
11
|
+
# - QUIC transport counters: process-wide connection/stream/packet/worker state.
|
|
12
|
+
|
|
13
|
+
require_relative "example_helper"
|
|
14
|
+
require "json"
|
|
15
|
+
|
|
16
|
+
HOST = ENV.fetch("HOST", "localhost")
|
|
17
|
+
PORT = Integer(ENV.fetch("PORT", "4433"))
|
|
18
|
+
|
|
19
|
+
server = nil
|
|
20
|
+
server_thread = nil
|
|
21
|
+
clients = []
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
app = ->(env) {
|
|
25
|
+
case env["PATH_INFO"]
|
|
26
|
+
when "/hello"
|
|
27
|
+
[200, { "content-type" => "text/plain" }, ["hello\n"]]
|
|
28
|
+
when %r{\A/slow}
|
|
29
|
+
sleep 0.25
|
|
30
|
+
[200, { "content-type" => "text/plain" }, ["slow response\n"]]
|
|
31
|
+
else
|
|
32
|
+
[404, { "content-type" => "text/plain" }, ["not found\n"]]
|
|
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
|
+
|
|
39
|
+
# Keep examples self-contained without requiring test helpers.
|
|
40
|
+
def wait_until(timeout: 3)
|
|
41
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
42
|
+
until yield
|
|
43
|
+
raise "timed out waiting" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
44
|
+
|
|
45
|
+
sleep 0.01
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
wait_until { server.running? }
|
|
50
|
+
|
|
51
|
+
# Print the useful subset first, then include the whole transport hash so it is
|
|
52
|
+
# easy to see every counter name currently exposed by Quicsilver.transport_counters.
|
|
53
|
+
def print_stats(title, server)
|
|
54
|
+
stats = server.stats
|
|
55
|
+
transport = stats["transport"] || {}
|
|
56
|
+
|
|
57
|
+
puts "\n#{title}"
|
|
58
|
+
puts "-" * title.length
|
|
59
|
+
puts JSON.pretty_generate(
|
|
60
|
+
"server" => {
|
|
61
|
+
"running" => stats["running"],
|
|
62
|
+
"shutting_down" => stats["shutting_down"],
|
|
63
|
+
"connections" => stats["connections"],
|
|
64
|
+
"requests" => stats["requests"],
|
|
65
|
+
"scheduler" => stats["scheduler"]
|
|
66
|
+
},
|
|
67
|
+
"transport_summary" => {
|
|
68
|
+
"connections_active" => transport["connections_active"],
|
|
69
|
+
"connections_connected" => transport["connections_connected"],
|
|
70
|
+
"streams_active" => transport["streams_active"],
|
|
71
|
+
"connection_queue_depth" => transport["connection_queue_depth"],
|
|
72
|
+
"connection_operations_queue_depth" => transport["connection_operations_queue_depth"],
|
|
73
|
+
"worker_operations_queue_depth" => transport["worker_operations_queue_depth"],
|
|
74
|
+
"connections_load_rejected" => transport["connections_load_rejected"],
|
|
75
|
+
"packets_dropped" => transport["packets_dropped"],
|
|
76
|
+
"app_bytes_received" => transport["app_bytes_received"],
|
|
77
|
+
"app_bytes_sent" => transport["app_bytes_sent"]
|
|
78
|
+
},
|
|
79
|
+
"transport" => transport
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
puts "Quicsilver Server Stats Demo"
|
|
84
|
+
puts "Listening on https://#{HOST}:#{PORT}"
|
|
85
|
+
puts "Note: transport counters are process-wide, so they include this example's clients too."
|
|
86
|
+
|
|
87
|
+
print_stats("1. Fresh server", server)
|
|
88
|
+
|
|
89
|
+
clients = 2.times.map { Quicsilver::Client.new(HOST, PORT, unsecure: true) }
|
|
90
|
+
clients.each_with_index do |client, index|
|
|
91
|
+
response = client.get("/hello")
|
|
92
|
+
puts "client #{index + 1}: GET /hello -> #{response.status}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
wait_until { server.stats.dig("connections", "active") >= 2 }
|
|
96
|
+
print_stats("2. After two client connections", server)
|
|
97
|
+
|
|
98
|
+
slow_requests = clients.each_with_index.map do |client, index|
|
|
99
|
+
Thread.new do
|
|
100
|
+
response = client.get("/slow/#{index + 1}")
|
|
101
|
+
puts "client #{index + 1}: GET /slow/#{index + 1} -> #{response.status}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
wait_until { server.stats.dig("requests", "active") >= 2 }
|
|
106
|
+
print_stats("3. While slow requests are in flight", server)
|
|
107
|
+
|
|
108
|
+
slow_requests.each(&:join)
|
|
109
|
+
print_stats("4. After slow requests complete", server)
|
|
110
|
+
|
|
111
|
+
puts "\nDone."
|
|
112
|
+
ensure
|
|
113
|
+
clients.each { |client| client.disconnect rescue nil }
|
|
114
|
+
Quicsilver::Client.close_pool rescue nil
|
|
115
|
+
server&.stop rescue nil
|
|
116
|
+
server_thread&.join(2)
|
|
117
|
+
end
|
|
@@ -7,15 +7,15 @@ puts "🔌 Simple HTTP/3 Client Test"
|
|
|
7
7
|
puts "=" * 40
|
|
8
8
|
|
|
9
9
|
begin
|
|
10
|
-
client = Quicsilver::Client.new("
|
|
10
|
+
client = Quicsilver::Client.new("localhost", 4433, unsecure: true)
|
|
11
11
|
|
|
12
12
|
client.connect
|
|
13
13
|
|
|
14
|
-
response = client.get("/
|
|
14
|
+
response = client.get("/")
|
|
15
15
|
|
|
16
|
-
puts "Status: #{response
|
|
17
|
-
puts "Headers: #{response
|
|
18
|
-
puts "Body: #{response
|
|
16
|
+
puts "Status: #{response.status}"
|
|
17
|
+
puts "Headers: #{response.headers.inspect}"
|
|
18
|
+
puts "Body: #{response.body}"
|
|
19
19
|
|
|
20
20
|
rescue => e
|
|
21
21
|
puts "❌ Error: #{e.class} - #{e.message}"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Server-Sent Events over HTTP/3.
|
|
4
|
+
#
|
|
5
|
+
# Demonstrates streaming responses — data arrives chunk by chunk,
|
|
6
|
+
# not buffered. Each chunk is sent as a separate HTTP/3 DATA frame.
|
|
7
|
+
#
|
|
8
|
+
# ruby examples/streaming_sse.rb
|
|
9
|
+
# curl --http3-only -k https://localhost:4433/
|
|
10
|
+
|
|
11
|
+
require_relative "example_helper"
|
|
12
|
+
|
|
13
|
+
app = ->(env) {
|
|
14
|
+
body = Enumerator.new do |y|
|
|
15
|
+
10.times do |i|
|
|
16
|
+
y << "data: {\"count\":#{i},\"time\":\"#{Time.now.iso8601}\"}\n\n"
|
|
17
|
+
sleep 0.2
|
|
18
|
+
end
|
|
19
|
+
y << "data: {\"done\":true}\n\n"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
[200, { "content-type" => "text/event-stream", "cache-control" => "no-cache" }, body]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
server = Quicsilver::Server.new(4433, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
|
|
26
|
+
|
|
27
|
+
puts "📡 Streaming SSE over HTTP/3"
|
|
28
|
+
puts " https://localhost:4433"
|
|
29
|
+
puts " curl --http3-only -k https://localhost:4433/"
|
|
30
|
+
puts " Events arrive every 200ms — no buffering."
|
|
31
|
+
puts
|
|
32
|
+
|
|
33
|
+
server.start
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# HTTP/3 Trailers — send headers after the body.
|
|
4
|
+
#
|
|
5
|
+
# Trailers let the server report status after streaming is complete.
|
|
6
|
+
# Useful for checksums, streaming error status, and gRPC.
|
|
7
|
+
#
|
|
8
|
+
# ruby examples/trailers.rb
|
|
9
|
+
|
|
10
|
+
require_relative "example_helper"
|
|
11
|
+
require "digest"
|
|
12
|
+
|
|
13
|
+
PORT = 4433
|
|
14
|
+
HOST = "localhost"
|
|
15
|
+
|
|
16
|
+
app = ->(env) {
|
|
17
|
+
path = env["PATH_INFO"]
|
|
18
|
+
|
|
19
|
+
case path
|
|
20
|
+
when "/stream-with-checksum"
|
|
21
|
+
body = ["chunk1\n", "chunk2\n", "chunk3\n"]
|
|
22
|
+
checksum = Digest::SHA256.hexdigest(body.join)[0..7]
|
|
23
|
+
env["rack.trailers"] = { "x-checksum" => checksum }
|
|
24
|
+
[200, { "content-type" => "text/plain" }, body]
|
|
25
|
+
|
|
26
|
+
when "/grpc-style"
|
|
27
|
+
env["rack.trailers"] = { "grpc-status" => "0", "grpc-message" => "OK" }
|
|
28
|
+
[200, { "content-type" => "application/grpc" }, ['{"result":"processed"}']]
|
|
29
|
+
|
|
30
|
+
when "/grpc-error"
|
|
31
|
+
env["rack.trailers"] = { "grpc-status" => "13", "grpc-message" => "INTERNAL" }
|
|
32
|
+
[200, { "content-type" => "application/grpc" }, ['{"error":"something broke"}']]
|
|
33
|
+
|
|
34
|
+
else
|
|
35
|
+
[200, { "content-type" => "text/plain" }, ["Try /stream-with-checksum, /grpc-style, or /grpc-error"]]
|
|
36
|
+
end
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
server = Quicsilver::Server.new(PORT, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
|
|
40
|
+
server_thread = Thread.new { server.start }
|
|
41
|
+
sleep 0.3
|
|
42
|
+
|
|
43
|
+
puts "HTTP/3 Trailers Demo"
|
|
44
|
+
puts "=" * 50
|
|
45
|
+
|
|
46
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
47
|
+
|
|
48
|
+
puts "\n Streaming with checksum trailer:"
|
|
49
|
+
response = client.get("/stream-with-checksum")
|
|
50
|
+
puts " Status: #{response.status}"
|
|
51
|
+
puts " Body: #{response.body.inspect}"
|
|
52
|
+
puts " Trailers: #{response.trailers}"
|
|
53
|
+
|
|
54
|
+
puts "\n gRPC-style success:"
|
|
55
|
+
response = client.get("/grpc-style")
|
|
56
|
+
puts " Status: #{response.status}"
|
|
57
|
+
puts " Body: #{response.body}"
|
|
58
|
+
puts " Trailers: #{response.trailers}"
|
|
59
|
+
|
|
60
|
+
puts "\n gRPC-style error:"
|
|
61
|
+
response = client.get("/grpc-error")
|
|
62
|
+
puts " Status: #{response.status}"
|
|
63
|
+
puts " Body: #{response.body}"
|
|
64
|
+
puts " Trailers: #{response.trailers}"
|
|
65
|
+
|
|
66
|
+
client.disconnect
|
|
67
|
+
server.stop
|
|
68
|
+
server_thread.join(2)
|
|
69
|
+
puts "\nDone"
|