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,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
|
-
#
|
|
1
|
+
# Examples
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Self-contained scripts demonstrating quicsilver features. Each boots its own server ā no external setup needed.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
### 1. Generate Certificates
|
|
5
|
+
## Getting Started
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
#
|
|
20
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
51
|
+
```ruby
|
|
52
|
+
require "falcon"
|
|
53
|
+
require "quicsilver"
|
|
87
54
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|