raptor 0.22.0 → 0.22.1
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/CHANGELOG.md +6 -0
- data/README.md +15 -15
- data/docs/raptor-vs-puma.md +1 -1
- data/lib/raptor/cli.rb +5 -4
- data/lib/raptor/cluster.rb +4 -4
- data/lib/raptor/control_server.rb +36 -17
- data/lib/raptor/detached_body.rb +47 -4
- data/lib/raptor/http.rb +10 -0
- data/lib/raptor/http1.rb +27 -3
- data/lib/raptor/http2.rb +33 -9
- data/lib/raptor/reactor.rb +128 -19
- data/lib/raptor/thread_locals.rb +4 -2
- data/lib/raptor/version.rb +1 -1
- data/lib/raptor.rb +0 -1
- data/sig/generated/raptor/control_server.rbs +17 -5
- data/sig/generated/raptor/detached_body.rbs +45 -1
- data/sig/generated/raptor/http.rbs +8 -0
- data/sig/generated/raptor/http1.rbs +24 -0
- data/sig/generated/raptor/http2.rbs +29 -6
- data/sig/generated/raptor/reactor.rbs +105 -3
- data/sig/generated/raptor/thread_locals.rbs +3 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bfbcd63c857341a9ad73f6d4d40d03dac757deb1074dde954dc966b9b268822b
|
|
4
|
+
data.tar.gz: 2d2d78611b34df0061f231c18d0c3c0e4bc13f8314b1e61bcfcb9ef7a215297d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6898ded5b172745d15dd411bc6638686cccb07485ca03cf5b6857194e4b8d263e9b02da8fd24f287dd1a60d43a128f7cd2e5524a1e95122233023c58b18fb086
|
|
7
|
+
data.tar.gz: 18081bd442db06fc3d60b2686eda2bfb0b36118f6709b23ed88ac8ede2e0a03a39b4ac4bc3c8d95c7d3b50fff52ec071bc414e9041ba7ae9d747fbb450bc0cf6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.22.1] - 2026-10-03
|
|
4
|
+
|
|
5
|
+
- Stop waiting for more HTTP/2 frames on the collector thread
|
|
6
|
+
- Run `DetachedBody#on_open` callbacks on the request thread
|
|
7
|
+
- Attach detached response bodies without polling
|
|
8
|
+
|
|
3
9
|
## [0.22.0] - 2026-10-03
|
|
4
10
|
|
|
5
11
|
- Add detached HTTP/1.1 response bodies
|
data/README.md
CHANGED
|
@@ -37,7 +37,7 @@ run proc { |_env| [200, { "content-type" => "text/plain" }, ["Hello, World!"]] }
|
|
|
37
37
|
```
|
|
38
38
|
> bundle exec raptor -w 10 -t 3 hello_world.ru
|
|
39
39
|
[Raptor 72876|Main|Main] Cluster initializing:
|
|
40
|
-
[Raptor 72876|Main|Main] ├─ Version: 0.22.
|
|
40
|
+
[Raptor 72876|Main|Main] ├─ Version: 0.22.1
|
|
41
41
|
[Raptor 72876|Main|Main] ├─ Ruby Version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
|
|
42
42
|
[Raptor 72876|Main|Main] ├─ Environment: development
|
|
43
43
|
[Raptor 72876|Main|Main] ├─ Master PID: 72876
|
|
@@ -208,7 +208,7 @@ Set `control_url` to a Unix socket URL such as `unix:///tmp/raptor-control.sock`
|
|
|
208
208
|
|
|
209
209
|
## (Micro) Benchmarks
|
|
210
210
|
|
|
211
|
-
Raptor 0.22.
|
|
211
|
+
Raptor 0.22.1 vs Puma 8.0.2 vs Falcon 0.57.0 across two workload profiles. **IO-bound** is a GET endpoint that
|
|
212
212
|
interleaves 5-10 short sleeps (total 2.5-15ms) with small CPU work, simulating a read path that makes several DB or
|
|
213
213
|
cache calls. **CPU-bound** is a POST endpoint that accepts a small JSON body, interleaves 3-5 chunks of JSON item
|
|
214
214
|
building (total 450-1500 items) with sub-100µs sleeps, and returns the built array, simulating a write path that does
|
|
@@ -222,22 +222,22 @@ disabled, and both threaded servers allow 999 requests per HTTP/1.1 keep-alive c
|
|
|
222
222
|
Each cell reports the median throughput and median p95 latency independently across 3 runs, so the two numbers in a row
|
|
223
223
|
may come from different runs. Every run starts a fresh server process so the samples are independent of each other;
|
|
224
224
|
state accumulated in a previous run cannot bias the next. Across the whole table, the widest spread
|
|
225
|
-
((max - min) / 2 / median) between runs of a single cell was ±
|
|
225
|
+
((max - min) / 2 / median) between runs of a single cell was ±19.6% for throughput and ±13.6% for p95.
|
|
226
226
|
|
|
227
227
|
| Protocol | Workload | Raptor mode | Raptor req/s | Raptor p95 | Puma req/s | Puma p95 | vs Puma req/s | vs Puma p95 | Falcon req/s | Falcon p95 | vs Falcon req/s | vs Falcon p95 |
|
|
228
228
|
| --------------------- | -------- | ----------- | ------------ | ---------- | ----------- | --------- | ------------- | ------------ | ------------ | ---------- | --------------- | ------------- |
|
|
229
|
-
| HTTP/1.1 | IO | Fixed | 2.
|
|
230
|
-
| HTTP/1.1 | IO | Scaling | 7.
|
|
231
|
-
| HTTP/1.1 | CPU | Fixed | 7.
|
|
232
|
-
| HTTP/1.1 | CPU | Scaling | 6.
|
|
233
|
-
| HTTP/1.1 (keep-alive) | IO | Fixed | 2.
|
|
234
|
-
| HTTP/1.1 (keep-alive) | IO | Scaling | 8.
|
|
235
|
-
| HTTP/1.1 (keep-alive) | CPU | Fixed | 7.
|
|
236
|
-
| HTTP/1.1 (keep-alive) | CPU | Scaling | 7.
|
|
237
|
-
| HTTP/2 | IO | Fixed | 1.
|
|
238
|
-
| HTTP/2 | IO | Scaling |
|
|
239
|
-
| HTTP/2 | CPU | Fixed |
|
|
240
|
-
| HTTP/2 | CPU | Scaling | 7.
|
|
229
|
+
| HTTP/1.1 | IO | Fixed | 2.95k req/s | 80.20 ms | 1.55k req/s | 122.60 ms | 90.1% higher | 34.6% lower | 12.24k req/s | 14.00 ms | 75.9% lower | 472.9% higher |
|
|
230
|
+
| HTTP/1.1 | IO | Scaling | 7.05k req/s | 30.40 ms | 1.55k req/s | 122.60 ms | 354.1% higher | 75.2% lower | 12.24k req/s | 14.00 ms | 42.4% lower | 117.1% higher |
|
|
231
|
+
| HTTP/1.1 | CPU | Fixed | 7.29k req/s | 35.00 ms | 8.73k req/s | 20.30 ms | 16.4% lower | 72.4% higher | 6.71k req/s | 26.80 ms | 8.7% higher | 30.6% higher |
|
|
232
|
+
| HTTP/1.1 | CPU | Scaling | 6.73k req/s | 36.90 ms | 8.73k req/s | 20.30 ms | 22.8% lower | 81.8% higher | 6.71k req/s | 26.80 ms | 0.4% higher | 37.7% higher |
|
|
233
|
+
| HTTP/1.1 (keep-alive) | IO | Fixed | 2.52k req/s | 70.70 ms | 1.50k req/s | 102.60 ms | 67.8% higher | 31.1% lower | 6.23k req/s | 28.20 ms | 59.5% lower | 150.7% higher |
|
|
234
|
+
| HTTP/1.1 (keep-alive) | IO | Scaling | 8.19k req/s | 21.90 ms | 1.50k req/s | 102.60 ms | 445.8% higher | 78.7% lower | 6.23k req/s | 28.20 ms | 31.6% higher | 22.3% lower |
|
|
235
|
+
| HTTP/1.1 (keep-alive) | CPU | Fixed | 7.04k req/s | 28.30 ms | 8.62k req/s | 21.50 ms | 18.3% lower | 31.6% higher | 7.07k req/s | 32.10 ms | 0.4% lower | 11.8% lower |
|
|
236
|
+
| HTTP/1.1 (keep-alive) | CPU | Scaling | 7.78k req/s | 25.70 ms | 8.62k req/s | 21.50 ms | 9.8% lower | 19.5% higher | 7.07k req/s | 32.10 ms | 9.9% higher | 19.9% lower |
|
|
237
|
+
| HTTP/2 | IO | Fixed | 1.50k req/s | 128.92 ms | N/A | N/A | - | - | 6.57k req/s | 27.29 ms | 77.2% lower | 372.4% higher |
|
|
238
|
+
| HTTP/2 | IO | Scaling | 9.41k req/s | 19.30 ms | N/A | N/A | - | - | 6.57k req/s | 27.29 ms | 43.1% higher | 29.3% lower |
|
|
239
|
+
| HTTP/2 | CPU | Fixed | 7.75k req/s | 26.55 ms | N/A | N/A | - | - | 7.24k req/s | 49.42 ms | 7.1% higher | 46.3% lower |
|
|
240
|
+
| HTTP/2 | CPU | Scaling | 7.54k req/s | 26.97 ms | N/A | N/A | - | - | 7.24k req/s | 49.42 ms | 4.2% higher | 45.4% lower |
|
|
241
241
|
|
|
242
242
|
> ruby 4.0.7 (2026-09-15 revision 229531a6cf) +YJIT +PRISM [aarch64-linux]
|
|
243
243
|
> 10 worker processes; fixed Raptor and Puma run 3 threads per worker; scaling Raptor starts at 3 with no fixed limit;
|
data/docs/raptor-vs-puma.md
CHANGED
|
@@ -349,7 +349,7 @@ The `Writer` hands serialized frames to the reactor, which writes as the socket
|
|
|
349
349
|
|
|
350
350
|
Flow control uses similar CAS-protected atoms. Ordinary Rack bodies wait for connection and stream capacity as they yield. A `Raptor::DetachedBody` instead returns its application thread immediately; the reactor schedules its bounded buffer as capacity becomes available and closes it when the client cancels. This keeps long-lived streams from consuming one application thread each.
|
|
351
351
|
|
|
352
|
-
Frame processing also has an eager loop. After processing one batch of frames, the h2 handler
|
|
352
|
+
Frame processing also has an eager loop. After processing one batch of frames, the h2 handler checks `wait_readable(0)` and reads the next batch only if it has already arrived. Up to eight rounds are consumed inline before handing back to the reactor, and the loop bails out early once the app thread pool has more queued work than worker slots so one busy connection cannot starve the collector. This is the same principle as the HTTP/1.1 eager keep-alive: amortise the reactor round-trip when the client is actively sending, but back off under saturation.
|
|
353
353
|
|
|
354
354
|
During worker shutdown, Raptor stops accepting connections and sends GOAWAY with the last stream handed to the Rack application. Later streams are refused while application work and detached bodies drain. The reactor remains active so in-flight responses can receive flow-control updates; detached bodies still open when the drain period expires are cancelled before their connections close.
|
|
355
355
|
|
data/lib/raptor/cli.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# rbs_inline: enabled
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "concurrent/utility/processor_counter"
|
|
5
4
|
require "json"
|
|
6
5
|
require "optparse"
|
|
7
6
|
|
|
7
|
+
require "concurrent/utility/processor_counter"
|
|
8
|
+
|
|
8
9
|
require_relative "cluster"
|
|
9
10
|
|
|
10
11
|
module Raptor
|
|
@@ -46,18 +47,18 @@ module Raptor
|
|
|
46
47
|
chunk_data_timeout: 10,
|
|
47
48
|
write_timeout: 5,
|
|
48
49
|
max_body_size: nil,
|
|
49
|
-
body_spool_threshold: 1024 * 1024
|
|
50
|
+
body_spool_threshold: 1024 * 1024
|
|
50
51
|
},
|
|
51
52
|
http1: {
|
|
52
53
|
ractors: nil,
|
|
53
54
|
persistent_data_timeout: 65,
|
|
54
|
-
max_keepalive_requests: 1000
|
|
55
|
+
max_keepalive_requests: 1000
|
|
55
56
|
},
|
|
56
57
|
http2: {
|
|
57
58
|
ractors: nil,
|
|
58
59
|
max_concurrent_streams: 100,
|
|
59
60
|
keepalive_interval: 10,
|
|
60
|
-
keepalive_timeout: 5
|
|
61
|
+
keepalive_timeout: 5
|
|
61
62
|
},
|
|
62
63
|
worker_boot_timeout: 60,
|
|
63
64
|
worker_timeout: 60,
|
data/lib/raptor/cluster.rb
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# rbs_inline: enabled
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "concurrent/utility/processor_counter"
|
|
5
4
|
require "json"
|
|
6
5
|
require "time"
|
|
7
6
|
|
|
8
7
|
require "atomic-ruby/atomic_thread_pool"
|
|
8
|
+
require "concurrent/utility/processor_counter"
|
|
9
9
|
require "rack/builder"
|
|
10
10
|
require "ractor-pool"
|
|
11
11
|
|
|
@@ -339,8 +339,8 @@ module Raptor
|
|
|
339
339
|
pool_capacity: [capacity - total_work, 0].max,
|
|
340
340
|
busy_threads: active,
|
|
341
341
|
max_threads: capacity,
|
|
342
|
-
requests_count: stat.fetch(:requests, 0)
|
|
343
|
-
}
|
|
342
|
+
requests_count: stat.fetch(:requests, 0)
|
|
343
|
+
}
|
|
344
344
|
}
|
|
345
345
|
end
|
|
346
346
|
|
|
@@ -350,7 +350,7 @@ module Raptor
|
|
|
350
350
|
phase: @phase,
|
|
351
351
|
booted_workers: worker_status.count { |worker| worker[:booted] },
|
|
352
352
|
old_workers: worker_status.count { |worker| worker[:phase] != @phase },
|
|
353
|
-
worker_status: worker_status
|
|
353
|
+
worker_status: worker_status
|
|
354
354
|
}
|
|
355
355
|
end
|
|
356
356
|
|
|
@@ -5,17 +5,19 @@ require "json"
|
|
|
5
5
|
require "socket"
|
|
6
6
|
require "uri"
|
|
7
7
|
|
|
8
|
+
require "atomic-ruby/atom"
|
|
9
|
+
|
|
8
10
|
module Raptor
|
|
9
11
|
# Serves cluster statistics over a Unix socket.
|
|
10
12
|
#
|
|
11
13
|
class ControlServer
|
|
14
|
+
SHUTDOWN = :shutdown
|
|
15
|
+
|
|
12
16
|
# @rbs @path: String
|
|
13
17
|
# @rbs @stats: ^() -> Hash[Symbol, untyped]
|
|
14
18
|
# @rbs @server: UNIXServer?
|
|
15
|
-
# @rbs @client:
|
|
19
|
+
# @rbs @client: Atom
|
|
16
20
|
# @rbs @thread: Thread?
|
|
17
|
-
# @rbs @running: bool
|
|
18
|
-
# @rbs @mutex: Mutex
|
|
19
21
|
|
|
20
22
|
# Creates a control server for `url` without binding it.
|
|
21
23
|
#
|
|
@@ -32,10 +34,8 @@ module Raptor
|
|
|
32
34
|
@path = uri.path
|
|
33
35
|
@stats = stats
|
|
34
36
|
@server = nil
|
|
35
|
-
@client = nil
|
|
37
|
+
@client = Atom.new(nil)
|
|
36
38
|
@thread = nil
|
|
37
|
-
@running = false
|
|
38
|
-
@mutex = Mutex.new
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
# Binds the Unix socket.
|
|
@@ -54,7 +54,6 @@ module Raptor
|
|
|
54
54
|
#
|
|
55
55
|
# @rbs () -> void
|
|
56
56
|
def start
|
|
57
|
-
@running = true
|
|
58
57
|
owner_pid = Process.pid
|
|
59
58
|
at_exit { File.delete(@path) rescue nil if Process.pid == owner_pid }
|
|
60
59
|
|
|
@@ -71,17 +70,24 @@ module Raptor
|
|
|
71
70
|
#
|
|
72
71
|
# @rbs () -> void
|
|
73
72
|
def shutdown
|
|
74
|
-
|
|
75
|
-
@
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
client = nil
|
|
74
|
+
@client.swap do |current|
|
|
75
|
+
client = current
|
|
76
|
+
SHUTDOWN
|
|
78
77
|
end
|
|
78
|
+
@server&.close
|
|
79
|
+
client.close if client.is_a?(UNIXSocket)
|
|
79
80
|
@thread&.join
|
|
80
81
|
File.delete(@path) rescue nil
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
private
|
|
84
85
|
|
|
86
|
+
# Removes a stale socket while refusing to replace an active server.
|
|
87
|
+
#
|
|
88
|
+
# @return [void]
|
|
89
|
+
# @raise [RuntimeError] if another server is listening on the socket
|
|
90
|
+
#
|
|
85
91
|
# @rbs () -> void
|
|
86
92
|
def remove_stale_socket
|
|
87
93
|
return unless File.exist?(@path)
|
|
@@ -94,21 +100,34 @@ module Raptor
|
|
|
94
100
|
end
|
|
95
101
|
end
|
|
96
102
|
|
|
103
|
+
# Accepts and handles control requests until shutdown begins.
|
|
104
|
+
#
|
|
105
|
+
# @return [void]
|
|
106
|
+
#
|
|
97
107
|
# @rbs () -> void
|
|
98
108
|
def serve
|
|
99
|
-
|
|
109
|
+
until @client.value == SHUTDOWN
|
|
100
110
|
readable, = IO.select([@server], nil, nil, 1)
|
|
101
111
|
next unless readable
|
|
102
112
|
|
|
103
|
-
@
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
client = @server.accept_nonblock(exception: false)
|
|
114
|
+
next unless client.is_a?(UNIXSocket)
|
|
115
|
+
|
|
116
|
+
if @client.swap { |current| current == SHUTDOWN ? current : client } == SHUTDOWN
|
|
117
|
+
client.close
|
|
118
|
+
return
|
|
106
119
|
end
|
|
107
|
-
|
|
120
|
+
|
|
121
|
+
handle(client)
|
|
108
122
|
end
|
|
109
123
|
rescue IOError, Errno::EBADF
|
|
110
124
|
end
|
|
111
125
|
|
|
126
|
+
# Writes the response for one control-socket request.
|
|
127
|
+
#
|
|
128
|
+
# @param client [UNIXSocket] connected control client
|
|
129
|
+
# @return [void]
|
|
130
|
+
#
|
|
112
131
|
# @rbs (UNIXSocket client) -> void
|
|
113
132
|
def handle(client)
|
|
114
133
|
request_line = client.gets
|
|
@@ -125,7 +144,7 @@ module Raptor
|
|
|
125
144
|
rescue IOError, SystemCallError
|
|
126
145
|
ensure
|
|
127
146
|
client.close rescue nil
|
|
128
|
-
@
|
|
147
|
+
@client.swap { |current| current.equal?(client) ? nil : current }
|
|
129
148
|
end
|
|
130
149
|
end
|
|
131
150
|
end
|
data/lib/raptor/detached_body.rb
CHANGED
|
@@ -16,12 +16,22 @@ module Raptor
|
|
|
16
16
|
# @rbs @max_size: Integer
|
|
17
17
|
# @rbs @size: Atom
|
|
18
18
|
|
|
19
|
+
# Creates a shared budget with the given byte limit.
|
|
20
|
+
#
|
|
21
|
+
# @param max_size [Integer] maximum bytes that may be reserved
|
|
22
|
+
# @return [void]
|
|
23
|
+
#
|
|
19
24
|
# @rbs (Integer max_size) -> void
|
|
20
25
|
def initialize(max_size)
|
|
21
26
|
@max_size = max_size
|
|
22
27
|
@size = Atom.new(0)
|
|
23
28
|
end
|
|
24
29
|
|
|
30
|
+
# Reserves bytes when they fit within the shared limit.
|
|
31
|
+
#
|
|
32
|
+
# @param bytes [Integer] number of bytes to reserve
|
|
33
|
+
# @return [Boolean] whether the bytes were reserved
|
|
34
|
+
#
|
|
25
35
|
# @rbs (Integer bytes) -> bool
|
|
26
36
|
def reserve(bytes)
|
|
27
37
|
reserved = false
|
|
@@ -32,6 +42,11 @@ module Raptor
|
|
|
32
42
|
reserved
|
|
33
43
|
end
|
|
34
44
|
|
|
45
|
+
# Releases bytes previously charged to the shared limit.
|
|
46
|
+
#
|
|
47
|
+
# @param bytes [Integer] number of bytes to release
|
|
48
|
+
# @return [void]
|
|
49
|
+
#
|
|
35
50
|
# @rbs (Integer bytes) -> void
|
|
36
51
|
def release(bytes)
|
|
37
52
|
@size.swap { |size| size - bytes }
|
|
@@ -57,7 +72,7 @@ module Raptor
|
|
|
57
72
|
raise ArgumentError, "max_buffer_size must be positive" unless max_buffer_size.positive?
|
|
58
73
|
|
|
59
74
|
@max_buffer_size = max_buffer_size
|
|
60
|
-
@state = Atom.new({chunks: [], size: 0, closing: false, closed: false, notified: false, trailers: {}})
|
|
75
|
+
@state = Atom.new({ chunks: [], size: 0, closing: false, closed: false, notified: false, trailers: {} })
|
|
61
76
|
@budgets = []
|
|
62
77
|
@wake = proc {}
|
|
63
78
|
@dispatch = proc { |callback| callback.call }
|
|
@@ -66,7 +81,8 @@ module Raptor
|
|
|
66
81
|
@on_close = nil
|
|
67
82
|
end
|
|
68
83
|
|
|
69
|
-
# Registers a callback
|
|
84
|
+
# Registers a callback that runs on the request thread once the server
|
|
85
|
+
# accepts the response stream.
|
|
70
86
|
#
|
|
71
87
|
# @yieldparam stream [DetachedBody] the opened response stream
|
|
72
88
|
# @return [DetachedBody]
|
|
@@ -169,6 +185,12 @@ module Raptor
|
|
|
169
185
|
|
|
170
186
|
# Connects the body to its reactor-owned response stream.
|
|
171
187
|
#
|
|
188
|
+
# @param budgets [Array<Budget>] buffer budgets shared with other bodies
|
|
189
|
+
# @param wake [Proc] called when buffered data or a close is ready to write
|
|
190
|
+
# @param dispatch [Proc] schedules application callbacks off the reactor thread
|
|
191
|
+
# @param finished [Proc] called with the close reason after `on_close`
|
|
192
|
+
# @return [Boolean] false when the bytes already buffered exceed a budget
|
|
193
|
+
#
|
|
172
194
|
# @rbs (Array[Budget] budgets, ^() -> void wake, ^(Proc) -> void dispatch, ^(Symbol) -> void finished) -> bool
|
|
173
195
|
def attach(budgets, wake, dispatch, finished)
|
|
174
196
|
@wake = wake
|
|
@@ -191,16 +213,19 @@ module Raptor
|
|
|
191
213
|
|
|
192
214
|
# Notifies the application that its response stream is ready.
|
|
193
215
|
#
|
|
216
|
+
# @return [void]
|
|
217
|
+
#
|
|
194
218
|
# @rbs () -> void
|
|
195
219
|
def open
|
|
196
|
-
|
|
197
|
-
@dispatch.call(proc { callback.call(self) }) if callback
|
|
220
|
+
@on_open&.call(self)
|
|
198
221
|
@wake.call if @state.value[:notified]
|
|
199
222
|
end
|
|
200
223
|
|
|
201
224
|
# Returns the size of the next buffered chunk, 0 when closing, or nil
|
|
202
225
|
# while waiting for more data.
|
|
203
226
|
#
|
|
227
|
+
# @return [Integer, nil]
|
|
228
|
+
#
|
|
204
229
|
# @rbs () -> Integer?
|
|
205
230
|
def next_size
|
|
206
231
|
state = @state.value
|
|
@@ -209,6 +234,9 @@ module Raptor
|
|
|
209
234
|
|
|
210
235
|
# Removes up to `max_bytes` from the next buffered chunk.
|
|
211
236
|
#
|
|
237
|
+
# @param max_bytes [Integer] the largest chunk to return
|
|
238
|
+
# @return [String, nil] the removed bytes, or nil when nothing is buffered
|
|
239
|
+
#
|
|
212
240
|
# @rbs (Integer max_bytes) -> String?
|
|
213
241
|
def shift(max_bytes)
|
|
214
242
|
chunk = nil
|
|
@@ -236,6 +264,8 @@ module Raptor
|
|
|
236
264
|
|
|
237
265
|
# Returns the response trailers supplied when the body closed.
|
|
238
266
|
#
|
|
267
|
+
# @return [Hash] trailing response headers
|
|
268
|
+
#
|
|
239
269
|
# @rbs () -> Hash[String, String | Array[String]]
|
|
240
270
|
def trailers
|
|
241
271
|
@state.value[:trailers]
|
|
@@ -243,6 +273,9 @@ module Raptor
|
|
|
243
273
|
|
|
244
274
|
# Closes the stream and invokes its callback exactly once.
|
|
245
275
|
#
|
|
276
|
+
# @param reason [Symbol] why the stream closed
|
|
277
|
+
# @return [void]
|
|
278
|
+
#
|
|
246
279
|
# @rbs (Symbol reason) -> void
|
|
247
280
|
def finish(reason)
|
|
248
281
|
callback = false
|
|
@@ -272,6 +305,11 @@ module Raptor
|
|
|
272
305
|
|
|
273
306
|
private
|
|
274
307
|
|
|
308
|
+
# Reserves bytes from every budget attached to this body.
|
|
309
|
+
#
|
|
310
|
+
# @param bytes [Integer] number of bytes to reserve
|
|
311
|
+
# @return [Boolean] whether every budget accepted the reservation
|
|
312
|
+
#
|
|
275
313
|
# @rbs (Integer bytes) -> bool
|
|
276
314
|
def reserve(bytes)
|
|
277
315
|
reserved = []
|
|
@@ -285,6 +323,11 @@ module Raptor
|
|
|
285
323
|
true
|
|
286
324
|
end
|
|
287
325
|
|
|
326
|
+
# Releases bytes from every budget attached to this body.
|
|
327
|
+
#
|
|
328
|
+
# @param bytes [Integer] number of bytes to release
|
|
329
|
+
# @return [void]
|
|
330
|
+
#
|
|
288
331
|
# @rbs (Integer bytes) -> void
|
|
289
332
|
def release(bytes)
|
|
290
333
|
@budgets.each { |budget| budget.release(bytes) }
|
data/lib/raptor/http.rb
CHANGED
|
@@ -27,6 +27,16 @@ module Raptor
|
|
|
27
27
|
def message = "could not write response"
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# Returns whether an HTTP status forbids an entity body.
|
|
31
|
+
#
|
|
32
|
+
# @param status [Integer] the response status code
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
#
|
|
35
|
+
# @rbs (Integer status) -> bool
|
|
36
|
+
def self.no_entity_body_status?(status)
|
|
37
|
+
(status >= 100 && status < 200) || status == 204 || status == 304
|
|
38
|
+
end
|
|
39
|
+
|
|
30
40
|
# Writes `string` in full, retrying on partial writes. Bounded by
|
|
31
41
|
# `timeout` so a slow client can't pin the writing thread.
|
|
32
42
|
#
|
data/lib/raptor/http1.rb
CHANGED
|
@@ -100,6 +100,9 @@ module Raptor
|
|
|
100
100
|
|
|
101
101
|
# Encodes one HTTP/1.1 response chunk.
|
|
102
102
|
#
|
|
103
|
+
# @param chunk [String] response body bytes
|
|
104
|
+
# @return [String] the encoded chunk
|
|
105
|
+
#
|
|
103
106
|
# @rbs (String chunk) -> String
|
|
104
107
|
def self.encode_chunk(chunk)
|
|
105
108
|
HttpParser.chunked_encode(String.new, chunk)
|
|
@@ -107,6 +110,9 @@ module Raptor
|
|
|
107
110
|
|
|
108
111
|
# Encodes the final HTTP/1.1 response chunk and its trailers.
|
|
109
112
|
#
|
|
113
|
+
# @param trailers [Hash] trailing response headers
|
|
114
|
+
# @return [String] the final chunk followed by the trailer section
|
|
115
|
+
#
|
|
110
116
|
# @rbs (Hash[String, String | Array[String]] trailers) -> String
|
|
111
117
|
def self.encode_trailers(trailers)
|
|
112
118
|
normalized = trailers.each_with_object({}) do |(key, value), headers|
|
|
@@ -547,6 +553,17 @@ module Raptor
|
|
|
547
553
|
|
|
548
554
|
# Calls the Rack app and writes its response for one request.
|
|
549
555
|
#
|
|
556
|
+
# @param socket [TCPSocket] the client socket
|
|
557
|
+
# @param id [Integer] unique client identifier
|
|
558
|
+
# @param env [Hash] partial env hash from the HTTP parser
|
|
559
|
+
# @param parse_data [Hash] metadata from the parsing pass
|
|
560
|
+
# @param body [String, nil] decoded request body
|
|
561
|
+
# @param reactor [Reactor] the reactor managing the client connection
|
|
562
|
+
# @param request_count [Integer] number of requests handled on this connection
|
|
563
|
+
# @param remote_addr [String] client IP address
|
|
564
|
+
# @param url_scheme [String] "http" or "https"
|
|
565
|
+
# @return [Boolean] true if the connection should be kept alive
|
|
566
|
+
#
|
|
550
567
|
# @rbs (TCPSocket socket, Integer id, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, Reactor reactor, Integer request_count, String remote_addr, String url_scheme) -> bool
|
|
551
568
|
def perform_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme)
|
|
552
569
|
rack_env = nil
|
|
@@ -566,7 +583,7 @@ module Raptor
|
|
|
566
583
|
body.close if body.respond_to?(:close)
|
|
567
584
|
else
|
|
568
585
|
hijacked = headers.is_a?(Hash) && !!headers[Rack::RACK_HIJACK]
|
|
569
|
-
no_body = rack_env[Rack::REQUEST_METHOD] == "HEAD" || (status
|
|
586
|
+
no_body = rack_env[Rack::REQUEST_METHOD] == "HEAD" || Http.no_entity_body_status?(status)
|
|
570
587
|
detached = body.is_a?(DetachedBody) && !no_body
|
|
571
588
|
if detached
|
|
572
589
|
chunked = rack_env[Rack::SERVER_PROTOCOL] == HTTP_11
|
|
@@ -582,7 +599,7 @@ module Raptor
|
|
|
582
599
|
socket,
|
|
583
600
|
id,
|
|
584
601
|
body,
|
|
585
|
-
{chunked: chunked, keep_alive: keep_alive, request_count: request_count, remote_addr: remote_addr, url_scheme: url_scheme},
|
|
602
|
+
{ chunked: chunked, keep_alive: keep_alive, request_count: request_count, remote_addr: remote_addr, url_scheme: url_scheme },
|
|
586
603
|
finished
|
|
587
604
|
)
|
|
588
605
|
else
|
|
@@ -950,6 +967,13 @@ module Raptor
|
|
|
950
967
|
# Starts a detached response, chunked on HTTP/1.1 and ended by closing
|
|
951
968
|
# the connection on HTTP/1.0.
|
|
952
969
|
#
|
|
970
|
+
# @param socket [TCPSocket] the client socket to write to
|
|
971
|
+
# @param status [Integer] HTTP status code
|
|
972
|
+
# @param headers [Hash] response headers from the Rack application
|
|
973
|
+
# @param chunked [Boolean] whether to use chunked transfer encoding
|
|
974
|
+
# @param keep_alive [Boolean] whether to send a keep-alive connection header
|
|
975
|
+
# @return [void]
|
|
976
|
+
#
|
|
953
977
|
# @rbs (TCPSocket socket, Integer status, Hash[String, String | Array[String]] headers, chunked: bool, keep_alive: bool) -> void
|
|
954
978
|
def write_detached_response(socket, status, headers, chunked:, keep_alive:)
|
|
955
979
|
validate_status(status)
|
|
@@ -980,7 +1004,7 @@ module Raptor
|
|
|
980
1004
|
validate_status(status)
|
|
981
1005
|
response_hijack = headers.is_a?(Hash) ? headers.delete(Rack::RACK_HIJACK) : nil
|
|
982
1006
|
headers = normalize_headers(headers)
|
|
983
|
-
no_entity_body = (status
|
|
1007
|
+
no_entity_body = Http.no_entity_body_status?(status)
|
|
984
1008
|
validate_headers(headers, status, no_entity_body)
|
|
985
1009
|
|
|
986
1010
|
headers["connection"] = keep_alive ? CONNECTION_KEEPALIVE : CONNECTION_CLOSE
|
data/lib/raptor/http2.rb
CHANGED
|
@@ -71,6 +71,11 @@ module Raptor
|
|
|
71
71
|
|
|
72
72
|
# Attaches a response body that outlives its application thread.
|
|
73
73
|
#
|
|
74
|
+
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
75
|
+
# @param body [DetachedBody] the response body
|
|
76
|
+
# @param finished [Proc] called with the close reason
|
|
77
|
+
# @return [Boolean] whether the reactor accepted the body
|
|
78
|
+
#
|
|
74
79
|
# @rbs (Integer stream_id, DetachedBody body, ^(Symbol) -> void finished) -> bool
|
|
75
80
|
def attach_body(stream_id, body, finished)
|
|
76
81
|
@reactor.attach_http2_body(@connection_id, stream_id, body, finished)
|
|
@@ -416,7 +421,6 @@ module Raptor
|
|
|
416
421
|
end
|
|
417
422
|
end
|
|
418
423
|
|
|
419
|
-
EAGER_READ_TIMEOUT = 0.001
|
|
420
424
|
EAGER_READ_BUFFER_SIZE = 64 * 1024
|
|
421
425
|
EAGER_MAX_ROUNDS = 8
|
|
422
426
|
|
|
@@ -997,15 +1001,15 @@ module Raptor
|
|
|
997
1001
|
end
|
|
998
1002
|
end
|
|
999
1003
|
|
|
1000
|
-
# Reads the next frame batch from `socket`
|
|
1001
|
-
#
|
|
1004
|
+
# Reads the next frame batch from `socket` if it has already arrived,
|
|
1005
|
+
# without waiting on the shared collector thread.
|
|
1002
1006
|
#
|
|
1003
1007
|
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
1004
1008
|
# @return [String, nil] the bytes read, or nil if nothing was available
|
|
1005
1009
|
#
|
|
1006
1010
|
# @rbs (OpenSSL::SSL::SSLSocket socket) -> String?
|
|
1007
1011
|
def eager_read_next_batch(socket)
|
|
1008
|
-
return unless socket.wait_readable(
|
|
1012
|
+
return unless socket.wait_readable(0)
|
|
1009
1013
|
|
|
1010
1014
|
data = begin
|
|
1011
1015
|
socket.read_nonblock(EAGER_READ_BUFFER_SIZE)
|
|
@@ -1048,6 +1052,15 @@ module Raptor
|
|
|
1048
1052
|
|
|
1049
1053
|
# Calls the Rack app and writes its response for one stream.
|
|
1050
1054
|
#
|
|
1055
|
+
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
1056
|
+
# @param writer [Writer] lock-free frame writer for the connection
|
|
1057
|
+
# @param flow_control [FlowControl] per-connection outbound flow controller
|
|
1058
|
+
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
1059
|
+
# @param headers [Array<Array(String, String)>] request headers
|
|
1060
|
+
# @param body [String] request body
|
|
1061
|
+
# @param remote_addr [String] the client IP address
|
|
1062
|
+
# @return [void]
|
|
1063
|
+
#
|
|
1051
1064
|
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
|
|
1052
1065
|
def perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
|
|
1053
1066
|
env = nil
|
|
@@ -1060,7 +1073,7 @@ module Raptor
|
|
|
1060
1073
|
env = build_rack_env(headers, body, socket, writer, flow_control, stream_id, remote_addr: remote_addr)
|
|
1061
1074
|
status, response_headers, response_body = @app.call(env)
|
|
1062
1075
|
|
|
1063
|
-
no_body = env[Rack::REQUEST_METHOD] == "HEAD" || (status
|
|
1076
|
+
no_body = env[Rack::REQUEST_METHOD] == "HEAD" || Http.no_entity_body_status?(status)
|
|
1064
1077
|
if response_body.is_a?(DetachedBody) && !no_body
|
|
1065
1078
|
finished = proc do |reason|
|
|
1066
1079
|
error = StreamClosedError.new unless reason == :closed
|
|
@@ -1068,9 +1081,10 @@ module Raptor
|
|
|
1068
1081
|
Http.call_response_finished(env, status, response_headers, error)
|
|
1069
1082
|
flow_control.discard_stream(stream_id)
|
|
1070
1083
|
end
|
|
1071
|
-
|
|
1084
|
+
write_http2_detached_response(socket, writer, flow_control, stream_id, status, response_headers, response_body, finished) do
|
|
1072
1085
|
response_started = true
|
|
1073
1086
|
end
|
|
1087
|
+
detached = true
|
|
1074
1088
|
return
|
|
1075
1089
|
end
|
|
1076
1090
|
|
|
@@ -1113,7 +1127,18 @@ module Raptor
|
|
|
1113
1127
|
|
|
1114
1128
|
# Starts a detached HTTP/2 response after writing its response headers.
|
|
1115
1129
|
#
|
|
1116
|
-
# @
|
|
1130
|
+
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
1131
|
+
# @param writer [Writer] lock-free frame writer for the connection
|
|
1132
|
+
# @param flow_control [FlowControl] per-connection outbound flow controller
|
|
1133
|
+
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
1134
|
+
# @param status [Integer] HTTP status code
|
|
1135
|
+
# @param headers [Hash] response headers from the Rack application
|
|
1136
|
+
# @param body [DetachedBody] the response body
|
|
1137
|
+
# @param finished [Proc] called with the close reason
|
|
1138
|
+
# @yield once the response headers are queued
|
|
1139
|
+
# @return [void]
|
|
1140
|
+
#
|
|
1141
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, DetachedBody body, ^(Symbol) -> void finished) { () -> void } -> void
|
|
1117
1142
|
def write_http2_detached_response(socket, writer, flow_control, stream_id, status, headers, body, finished)
|
|
1118
1143
|
flow_control.check(stream_id)
|
|
1119
1144
|
parser = Http2Parser.new
|
|
@@ -1122,7 +1147,6 @@ module Raptor
|
|
|
1122
1147
|
yield
|
|
1123
1148
|
attached = writer.attach_body(stream_id, body, finished)
|
|
1124
1149
|
write_http2_reset_stream(socket, writer, stream_id, ERROR_REFUSED_STREAM) unless attached
|
|
1125
|
-
true
|
|
1126
1150
|
end
|
|
1127
1151
|
|
|
1128
1152
|
# Writes a Rack response as HTTP/2 frames to the socket, partitioning
|
|
@@ -1145,7 +1169,7 @@ module Raptor
|
|
|
1145
1169
|
|
|
1146
1170
|
encoded_headers = parser.encode_response_headers(status, headers)
|
|
1147
1171
|
flow_control.check(stream_id)
|
|
1148
|
-
no_body = request_method == "HEAD" || (status
|
|
1172
|
+
no_body = request_method == "HEAD" || Http.no_entity_body_status?(status)
|
|
1149
1173
|
if no_body
|
|
1150
1174
|
writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded_headers)])
|
|
1151
1175
|
yield if block_given?
|