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
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