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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +49 -0
- data/.github/workflows/cibuildgem.yaml +117 -0
- data/.gitignore +21 -0
- data/.gitmodules +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +175 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +67 -0
- data/LICENSE +21 -0
- data/README.md +261 -0
- data/Rakefile +130 -0
- data/benchmarks/baseline.rb +175 -0
- data/benchmarks/compare.rb +398 -0
- data/benchmarks/components.rb +191 -0
- data/benchmarks/helpers.rb +162 -0
- data/benchmarks/latency.rb +128 -0
- data/benchmarks/loss.rb +193 -0
- data/benchmarks/mobile.rb +125 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/connection_identity.md +141 -0
- data/examples/README.md +58 -0
- 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 +14 -0
- data/examples/priorities.rb +60 -0
- data/examples/protocol_http_server.rb +31 -0
- data/examples/rack_http3_server.rb +42 -0
- data/examples/rails_feature_test.rb +280 -0
- data/examples/server_stats.rb +117 -0
- data/examples/simple_client_test.rb +26 -0
- 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 +124 -0
- 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 +2741 -0
- data/lib/quicsilver/3.4/quicsilver.so +0 -0
- data/lib/quicsilver/4.0/quicsilver.so +0 -0
- data/lib/quicsilver/client/client.rb +708 -0
- data/lib/quicsilver/client/connection_pool.rb +202 -0
- data/lib/quicsilver/client/request.rb +190 -0
- data/lib/quicsilver/errors.rb +39 -0
- data/lib/quicsilver/libmsquic.so.2 +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 +454 -0
- data/lib/quicsilver/protocol/priority.rb +65 -0
- data/lib/quicsilver/protocol/qpack/decoder.rb +127 -0
- data/lib/quicsilver/protocol/qpack/encoder.rb +217 -0
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +140 -0
- data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
- data/lib/quicsilver/protocol/request_encoder.rb +50 -0
- data/lib/quicsilver/protocol/request_parser.rb +289 -0
- data/lib/quicsilver/protocol/response_encoder.rb +107 -0
- data/lib/quicsilver/protocol/response_parser.rb +147 -0
- 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/listener_data.rb +14 -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 +149 -0
- data/lib/quicsilver/server/request_registry.rb +63 -0
- 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 +1080 -0
- 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 +234 -0
- data/lib/quicsilver/transport/connection.rb +594 -0
- data/lib/quicsilver/transport/connection_stats.rb +53 -0
- data/lib/quicsilver/transport/event_loop.rb +74 -0
- data/lib/quicsilver/transport/inbound_stream.rb +53 -0
- data/lib/quicsilver/transport/stream.rb +43 -0
- data/lib/quicsilver/transport/stream_event.rb +49 -0
- data/lib/quicsilver/transport/stream_id.rb +26 -0
- data/lib/quicsilver/version.rb +3 -0
- data/lib/quicsilver.rb +96 -0
- data/lib/rackup/handler/quicsilver.rb +96 -0
- data/quicsilver.gemspec +66 -0
- metadata +362 -0
data/README.md
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# Quicsilver
|
|
2
|
+
|
|
3
|
+
HTTP/3 server and client for Ruby with Rack support.
|
|
4
|
+
|
|
5
|
+
## Why HTTP/3?
|
|
6
|
+
|
|
7
|
+
- **No head-of-line blocking** — one slow response doesn't stall others. HTTP/2 multiplexes over TCP, but a single lost packet freezes all streams. QUIC streams are independent.
|
|
8
|
+
- **Faster connections** — QUIC handshake is 1 round trip (TCP+TLS is 3). Returning users get 0-RTT — zero round trips.
|
|
9
|
+
- **Connection migration** — users switch from wifi to cellular without dropping the connection.
|
|
10
|
+
- **True multiplexing** — many concurrent requests on one connection, each on its own stream.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **HTTP/3 server** — serve any Rack app over QUIC/HTTP/3
|
|
15
|
+
- **HTTP/3 client** — make requests with automatic connection pooling
|
|
16
|
+
- **Rack integration** — `rackup -s quicsilver` works with Rails, Sinatra, any Rack app
|
|
17
|
+
- **Streaming** — dispatch on HEADERS, stream body chunks as they arrive
|
|
18
|
+
- **Extensible Priorities** (RFC 9218) — CSS before images, server respects client priority hints
|
|
19
|
+
- **Trailers** (RFC 9114 §4.1) — send/receive trailing headers after the body
|
|
20
|
+
- **GREASE** (RFC 9297) — extensibility testing on settings, frames, and streams
|
|
21
|
+
- **GOAWAY** (RFC 9114 §7.2.6) — graceful connection draining with validation
|
|
22
|
+
- **0-RTT** — fast reconnection with replay protection
|
|
23
|
+
- **Connection pooling** — client reuses connections automatically
|
|
24
|
+
- **Falcon middleware compatible** — use Falcon's caching and content encoding over HTTP/3
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/hahmed/quicsilver
|
|
30
|
+
cd quicsilver
|
|
31
|
+
bundle install
|
|
32
|
+
rake compile
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
### Server
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
require "quicsilver"
|
|
41
|
+
require "localhost/authority"
|
|
42
|
+
|
|
43
|
+
app = ->(env) {
|
|
44
|
+
[200, {"content-type" => "text/plain"}, ["Hello HTTP/3!"]]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
authority = Localhost::Authority.fetch
|
|
48
|
+
config = Quicsilver::Transport::Configuration.new(
|
|
49
|
+
authority.certificate_path,
|
|
50
|
+
authority.key_path
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
server = Quicsilver::Server.new(4433, app: app, server_configuration: config)
|
|
54
|
+
server.start
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Client
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
require "quicsilver"
|
|
61
|
+
|
|
62
|
+
# Class-level API with automatic connection pooling.
|
|
63
|
+
# Clients verify TLS certificates by default, so the hostname must match the cert.
|
|
64
|
+
response = Quicsilver::Client.get("localhost", 4433, "/")
|
|
65
|
+
puts response[:status] # => 200
|
|
66
|
+
puts response[:body] # => "Hello HTTP/3!"
|
|
67
|
+
|
|
68
|
+
# POST with body
|
|
69
|
+
response = Quicsilver::Client.post("localhost", 4433, "/api/users",
|
|
70
|
+
body: '{"name": "alice"}',
|
|
71
|
+
headers: { "content-type" => "application/json" })
|
|
72
|
+
|
|
73
|
+
# Instance-level for more control. Use unsecure: true only for
|
|
74
|
+
# local/self-signed test servers.
|
|
75
|
+
client = Quicsilver::Client.new("localhost", 4433, unsecure: true)
|
|
76
|
+
response = client.get("/")
|
|
77
|
+
client.disconnect
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Rails
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
rackup -s quicsilver -p 4433
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
In development, the Rackup handler can use the `localhost` gem to generate
|
|
87
|
+
local certificates automatically. In production, pass both `cert_file` and
|
|
88
|
+
`key_file` Rackup options explicitly:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
rackup -s quicsilver -p 4433 \
|
|
92
|
+
-O cert_file=/path/to/fullchain.pem \
|
|
93
|
+
-O key_file=/path/to/privkey.pem
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### curl
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
curl --http3-only -k https://localhost:4433/
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### TLS Certificates
|
|
103
|
+
|
|
104
|
+
HTTP/3 always runs over TLS. Core server configuration never guesses
|
|
105
|
+
certificate paths: `Quicsilver::Transport::Configuration` requires both a
|
|
106
|
+
certificate file and a private key file.
|
|
107
|
+
|
|
108
|
+
For production, point Quicsilver at the same publicly-trusted certificate and
|
|
109
|
+
key you use for your TCP HTTPS server:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
config = Quicsilver::Transport::Configuration.new(
|
|
113
|
+
ENV.fetch("TLS_CERT_FILE"),
|
|
114
|
+
ENV.fetch("TLS_KEY_FILE")
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
For local development, Quicsilver depends on the
|
|
119
|
+
[localhost](https://github.com/socketry/localhost) gem, which can generate a
|
|
120
|
+
certificate for `localhost` or your local development hostname:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
require "localhost/authority"
|
|
124
|
+
|
|
125
|
+
authority = Localhost::Authority.fetch
|
|
126
|
+
config = Quicsilver::Transport::Configuration.new(
|
|
127
|
+
authority.certificate_path,
|
|
128
|
+
authority.key_path
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Run `bake localhost:install` once to add the localhost CA to your system trust
|
|
133
|
+
store. You can still use `curl -k` or `unsecure: true` for quick local testing
|
|
134
|
+
without trusting the CA.
|
|
135
|
+
|
|
136
|
+
Quicsilver clients verify certificates by default. Disable verification only
|
|
137
|
+
for local development or tests:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
client = Quicsilver::Client.new("localhost", 4433, unsecure: true)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Browser Access
|
|
144
|
+
|
|
145
|
+
Any HTTP/3 client connects directly — no extra setup needed.
|
|
146
|
+
|
|
147
|
+
Browsers discover HTTP/3 via the `Alt-Svc` header from an existing HTTP/1.1
|
|
148
|
+
or HTTP/2 server. Run quicsilver alongside your regular Rails/Rack server on
|
|
149
|
+
the **same port** — TCP for HTTP/1.1+2, UDP for HTTP/3:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Your normal Rails server (HTTP/1.1 + HTTP/2 over TCP)
|
|
153
|
+
bin/rails server -p 3000
|
|
154
|
+
|
|
155
|
+
# Quicsilver (HTTP/3 over UDP, same port)
|
|
156
|
+
rackup -s quicsilver -p 3000
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Add the `Alt-Svc` header so browsers discover HTTP/3. The port must match
|
|
160
|
+
the port quicsilver is listening on:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
# config/application.rb
|
|
164
|
+
config.action_dispatch.default_headers["Alt-Svc"] = 'h3=":3000"; ma=86400'
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
With a publicly-trusted certificate (e.g. Let's Encrypt), browsers upgrade
|
|
168
|
+
to HTTP/3 automatically via Alt-Svc — no special configuration needed.
|
|
169
|
+
|
|
170
|
+
### Local Development
|
|
171
|
+
|
|
172
|
+
Both your TCP server and quicsilver need a TLS certificate trusted by
|
|
173
|
+
your system. Both servers **must use the same certificate**. The
|
|
174
|
+
`localhost` gem can generate local certificates — run `bake localhost:install`
|
|
175
|
+
to add the CA to your system trust store.
|
|
176
|
+
|
|
177
|
+
Chrome requires one additional flag for local development because it
|
|
178
|
+
does not allow QUIC connections with locally-trusted certificates:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
chrome --origin-to-force-quic-on=myapp.test:3000 https://myapp.test:3000
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
This is not needed in production with publicly-trusted certificates.
|
|
185
|
+
|
|
186
|
+
## Configuration
|
|
187
|
+
|
|
188
|
+
Pass both certificate paths explicitly. Omitting either path raises a
|
|
189
|
+
configuration error.
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
config = Quicsilver::Transport::Configuration.new(
|
|
193
|
+
"certificates/server.crt",
|
|
194
|
+
"certificates/server.key",
|
|
195
|
+
idle_timeout_ms: 10_000,
|
|
196
|
+
max_concurrent_requests: 100,
|
|
197
|
+
max_body_size: 10 * 1024 * 1024, # 10MB body limit (optional)
|
|
198
|
+
max_header_size: 64 * 1024, # 64KB header limit (optional)
|
|
199
|
+
max_header_count: 128, # Header count limit (optional)
|
|
200
|
+
stream_receive_window: 262_144, # 256KB per stream
|
|
201
|
+
connection_flow_control_window: 16_777_216 # 16MB per connection
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
server = Quicsilver::Server.new(4433, app: app, server_configuration: config)
|
|
205
|
+
server.start
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Priorities
|
|
209
|
+
|
|
210
|
+
Browsers send priority hints on requests. Quicsilver parses them and schedules high-priority streams first.
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
GET /style.css → priority: u=0 → sent first (highest urgency)
|
|
214
|
+
GET /app.js → priority: u=1 → sent second
|
|
215
|
+
GET /hero.png → priority: u=5 → sent later
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
No configuration needed — it works automatically.
|
|
219
|
+
|
|
220
|
+
## Trailers
|
|
221
|
+
|
|
222
|
+
Send headers after the body — useful for checksums, streaming status, and gRPC.
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
# Trailers work with protocol-http's Headers#trailer! API
|
|
226
|
+
headers = Protocol::HTTP::Headers.new
|
|
227
|
+
headers.add("content-type", "text/plain")
|
|
228
|
+
headers.trailer!
|
|
229
|
+
headers.add("x-checksum", "abc123")
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Falcon Middleware Mode
|
|
233
|
+
|
|
234
|
+
Use Falcon's middleware stack (caching, content encoding) over HTTP/3. Quicsilver handles the transport, Falcon's middleware handles the request pipeline:
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
config = Quicsilver::Transport::Configuration.new(
|
|
238
|
+
"certificates/server.crt",
|
|
239
|
+
"certificates/server.key",
|
|
240
|
+
mode: :falcon
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
server = Quicsilver::Server.new(4433, app: app, server_configuration: config)
|
|
244
|
+
server.start
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
| Mode | App Interface | Use Case |
|
|
248
|
+
|------|---------------|----------|
|
|
249
|
+
| `:rack` (default) | Rack env hash | Rails, Sinatra, any Rack app |
|
|
250
|
+
| `:falcon` | Protocol::HTTP::Request | Falcon middleware stack |
|
|
251
|
+
|
|
252
|
+
## Development
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
rake compile # Build C extension (macOS: uses Apple clang automatically)
|
|
256
|
+
rake test # Run tests
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT License
|
data/Rakefile
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
2
|
+
require "bundler/gem_tasks"
|
|
3
|
+
require "rake/testtask"
|
|
4
|
+
require "rake/extensiontask"
|
|
5
|
+
require_relative "ext/quicsilver/msquic_patch"
|
|
6
|
+
|
|
7
|
+
Rake::ExtensionTask.new('quicsilver') do |ext|
|
|
8
|
+
ext.lib_dir = 'lib/quicsilver'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Keep `rake compile` useful in source checkouts, but let cibuildgem own
|
|
12
|
+
# extension compilation after the workflow has built and bundled MsQuic.
|
|
13
|
+
task :compile => [:build_msquic, :bundle_msquic] unless ENV["CIBUILDGEM"]
|
|
14
|
+
|
|
15
|
+
# Copy MsQuic dylib next to the compiled extension for gem packaging
|
|
16
|
+
task :bundle_msquic do
|
|
17
|
+
lib_dir = 'lib/quicsilver'
|
|
18
|
+
if RUBY_PLATFORM =~ /darwin/
|
|
19
|
+
dylib = 'vendor/msquic/build/bin/Release/libmsquic.2.dylib'
|
|
20
|
+
if File.exist?(dylib)
|
|
21
|
+
cp dylib, "#{lib_dir}/libmsquic.2.dylib"
|
|
22
|
+
# Update rpath so the bundle finds the dylib in the same directory
|
|
23
|
+
sh "install_name_tool -add_rpath @loader_path #{lib_dir}/quicsilver.bundle 2>/dev/null || true"
|
|
24
|
+
end
|
|
25
|
+
elsif RUBY_PLATFORM =~ /linux/
|
|
26
|
+
so = 'vendor/msquic/build/bin/Release/libmsquic.so.2'
|
|
27
|
+
cp so, "#{lib_dir}/libmsquic.so.2" if File.exist?(so)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
task :build => :bundle_msquic
|
|
32
|
+
|
|
33
|
+
task :setup do
|
|
34
|
+
# Initialize git submodule if it doesn't exist
|
|
35
|
+
unless File.exist?('vendor/msquic')
|
|
36
|
+
sh 'git submodule add https://github.com/microsoft/msquic.git vendor/msquic'
|
|
37
|
+
sh 'cd vendor/msquic && git submodule update --init --recursive'
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
task :build_msquic => :setup do
|
|
42
|
+
MsquicPatch.apply!(File.expand_path("vendor/msquic", __dir__))
|
|
43
|
+
cmake_args = ['-B build', '-DCMAKE_BUILD_TYPE=Release', '-DQUIC_TLS_LIB=quictls', '-DQUIC_LINUX_IOURING_ENABLED=OFF']
|
|
44
|
+
if RUBY_PLATFORM =~ /darwin/
|
|
45
|
+
cmake_args << '-DCMAKE_EXE_LINKER_FLAGS="-framework CoreServices"'
|
|
46
|
+
cmake_args << '-DCMAKE_SHARED_LINKER_FLAGS="-framework CoreServices"'
|
|
47
|
+
# Ensure QuicTLS uses Xcode SDK, not Homebrew OpenSSL
|
|
48
|
+
sdk_path = `xcrun --show-sdk-path 2>/dev/null`.strip
|
|
49
|
+
cmake_args << "-DCMAKE_OSX_SYSROOT=#{sdk_path}" unless sdk_path.empty?
|
|
50
|
+
end
|
|
51
|
+
# Override PATH so QuicTLS openssldir detection finds system openssl, not Homebrew
|
|
52
|
+
env = { 'PATH' => "/usr/bin:#{ENV['PATH']}" }
|
|
53
|
+
sh env, "cd vendor/msquic && cmake #{cmake_args.join(' ')}"
|
|
54
|
+
sh env, 'cd vendor/msquic && cmake --build build --config Release'
|
|
55
|
+
MsquicPatch.record_build!(File.expand_path("vendor/msquic", __dir__))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
task :build => [:build_msquic, :compile]
|
|
59
|
+
|
|
60
|
+
Rake::TestTask.new(:test) do |t|
|
|
61
|
+
t.libs << "test"
|
|
62
|
+
t.libs << "lib"
|
|
63
|
+
t.ruby_opts << "-W0"
|
|
64
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
Rake::TestTask.new(:test_unit) do |t|
|
|
68
|
+
t.libs << "test"
|
|
69
|
+
t.libs << "lib"
|
|
70
|
+
t.ruby_opts << "-W0"
|
|
71
|
+
t.test_files = FileList["test/**/*_test.rb"].reject { |f|
|
|
72
|
+
f.include?("integration") || f.include?("stream_control") ||
|
|
73
|
+
f =~ /quicsilver_test|event_loop_test/
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
Rake::TestTask.new(:test_integration) do |t|
|
|
78
|
+
t.libs << "test"
|
|
79
|
+
t.libs << "lib"
|
|
80
|
+
t.ruby_opts << "-W0"
|
|
81
|
+
t.test_files = FileList[
|
|
82
|
+
"test/stream_control_integration_test.rb",
|
|
83
|
+
"test/integration/**/*_test.rb",
|
|
84
|
+
"test/quicsilver_test.rb",
|
|
85
|
+
"test/event_loop_test.rb"
|
|
86
|
+
]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
desc "Run unit and integration tests in parallel"
|
|
90
|
+
task :test_parallel do
|
|
91
|
+
threads = []
|
|
92
|
+
results = {}
|
|
93
|
+
|
|
94
|
+
threads << Thread.new {
|
|
95
|
+
results[:unit] = system("bundle exec rake test_unit 2>&1 > /dev/null")
|
|
96
|
+
}
|
|
97
|
+
threads << Thread.new {
|
|
98
|
+
results[:integration] = system("bundle exec rake test_integration 2>&1 > /dev/null")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
threads.each(&:join)
|
|
102
|
+
unless results.values.all?
|
|
103
|
+
abort "Tests failed: #{results.inspect}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
namespace :benchmark do
|
|
108
|
+
desc "Run throughput benchmark"
|
|
109
|
+
task :throughput do
|
|
110
|
+
ruby "benchmarks/throughput.rb"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
desc "Run concurrency benchmark"
|
|
114
|
+
task :concurrent do
|
|
115
|
+
ruby "benchmarks/concurrent.rb"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
desc "Run component micro-benchmarks"
|
|
119
|
+
task :components do
|
|
120
|
+
ruby "benchmarks/components.rb"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
desc "Run all benchmarks"
|
|
124
|
+
task :all => [:components, :throughput, :concurrent]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
desc "Run all benchmarks"
|
|
128
|
+
task :benchmark => "benchmark:all"
|
|
129
|
+
|
|
130
|
+
task :default => :test
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Quicsilver baseline benchmark with a single-file Rails app.
|
|
5
|
+
#
|
|
6
|
+
# HTTP/3 is always TLS, so the client connects by the name on the certificate
|
|
7
|
+
# and the server binds an address that name resolves to. Dialling 127.0.0.1
|
|
8
|
+
# against a certificate issued for localhost fails verification, and on macOS
|
|
9
|
+
# localhost resolves to ::1 first, so binding 127.0.0.1 is unreachable. Run
|
|
10
|
+
# `bake localhost:install` once so the localhost CA is trusted.
|
|
11
|
+
#
|
|
12
|
+
# Read Failed before Req/s. A server that sheds reports better latency than one
|
|
13
|
+
# that serves, because rejecting is quicker than answering.
|
|
14
|
+
#
|
|
15
|
+
# STREAMS is the only knob that loads the server. With one connection and one
|
|
16
|
+
# stream nothing is ever concurrent, so Req/s is just 1000 / p50.
|
|
17
|
+
#
|
|
18
|
+
# REQUESTS=2000 latency, one request at a time
|
|
19
|
+
# STREAMS=10 REQUESTS=2000 concurrent, inside capacity
|
|
20
|
+
# STREAMS=50 REQUESTS=5000 concurrent, above the advertised
|
|
21
|
+
# stream limit: QUIC makes the
|
|
22
|
+
# extra streams wait
|
|
23
|
+
# WORKERS=50 STREAMS=50 REQUESTS=5000 capacity matched to the load
|
|
24
|
+
# WORKLOAD=big REQUESTS=50 large bodies
|
|
25
|
+
# WORKLOAD=sleep SLEEP_SECONDS=0.1 REQUESTS=100 slow handler
|
|
26
|
+
#
|
|
27
|
+
# Capacity is WORKERS threads plus a queue of WORKERS * 4, and that total is
|
|
28
|
+
# what the server advertises as its stream limit. Ten requests is a warm-up,
|
|
29
|
+
# not a result: two runs of ten differed by 48% here.
|
|
30
|
+
#
|
|
31
|
+
# HOST name to connect to, must match the certificate (default localhost)
|
|
32
|
+
# ADDRESS address to bind (default ::1, what localhost resolves to first)
|
|
33
|
+
|
|
34
|
+
require 'bundler/inline'
|
|
35
|
+
|
|
36
|
+
gemfile do
|
|
37
|
+
source 'https://rubygems.org'
|
|
38
|
+
gem "rails"
|
|
39
|
+
gem "localhost"
|
|
40
|
+
gem "falcon"
|
|
41
|
+
gem "puma"
|
|
42
|
+
gem "protocol-http"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
46
|
+
|
|
47
|
+
require "benchmark"
|
|
48
|
+
require "localhost/authority"
|
|
49
|
+
require "quicsilver"
|
|
50
|
+
require_relative "helpers"
|
|
51
|
+
|
|
52
|
+
REQUESTS = Integer(ENV.fetch("REQUESTS", "10"))
|
|
53
|
+
CONNECTIONS = Integer(ENV.fetch("CONNECTIONS", "1"))
|
|
54
|
+
STREAMS = Integer(ENV.fetch("STREAMS", "1"))
|
|
55
|
+
WORKERS = Integer(ENV.fetch("WORKERS", "5"))
|
|
56
|
+
WORKLOAD = ENV.fetch("WORKLOAD", "tiny")
|
|
57
|
+
SLEEP_SECONDS = Float(ENV.fetch("SLEEP_SECONDS", "0.1"))
|
|
58
|
+
HOST = ENV.fetch("HOST", "localhost")
|
|
59
|
+
PORT = Integer(ENV.fetch("PORT", Benchmarks.random_port.to_s))
|
|
60
|
+
PATH = Benchmarks.path_for(WORKLOAD)
|
|
61
|
+
APP = Benchmarks.rails_app(sleep_seconds: SLEEP_SECONDS, secret_key_base: "quicsilver-benchmark")
|
|
62
|
+
|
|
63
|
+
def start_server
|
|
64
|
+
authority = Localhost::Authority.fetch
|
|
65
|
+
config = Quicsilver::Transport::Configuration.new(authority.certificate_path, authority.key_path)
|
|
66
|
+
server = Quicsilver::Server.new(
|
|
67
|
+
PORT,
|
|
68
|
+
address: ENV.fetch("ADDRESS", "::1"),
|
|
69
|
+
app: APP,
|
|
70
|
+
server_configuration: config,
|
|
71
|
+
threads: WORKERS,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
thread = Thread.new { server.start }
|
|
75
|
+
thread.abort_on_exception = true
|
|
76
|
+
wait_for_server(thread)
|
|
77
|
+
[server, thread]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def wait_for_server(thread)
|
|
81
|
+
deadline = Time.now + 10
|
|
82
|
+
|
|
83
|
+
while Time.now < deadline
|
|
84
|
+
abort "server exited while booting" unless thread.alive?
|
|
85
|
+
|
|
86
|
+
begin
|
|
87
|
+
client = Quicsilver::Client.new(HOST, PORT, connection_timeout: 500, request_timeout: 1)
|
|
88
|
+
client.open_connection
|
|
89
|
+
response = client.get("/")
|
|
90
|
+
return if response&.status == 200
|
|
91
|
+
warn "readiness check returned HTTP #{response&.status}: #{response&.body&.byteslice(0, 120).inspect}" if ENV["DEBUG"]
|
|
92
|
+
rescue StandardError => error
|
|
93
|
+
warn "readiness check failed: #{error.class}: #{error.message}" if ENV["DEBUG"]
|
|
94
|
+
sleep 0.1
|
|
95
|
+
ensure
|
|
96
|
+
client&.disconnect rescue nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
abort "server failed to boot: quicsilver://127.0.0.1:#{PORT}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def stop_server(server, thread)
|
|
104
|
+
server&.stop rescue nil
|
|
105
|
+
thread&.join(5)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def start_request(connection, path)
|
|
109
|
+
connection.get(path) { |request| request }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def finish_request(request)
|
|
113
|
+
request.response
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def request(connection, path)
|
|
117
|
+
finish_request(start_request(connection, path))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def open_connections
|
|
121
|
+
Array.new(CONNECTIONS) do
|
|
122
|
+
Quicsilver::Client.new(HOST, PORT, connection_timeout: 5000, request_timeout: 10).tap(&:open_connection)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def measure
|
|
127
|
+
connections = open_connections
|
|
128
|
+
# Untimed request per connection to warm Rails and establish steady connection state.
|
|
129
|
+
connections.each { |connection| request(connection, PATH) }
|
|
130
|
+
|
|
131
|
+
times = []
|
|
132
|
+
failed = 0
|
|
133
|
+
|
|
134
|
+
elapsed = Benchmark.realtime do
|
|
135
|
+
remaining = REQUESTS
|
|
136
|
+
|
|
137
|
+
while remaining.positive?
|
|
138
|
+
batch = []
|
|
139
|
+
|
|
140
|
+
connections.each do |connection|
|
|
141
|
+
STREAMS.times do
|
|
142
|
+
break if batch.length >= remaining
|
|
143
|
+
|
|
144
|
+
batch << [Benchmarks.now, start_request(connection, PATH)]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
batch.each do |started_at, pending_request|
|
|
149
|
+
finish_request(pending_request)&.status == 200 ? times << (Benchmarks.now - started_at) : failed += 1
|
|
150
|
+
rescue StandardError
|
|
151
|
+
failed += 1
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
remaining -= batch.length
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
Benchmarks::Result.new(
|
|
159
|
+
requests: REQUESTS,
|
|
160
|
+
connections: CONNECTIONS,
|
|
161
|
+
streams: STREAMS,
|
|
162
|
+
times: times,
|
|
163
|
+
failed: failed,
|
|
164
|
+
elapsed: elapsed,
|
|
165
|
+
)
|
|
166
|
+
ensure
|
|
167
|
+
connections&.each { |connection| connection.disconnect rescue nil }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
begin
|
|
171
|
+
server, thread = start_server
|
|
172
|
+
Benchmarks.print_result("Quicsilver Rails baseline", measure, path: PATH, workers: WORKERS)
|
|
173
|
+
ensure
|
|
174
|
+
stop_server(server, thread)
|
|
175
|
+
end
|