quicsilver 0.5.0-x86_64-linux

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 (107) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +49 -0
  3. data/.github/workflows/cibuildgem.yaml +117 -0
  4. data/.gitignore +21 -0
  5. data/.gitmodules +3 -0
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +175 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +67 -0
  10. data/LICENSE +21 -0
  11. data/README.md +261 -0
  12. data/Rakefile +130 -0
  13. data/benchmarks/baseline.rb +175 -0
  14. data/benchmarks/compare.rb +398 -0
  15. data/benchmarks/components.rb +191 -0
  16. data/benchmarks/helpers.rb +162 -0
  17. data/benchmarks/latency.rb +128 -0
  18. data/benchmarks/loss.rb +193 -0
  19. data/benchmarks/mobile.rb +125 -0
  20. data/bin/console +14 -0
  21. data/bin/setup +8 -0
  22. data/docs/connection_identity.md +141 -0
  23. data/examples/README.md +58 -0
  24. data/examples/benchmark.rb +191 -0
  25. data/examples/connection_pool_demo.rb +47 -0
  26. data/examples/example_helper.rb +69 -0
  27. data/examples/falcon_middleware.rb +44 -0
  28. data/examples/feature_demo.rb +125 -0
  29. data/examples/grpc_style.rb +97 -0
  30. data/examples/latency_aware_routing.rb +382 -0
  31. data/examples/lifecycle_drain.rb +264 -0
  32. data/examples/minimal_http3_server.rb +14 -0
  33. data/examples/priorities.rb +60 -0
  34. data/examples/protocol_http_server.rb +31 -0
  35. data/examples/rack_http3_server.rb +42 -0
  36. data/examples/rails_feature_test.rb +280 -0
  37. data/examples/server_stats.rb +117 -0
  38. data/examples/simple_client_test.rb +26 -0
  39. data/examples/streaming_sse.rb +33 -0
  40. data/examples/trailers.rb +69 -0
  41. data/examples/webtransport_smoke.rb +546 -0
  42. data/examples/worker_load_balancing.rb +425 -0
  43. data/ext/quicsilver/extconf.rb +124 -0
  44. data/ext/quicsilver/include/msquic.h +2226 -0
  45. data/ext/quicsilver/include/msquic_posix.h +696 -0
  46. data/ext/quicsilver/include/quic_sal_stub.h +314 -0
  47. data/ext/quicsilver/msquic_patch.rb +68 -0
  48. data/ext/quicsilver/patches/reliable-reset.patch +723 -0
  49. data/ext/quicsilver/patches/stream-final-size.patch +47 -0
  50. data/ext/quicsilver/quicsilver.c +2741 -0
  51. data/lib/quicsilver/3.4/quicsilver.so +0 -0
  52. data/lib/quicsilver/4.0/quicsilver.so +0 -0
  53. data/lib/quicsilver/client/client.rb +708 -0
  54. data/lib/quicsilver/client/connection_pool.rb +202 -0
  55. data/lib/quicsilver/client/request.rb +190 -0
  56. data/lib/quicsilver/errors.rb +39 -0
  57. data/lib/quicsilver/libmsquic.so.2 +0 -0
  58. data/lib/quicsilver/protocol/adapter.rb +196 -0
  59. data/lib/quicsilver/protocol/capsule.rb +38 -0
  60. data/lib/quicsilver/protocol/connect_stream_decoder.rb +75 -0
  61. data/lib/quicsilver/protocol/control_stream_parser.rb +117 -0
  62. data/lib/quicsilver/protocol/datagram.rb +40 -0
  63. data/lib/quicsilver/protocol/frame_parser.rb +141 -0
  64. data/lib/quicsilver/protocol/frame_reader.rb +84 -0
  65. data/lib/quicsilver/protocol/frames.rb +454 -0
  66. data/lib/quicsilver/protocol/priority.rb +65 -0
  67. data/lib/quicsilver/protocol/qpack/decoder.rb +127 -0
  68. data/lib/quicsilver/protocol/qpack/encoder.rb +217 -0
  69. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +140 -0
  70. data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
  71. data/lib/quicsilver/protocol/request_encoder.rb +50 -0
  72. data/lib/quicsilver/protocol/request_parser.rb +289 -0
  73. data/lib/quicsilver/protocol/response_encoder.rb +107 -0
  74. data/lib/quicsilver/protocol/response_parser.rb +147 -0
  75. data/lib/quicsilver/protocol/stream_input.rb +98 -0
  76. data/lib/quicsilver/protocol/stream_output.rb +59 -0
  77. data/lib/quicsilver/protocol/unidirectional_stream.rb +12 -0
  78. data/lib/quicsilver/protocol/web_transport.rb +214 -0
  79. data/lib/quicsilver/rack/context.rb +33 -0
  80. data/lib/quicsilver/response.rb +40 -0
  81. data/lib/quicsilver/server/listener_data.rb +14 -0
  82. data/lib/quicsilver/server/rack_adapter.rb +77 -0
  83. data/lib/quicsilver/server/receive_queue.rb +106 -0
  84. data/lib/quicsilver/server/request_handler.rb +149 -0
  85. data/lib/quicsilver/server/request_registry.rb +63 -0
  86. data/lib/quicsilver/server/scheduler.rb +50 -0
  87. data/lib/quicsilver/server/schedulers/fiber_scheduler.rb +79 -0
  88. data/lib/quicsilver/server/schedulers/thread_scheduler.rb +76 -0
  89. data/lib/quicsilver/server/server.rb +1080 -0
  90. data/lib/quicsilver/server/web_transport_flow_control.rb +270 -0
  91. data/lib/quicsilver/server/web_transport_manager.rb +281 -0
  92. data/lib/quicsilver/server/web_transport_registry.rb +40 -0
  93. data/lib/quicsilver/server/web_transport_session.rb +812 -0
  94. data/lib/quicsilver/server/web_transport_stream.rb +326 -0
  95. data/lib/quicsilver/transport/configuration.rb +234 -0
  96. data/lib/quicsilver/transport/connection.rb +594 -0
  97. data/lib/quicsilver/transport/connection_stats.rb +53 -0
  98. data/lib/quicsilver/transport/event_loop.rb +74 -0
  99. data/lib/quicsilver/transport/inbound_stream.rb +53 -0
  100. data/lib/quicsilver/transport/stream.rb +43 -0
  101. data/lib/quicsilver/transport/stream_event.rb +49 -0
  102. data/lib/quicsilver/transport/stream_id.rb +26 -0
  103. data/lib/quicsilver/version.rb +3 -0
  104. data/lib/quicsilver.rb +96 -0
  105. data/lib/rackup/handler/quicsilver.rb +96 -0
  106. data/quicsilver.gemspec +66 -0
  107. metadata +362 -0
@@ -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.
@@ -0,0 +1,58 @@
1
+ # Examples
2
+
3
+ Self-contained scripts demonstrating quicsilver features. Each boots its own server — no external setup needed.
4
+
5
+ ## Getting Started
6
+
7
+ ```bash
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
35
+
36
+ ```bash
37
+ # 1. Add to Gemfile
38
+ gem "quicsilver"
39
+
40
+ # 2. Start
41
+ rackup -s quicsilver -p 4433
42
+
43
+ # 3. Test
44
+ curl --http3-only -k https://localhost:4433/
45
+ ```
46
+
47
+ ## Falcon Integration
48
+
49
+ No extra gems or config needed — just pass Falcon's middleware:
50
+
51
+ ```ruby
52
+ require "falcon"
53
+ require "quicsilver"
54
+
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
58
+ ```
@@ -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
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Demonstrates all major quicsilver features in one script.
4
+ #
5
+ # ruby examples/feature_demo.rb
6
+
7
+ require_relative "example_helper"
8
+
9
+ PORT = 4433
10
+ HOST = "localhost"
11
+
12
+ # === Server with multiple endpoints ===
13
+ app = ->(env) {
14
+ path = env["PATH_INFO"]
15
+ method = env["REQUEST_METHOD"]
16
+
17
+ case path
18
+ when "/"
19
+ [200, { "content-type" => "text/plain" }, ["Hello HTTP/3!\n"]]
20
+
21
+ when "/api/users"
22
+ [200, { "content-type" => "application/json" },
23
+ ['{"users":["alice","bob","charlie"]}']]
24
+
25
+ when "/stream"
26
+ # Streaming body — chunks sent as they're generated
27
+ body = Enumerator.new do |y|
28
+ 5.times do |i|
29
+ y << "chunk #{i}\n"
30
+ sleep 0.01
31
+ end
32
+ end
33
+ [200, { "content-type" => "text/plain" }, body]
34
+
35
+ when "/large"
36
+ # Large response for priority testing
37
+ [200, { "content-type" => "text/plain" }, ["x" * 10_000]]
38
+
39
+ when "/echo"
40
+ # Echo POST body back
41
+ body = env["rack.input"]&.read || ""
42
+ [200, { "content-type" => "text/plain", "content-length" => body.bytesize.to_s }, [body]]
43
+
44
+ else
45
+ [404, { "content-type" => "text/plain" }, ["Not Found"]]
46
+ end
47
+ }
48
+
49
+ server = Quicsilver::Server.new(PORT, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
50
+ server_thread = Thread.new { server.start }
51
+ sleep 0.3
52
+
53
+ puts "🚀 Quicsilver Feature Demo"
54
+ puts "=" * 60
55
+
56
+ # === 1. Connection Pooling ===
57
+ puts "\n1️⃣ Connection Pooling"
58
+ puts "-" * 60
59
+
60
+ 6.times do |i|
61
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
62
+ response = Quicsilver::Client.get(HOST, PORT, "/", unsecure: true)
63
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
64
+ label = i == 0 ? "← handshake" : "← reused"
65
+ puts " Request #{i}: #{response.status} — #{elapsed}ms #{label}"
66
+ end
67
+
68
+ # === 2. Multiple Endpoints ===
69
+ puts "\n2️⃣ Multiple Endpoints"
70
+ puts "-" * 60
71
+
72
+ ["/", "/api/users", "/stream", "/nonexistent"].each do |path|
73
+ response = Quicsilver::Client.get(HOST, PORT, path, unsecure: true)
74
+ body_preview = response.body[0..50].gsub("\n", "\\n")
75
+ puts " GET #{path} → #{response.status} | #{body_preview}"
76
+ end
77
+
78
+ # === 3. POST with Body ===
79
+ puts "\n3️⃣ POST with Echo"
80
+ puts "-" * 60
81
+
82
+ response = Quicsilver::Client.post(HOST, PORT, "/echo",
83
+ body: "Hello from quicsilver client!",
84
+ headers: { "content-type" => "text/plain" },
85
+ unsecure: true)
86
+ puts " POST /echo → #{response.status} | #{response.body}"
87
+
88
+ # === 4. Concurrent Requests ===
89
+ puts "\n4️⃣ Concurrent Requests (multiplexing)"
90
+ puts "-" * 60
91
+
92
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
93
+ t_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
94
+ 10.times do |i|
95
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
96
+ response = client.get("/api/users")
97
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
98
+ puts " Request #{i}: #{response.status} — #{elapsed}ms"
99
+ end
100
+ total = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t_start) * 1000).round(1)
101
+ puts " All 10 completed in #{total}ms (single connection)"
102
+ client.disconnect
103
+
104
+ # === 5. HTTP Methods ===
105
+ puts "\n5️⃣ HTTP Methods"
106
+ puts "-" * 60
107
+
108
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
109
+ %i[get post put patch delete head].each do |method|
110
+ response = client.public_send(method, "/api/users")
111
+ body_size = response.body&.bytesize || 0
112
+ puts " #{method.to_s.upcase.ljust(6)} /api/users → #{response.status} (#{body_size} bytes)"
113
+ end
114
+ client.disconnect
115
+
116
+ # === Summary ===
117
+ puts "\n" + "=" * 60
118
+ puts "✅ All features working!"
119
+ puts " Pool: #{Quicsilver::Client.pool.size} connection(s)"
120
+
121
+ # === Cleanup ===
122
+ Quicsilver::Client.close_pool
123
+ server.stop
124
+ server_thread.join(2)
125
+ puts "👋 Done"