quicsilver 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e879e4531621883da97721256a2b0db478ba995d8bf54c93ddf959fca9cd36d
4
- data.tar.gz: 579023d40239d8c7a77671be44fb37265d735f864a13072887598406908c0aae
3
+ metadata.gz: eb40bd31dbb1c684ae68ec9b8a5f9fd0caee6972d360dc2be67984e473fdd15a
4
+ data.tar.gz: 2f46305042b7253d9fedc0018947404d6f27b045aef43be7a03ec32d3fe0bb8d
5
5
  SHA512:
6
- metadata.gz: 25c070017404e15d673cfcb267ef90722dd255af0c68dd3488f1065ebe8fcc84614054b7ae3b5b0e2cd1bc74ca725a7ddd4e0500b80f849580ebef3306705443
7
- data.tar.gz: 61f273afb32f2a45d07bc2f634a8f6d52973654b4e874ed01dd87a674126046f2e332741ca464b6f41a025e206064172a9614c602bb7285dab0b68b6efa7d09a
6
+ metadata.gz: 48810a1f5a78021b027cb416bca181388b23889456e61c4c05702bd228f15da52026c25b2eedf375473054133300c62efd88c486367ddb987490676c8575a302
7
+ data.tar.gz: 7ebe3bdb83efaf1854655f6553e324ee4c425c4b2d202e76cb2c2adce0bfd814e567003fb2b315cb6b373acf8cfed8fb7f08dffa3859e4e103e3480d4877d8c6
@@ -0,0 +1,42 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ push:
12
+ branches: [ "main" ]
13
+ pull_request:
14
+ branches: [ "main" ]
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ test:
21
+
22
+ runs-on: ubuntu-24.04
23
+ strategy:
24
+ matrix:
25
+ ruby-version: ["3.2"]
26
+
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - name: Set up Ruby
30
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
31
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
32
+ # uses: ruby/setup-ruby@v1
33
+ uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0
34
+ with:
35
+ ruby-version: ${{ matrix.ruby-version }}
36
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
37
+ - name: Build MSQUIC
38
+ run: bundle exec rake build_msquic
39
+ - name: Compile
40
+ run: bundle exec rake compile
41
+ - name: Run tests
42
+ run: bundle exec rake
data/.gitignore CHANGED
@@ -8,5 +8,7 @@
8
8
  /tmp/
9
9
  /vendor/*
10
10
  /certs/*
11
+ /certificates/*
11
12
  *.bundle
12
- lib/quicsilver/quicsilver.bundle
13
+ lib/quicsilver/quicsilver.bundle
14
+ *.gem
data/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.0] - 2025-12-17
9
+
10
+ ### Added
11
+ - Graceful shutdown with GOAWAY frames (RFC 9114 compliant)
12
+ - Streaming response support for lazy/chunked bodies
13
+ - Flow control settings for backpressure management
14
+ - Client HTTP verb helpers: `get`, `post`, `patch`, `delete`, `head`
15
+ - Integration test suite
16
+ - Benchmarking examples for Rails
17
+
18
+ ### Fixed
19
+ - Memory leak: send buffers now freed on SEND_COMPLETE callback
20
+ - Segfault in event loop when client_obj was invalid
21
+ - Content-Type header handling for Rack compatibility
22
+ - String concatenation performance using StringIO
23
+
24
+ ### Changed
25
+ - `server.start` now blocks until shutdown (no separate `wait_for_connections` needed)
26
+ - Refactored to global event loop architecture
27
+ - Simplified server internals with Connection and QuicStream classes
28
+ - Replaced debug puts with proper logging
29
+
30
+ ### Limitations
31
+ - No server push or trailer support
32
+ - No dynamic QPACK table (static table only)
33
+ - Client does not reuse connections
34
+
8
35
  ## [0.1.0] - 2025-10-28
9
36
 
10
37
  ### Added
@@ -15,8 +42,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
42
  - QPACK header compression (static table support)
16
43
  - Bidirectional request/response streams
17
44
  - Request body buffering for large payloads
18
-
19
- ### Limitations
20
- This is still in prototype, it has the following known limitations:
21
- - No server push, GOAWAY, or trailer support
22
- - Limited error handling
data/Gemfile.lock CHANGED
@@ -2,11 +2,20 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  quicsilver (0.1.0)
5
+ localhost (~> 1.6)
6
+ rack (~> 3.0)
7
+ rackup (~> 2.0)
5
8
 
6
9
  GEM
7
10
  remote: https://rubygems.org/
8
11
  specs:
12
+ localhost (1.6.0)
9
13
  minitest (5.25.5)
14
+ minitest-focus (1.4.0)
15
+ minitest (>= 4, < 6)
16
+ rack (3.2.4)
17
+ rackup (2.2.1)
18
+ rack (>= 3)
10
19
  rake (10.5.0)
11
20
  rake-compiler (1.3.0)
12
21
  rake
@@ -19,6 +28,7 @@ PLATFORMS
19
28
  DEPENDENCIES
20
29
  bundler (~> 2.0)
21
30
  minitest (~> 5.0)
31
+ minitest-focus (~> 1.3)
22
32
  quicsilver!
23
33
  rake (~> 10.0)
24
34
  rake-compiler (~> 1.2)
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Haroon Ahmed
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  HTTP/3 server for Ruby with Rack support.
4
4
 
5
- Disclaimer: currenly in early prototype.
5
+ **Status:** Experimental (v0.2.0)
6
6
 
7
7
  ## Installation
8
8
 
@@ -15,89 +15,68 @@ rake compile
15
15
 
16
16
  ## Quick Start
17
17
 
18
- ### 1. Set up certificates
19
-
20
- ```bash
21
- bash examples/setup_certs.sh
22
- ```
23
-
24
- ### 2. Run a Rack app over HTTP/3
18
+ ### Server
25
19
 
26
20
  ```ruby
27
21
  require "quicsilver"
28
22
 
29
- # Define your Rack app
30
23
  app = ->(env) {
31
- path = env['PATH_INFO']
32
-
33
- case path
24
+ case env['PATH_INFO']
34
25
  when '/'
35
- [200, {'Content-Type' => 'text/plain'}, ["Hello HTTP/3!"]]
26
+ [200, {'content-type' => 'text/plain'}, ["Hello HTTP/3!"]]
36
27
  when '/api/users'
37
- [200, {'Content-Type' => 'application/json'}, ['{"users": ["alice", "bob"]}']]
28
+ [200, {'content-type' => 'application/json'}, ['{"users": ["alice", "bob"]}']]
38
29
  else
39
- [404, {'Content-Type' => 'text/plain'}, ["Not Found"]]
30
+ [404, {'content-type' => 'text/plain'}, ["Not Found"]]
40
31
  end
41
32
  }
42
33
 
43
- # Start HTTP/3 server with Rack app
44
34
  server = Quicsilver::Server.new(4433, app: app)
45
- server.start
46
- server.wait_for_connections
35
+ server.start # Blocks until shutdown
47
36
  ```
48
37
 
49
- ### 3. Test with the client
50
-
51
- ```bash
52
- ruby examples/minimal_http3_client.rb
53
- ```
54
-
55
- ## Usage
56
-
57
- ### Rack HTTP/3 Server
38
+ ### Client
58
39
 
59
40
  ```ruby
60
41
  require "quicsilver"
61
42
 
62
- app = ->(env) {
63
- [200, {'Content-Type' => 'text/html'}, ["<h1>Hello from HTTP/3!</h1>"]]
64
- }
43
+ client = Quicsilver::Client.new("127.0.0.1", 4433, unsecure: true)
44
+ client.connect
65
45
 
66
- server = Quicsilver::Server.new(4433, app: app)
67
- server.start
68
- server.wait_for_connections
46
+ response = client.get("/api/users")
47
+ puts response[:body]
48
+
49
+ response = client.post("/api/users", body: '{"name": "charlie"}')
50
+
51
+ client.disconnect
69
52
  ```
70
53
 
71
- ### HTTP/3 Client
54
+ ## Usage with Rails
72
55
 
73
- ```ruby
74
- require "quicsilver"
56
+ ```bash
57
+ rackup -s quicsilver -p 4433
58
+ ```
75
59
 
76
- client = Quicsilver::Client.new("127.0.0.1", 4433, unsecure: true)
77
- client.connect
60
+ ## Configuration
78
61
 
79
- # Send HTTP/3 request
80
- request = Quicsilver::HTTP3::RequestEncoder.new(
81
- method: 'GET',
82
- path: '/api/users',
83
- authority: 'example.com'
62
+ ```ruby
63
+ server = Quicsilver::Server.new(4433,
64
+ app: app,
65
+ address: "0.0.0.0",
66
+ idle_timeout: 10_000, # Connection idle timeout (ms)
67
+ initial_window_size: 65536, # Flow control window
68
+ max_streams_bidi: 100, # Max concurrent requests
69
+ cert_path: "/path/to/cert.pem", # TLS certificate
70
+ key_path: "/path/to/key.pem" # TLS private key
84
71
  )
85
- client.send_data(request.encode)
86
-
87
- client.disconnect
88
72
  ```
89
73
 
90
74
  ## Development
91
75
 
92
76
  ```bash
93
- # Run tests
94
- rake test
95
-
96
- # Build extension
97
- rake compile
98
-
99
- # Clean build artifacts
100
- rake clean
77
+ rake compile # Build C extension
78
+ rake test # Run tests (122 passing)
79
+ rake clean # Clean build artifacts
101
80
  ```
102
81
 
103
82
  ## License
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+ # Usage: REQUESTS=1000 CONNECTIONS=10 ruby benchmarks/benchmark.rb
3
+
4
+ require 'bundler/setup'
5
+ require 'quicsilver'
6
+ require 'benchmark'
7
+
8
+ REQUESTS = ENV.fetch('REQUESTS', 1000).to_i
9
+ CONNECTIONS = ENV.fetch('CONNECTIONS', 1).to_i
10
+ HOST = ENV.fetch('HOST', '127.0.0.1')
11
+ PORT = ENV.fetch('PORT', 4433).to_i
12
+
13
+ puts "Quicsilver HTTP/3 Benchmark"
14
+ puts "=" * 50
15
+ puts "Target: #{HOST}:#{PORT}"
16
+ puts "Requests: #{REQUESTS} | Connections: #{CONNECTIONS}"
17
+ puts "=" * 50
18
+
19
+ results = []
20
+ mutex = Mutex.new
21
+
22
+ elapsed = Benchmark.realtime do
23
+ threads = CONNECTIONS.times.map do |conn_id|
24
+ Thread.new do
25
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
26
+ client.connect
27
+
28
+ local_times = []
29
+ per_conn = REQUESTS / CONNECTIONS
30
+
31
+ per_conn.times do |i|
32
+ start = Time.now
33
+ begin
34
+ client.get("/")
35
+ local_times << (Time.now - start)
36
+ rescue
37
+ local_times << nil
38
+ end
39
+ print "." if conn_id == 0 && i % (per_conn / 10).clamp(1, 100) == 0
40
+ end
41
+
42
+ client.disconnect
43
+ mutex.synchronize { results.concat(local_times) }
44
+ end
45
+ end
46
+ threads.each(&:join)
47
+ end
48
+
49
+ times = results.compact
50
+ failed = results.count(&:nil?)
51
+
52
+ puts "\n" + "=" * 50
53
+ puts "RESULTS"
54
+ puts "=" * 50
55
+ puts "Total: #{elapsed.round(3)}s"
56
+ puts "Req/sec: #{(REQUESTS / elapsed).round(2)}"
57
+ puts "Success: #{times.size} | Failed: #{failed}"
58
+
59
+ if times.any?
60
+ sorted = times.sort
61
+ puts "Latency: avg=#{(times.sum / times.size * 1000).round(2)}ms " \
62
+ "min=#{(sorted.first * 1000).round(2)}ms " \
63
+ "max=#{(sorted.last * 1000).round(2)}ms"
64
+ puts " p50=#{(sorted[sorted.size / 2] * 1000).round(2)}ms " \
65
+ "p95=#{(sorted[(sorted.size * 0.95).to_i] * 1000).round(2)}ms " \
66
+ "p99=#{(sorted[(sorted.size * 0.99).to_i] * 1000).round(2)}ms"
67
+ end
68
+ puts "=" * 50
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "quicsilver"
5
+ require 'localhost/authority'
6
+ require 'json'
7
+
8
+ SIMPLE_APP = lambda do |env|
9
+ case env['PATH_INFO']
10
+ when '/'
11
+ [200, {'Content-Type' => 'text/plain'}, ['OK']]
12
+ when '/json'
13
+ body = JSON.generate({status: 'ok', timestamp: Time.now.to_i})
14
+ [200, {'Content-Type' => 'application/json'}, [body]]
15
+ when '/echo'
16
+ [200, {'Content-Type' => 'text/plain'}, [env['REQUEST_METHOD']]]
17
+ else
18
+ [404, {'Content-Type' => 'text/plain'}, ['Not Found']]
19
+ end
20
+ end
21
+
22
+ port = ENV["PORT"] || 4433
23
+ default_host = ENV["HOST"] || "0.0.0.0"
24
+
25
+ authority = Localhost::Authority.fetch
26
+ cert_file = authority.certificate_path
27
+ key_file = authority.key_path
28
+
29
+ config = ::Quicsilver::ServerConfiguration.new(cert_file, key_file)
30
+
31
+ server = ::Quicsilver::Server.new(
32
+ port.to_i,
33
+ address: default_host,
34
+ app: SIMPLE_APP,
35
+ server_configuration: config
36
+ )
37
+
38
+ puts "Starting Quicsilver on port #{port}..."
39
+
40
+ trap("INT") do
41
+ puts "\nStopping server..."
42
+ server.stop
43
+ exit
44
+ end
45
+
46
+ server.start # Blocks until shutdown
@@ -6,12 +6,6 @@ require "quicsilver"
6
6
  puts "🚀 Minimal HTTP/3 Server Example"
7
7
  puts "=" * 40
8
8
 
9
- # First, set up certificates if they don't exist
10
- unless File.exist?("certs/server.crt") && File.exist?("certs/server.key")
11
- puts "📝 Setting up certificates..."
12
- system("bash examples/setup_certs.sh")
13
- end
14
-
15
9
  # Create and start the server
16
10
  server = Quicsilver::Server.new(4433)
17
11
 
@@ -6,12 +6,6 @@ require "quicsilver"
6
6
  puts "🚀 Rack HTTP/3 Server Example"
7
7
  puts "=" * 40
8
8
 
9
- # First, set up certificates if they don't exist
10
- unless File.exist?("certs/server.crt") && File.exist?("certs/server.key")
11
- puts "📝 Setting up certificates..."
12
- system("bash examples/setup_certs.sh")
13
- end
14
-
15
9
  # Define a simple Rack app
16
10
  app = ->(env) {
17
11
  path = env['PATH_INFO']
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "quicsilver"
5
+
6
+ puts "🔌 Simple HTTP/3 Client Test"
7
+ puts "=" * 40
8
+
9
+ begin
10
+ client = Quicsilver::Client.new("127.0.0.1", 4433, unsecure: true)
11
+
12
+ client.connect
13
+
14
+ response = client.get("/posts")
15
+
16
+ puts "Status: #{response[:status]}"
17
+ puts "Headers: #{response[:headers].inspect}"
18
+ puts "Body: #{response[:body]}"
19
+
20
+ rescue => e
21
+ puts "❌ Error: #{e.class} - #{e.message}"
22
+ puts e.backtrace.first(10)
23
+ ensure
24
+ client&.disconnect
25
+ puts "👋 Done"
26
+ end