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,141 @@
1
+ # Connection identity and QUIC routing IDs
2
+
3
+ ## Rack request identity
4
+
5
+ Rack apps receive these env values:
6
+
7
+ - `env["quicsilver.connection_id"]`
8
+ - Opaque QUIC connection identity.
9
+ - Stable across requests/streams on the same QUIC connection.
10
+ - Currently backed by MsQuic's original destination connection ID.
11
+ - Do not parse this for load-balancing or backend routing decisions.
12
+
13
+ - `env["quicsilver.stream_id"]`
14
+ - QUIC stream id for this HTTP/3 request.
15
+ - Different requests on the same connection have different stream ids.
16
+
17
+ - `env["quicsilver.request_id"]`
18
+ - Recommended app/log/correlation identity.
19
+ - Format without fixed server id:
20
+ - `<connection_id>:<stream_id>`
21
+ - Format with fixed server id:
22
+ - `<transport_server_id>:<connection_id>:<stream_id>`
23
+ - This is the value applications and middleware should prefer.
24
+
25
+ Quicsilver does not expose separate Rack env fields for transport-only routing metadata such as `transport_server_id` or the original destination CID. Use `quicsilver.request_id` for request correlation.
26
+
27
+ ## Fixed MsQuic Server ID
28
+
29
+ `transport_server_id` configures MsQuic's fixed 4-byte Server ID.
30
+
31
+ - Purpose:
32
+ - Allows QUIC-aware load balancers to route using the server id embedded in MsQuic-generated connection IDs.
33
+ - Useful after the server has generated CIDs for an established connection.
34
+
35
+ - Configuration:
36
+
37
+ ```ruby
38
+ config = Quicsilver::Transport::Configuration.new(
39
+ cert_file,
40
+ key_file,
41
+ transport_server_id: "01020304"
42
+ )
43
+ ```
44
+
45
+ - Rules:
46
+ - Must be exactly 4 bytes encoded as 8 hex characters.
47
+ - Hex is normalized to lowercase.
48
+ - Server startup applies this before MsQuic is opened.
49
+ - This is MsQuic process-global state, so set it once per server process/pod.
50
+
51
+ - Common deployment pattern:
52
+
53
+ ```ruby
54
+ config = Quicsilver::Transport::Configuration.new(
55
+ cert_file,
56
+ key_file,
57
+ transport_server_id: ENV["QUICSILVER_TRANSPORT_SERVER_ID"]
58
+ )
59
+ ```
60
+
61
+ ```sh
62
+ QUICSILVER_TRANSPORT_SERVER_ID=01020304 bundle exec ruby server.rb
63
+ ```
64
+
65
+ When configured, Rack request ids include the server id prefix:
66
+
67
+ ```text
68
+ 01020304:7f9a23c001de88b0:4
69
+ ```
70
+
71
+ ## CIBIR ID
72
+
73
+ `cibir_id` configures MsQuic CIBIR: Connection ID Based Implicit Routing.
74
+
75
+ - Purpose:
76
+ - Provides routing bytes in the client's destination connection ID.
77
+ - Useful for routing the first packet/initial connection to the right listener or shard.
78
+ - The meaning of the bytes is application/load-balancer defined.
79
+
80
+ - Server/listener configuration:
81
+
82
+ ```ruby
83
+ config = Quicsilver::Transport::Configuration.new(
84
+ cert_file,
85
+ key_file,
86
+ cibir_id: "0a"
87
+ )
88
+ ```
89
+
90
+ - Client configuration:
91
+
92
+ ```ruby
93
+ client = Quicsilver::Client.new(
94
+ "example.com",
95
+ 4433,
96
+ transport_cibir_id: "0a"
97
+ )
98
+ ```
99
+
100
+ - Rules:
101
+ - Must be 1..6 bytes encoded as an even-length hex string.
102
+ - Hex is normalized to lowercase.
103
+ - MsQuic currently supports CIBIR at offset `0` only.
104
+ - CIBIR must be configured consistently on both ends:
105
+ - Server/listener: `cibir_id: "0a"`
106
+ - Client: `transport_cibir_id: "0a"`
107
+ - If one side uses CIBIR and the other side does not, MsQuic rejects the connection during handshake.
108
+ - If the client sends the wrong CIBIR bytes, packets may not route to the configured listener/backend.
109
+ - CIBIR is transport routing configuration, not Rack request identity.
110
+
111
+ ## How the pieces differ
112
+
113
+ - `cibir_id`
114
+ - Client-selected routing bytes.
115
+ - Used at listener/connection start time.
116
+ - Helps route initial packets before server-generated CIDs matter.
117
+
118
+ - `transport_server_id`
119
+ - Server-selected fixed 4-byte id.
120
+ - Embedded by MsQuic in server-generated CIDs.
121
+ - Helps QUIC-aware load balancers route established connections by backend/server.
122
+
123
+ - `connection_id`
124
+ - App-facing opaque connection identity.
125
+ - Stable across streams on the same QUIC connection.
126
+ - Not guaranteed to be the same CID a load balancer sees on the wire.
127
+
128
+ - `stream_id`
129
+ - Per-request QUIC stream identity.
130
+
131
+ - `request_id`
132
+ - App-facing request correlation value.
133
+ - Combines the stable connection identity and stream id.
134
+ - Includes `transport_server_id` as a prefix when configured.
135
+
136
+ ## Recommended usage
137
+
138
+ - Log `env["quicsilver.request_id"]` for application request correlation.
139
+ - Treat `env["quicsilver.connection_id"]` as opaque.
140
+ - Configure `transport_server_id` when QUIC-aware infrastructure needs backend routing by server-generated CIDs.
141
+ - Configure `cibir_id` / `transport_cibir_id` together when client-chosen destination CID bytes are part of your routing scheme.
data/examples/README.md CHANGED
@@ -1,105 +1,58 @@
1
- # Quicsilver Examples
1
+ # Examples
2
2
 
3
- This directory contains examples for testing your Ruby QUIC implementation.
3
+ Self-contained scripts demonstrating quicsilver features. Each boots its own server — no external setup needed.
4
4
 
5
- ## šŸš€ Quick Start
6
-
7
- ### 1. Generate Certificates
5
+ ## Getting Started
8
6
 
9
7
  ```bash
10
- cd examples
11
- ./setup_certs.sh
12
- ```
13
-
14
- This creates the necessary certificate files in the `certs/` directory.
15
-
16
- ### 2. Run the Server
8
+ # Server
9
+ ruby examples/minimal_http3_server.rb
10
+ curl --http3-only -k https://localhost:4433/
11
+
12
+ # Client
13
+ ruby examples/simple_client_test.rb
14
+ ```
15
+
16
+ ## Examples
17
+
18
+ | Script | Feature |
19
+ |--------|---------|
20
+ | `minimal_http3_server.rb` | Simplest HTTP/3 server |
21
+ | `rack_http3_server.rb` | Rack app with multiple routes |
22
+ | `protocol_http_server.rb` | Protocol-http mode |
23
+ | `simple_client_test.rb` | Basic client request |
24
+ | `connection_pool_demo.rb` | Connection reuse — 6ms first, 0.2ms reused |
25
+ | `feature_demo.rb` | All features in one script |
26
+ | `streaming_sse.rb` | Server-Sent Events over HTTP/3 |
27
+ | `priorities.rb` | Extensible Priorities (RFC 9218) — CSS before images |
28
+ | `trailers.rb` | Trailing headers after body |
29
+ | `grpc_style.rb` | gRPC-style request/response with JSON (no protobuf needed) |
30
+ | `falcon_middleware.rb` | Falcon's middleware stack over HTTP/3 (requires falcon gem) |
31
+ | `benchmark.rb` | Throughput benchmark |
32
+ | `rails_feature_test.rb` | 15 feature tests against a Rails app |
33
+
34
+ ## Rails Integration
17
35
 
18
36
  ```bash
19
- # Terminal 1
20
- ruby examples/test_server.rb
21
- ```
22
-
23
- ### 3. Test the Connection
24
-
25
- ```bash
26
- # Terminal 2
27
- ruby examples/test_connection.rb
28
- ```
29
-
30
- ## šŸ“ Files
31
-
32
- | File | Purpose |
33
- |------|---------|
34
- | `setup_certs.sh` | Generate certificates for testing |
35
- | `test_server.rb` | Ruby QUIC server |
36
- | `test_connection.rb` | QUIC client test |
37
-
38
- ## āœ… Expected Output
39
-
40
- **Server:**
41
- ```
42
- šŸ”„ Quicsilver QUIC Server Test
43
- ════════════════════════════════════════
44
- šŸ“‹ Server Info:
45
- server_id: a47271c7ae4276d5
46
- address: 127.0.0.1
47
- port: 4433
48
- running: false
49
- cert_file: /path/to/certs/server.crt
50
- key_file: /path/to/certs/server.key
51
- max_connections: 100
37
+ # 1. Add to Gemfile
38
+ gem "quicsilver"
52
39
 
53
- šŸš€ Starting QUIC server on 127.0.0.1:4433
54
- āœ… QUIC server started successfully!
55
- šŸ”— Listening for connections...
56
- šŸŽÆ Server is running! Press Ctrl+C to stop
57
- ```
40
+ # 2. Start
41
+ rackup -s quicsilver -p 4433
58
42
 
59
- **Client:**
43
+ # 3. Test
44
+ curl --http3-only -k https://localhost:4433/
60
45
  ```
61
- šŸ”— Testing QUIC Connection to 127.0.0.1:4433...
62
46
 
63
- Connected to 127.0.0.1:4433
64
- āœ… SUCCESS! Connected to QUIC server
65
- šŸ• Connection time: 13.5ms
66
- šŸ“Š Connected status: true
67
- šŸ“‹ Connection info: {"connected" => true, "failed" => false, ...}
68
-
69
- 🧪 Testing connection stability...
70
- ā±ļø Tick 1: Connected = true
71
- ā±ļø Tick 2: Connected = true
72
- ā±ļø Tick 3: Connected = true
73
- āœ… Connection test completed!
74
- šŸ”’ Connection closed cleanly
75
- ```
47
+ ## Falcon Integration
76
48
 
77
- ## šŸ”§ Manual Certificate Setup
78
-
79
- If you prefer to create certificates manually:
80
-
81
- ```bash
82
- mkdir -p certs
83
- cd certs
49
+ No extra gems or config needed — just pass Falcon's middleware:
84
50
 
85
- # Generate private key
86
- openssl genrsa -out server.key 2048
51
+ ```ruby
52
+ require "falcon"
53
+ require "quicsilver"
87
54
 
88
- # Generate certificate with proper QUIC extensions
89
- openssl req -new -x509 -key server.key -out server.crt -days 365 \
90
- -subj "/CN=localhost/O=QuicsilverTest/C=US" \
91
- -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
92
- -addext "extendedKeyUsage=serverAuth"
55
+ middleware = Falcon::Server.middleware(Rails.application)
56
+ config = Quicsilver::Transport::Configuration.new(cert, key, mode: :falcon)
57
+ Quicsilver::Server.new(4433, app: middleware, server_configuration: config).start
93
58
  ```
94
-
95
- ## šŸ› Troubleshooting
96
-
97
- **Port in use error:** The server automatically cleans up port 4433 before starting.
98
-
99
- **Connection refused:** Make sure the server is running before testing the client.
100
-
101
- **Certificate errors:** Run `./setup_certs.sh` to regenerate certificates with proper QUIC extensions.
102
-
103
- ---
104
-
105
- That's it! No Docker, no external servers, just pure Ruby QUIC. šŸŽÆ
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Quicsilver benchmark — self-contained, boots its own server.
4
+ #
5
+ # ruby examples/benchmark.rb
6
+
7
+ require_relative "example_helper"
8
+
9
+ HOST = "localhost"
10
+ PORT = 4433
11
+ WARMUP = 10
12
+ ITERATIONS = 200
13
+
14
+ app = ->(env) {
15
+ case env["PATH_INFO"]
16
+ when "/large"
17
+ [200, { "content-type" => "application/octet-stream" }, ["x" * 50_000]]
18
+ when "/work"
19
+ sleep 0.001
20
+ [200, { "content-type" => "application/json" }, ['{"ok":true}']]
21
+ else
22
+ [200, { "content-type" => "application/json" }, ['{"ok":true}']]
23
+ end
24
+ }
25
+
26
+ server = Quicsilver::Server.new(PORT, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
27
+ server_thread = Thread.new { server.start }
28
+ sleep 0.3
29
+
30
+ def run_benchmark(name, iterations)
31
+ times = iterations.times.map { yield }
32
+ total = times.sum
33
+ avg = (total / times.size).round(2)
34
+ p50 = times.sort[times.size / 2].round(2)
35
+ p99 = times.sort[(times.size * 0.99).to_i].round(2)
36
+ rps = (times.size / (total / 1000.0)).round(0)
37
+ puts " Total: #{total.round(1)}ms"
38
+ puts " Avg: #{avg}ms"
39
+ puts " p50: #{p50}ms"
40
+ puts " p99: #{p99}ms"
41
+ puts " RPS: #{rps} req/s"
42
+ end
43
+
44
+ puts "šŸ”Ø Quicsilver Benchmark"
45
+ puts "=" * 60
46
+
47
+ # === 1. Sequential (single connection) ===
48
+ puts "\nšŸ“Š Sequential — single connection, #{ITERATIONS} requests"
49
+ puts "-" * 60
50
+
51
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
52
+ WARMUP.times { client.get("/ping") }
53
+
54
+ run_benchmark("Sequential", ITERATIONS) do
55
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
56
+ client.get("/ping")
57
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000
58
+ end
59
+ client.disconnect
60
+
61
+ # === 2. Pooled ===
62
+ puts "\nšŸ“Š Pooled — connection pool, #{ITERATIONS} requests"
63
+ puts "-" * 60
64
+
65
+ WARMUP.times { Quicsilver::Client.get(HOST, PORT, "/ping", unsecure: true) }
66
+
67
+ run_benchmark("Pooled", ITERATIONS) do
68
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
69
+ Quicsilver::Client.get(HOST, PORT, "/ping", unsecure: true)
70
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000
71
+ end
72
+ Quicsilver::Client.close_pool
73
+
74
+ # === 3. Large payload ===
75
+ puts "\nšŸ“Š Large payload — 50KB response, #{ITERATIONS} requests"
76
+ puts "-" * 60
77
+
78
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
79
+ WARMUP.times { client.get("/large") }
80
+
81
+ run_benchmark("Large", ITERATIONS) do
82
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
83
+ client.get("/large")
84
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000
85
+ end
86
+
87
+ client.disconnect
88
+
89
+ # === 4. True multiplexing ===
90
+ puts "\nšŸ“Š True multiplexing — concurrent streams, single connection"
91
+ puts "-" * 60
92
+
93
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
94
+ WARMUP.times { client.get("/ping") }
95
+
96
+ [1, 5, 10, 20, 50].each do |concurrent|
97
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
98
+ requests = concurrent.times.map { client.get("/ping") { |req| req } }
99
+ responses = requests.map(&:response)
100
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
101
+ ok = responses.count { |r| r[:status] == 200 }
102
+ rps = (concurrent / (elapsed / 1000.0)).round(0)
103
+ puts " #{concurrent.to_s.rjust(3)} streams: #{elapsed.to_s.rjust(7)}ms #{ok}/#{concurrent} ok #{rps} req/s"
104
+ end
105
+
106
+ client.disconnect
107
+
108
+ # Cleanup
109
+ Quicsilver::Client.close_pool
110
+ # === 5. Multi-threaded concurrency ===
111
+ puts "\nšŸ“Š Multi-threaded — concurrent clients, #{ITERATIONS} total requests"
112
+ puts "-" * 60
113
+
114
+ [1, 2, 4, 8].each do |thread_count|
115
+ requests_per_thread = ITERATIONS / thread_count
116
+ err_count = 0
117
+ mu = Mutex.new
118
+
119
+ # Connect all clients sequentially (connection setup is serialized)
120
+ clients = thread_count.times.map do
121
+ c = Quicsilver::Client.new(HOST, PORT, unsecure: true, request_timeout: 10)
122
+ c.open_connection
123
+ c
124
+ end
125
+
126
+ # Run requests in parallel
127
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
128
+ threads = clients.map do |client|
129
+ Thread.new do
130
+ requests_per_thread.times do
131
+ client.get("/ping")
132
+ rescue => e
133
+ mu.synchronize { err_count += 1 }
134
+ end
135
+ end
136
+ end
137
+ threads.each(&:join)
138
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(1)
139
+
140
+ clients.each(&:disconnect)
141
+
142
+ rps = (ITERATIONS / (elapsed / 1000.0)).round(0)
143
+ err = err_count > 0 ? " (#{err_count} errors)" : ""
144
+ puts " #{thread_count.to_s.rjust(3)} threads: #{elapsed.to_s.rjust(7)}ms #{rps} req/s#{err}"
145
+ sleep 0.1
146
+ end
147
+
148
+ # === 6. Multi-threaded with simulated work ===
149
+ puts "\nšŸ“Š Multi-threaded with 1ms app work, 50 total requests"
150
+ puts "-" * 60
151
+
152
+ [1, 2, 4, 8].each do |thread_count|
153
+ reqs = 50
154
+ requests_per_thread = reqs / thread_count
155
+ err_count = 0
156
+ mu = Mutex.new
157
+
158
+ clients = thread_count.times.map do
159
+ c = Quicsilver::Client.new(HOST, PORT, unsecure: true, request_timeout: 10)
160
+ c.open_connection
161
+ c
162
+ end
163
+
164
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
165
+ threads = clients.map do |client|
166
+ Thread.new do
167
+ requests_per_thread.times do
168
+ client.get("/work")
169
+ rescue => e
170
+ mu.synchronize { err_count += 1 }
171
+ end
172
+ end
173
+ end
174
+ threads.each(&:join)
175
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(1)
176
+
177
+ clients.each(&:disconnect)
178
+
179
+ rps = (reqs / (elapsed / 1000.0)).round(0)
180
+ err = err_count > 0 ? " (#{err_count} errors)" : ""
181
+ puts " #{thread_count.to_s.rjust(3)} threads: #{elapsed.to_s.rjust(7)}ms #{rps} req/s#{err}"
182
+ sleep 0.1
183
+ end
184
+
185
+ # Cleanup
186
+ Quicsilver::Client.close_pool
187
+ server.stop
188
+ server_thread.join(2)
189
+
190
+ puts "\n" + "=" * 60
191
+ puts "āœ… Benchmark complete"
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Demonstrates connection pooling.
4
+ #
5
+ # ruby examples/connection_pool_demo.rb
6
+ #
7
+ # To compare with main (no pooling), checkout main and run:
8
+ #
9
+ # ruby examples/simple_client_test.rb
10
+ #
11
+ # Each request on main pays the full QUIC handshake cost.
12
+ # With pooling, only the first request pays it.
13
+
14
+ require_relative "example_helper"
15
+
16
+ PORT = 4433
17
+ HOST = "localhost"
18
+
19
+ # Start a simple server in-process
20
+ app = ->(env) { [200, { "content-type" => "text/plain" }, ["Hello from #{env['PATH_INFO']}"]] }
21
+ server = Quicsilver::Server.new(PORT, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
22
+ server_thread = Thread.new { server.start }
23
+ sleep 0.3
24
+
25
+ puts "Connection Pool Demo"
26
+ puts "=" * 50
27
+
28
+ puts "\nšŸ”„ Client.get — pooling is automatic"
29
+ puts "-" * 50
30
+
31
+ 6.times do |i|
32
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
33
+ response = Quicsilver::Client.get(HOST, PORT, "/request-#{i}", unsecure: true)
34
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
35
+
36
+ label = i == 0 ? "← new connection + QUIC handshake" : "← reused"
37
+ puts " Request #{i}: #{response.status} — #{elapsed}ms #{label}"
38
+ end
39
+
40
+ puts "\n Pool: #{Quicsilver::Client.pool.size} connection(s) ready for reuse"
41
+
42
+ # --- Cleanup ---
43
+ Quicsilver::Client.close_pool
44
+ server.stop
45
+ server_thread.join(2)
46
+
47
+ puts "\nāœ… Done"
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared helper for examples — uses the `localhost` gem to generate
4
+ # self-signed TLS certificates so examples work without manual setup.
5
+ #
6
+ # Usage:
7
+ # require_relative "example_helper"
8
+ # server = Quicsilver::Server.new(4433, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
9
+
10
+ require "bundler/setup"
11
+ require "json"
12
+ require "socket"
13
+ require "thread"
14
+ require "quicsilver"
15
+ require "localhost/authority"
16
+
17
+ authority = Localhost::Authority.fetch
18
+ EXAMPLE_TLS_CONFIG = Quicsilver::Transport::Configuration.new(
19
+ authority.certificate_path,
20
+ authority.key_path
21
+ )
22
+
23
+ module Example
24
+ module_function
25
+
26
+ def wait_until(timeout: 5, interval: 0.01)
27
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
28
+ until yield
29
+ raise "timed out waiting" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
30
+
31
+ sleep interval
32
+ end
33
+ end
34
+
35
+ def available_udp_port(host = "127.0.0.1")
36
+ socket = UDPSocket.new
37
+ socket.bind(host, 0)
38
+ socket.addr[1]
39
+ ensure
40
+ socket&.close
41
+ end
42
+
43
+ def json_response(status, body)
44
+ [status, { "content-type" => "application/json" }, [JSON.generate(body) << "\n"]]
45
+ end
46
+
47
+ def text_response(status, body)
48
+ [status, { "content-type" => "text/plain" }, [body]]
49
+ end
50
+
51
+ def say(line = "")
52
+ puts line
53
+ end
54
+
55
+ def heading(text, level: 2)
56
+ say unless heading_count.zero?
57
+ say "#{'#' * level} #{text}"
58
+ @heading_count += 1
59
+ end
60
+
61
+ def detail(label, value, width: 22)
62
+ say format(" %-#{width}s %s", label, value)
63
+ end
64
+
65
+
66
+ def heading_count
67
+ @heading_count ||= 0
68
+ end
69
+ end
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Quicsilver with Falcon's middleware stack over HTTP/3.
4
+ #
5
+ # Falcon provides caching, content encoding, and protocol-rack.
6
+ # Quicsilver provides the HTTP/3 transport.
7
+ #
8
+ # Prerequisites: gem install falcon
9
+ #
10
+ # ruby examples/falcon_middleware.rb
11
+ # curl --http3-only -k https://localhost:4433/
12
+
13
+ require_relative "example_helper"
14
+
15
+ begin
16
+ require "falcon"
17
+ rescue LoadError
18
+ puts "āŒ Falcon not installed. Run: gem install falcon"
19
+ exit 1
20
+ end
21
+
22
+ app = ->(env) {
23
+ [200, { "content-type" => "application/json" }, [
24
+ %({"protocol":"#{env['SERVER_PROTOCOL']}","server":"quicsilver+falcon"})
25
+ ]]
26
+ }
27
+
28
+ # Falcon's middleware adds caching, content encoding, etc.
29
+ middleware = Falcon::Server.middleware(app)
30
+
31
+ config = Quicsilver::Transport::Configuration.new(
32
+ EXAMPLE_TLS_CONFIG.cert_file,
33
+ EXAMPLE_TLS_CONFIG.key_file,
34
+ mode: :falcon
35
+ )
36
+
37
+ server = Quicsilver::Server.new(4433, app: middleware, server_configuration: config)
38
+
39
+ puts "šŸ¦… Quicsilver + Falcon Middleware"
40
+ puts " https://localhost:4433"
41
+ puts " curl --http3-only -k https://localhost:4433/"
42
+ puts
43
+
44
+ server.start