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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
class Server
|
|
5
|
+
class RequestRegistry
|
|
6
|
+
def initialize
|
|
7
|
+
@requests = {}
|
|
8
|
+
@mutex = Mutex.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def track(stream_id, connection_handle, path:, method:, started_at: Time.now)
|
|
12
|
+
@mutex.synchronize do
|
|
13
|
+
@requests[key_for(stream_id, connection_handle)] = {
|
|
14
|
+
connection_handle: connection_handle,
|
|
15
|
+
stream_id: stream_id,
|
|
16
|
+
path: path,
|
|
17
|
+
method: method,
|
|
18
|
+
started_at: started_at
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def complete(stream_id, connection_handle)
|
|
24
|
+
@mutex.synchronize do
|
|
25
|
+
@requests.delete(key_for(stream_id, connection_handle))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def active_count
|
|
30
|
+
@mutex.synchronize { @requests.size }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def active_requests
|
|
34
|
+
@mutex.synchronize { @requests.dup }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def requests_older_than(seconds)
|
|
38
|
+
cutoff = Time.now - seconds
|
|
39
|
+
@mutex.synchronize do
|
|
40
|
+
@requests.select { |_, r| r[:started_at] < cutoff }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def empty?
|
|
45
|
+
@mutex.synchronize { @requests.empty? }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def include?(stream_id, connection_handle)
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
@requests.key?(key_for(stream_id, connection_handle))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def key_for(stream_id, connection_handle)
|
|
57
|
+
raise ArgumentError, "Connection handle is required" if connection_handle.nil?
|
|
58
|
+
|
|
59
|
+
[connection_handle, stream_id]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
class Server
|
|
5
|
+
# Base class for work schedulers.
|
|
6
|
+
#
|
|
7
|
+
# A scheduler controls how incoming request work units are executed —
|
|
8
|
+
# via threads, fibers, or any other concurrency model. Subclasses
|
|
9
|
+
# implement the delivery mechanism.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# scheduler = ThreadScheduler.new(size: 10, max_queue_size: 100, &handler)
|
|
13
|
+
# scheduler.start # start workers
|
|
14
|
+
# scheduler.enqueue(work) # schedule a work unit
|
|
15
|
+
# scheduler.drain(timeout: 5) # wait for queue to empty
|
|
16
|
+
# scheduler.stop # shut down workers
|
|
17
|
+
#
|
|
18
|
+
class Scheduler
|
|
19
|
+
# Schedule a work unit for execution.
|
|
20
|
+
def enqueue(work)
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Is the work queue at capacity?
|
|
25
|
+
def full?
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Number of pending work units.
|
|
30
|
+
def pending
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Wait for the queue to empty.
|
|
35
|
+
def drain(timeout: 5)
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Start processing work units.
|
|
40
|
+
def start
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Shut down workers.
|
|
45
|
+
def stop
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../scheduler"
|
|
4
|
+
|
|
5
|
+
module Quicsilver
|
|
6
|
+
class Server
|
|
7
|
+
module Schedulers
|
|
8
|
+
# Fiber-based scheduler — uses Async::Queue for cooperative scheduling.
|
|
9
|
+
# Expects to be running inside an Async reactor (e.g. Falcon provides one).
|
|
10
|
+
# Requires the async gem.
|
|
11
|
+
class FiberScheduler < Scheduler
|
|
12
|
+
def initialize(concurrency:, max_queue_size:, &handler)
|
|
13
|
+
require "async"
|
|
14
|
+
require "async/queue"
|
|
15
|
+
require "async/barrier"
|
|
16
|
+
|
|
17
|
+
@concurrency = concurrency
|
|
18
|
+
@max_queue_size = max_queue_size
|
|
19
|
+
@handler = handler
|
|
20
|
+
@queue = Async::Queue.new
|
|
21
|
+
@running = false
|
|
22
|
+
@barrier = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def enqueue(work)
|
|
26
|
+
@queue.enqueue(work)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def full?
|
|
30
|
+
@queue.size >= @max_queue_size
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def pending
|
|
34
|
+
@queue.size
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Spawn fiber workers inside the current Async reactor.
|
|
38
|
+
# Caller must already be inside Async do (e.g. Falcon).
|
|
39
|
+
def start
|
|
40
|
+
@running = true
|
|
41
|
+
@barrier = Async::Barrier.new
|
|
42
|
+
|
|
43
|
+
@concurrency.times do
|
|
44
|
+
@barrier.async do
|
|
45
|
+
while @running
|
|
46
|
+
work = @queue.dequeue
|
|
47
|
+
break if work == :shutdown
|
|
48
|
+
|
|
49
|
+
run(work)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# One unit of work must not take the worker fiber down with it; see
|
|
56
|
+
# the thread scheduler for the same reasoning.
|
|
57
|
+
private def run(work)
|
|
58
|
+
@handler.call(work)
|
|
59
|
+
rescue => e
|
|
60
|
+
Quicsilver.logger.error("Unhandled error in worker: #{e.class} - #{e.message}")
|
|
61
|
+
Quicsilver.logger.debug(e.backtrace&.first(5)&.join("\n").to_s)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def drain(timeout: 5)
|
|
65
|
+
deadline = Time.now + timeout
|
|
66
|
+
while @queue.size > 0 && Time.now < deadline
|
|
67
|
+
sleep 0.05
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def stop
|
|
72
|
+
@running = false
|
|
73
|
+
@concurrency.times { @queue.enqueue(:shutdown) }
|
|
74
|
+
@barrier&.wait
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../scheduler"
|
|
4
|
+
|
|
5
|
+
module Quicsilver
|
|
6
|
+
class Server
|
|
7
|
+
module Schedulers
|
|
8
|
+
# Thread-based scheduler — uses a thread pool with Thread::Queue.
|
|
9
|
+
class ThreadScheduler < Scheduler
|
|
10
|
+
def initialize(concurrency:, max_queue_size:, &handler)
|
|
11
|
+
@size = concurrency
|
|
12
|
+
@max_queue_size = max_queue_size
|
|
13
|
+
@handler = handler
|
|
14
|
+
@queue = Queue.new
|
|
15
|
+
@threads = []
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def enqueue(work)
|
|
20
|
+
@queue.push(work)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def full?
|
|
24
|
+
@queue.size >= @max_queue_size
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def pending
|
|
28
|
+
@queue.size
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def start
|
|
32
|
+
@size.times do
|
|
33
|
+
thread = Thread.new do
|
|
34
|
+
while (work = @queue.pop)
|
|
35
|
+
break if work == :shutdown
|
|
36
|
+
|
|
37
|
+
run(work)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
@mutex.synchronize { @threads << thread }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def drain(timeout: 5)
|
|
45
|
+
deadline = Time.now + timeout
|
|
46
|
+
while @queue.size > 0 && Time.now < deadline
|
|
47
|
+
sleep 0.05
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# One unit of work must not take the worker down with it. An escaping
|
|
52
|
+
# error would end the thread, shrinking the pool for the rest of the
|
|
53
|
+
# process, and Thread#join would re-raise it in whoever stops the
|
|
54
|
+
# server. DrainTimeoutError is our own interruption, so it ends the
|
|
55
|
+
# worker as intended.
|
|
56
|
+
private def run(work)
|
|
57
|
+
@handler.call(work)
|
|
58
|
+
rescue DrainTimeoutError
|
|
59
|
+
raise
|
|
60
|
+
rescue => e
|
|
61
|
+
Quicsilver.logger.error("Unhandled error in worker: #{e.class} - #{e.message}")
|
|
62
|
+
Quicsilver.logger.debug(e.backtrace&.first(5)&.join("\n").to_s)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def stop
|
|
66
|
+
@size.times { @queue.push(:shutdown) }
|
|
67
|
+
@mutex.synchronize do
|
|
68
|
+
@threads.each { |t| t.join(2) }
|
|
69
|
+
@threads.each { |t| t.raise(DrainTimeoutError, "drain timeout") if t.alive? }
|
|
70
|
+
@threads.clear
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|