raptor 0.15.1 → 0.16.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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +35 -27
- data/docs/brisrails-talk.md +19 -13
- data/docs/raptor-vs-puma.md +46 -30
- data/lib/rackup/handler/raptor.rb +0 -1
- data/lib/raptor/cli.rb +10 -5
- data/lib/raptor/cluster.rb +60 -15
- data/lib/raptor/http.rb +3 -7
- data/lib/raptor/http2.rb +62 -1
- data/lib/raptor/reactor.rb +44 -6
- data/lib/raptor/server.rb +7 -11
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/cluster.rbs +32 -5
- data/sig/generated/raptor/http.rbs +2 -2
- data/sig/generated/raptor/http2.rbs +25 -1
- data/sig/generated/raptor/reactor.rbs +39 -15
- 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: 02fe742ea31e44d742de9f0ed5b94cc53901d39e25b054fe0bd4affd14e169c1
|
|
4
|
+
data.tar.gz: 4279672bfd532f9bcadfb5ce81d85b8ba4921b8072e302bfc33bf74ce4609224
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 283c078528cfd9cdfdfb795e4587a60607fa382d09efe1194330d5a9a6c7f319af94bc63a2da62396c7aba8484a0041260fcd70b0d82a5c324c7fd6b9221abc5
|
|
7
|
+
data.tar.gz: 9ee16f87805c340d17da270a81131f191b09500cbd622e3bd06bd32acff3f2ec215004e110187de1d01f6815fdcdfdf962c50c521398ce45e469358d3c155dd8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.16.0] - 2026-08-22
|
|
4
|
+
|
|
5
|
+
- Parse HTTP/2 requests in a dedicated ractor pool
|
|
6
|
+
- Parse the first HTTP/2 frame batch inline on accept
|
|
7
|
+
- Nest the `ractors` config under `http1` and `http2`, defaulting both to `round(cores / workers)` clamped to 1..3 for HTTP/1.1 and 1..2 for HTTP/2
|
|
8
|
+
|
|
3
9
|
## [0.15.1] - 2026-08-16
|
|
4
10
|
|
|
5
11
|
- Regenerate the server RBS signatures
|
data/README.md
CHANGED
|
@@ -35,24 +35,31 @@ run proc { |_env| [200, { "content-type" => "text/plain" }, ["Hello, World!"]] }
|
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
```
|
|
38
|
-
> bundle exec raptor -w
|
|
39
|
-
[Raptor
|
|
40
|
-
[Raptor
|
|
41
|
-
[Raptor
|
|
42
|
-
[Raptor
|
|
43
|
-
[Raptor
|
|
44
|
-
[Raptor
|
|
45
|
-
[Raptor
|
|
46
|
-
[Raptor
|
|
47
|
-
[Raptor
|
|
48
|
-
[Raptor
|
|
49
|
-
[Raptor
|
|
50
|
-
[Raptor
|
|
51
|
-
[Raptor
|
|
52
|
-
[Raptor
|
|
53
|
-
[Raptor
|
|
54
|
-
[Raptor
|
|
55
|
-
[Raptor
|
|
38
|
+
> bundle exec raptor -w 10 -t 3 hello_world.ru
|
|
39
|
+
[Raptor 83654|Main|Main] Cluster initializing:
|
|
40
|
+
[Raptor 83654|Main|Main] ├─ Version: 0.16.0
|
|
41
|
+
[Raptor 83654|Main|Main] ├─ Ruby Version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
|
|
42
|
+
[Raptor 83654|Main|Main] ├─ Environment: development
|
|
43
|
+
[Raptor 83654|Main|Main] ├─ Master PID: 83654
|
|
44
|
+
[Raptor 83654|Main|Main] │ └─ 10 worker processes
|
|
45
|
+
[Raptor 83654|Main|Main] │ ├─ 1 server thread
|
|
46
|
+
[Raptor 83654|Main|Main] │ ├─ 1 reactor thread
|
|
47
|
+
[Raptor 83654|Main|Main] │ ├─ 1 HTTP/1.1 pipeline ractor
|
|
48
|
+
[Raptor 83654|Main|Main] │ ├─ 1 HTTP/2 pipeline ractor
|
|
49
|
+
[Raptor 83654|Main|Main] │ ├─ 2 pipeline collector threads
|
|
50
|
+
[Raptor 83654|Main|Main] │ ├─ 3 worker threads
|
|
51
|
+
[Raptor 83654|Main|Main] │ └─ 1 stats thread
|
|
52
|
+
[Raptor 83654|Main|Main] └─ Listening on 0.0.0.0:9292
|
|
53
|
+
[Raptor 83658|Main|Main] Worker 2 booted
|
|
54
|
+
[Raptor 83659|Main|Main] Worker 3 booted
|
|
55
|
+
[Raptor 83656|Main|Main] Worker 0 booted
|
|
56
|
+
[Raptor 83660|Main|Main] Worker 4 booted
|
|
57
|
+
[Raptor 83662|Main|Main] Worker 6 booted
|
|
58
|
+
[Raptor 83657|Main|Main] Worker 1 booted
|
|
59
|
+
[Raptor 83661|Main|Main] Worker 5 booted
|
|
60
|
+
[Raptor 83663|Main|Main] Worker 7 booted
|
|
61
|
+
[Raptor 83664|Main|Main] Worker 8 booted
|
|
62
|
+
[Raptor 83665|Main|Main] Worker 9 booted
|
|
56
63
|
```
|
|
57
64
|
|
|
58
65
|
```
|
|
@@ -85,7 +92,6 @@ The config file is a Ruby file that evaluates to a hash of options. By default R
|
|
|
85
92
|
socket_backlog: 1024,
|
|
86
93
|
drain_accept_queue: false,
|
|
87
94
|
workers: 4, # `Etc.nprocessors`
|
|
88
|
-
ractors: 1,
|
|
89
95
|
threads: 3,
|
|
90
96
|
chdir: nil,
|
|
91
97
|
environment: nil, # falls back to `RAILS_ENV`, then `RACK_ENV`, then `"development"`
|
|
@@ -97,10 +103,12 @@ The config file is a Ruby file that evaluates to a hash of options. By default R
|
|
|
97
103
|
body_spool_threshold: 1024 * 1024,
|
|
98
104
|
},
|
|
99
105
|
http1: {
|
|
106
|
+
ractors: nil,
|
|
100
107
|
persistent_data_timeout: 65,
|
|
101
108
|
max_keepalive_requests: 100,
|
|
102
109
|
},
|
|
103
110
|
http2: {
|
|
111
|
+
ractors: nil,
|
|
104
112
|
max_concurrent_streams: 100,
|
|
105
113
|
},
|
|
106
114
|
worker_boot_timeout: 60,
|
|
@@ -193,7 +201,7 @@ Worker 1 (phase 0): pid=91351, requests=1199, busy=1/3, backlog=0, booted, last_
|
|
|
193
201
|
|
|
194
202
|
## (Micro) Benchmarks
|
|
195
203
|
|
|
196
|
-
Raptor 0.
|
|
204
|
+
Raptor 0.16.0 vs Puma 8.0.2 vs PumaPlus 0.1.0 vs Falcon 0.57.0 across two workload profiles. **IO-bound** is a GET
|
|
197
205
|
endpoint that interleaves 5-10 short sleeps (total 2.5-15ms) with small CPU work, simulating a read path that makes
|
|
198
206
|
several DB or cache calls. **CPU-bound** is a POST endpoint that accepts a small JSON body, interleaves 3-5 chunks of
|
|
199
207
|
JSON item building (total 450-1500 items) with sub-100µs sleeps, and returns the built array, simulating a write path
|
|
@@ -202,16 +210,16 @@ that does most of its work in Ruby with a few near-zero-cost cache hits.
|
|
|
202
210
|
Each cell reports the median throughput and median p95 latency independently across 5 runs, so the two numbers in a row
|
|
203
211
|
may come from different runs. Every run starts a fresh server process so the samples are independent of each other;
|
|
204
212
|
state accumulated in a previous run cannot bias the next. Across the whole table, the widest spread
|
|
205
|
-
((max - min) / 2 / median) between runs of a single cell was ±
|
|
213
|
+
((max - min) / 2 / median) between runs of a single cell was ±24.8% for throughput and ±40.3% for p95.
|
|
206
214
|
|
|
207
215
|
| Protocol | Workload | Raptor req/s | Raptor p95 | Puma req/s | Puma p95 | vs Puma req/s | vs Puma p95 | PumaPlus req/s | PumaPlus p95 | vs PumaPlus req/s | vs PumaPlus p95 | Falcon req/s | Falcon p95 | vs Falcon req/s | vs Falcon p95 |
|
|
208
216
|
| --------------------- | -------- | ------------ | ---------- | ----------- | --------- | ------------- | ------------ | -------------- | ------------ | ----------------- | --------------- | ------------ | ---------- | --------------- | ------------- |
|
|
209
|
-
| HTTP/1.1 | IO | 3.
|
|
210
|
-
| HTTP/1.1 | CPU | 8.
|
|
211
|
-
| HTTP/1.1 (keep-alive) | IO | 2.
|
|
212
|
-
| HTTP/1.1 (keep-alive) | CPU |
|
|
213
|
-
| HTTP/2 | IO | 0.97k req/s |
|
|
214
|
-
| HTTP/2 | CPU |
|
|
217
|
+
| HTTP/1.1 | IO | 3.21k req/s | 63.20 ms | 1.48k req/s | 131.00 ms | 117.7% higher | 51.8% lower | 1.63k req/s | 84.20 ms | 97.0% higher | 24.9% lower | 12.27k req/s | 14.00 ms | 73.8% lower | 351.4% higher |
|
|
218
|
+
| HTTP/1.1 | CPU | 8.29k req/s | 33.30 ms | 8.56k req/s | 21.20 ms | 3.2% lower | 57.1% higher | 8.08k req/s | 19.20 ms | 2.6% higher | 73.4% higher | 6.62k req/s | 27.00 ms | 25.2% higher | 23.3% higher |
|
|
219
|
+
| HTTP/1.1 (keep-alive) | IO | 2.43k req/s | 54.40 ms | 1.44k req/s | 110.20 ms | 68.7% higher | 50.6% lower | 1.58k req/s | 87.10 ms | 54.2% higher | 37.5% lower | 6.28k req/s | 27.90 ms | 61.3% lower | 95.0% higher |
|
|
220
|
+
| HTTP/1.1 (keep-alive) | CPU | 8.40k req/s | 18.80 ms | 8.49k req/s | 21.90 ms | 1.1% lower | 14.2% lower | 8.36k req/s | 18.40 ms | 0.4% higher | 2.2% higher | 7.09k req/s | 31.50 ms | 18.5% higher | 40.3% lower |
|
|
221
|
+
| HTTP/2 | IO | 0.97k req/s | 197.97 ms | N/A | N/A | - | - | 1.52k req/s | 90.13 ms | 36.0% lower | 119.6% higher | 6.59k req/s | 27.21 ms | 85.2% lower | 627.6% higher |
|
|
222
|
+
| HTTP/2 | CPU | 6.61k req/s | 31.98 ms | N/A | N/A | - | - | 8.05k req/s | 19.23 ms | 17.9% lower | 66.3% higher | 8.09k req/s | 45.04 ms | 18.3% lower | 29.0% lower |
|
|
215
223
|
|
|
216
224
|
> ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [aarch64-linux]
|
|
217
225
|
> 10 worker processes; Raptor, Puma, and PumaPlus run 3 threads per worker, Falcon runs unbounded fibers per worker;
|
data/docs/brisrails-talk.md
CHANGED
|
@@ -571,13 +571,17 @@ flowchart TB
|
|
|
571
571
|
SRV["Server thread<br/>IO.select + accept_nonblock"]
|
|
572
572
|
RCT["Reactor thread<br/>NIO::Selector<br/>red-black tree of timeouts"]
|
|
573
573
|
|
|
574
|
-
subgraph
|
|
575
|
-
CRD["Coordinator Ractor"]
|
|
574
|
+
subgraph RP1["HTTP/1.1 Ractor pool"]
|
|
576
575
|
RW1["Pipeline Ractor 1"]
|
|
577
576
|
RWM["Pipeline Ractor M"]
|
|
578
577
|
end
|
|
579
578
|
|
|
580
|
-
|
|
579
|
+
subgraph RP2["HTTP/2 Ractor pool"]
|
|
580
|
+
RW2["Pipeline Ractor 1"]
|
|
581
|
+
RWN["Pipeline Ractor N"]
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
COL["Collector threads<br/>drain Ractor::Ports"]
|
|
581
585
|
|
|
582
586
|
subgraph ATP["Thread pool, lock-free CAS queue"]
|
|
583
587
|
T1["App thread 1"]
|
|
@@ -588,8 +592,10 @@ flowchart TB
|
|
|
588
592
|
STA["Stats thread<br/>writes 1 Hz"]
|
|
589
593
|
|
|
590
594
|
SRV --> RCT
|
|
591
|
-
RCT -->
|
|
592
|
-
|
|
595
|
+
RCT --> RP1
|
|
596
|
+
RCT --> RP2
|
|
597
|
+
RP1 --> COL
|
|
598
|
+
RP2 --> COL
|
|
593
599
|
COL --> ATP
|
|
594
600
|
STA --> SHM[("mmap")]
|
|
595
601
|
end
|
|
@@ -620,9 +626,9 @@ Every part earns its keep. Let's walk it.
|
|
|
620
626
|
<br>
|
|
621
627
|
<br>
|
|
622
628
|
|
|
623
|
-
## The Ractor
|
|
629
|
+
## The Ractor pools
|
|
624
630
|
|
|
625
|
-
|
|
631
|
+
Parsing runs in dedicated Ractor pools, one per HTTP protocol. This is where the parallelism actually kicks in.
|
|
626
632
|
|
|
627
633
|
```mermaid
|
|
628
634
|
flowchart LR
|
|
@@ -1271,8 +1277,8 @@ flowchart TB
|
|
|
1271
1277
|
Client(["client"])
|
|
1272
1278
|
Srv["Server thread<br/>accept_nonblock"]
|
|
1273
1279
|
Rct["Reactor thread<br/>read_nonblock 64KB"]
|
|
1274
|
-
RP["Ractor pool<br/>parses in own GVL"]
|
|
1275
|
-
Col["Collector
|
|
1280
|
+
RP["Protocol-specific Ractor pool<br/>parses in own GVL"]
|
|
1281
|
+
Col["Collector threads<br/>drain Ractor::Ports"]
|
|
1276
1282
|
ATP["App thread pool<br/>calls Rack app<br/>writes response"]
|
|
1277
1283
|
|
|
1278
1284
|
Client -->|"SYN"| Srv
|
|
@@ -1311,7 +1317,7 @@ flowchart TB
|
|
|
1311
1317
|
**Fast path**
|
|
1312
1318
|
|
|
1313
1319
|
- <big>First `read_nonblock` gives us a complete request</big>
|
|
1314
|
-
- <big>Server thread parses inline</big>
|
|
1320
|
+
- <big>Server thread (HTTP/1.1) or accepting pool worker (HTTP/2) parses inline</big>
|
|
1315
1321
|
- <big>Pushes a proc to the app pool</big>
|
|
1316
1322
|
- <big>No reactor. No Ractor pool.</big>
|
|
1317
1323
|
- <big>Common for a normal request that fits in one TCP packet.</big>
|
|
@@ -1320,7 +1326,7 @@ flowchart TB
|
|
|
1320
1326
|
|
|
1321
1327
|
- <big>Bytes not ready, or the request is incomplete</big>
|
|
1322
1328
|
- <big>Reactor takes over, waits for more bytes</big>
|
|
1323
|
-
- <big>When bytes arrive: read up to 64KB per syscall, hand the buffer to the Ractor pool</big>
|
|
1329
|
+
- <big>When bytes arrive: read up to 64KB per syscall, hand the buffer to the protocol's Ractor pool</big>
|
|
1324
1330
|
- 64KB is the per-read size, not a request size limit. Bigger requests just come in over more reads.
|
|
1325
1331
|
- <big>Ractor parses on a **separate GVL**</big>
|
|
1326
1332
|
- <big>Parsed env comes back via the collector, which pushes to the app pool</big>
|
|
@@ -1487,7 +1493,7 @@ Notable: a single HTTP/2 client connection in Raptor can have many streams in fl
|
|
|
1487
1493
|
- <big>Each stream is a separate work item in the queue</big>
|
|
1488
1494
|
- <big>Different streams from the same connection can end up on different app threads</big>
|
|
1489
1495
|
- <big>Those threads still share the main GVL, so they overlap productively when the app is in I/O (the common Rails case), the same way Puma's keep-alive requests do</big>
|
|
1490
|
-
- <big>The parsing for each stream happens in the Ractor pool on its own GVL, actually in parallel with the app work</big>
|
|
1496
|
+
- <big>The parsing for each stream happens in the HTTP/2 Ractor pool on its own GVL, actually in parallel with the app work</big>
|
|
1491
1497
|
|
|
1492
1498
|
<br>
|
|
1493
1499
|
<br>
|
|
@@ -2204,7 +2210,7 @@ I didn't set out to build a small library ecosystem. It's what happens when you
|
|
|
2204
2210
|
|
|
2205
2211
|
Real numbers are in the [README benchmarks section](../README.md#micro-benchmarks). The shape:
|
|
2206
2212
|
|
|
2207
|
-
- <big>**IO-bound work**: Falcon wins by a wide margin. Its fibers keep every
|
|
2213
|
+
- <big>**IO-bound work**: Falcon wins by a wide margin. Its fibers keep every client connection in flight at once. Thread-based servers cap out at their pool size.</big>
|
|
2208
2214
|
- <big>**CPU-bound HTTP/1.1**: Raptor beats Puma on both throughput and tail latency.</big>
|
|
2209
2215
|
- Tail latency ("p95") is the response time that 5% of requests exceed. It's what your slowest users see. Lower is better.
|
|
2210
2216
|
- <big>**HTTP/2**: Raptor and Falcon both implement it. Puma doesn't.</big>
|
data/docs/raptor-vs-puma.md
CHANGED
|
@@ -20,7 +20,7 @@ This document focuses on Puma because Puma is the incumbent that any new Ruby we
|
|
|
20
20
|
|
|
21
21
|
Raptor is a research project. It hasn't run production traffic. The numbers in the README come from a repeatable microbenchmark, not from a real deployment. The benchmark measures only the **server** work: accepting connections, parsing, dispatching, and writing responses. It does not measure your application. In a typical Rails app where most of a request's time goes to ActiveRecord and downstream services, the server accounts for maybe 5 to 15 percent of the total, so a +N% number in this table will show up as a much smaller improvement in production. The gap is real, but it isn't what a Rails app against a real database will report.
|
|
22
22
|
|
|
23
|
-
The Raptor README carries the [current head-to-head numbers](../README.md#micro-benchmarks) against the latest Puma and Falcon releases, run on the same hardware with the same Rack app on a recent Ruby with YJIT enabled. All
|
|
23
|
+
The Raptor README carries the [current head-to-head numbers](../README.md#micro-benchmarks) against the latest Puma and Falcon releases, run on the same hardware with the same Rack app on a recent Ruby with YJIT enabled. All servers run one worker process per available CPU; Raptor, Puma, and PumaPlus use three threads per worker, and Falcon uses unbounded fibers per worker. Load generators use four client connections per app thread, so on the 10-core machine that produced the current numbers that's 120 concurrent HTTP/1.1 client connections and 40 h2 connections × 3 streams each.
|
|
24
24
|
|
|
25
25
|
Two workload profiles are measured. **IO-bound** is a GET endpoint that does 5 to 10 short sleeps interleaved with small CPU work per request, simulating a read path that makes several DB or cache calls throughout its lifetime. **CPU-bound** is a POST endpoint with a small JSON body that builds a JSON response in 3 to 5 chunks interleaved with sub-100µs sleeps, simulating a write path that does most of its work in Ruby with a few near-zero-cost cache hits. The workloads are interleaved rather than a single bulk sleep or single bulk serialise so a fiber-per-connection server like Falcon doesn't look artificially good from one-shot IO, and the CPU-bound workload is heavily CPU-dominated by design (roughly 95% CPU / 5% IO by wall time) so it actually measures CPU work rather than smuggling in enough IO for fibers to multiplex.
|
|
26
26
|
|
|
@@ -212,14 +212,15 @@ The design is Pitchfork's, adapted for Raptor's process model. [Pitchfork](https
|
|
|
212
212
|
|
|
213
213
|
### Threading model
|
|
214
214
|
|
|
215
|
-
This is where Raptor diverges dramatically. Inside a worker there are
|
|
215
|
+
This is where Raptor diverges dramatically. Inside a worker there are six kinds of concurrent activity:
|
|
216
216
|
|
|
217
217
|
1. **One server thread** running the accept loop.
|
|
218
218
|
2. **One reactor thread** running the NIO event loop plus timeout tree.
|
|
219
|
-
3. **A `RactorPool`
|
|
220
|
-
4. **A
|
|
221
|
-
5. **
|
|
222
|
-
6.
|
|
219
|
+
3. **A `RactorPool` for HTTP/1.1 parsing** sized by `http1.ractors`, defaulting to `round(cores / workers)` clamped to `[1, 3]`.
|
|
220
|
+
4. **A `RactorPool` for HTTP/2 parsing** sized by `http2.ractors`, defaulting to `round(cores / workers)` clamped to `[1, 2]`.
|
|
221
|
+
5. **A collector thread per Ractor pool** that receives parsed results via a `Ractor::Port`.
|
|
222
|
+
6. **An `AtomicThreadPool` of T app threads** running the Rack app and writing responses.
|
|
223
|
+
7. Plus one stats thread that writes the shared-memory slot every second.
|
|
223
224
|
|
|
224
225
|
That is a lot of moving parts. Let us go through why.
|
|
225
226
|
|
|
@@ -231,7 +232,9 @@ The HTTP/1 parser also pre-interns the ~40 most common header keys (`HTTP_HOST`,
|
|
|
231
232
|
|
|
232
233
|
The upshot is that while your Rack app runs on regular threads under the GVL (so your app does not need to be Ractor-safe), the protocol-level work runs in parallel across Ractors. Under heavy load with lots of small requests, the GVL contention that would otherwise dominate parsing simply is not there.
|
|
233
234
|
|
|
234
|
-
**How the Ractor
|
|
235
|
+
**How the Ractor pools actually work.** Raptor uses the `ractor-pool` gem, which is another one of my libraries. Each pool has one coordinator Ractor and M pipeline Ractors. When a pipeline Ractor is idle, it sends itself back to the coordinator via `coordinator.send(Ractor.current, move: true)`. When work arrives at the coordinator, it either forwards it to a waiting Ractor (if any) or queues it. This coordinator-dispatch pattern guarantees that no Ractor sits idle while there is work. Results flow back through a shared `Ractor::Port` (a many-to-one channel added in recent Ruby versions and stable in 4.0) to a Ruby-side collector thread. If `M == 1` the coordinator is skipped and work goes straight to the single pipeline Ractor.
|
|
236
|
+
|
|
237
|
+
Raptor runs two independent pools per worker, one for HTTP/1.1 parsing and one for HTTP/2 parsing. Both defaults scale with headroom via `round(cores / workers)`, clamped to `[1, 3]` for `http1.ractors` and `[1, 2]` for `http2.ractors`. Splitting the pools means h1 and h2 parsing never share ractor slots, so a burst of small HTTP/1.1 requests cannot delay HTTP/2 frame handling on the same connection, and vice versa.
|
|
235
238
|
|
|
236
239
|
Note that Ractors also have their own copy of the code, so booting them means loading dependencies inside each Ractor context. Raptor pre-loads only what the parser needs.
|
|
237
240
|
|
|
@@ -243,15 +246,22 @@ The knock-on effect is that reading pool state to make backpressure decisions is
|
|
|
243
246
|
|
|
244
247
|
### I/O model
|
|
245
248
|
|
|
246
|
-
The server accept loop is an `IO.select` + `accept_nonblock` loop, similar to Puma. What is different is the
|
|
249
|
+
The server accept loop is an `IO.select` + `accept_nonblock` loop, similar to Puma. What is different is the pair of checks right before `accept`:
|
|
247
250
|
|
|
248
251
|
```ruby
|
|
249
|
-
backpressure_threshold = [(@thread_pool.size *
|
|
252
|
+
backpressure_threshold = [(@thread_pool.size * BACKPRESSURE_THRESHOLD_MULTIPLIER).ceil, MIN_BACKPRESSURE_THRESHOLD].max
|
|
250
253
|
# ...
|
|
251
254
|
next if @reactor.backlog >= backpressure_threshold
|
|
255
|
+
|
|
256
|
+
if @thread_pool.queue_size > @thread_pool.size
|
|
257
|
+
Thread.pass
|
|
258
|
+
next
|
|
259
|
+
end
|
|
252
260
|
```
|
|
253
261
|
|
|
254
|
-
|
|
262
|
+
The first is a hard skip. `@reactor.backlog` is `thread_pool.queue_size + thread_pool.active_count`; when the total load reaches 120% of the pool size, this worker stops accepting until it drains. `MIN_BACKPRESSURE_THRESHOLD` is 8, so small pools (say, 3 threads) trip backpressure at 8 concurrent items rather than at 120% of a very small number; the floor keeps saturated workers signaling early so the load-aware dispatcher (below) has time to route around them.
|
|
263
|
+
|
|
264
|
+
The second is a softer yield. When the queue alone exceeds the pool size — the app threads are all busy and there's a queue building on top of them — the accept loop yields the GVL via `Thread.pass` and re-checks the queue on the next iteration instead of accepting more work. This lets the app threads make progress before the server thread grabs another connection, and only fires under real pool pressure (queue depth greater than pool size), so IO-bound workloads where threads spend most of their time in `sleep` and rarely queue past the pool size aren't affected.
|
|
255
265
|
|
|
256
266
|
Because Raptor is always in cluster mode and every worker listens in the same `SO_REUSEPORT` group, load balancing across workers happens at the kernel level. On Linux, Raptor attaches a small BPF program to the reuseport group. Each worker binds its own listener registered in a sockmap, and a dedicated reporter thread publishes the worker's current reactor backlog into a loads map every millisecond. The BPF program consults the map on every incoming connection. When the spread between the busiest and idlest worker is within one, it treats all workers as tied and hash-distributes the connection by its 4-tuple; otherwise it routes to the least-loaded worker. The tie band matters. Without it, a worker that briefly drained one request would attract every subsequent connection until the next load update, herding bursts onto whichever worker most recently reported the lowest load. If the `libbpf-ruby` gem is not installed or the BPF object has not been compiled, Raptor falls back silently to the default four-tuple-hash routing; if the kernel refuses the program, startup raises. Either way, if a worker is saturated and stops calling `accept`, other workers pick up the slack.
|
|
257
267
|
|
|
@@ -259,7 +269,7 @@ The BPF-based approach was inspired by [a comment](https://github.com/puma/puma/
|
|
|
259
269
|
|
|
260
270
|
The reactor is again an `NIO::Selector` loop. Two things make it different from Puma's:
|
|
261
271
|
|
|
262
|
-
1. **Read strategy.** When a socket is readable, the reactor does one `read_nonblock(64KB)` right there in the reactor thread, updates the buffered state for that connection, and only then decides what to do. If the request is not yet complete, the state stays in the reactor and awaits more data. If it is complete (headers parsed, body received), the state is pushed to the Ractor pool for parsing
|
|
272
|
+
1. **Read strategy.** When a socket is readable, the reactor does one `read_nonblock(64KB)` right there in the reactor thread, updates the buffered state for that connection, and only then decides what to do. If the request is not yet complete, the state stays in the reactor and awaits more data. If it is complete (headers parsed, body received), the state is pushed to the protocol's Ractor pool for parsing. The reactor does not try to parse; it does the I/O and hands off the raw buffer.
|
|
263
273
|
|
|
264
274
|
2. **Timeout data structure.** Instead of a sorted linked list, timeouts are stored in a red-black tree (`red-black-tree` gem, yes, also one of mine). Each connection is represented by a `TimeoutClient < RedBlackTree::Node` ordered by its `timeout_at` value. Insertion is O(log n), deletion by key (needed when a connection's timeout is updated mid-flight, which happens on every read) is O(log n), and in-order traversal is O(k) where k is the number of expired connections. After every selector poll, the reactor walks the tree in order and breaks on the first non-expired node.
|
|
265
275
|
|
|
@@ -312,12 +322,12 @@ The `reactor.persist` call re-registers the socket with the reactor using `persi
|
|
|
312
322
|
|
|
313
323
|
Raptor speaks HTTP/2 on TLS connections where the client negotiates it via ALPN. The binder sets `alpn_protocols = ["h2", "http/1.1"]` on the SSL context and the ALPN callback picks h2 whenever the client offers it. Puma does not do this. Puma's SSL context does not advertise `h2` in ALPN, so clients transparently fall back to HTTP/1.1.
|
|
314
324
|
|
|
315
|
-
Once ALPN selects h2, the initial `SETTINGS` frame
|
|
325
|
+
Once ALPN selects h2, the pool worker that ran the TLS handshake calls `Http2#eager_accept`, which creates the per-connection `Writer` and `FlowControl`, attaches them to the reactor, writes the initial `SETTINGS` frame, and tries a non-blocking read on the socket. If bytes are already there, it parses the first frame batch inline via `Http2.process_frames` and dispatches completed streams to the thread pool without ever touching the ractor pool. If not, it hands the socket to the reactor to watch for the first bytes.
|
|
316
326
|
|
|
317
327
|
From there the shape is similar to HTTP/1.1:
|
|
318
328
|
|
|
319
329
|
1. Reactor reads frames.
|
|
320
|
-
2. The HTTP/2 parser (native C, with an HPACK decoder using a static Huffman table) parses the frames in the Ractor pool.
|
|
330
|
+
2. The HTTP/2 parser (native C, with an HPACK decoder using a static Huffman table) parses the frames in the HTTP/2 Ractor pool.
|
|
321
331
|
3. Completed requests (once `HEADERS` and `DATA` are complete for a stream) go to the thread pool as separate work items. **A single connection can be servicing many streams in parallel across the thread pool.**
|
|
322
332
|
4. Each stream's response is written back through the connection's `Writer`, which serialises frame writes across threads without a mutex.
|
|
323
333
|
|
|
@@ -350,15 +360,17 @@ flowchart TB
|
|
|
350
360
|
|
|
351
361
|
RCT["Reactor thread<br/>NIO::Selector<br/>read_nonblock 64KB<br/>Red-Black-Tree timeouts"]
|
|
352
362
|
|
|
353
|
-
subgraph
|
|
354
|
-
|
|
355
|
-
RW1["Pipeline Ractor 1<br/>HTTP1 + HTTP2 parser<br/>chunked decode<br/>HPACK decode"]
|
|
363
|
+
subgraph RP1["HTTP/1.1 RactorPool"]
|
|
364
|
+
RW1["Pipeline Ractor 1<br/>HTTP/1.x parser<br/>chunked decode"]
|
|
356
365
|
RWM["Pipeline Ractor M"]
|
|
357
|
-
CRD --> RW1
|
|
358
|
-
CRD --> RWM
|
|
359
366
|
end
|
|
360
367
|
|
|
361
|
-
|
|
368
|
+
subgraph RP2["HTTP/2 RactorPool"]
|
|
369
|
+
RW2["Pipeline Ractor 1<br/>HTTP/2 parser<br/>HPACK decode"]
|
|
370
|
+
RWN["Pipeline Ractor N"]
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
COL["Collector threads<br/>Ractor::Port receive<br/>dispatch to thread pool<br/>or back to reactor"]
|
|
362
374
|
|
|
363
375
|
subgraph ATP["AtomicThreadPool, CAS work queue"]
|
|
364
376
|
TR1["App thread 1"]
|
|
@@ -372,10 +384,13 @@ flowchart TB
|
|
|
372
384
|
KA{"Keep-alive?"}
|
|
373
385
|
EAG{"wait_readable<br/>1ms"}
|
|
374
386
|
|
|
375
|
-
SRV -->|"eager_accept, parse inline, push proc"| ATP
|
|
376
|
-
SRV -->|"
|
|
377
|
-
|
|
378
|
-
|
|
387
|
+
SRV -->|"HTTP/1.1 eager_accept, parse inline, push proc"| ATP
|
|
388
|
+
SRV -->|"HTTP/2 eager_accept, parse inline, push proc"| ATP
|
|
389
|
+
SRV -->|"data not ready or incomplete, hand to reactor"| RCT
|
|
390
|
+
RCT -->|"HTTP/1.1 state via Ractor.make_shareable"| RP1
|
|
391
|
+
RCT -->|"HTTP/2 state via Ractor.make_shareable"| RP2
|
|
392
|
+
RP1 -->|"result via Ractor::Port"| COL
|
|
393
|
+
RP2 -->|"result via Ractor::Port"| COL
|
|
379
394
|
COL --> CHK
|
|
380
395
|
CHK -->|"no, more bytes needed"| RCT
|
|
381
396
|
CHK -->|"yes, push proc"| ATP
|
|
@@ -399,13 +414,14 @@ flowchart TB
|
|
|
399
414
|
classDef storage fill:#e5e7eb,stroke:#6b7280,color:#374151
|
|
400
415
|
class SRV accept
|
|
401
416
|
class RCT reactor
|
|
402
|
-
class
|
|
417
|
+
class RP1 parse
|
|
418
|
+
class RP2 parse
|
|
403
419
|
class ATP pool
|
|
404
420
|
class COL collector
|
|
405
421
|
class SHM storage
|
|
406
422
|
```
|
|
407
423
|
|
|
408
|
-
The critical structural difference from Puma is that parsing is not on the app thread. It is on the Ractor pool. The app thread only does the Rack call and the response write. This decouples the two costs and lets Ruby actually use more than one CPU for the protocol work.
|
|
424
|
+
The critical structural difference from Puma is that parsing is not on the app thread. It is on the protocol's Ractor pool. The app thread only does the Rack call and the response write. This decouples the two costs and lets Ruby actually use more than one CPU for the protocol work.
|
|
409
425
|
|
|
410
426
|
## Part III: Head to head
|
|
411
427
|
|
|
@@ -413,9 +429,9 @@ The critical structural difference from Puma is that parsing is not on the app t
|
|
|
413
429
|
|
|
414
430
|
**Puma.** Parsing happens on the app thread. The C parser callbacks build the env hash. Between requests, the thread either loops (if there's back-to-back data and a spare thread) or hands off to the reactor. Every request goes through: reactor → thread pool → parser (Ruby thread + C ext) → app → write → reactor. Parsing shares the GVL with the app.
|
|
415
431
|
|
|
416
|
-
**Raptor.** Parsing happens on Ractors. Between requests, the app thread does a 1ms micro-poll before handing back. Every request goes through: reactor (I/O only) → Ractor pool (parse only) → collector → thread pool (app + write) → 1ms wait → maybe repeat. Parsing does not share the GVL with the app because Ractors have their own GVLs.
|
|
432
|
+
**Raptor.** Parsing happens on Ractors. Between requests, the app thread does a 1ms micro-poll before handing back. Every request goes through: reactor (I/O only) → protocol's Ractor pool (parse only) → collector → thread pool (app + write) → 1ms wait → maybe repeat. Parsing does not share the GVL with the app because Ractors have their own GVLs.
|
|
417
433
|
|
|
418
|
-
The concrete effect is that Puma has one process-wide GVL. Every thread inside a worker (server, reactor, and every app thread) has to take turns holding it. Raptor has that same main-process GVL plus one additional GVL per Ractor. With 3 app threads and 1 pipeline Ractor, Raptor can genuinely run
|
|
434
|
+
The concrete effect is that Puma has one process-wide GVL. Every thread inside a worker (server, reactor, and every app thread) has to take turns holding it. Raptor has that same main-process GVL plus one additional GVL per Ractor. With 3 app threads and one HTTP/1.1 pipeline Ractor plus one HTTP/2 pipeline Ractor, Raptor can genuinely run three things at once on three CPU cores. A Ractor parses an incoming request while an app thread executes the Rack app. Puma cannot. Under CPU-heavy parsing (many small requests, high header count), Raptor's parsing throughput is straightforwardly higher because the parse never contends with the app for the same GVL.
|
|
419
435
|
|
|
420
436
|
### Timeout management
|
|
421
437
|
|
|
@@ -449,7 +465,7 @@ This is where most of Raptor's keep-alive edge comes from. Subsequent requests a
|
|
|
449
465
|
|
|
450
466
|
**Puma.** Cluster mode uses `accept_loop_delay` (sleep proportional to busy ratio) to prevent thundering herd across workers. Single-worker backpressure is implicit; if all threads are busy and the queue is growing, new accepts pile up in the kernel accept queue. Puma does have `queue_requests` (default true) which pushes partial requests into the reactor, freeing the accept loop, but there is no explicit "stop accepting" signal from the worker.
|
|
451
467
|
|
|
452
|
-
**Raptor.** Explicit backpressure
|
|
468
|
+
**Raptor.** Explicit backpressure, read every iteration of the accept loop: hard skip when `backlog >= max(pool_size * 1.2, 8)`, and a softer `Thread.pass` yield when the queue alone exceeds the pool size. When a worker is saturated, other workers pick up the traffic. On Linux, an eBPF program attached to the reuseport group actively routes new connections to the least-loaded worker (see the I/O model section); elsewhere Raptor relies on the kernel's default four-tuple-hash routing.
|
|
453
469
|
|
|
454
470
|
### Shared state (worker ↔ master)
|
|
455
471
|
|
|
@@ -551,7 +567,7 @@ Puma has to bounce Request 3 through the reactor and back through the mutex-prot
|
|
|
551
567
|
|
|
552
568
|
### IO-bound work, where Falcon wins and Raptor clearly beats Puma
|
|
553
569
|
|
|
554
|
-
On the IO-bound benchmark profile, each request does 5 to 10 short sleeps interleaved with small CPU work, simulating a request that makes several DB or cache calls throughout its lifetime. The bottleneck is how many requests a worker can keep in flight while they wait on IO. Raptor and Puma cap out at
|
|
570
|
+
On the IO-bound benchmark profile, each request does 5 to 10 short sleeps interleaved with small CPU work, simulating a request that makes several DB or cache calls throughout its lifetime. The bottleneck is how many requests a worker can keep in flight while they wait on IO. Raptor and Puma cap out at three threads per worker, so the client connections beyond that (four per app thread, per the load generator's ratio) sit in the pool queue at any given moment. Falcon spawns a fiber per connection and cooperatively yields on every sleep, so every client connection can be in flight simultaneously. That gap shows up as roughly 3-4x throughput and much lower p95 for Falcon.
|
|
555
571
|
|
|
556
572
|
Between the thread-based servers, Raptor holds a clear lead over Puma on both throughput and p95. The eager accept path, writev-batched responses, and lock-free work queue all shave a bit of per-request time, and those savings stack.
|
|
557
573
|
|
|
@@ -573,7 +589,7 @@ Where Raptor's HTTP/2 support does matter is the all-Ruby stack. No proxy in fro
|
|
|
573
589
|
|
|
574
590
|
Falcon also speaks HTTP/2 natively, so it's the interesting comparison there rather than Puma. The shape of the h2 result follows the h1 shape. On CPU-bound h2 Raptor lands close to Falcon on throughput and holds a meaningful edge on p95, for the same reasons it wins CPU-bound h1 keep-alive. Parsing runs in parallel with the app, the lock-free writer serialises frames without a mutex, and per-stream flow-control atoms don't contend. On IO-bound h2 Falcon wins by a wide margin, again because fibers can keep every stream in flight simultaneously while Raptor's thread pool caps concurrency.
|
|
575
591
|
|
|
576
|
-
Raptor's h2 IO numbers still show some run-to-run variance where h1 doesn't. With only
|
|
592
|
+
Raptor's h2 IO numbers still show some run-to-run variance where h1 doesn't. With only four physical h2 connections per worker, even the BPF program's hash-based tie-break can leave one worker over-subscribed, and the concentrated worker becomes the bottleneck for that whole run. h2 CPU stays tight because once workers report distinguishable load the load-aware routing has enough signal to place new connections cleanly regardless of tie-break behaviour.
|
|
577
593
|
|
|
578
594
|
Beyond that, Raptor's HTTP/2 CPU-bound throughput in the benchmark is on the same order as its HTTP/1.1 keep-alive throughput on the same profile, despite each connection multiplexing dozens of concurrent streams into a single socket. That only happens if the per-stream coordination is essentially free. The lock-free `Writer` and flow-control atoms are doing real work here. If they used mutexes, throughput would be capped by lock contention rather than by CPU.
|
|
579
595
|
|
|
@@ -81,7 +81,6 @@ module Rackup
|
|
|
81
81
|
socket_backlog: (config[:socket_backlog] || cli_defaults[:socket_backlog]).to_i,
|
|
82
82
|
drain_accept_queue: config.key?(:drain_accept_queue) ? config[:drain_accept_queue] : cli_defaults[:drain_accept_queue],
|
|
83
83
|
workers: (options[:Workers] || config[:workers] || Concurrent.available_processor_count).to_i,
|
|
84
|
-
ractors: (options[:Ractors] || config[:ractors] || cli_defaults[:ractors]).to_i,
|
|
85
84
|
threads: (options[:Threads] || config[:threads] || cli_defaults[:threads]).to_i,
|
|
86
85
|
app: app
|
|
87
86
|
}
|
data/lib/raptor/cli.rb
CHANGED
|
@@ -33,7 +33,6 @@ module Raptor
|
|
|
33
33
|
socket_backlog: 1024,
|
|
34
34
|
drain_accept_queue: false,
|
|
35
35
|
workers: DEFAULT_WORKER_COUNT,
|
|
36
|
-
ractors: 1,
|
|
37
36
|
threads: 3,
|
|
38
37
|
rackup: "config.ru",
|
|
39
38
|
chdir: nil,
|
|
@@ -46,10 +45,12 @@ module Raptor
|
|
|
46
45
|
body_spool_threshold: 1024 * 1024,
|
|
47
46
|
},
|
|
48
47
|
http1: {
|
|
48
|
+
ractors: nil,
|
|
49
49
|
persistent_data_timeout: 65,
|
|
50
50
|
max_keepalive_requests: 100,
|
|
51
51
|
},
|
|
52
52
|
http2: {
|
|
53
|
+
ractors: nil,
|
|
53
54
|
max_concurrent_streams: 100,
|
|
54
55
|
},
|
|
55
56
|
worker_boot_timeout: 60,
|
|
@@ -237,10 +238,6 @@ module Raptor
|
|
|
237
238
|
@options[:workers] = num
|
|
238
239
|
end
|
|
239
240
|
|
|
240
|
-
opts.on("-r", "--ractors NUM", Integer, "Number of pipeline ractors per worker (default: 1)") do |num|
|
|
241
|
-
@options[:ractors] = num
|
|
242
|
-
end
|
|
243
|
-
|
|
244
241
|
opts.on("-t", "--threads NUM", Integer, "Number of application threads per worker (default: 3)") do |num|
|
|
245
242
|
@options[:threads] = num
|
|
246
243
|
end
|
|
@@ -273,6 +270,10 @@ module Raptor
|
|
|
273
270
|
@options[:connection][:body_spool_threshold] = bytes
|
|
274
271
|
end
|
|
275
272
|
|
|
273
|
+
opts.on("--http1-ractors NUM", Integer, "Number of HTTP/1.1 pipeline ractors per worker (default: `round(cores / workers)`, clamped to 1..3)") do |num|
|
|
274
|
+
@options[:http1][:ractors] = num
|
|
275
|
+
end
|
|
276
|
+
|
|
276
277
|
opts.on("--http1-persistent-data-timeout SECONDS", Integer, "HTTP/1.1 keep-alive idle timeout in seconds (default: 65)") do |timeout|
|
|
277
278
|
@options[:http1][:persistent_data_timeout] = timeout
|
|
278
279
|
end
|
|
@@ -281,6 +282,10 @@ module Raptor
|
|
|
281
282
|
@options[:http1][:max_keepalive_requests] = num
|
|
282
283
|
end
|
|
283
284
|
|
|
285
|
+
opts.on("--http2-ractors NUM", Integer, "Number of HTTP/2 pipeline ractors per worker (default: `round(cores / workers)`, clamped to 1..2)") do |num|
|
|
286
|
+
@options[:http2][:ractors] = num
|
|
287
|
+
end
|
|
288
|
+
|
|
284
289
|
opts.on("--http2-max-concurrent-streams NUM", Integer, "Maximum HTTP/2 concurrent streams per connection (default: 100)") do |num|
|
|
285
290
|
@options[:http2][:max_concurrent_streams] = num
|
|
286
291
|
end
|
data/lib/raptor/cluster.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# rbs_inline: enabled
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require "concurrent/utility/processor_counter"
|
|
4
5
|
require "json"
|
|
5
6
|
|
|
6
7
|
require "atomic-ruby/atomic_thread_pool"
|
|
@@ -26,6 +27,9 @@ module Raptor
|
|
|
26
27
|
class Cluster
|
|
27
28
|
INHERITED_FDS_ENV = "RAPTOR_INHERITED_FDS"
|
|
28
29
|
|
|
30
|
+
HTTP1_RACTOR_COUNT_CAP = 3
|
|
31
|
+
HTTP2_RACTOR_COUNT_CAP = 2
|
|
32
|
+
|
|
29
33
|
# Creates and runs a cluster with the given options.
|
|
30
34
|
#
|
|
31
35
|
# @param options [Hash] cluster configuration options
|
|
@@ -36,9 +40,38 @@ module Raptor
|
|
|
36
40
|
new(options).run
|
|
37
41
|
end
|
|
38
42
|
|
|
43
|
+
# Returns the default number of HTTP/1.1 pipeline ractors per
|
|
44
|
+
# worker for the given worker count, rounded from the available
|
|
45
|
+
# processor count per worker and clamped to
|
|
46
|
+
# `[1, HTTP1_RACTOR_COUNT_CAP]`.
|
|
47
|
+
#
|
|
48
|
+
# @param worker_count [Integer] the configured worker count
|
|
49
|
+
# @return [Integer]
|
|
50
|
+
#
|
|
51
|
+
# @rbs (Integer worker_count) -> Integer
|
|
52
|
+
def self.default_http1_ractor_count(worker_count)
|
|
53
|
+
cores = Integer(Concurrent.available_processor_count)
|
|
54
|
+
(cores.to_f / worker_count).round.clamp(1, HTTP1_RACTOR_COUNT_CAP)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns the default number of HTTP/2 pipeline ractors per
|
|
58
|
+
# worker for the given worker count, rounded from the available
|
|
59
|
+
# processor count per worker and clamped to
|
|
60
|
+
# `[1, HTTP2_RACTOR_COUNT_CAP]`.
|
|
61
|
+
#
|
|
62
|
+
# @param worker_count [Integer] the configured worker count
|
|
63
|
+
# @return [Integer]
|
|
64
|
+
#
|
|
65
|
+
# @rbs (Integer worker_count) -> Integer
|
|
66
|
+
def self.default_http2_ractor_count(worker_count)
|
|
67
|
+
cores = Integer(Concurrent.available_processor_count)
|
|
68
|
+
(cores.to_f / worker_count).round.clamp(1, HTTP2_RACTOR_COUNT_CAP)
|
|
69
|
+
end
|
|
70
|
+
|
|
39
71
|
# @rbs @drain_accept_queue: bool
|
|
40
72
|
# @rbs @worker_count: Integer
|
|
41
|
-
# @rbs @
|
|
73
|
+
# @rbs @http1_ractor_count: Integer
|
|
74
|
+
# @rbs @http2_ractor_count: Integer
|
|
42
75
|
# @rbs @thread_count: Integer
|
|
43
76
|
# @rbs @environment: String
|
|
44
77
|
# @rbs @connection_options: Hash[Symbol, untyped]
|
|
@@ -91,7 +124,6 @@ module Raptor
|
|
|
91
124
|
# @option options [Integer] :socket_backlog kernel listen() queue depth for TCP/SSL listeners
|
|
92
125
|
# @option options [Boolean] :drain_accept_queue whether to drain the kernel accept queue on shutdown
|
|
93
126
|
# @option options [Integer] :workers number of worker processes
|
|
94
|
-
# @option options [Integer] :ractors number of ractors per worker process
|
|
95
127
|
# @option options [Integer] :threads number of threads per worker process
|
|
96
128
|
# @option options [#call] :app pre-built Rack application
|
|
97
129
|
# @option options [String] :rackup path to Rack configuration file
|
|
@@ -123,7 +155,8 @@ module Raptor
|
|
|
123
155
|
def initialize(options)
|
|
124
156
|
@drain_accept_queue = options[:drain_accept_queue]
|
|
125
157
|
@worker_count = options[:workers]
|
|
126
|
-
@
|
|
158
|
+
@http1_ractor_count = options[:http1][:ractors] || self.class.default_http1_ractor_count(@worker_count)
|
|
159
|
+
@http2_ractor_count = options[:http2][:ractors] || self.class.default_http2_ractor_count(@worker_count)
|
|
127
160
|
@thread_count = options[:threads]
|
|
128
161
|
@environment = options[:environment] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
|
|
129
162
|
@connection_options = options[:connection]
|
|
@@ -736,23 +769,33 @@ module Raptor
|
|
|
736
769
|
access_log_io: @access_log_io,
|
|
737
770
|
on_error: @on_error
|
|
738
771
|
)
|
|
739
|
-
|
|
740
|
-
size: @
|
|
741
|
-
worker: http1.parser_worker
|
|
772
|
+
http1_ractor_pool = RactorPool.new(
|
|
773
|
+
size: @http1_ractor_count,
|
|
774
|
+
worker: http1.parser_worker,
|
|
775
|
+
name: "HTTP/1.1"
|
|
742
776
|
) do |parsed_result|
|
|
743
777
|
begin
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
778
|
+
http1.handle_parsed_request(parsed_result, reactor, thread_pool)
|
|
779
|
+
rescue => error
|
|
780
|
+
Log.rescued_error(error)
|
|
781
|
+
end
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
http2_ractor_pool = RactorPool.new(
|
|
785
|
+
size: @http2_ractor_count,
|
|
786
|
+
worker: http2.parser_worker,
|
|
787
|
+
name: "HTTP/2"
|
|
788
|
+
) do |parsed_result|
|
|
789
|
+
begin
|
|
790
|
+
http2.handle_parsed_request(parsed_result, reactor, thread_pool)
|
|
749
791
|
rescue => error
|
|
750
792
|
Log.rescued_error(error)
|
|
751
793
|
end
|
|
752
794
|
end
|
|
753
795
|
|
|
754
796
|
reactor = Reactor.new(
|
|
755
|
-
|
|
797
|
+
http1_ractor_pool,
|
|
798
|
+
http2_ractor_pool,
|
|
756
799
|
thread_pool,
|
|
757
800
|
connection_options: @connection_options,
|
|
758
801
|
http1_options: @http1_options
|
|
@@ -824,7 +867,8 @@ module Raptor
|
|
|
824
867
|
server_thread.join
|
|
825
868
|
reactor.shutdown
|
|
826
869
|
reactor_thread.join
|
|
827
|
-
|
|
870
|
+
http1_ractor_pool.shutdown
|
|
871
|
+
http2_ractor_pool.shutdown
|
|
828
872
|
http1.shutdown
|
|
829
873
|
drain_thread_pool(thread_pool)
|
|
830
874
|
stats_thread.join
|
|
@@ -931,8 +975,9 @@ module Raptor
|
|
|
931
975
|
Log.info "│ └─ #{@worker_count} worker process#{"es" if @worker_count > 1}"
|
|
932
976
|
Log.info "│ ├─ 1 server thread"
|
|
933
977
|
Log.info "│ ├─ 1 reactor thread"
|
|
934
|
-
Log.info "│ ├─ #{@
|
|
935
|
-
Log.info "│ ├─
|
|
978
|
+
Log.info "│ ├─ #{@http1_ractor_count} HTTP/1.1 pipeline ractor#{"s" if @http1_ractor_count > 1}"
|
|
979
|
+
Log.info "│ ├─ #{@http2_ractor_count} HTTP/2 pipeline ractor#{"s" if @http2_ractor_count > 1}"
|
|
980
|
+
Log.info "│ ├─ 2 pipeline collector threads"
|
|
936
981
|
Log.info "│ ├─ #{@thread_count} worker thread#{"s" if @thread_count > 1}"
|
|
937
982
|
Log.info "│ └─ 1 stats thread"
|
|
938
983
|
Log.info "└─ Listening on #{@binder.addresses.join(", ")}"
|
data/lib/raptor/http.rb
CHANGED
|
@@ -93,8 +93,8 @@ module Raptor
|
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
# Returns a Ractor-safe proc that
|
|
97
|
-
#
|
|
96
|
+
# Returns a Ractor-safe proc that parses an HTTP/1.x request from
|
|
97
|
+
# the state hash's buffered bytes.
|
|
98
98
|
#
|
|
99
99
|
# @param env_template [Hash] the Rack env template to seed each HTTP/1.x request with
|
|
100
100
|
# @param max_body_size [Integer, nil] byte limit for HTTP/1.x request bodies, or nil for no limit
|
|
@@ -103,11 +103,7 @@ module Raptor
|
|
|
103
103
|
# @rbs (Hash[String, untyped] env_template, Integer? max_body_size) -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
104
104
|
def self.parser_worker(env_template, max_body_size)
|
|
105
105
|
proc do |data|
|
|
106
|
-
|
|
107
|
-
Raptor::Http2.process_frames(data)
|
|
108
|
-
else
|
|
109
|
-
Raptor::Http1.parse(data, env_template, max_body_size)
|
|
110
|
-
end
|
|
106
|
+
Raptor::Http1.parse(data, env_template, max_body_size)
|
|
111
107
|
end
|
|
112
108
|
end
|
|
113
109
|
|
data/lib/raptor/http2.rb
CHANGED
|
@@ -315,6 +315,18 @@ module Raptor
|
|
|
315
315
|
Writer.new(write_timeout: @write_timeout)
|
|
316
316
|
end
|
|
317
317
|
|
|
318
|
+
# Returns a Ractor-safe proc that parses HTTP/2 frames from the
|
|
319
|
+
# state hash's buffered bytes.
|
|
320
|
+
#
|
|
321
|
+
# @return [Proc]
|
|
322
|
+
#
|
|
323
|
+
# @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
324
|
+
def parser_worker
|
|
325
|
+
proc do |data|
|
|
326
|
+
Raptor::Http2.process_frames(data)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
318
330
|
# Advances HTTP/2 frame parsing from the connection buffer, returning
|
|
319
331
|
# updated connection state along with any outgoing protocol frames and
|
|
320
332
|
# completed stream requests.
|
|
@@ -550,12 +562,61 @@ module Raptor
|
|
|
550
562
|
end
|
|
551
563
|
private_class_method :build_result
|
|
552
564
|
|
|
565
|
+
# Sends the server SETTINGS frame on a freshly negotiated HTTP/2
|
|
566
|
+
# connection, then eagerly reads and parses the first client frame
|
|
567
|
+
# batch, dispatching completed streams directly to the thread pool.
|
|
568
|
+
# Falls back to the reactor when no initial data is ready.
|
|
569
|
+
#
|
|
570
|
+
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
571
|
+
# @param id [Integer] unique client identifier
|
|
572
|
+
# @param reactor [Reactor] the reactor managing the connection
|
|
573
|
+
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
574
|
+
# @param remote_addr [String] client IP address
|
|
575
|
+
# @param url_scheme [String] "https"
|
|
576
|
+
# @return [void]
|
|
577
|
+
#
|
|
578
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
|
|
579
|
+
def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
|
|
580
|
+
writer = create_writer
|
|
581
|
+
flow_control = FlowControl.new
|
|
582
|
+
initial_state = {
|
|
583
|
+
id: id,
|
|
584
|
+
protocol: :http2,
|
|
585
|
+
remote_addr: remote_addr,
|
|
586
|
+
url_scheme: url_scheme
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
reactor.attach_http2(id: id, socket: socket, state: initial_state, writer: writer, flow_control: flow_control)
|
|
590
|
+
|
|
591
|
+
socket.write(@initial_settings_frame) rescue nil
|
|
592
|
+
|
|
593
|
+
buffer = begin
|
|
594
|
+
socket.read_nonblock(EAGER_READ_BUFFER_SIZE)
|
|
595
|
+
rescue IO::WaitReadable
|
|
596
|
+
reactor.watch(id)
|
|
597
|
+
return
|
|
598
|
+
rescue EOFError, IOError
|
|
599
|
+
reactor.close_connection(id)
|
|
600
|
+
return
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
while socket.pending.positive?
|
|
604
|
+
buffer << socket.read_nonblock(socket.pending)
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
result = Raptor::Http2.process_frames(initial_state.merge(buffer: buffer))
|
|
608
|
+
handle_parsed_request(result, reactor, thread_pool)
|
|
609
|
+
rescue => error
|
|
610
|
+
Log.rescued_error(error)
|
|
611
|
+
reactor.close_connection(id)
|
|
612
|
+
end
|
|
613
|
+
|
|
553
614
|
# Handles a parsed HTTP/2 result. Writes outgoing frames, dispatches
|
|
554
615
|
# completed stream requests to the thread pool, and eagerly consumes
|
|
555
616
|
# further buffered frame batches before returning control to the
|
|
556
617
|
# reactor.
|
|
557
618
|
#
|
|
558
|
-
# @param result [Hash] the parsed result
|
|
619
|
+
# @param result [Hash] the parsed result produced by {process_frames}
|
|
559
620
|
# @param reactor [Reactor] the reactor managing the connection
|
|
560
621
|
# @param thread_pool [AtomicThreadPool] thread pool for Rack app dispatch
|
|
561
622
|
# @return [void]
|
data/lib/raptor/reactor.rb
CHANGED
|
@@ -54,7 +54,8 @@ module Raptor
|
|
|
54
54
|
TIMEOUT_RESPONSE = "HTTP/1.1 408 Request Timeout\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
55
55
|
|
|
56
56
|
# @rbs @thread_pool: untyped
|
|
57
|
-
# @rbs @
|
|
57
|
+
# @rbs @http1_ractor_pool: untyped
|
|
58
|
+
# @rbs @http2_ractor_pool: untyped
|
|
58
59
|
# @rbs @first_data_timeout: Integer
|
|
59
60
|
# @rbs @chunk_data_timeout: Integer
|
|
60
61
|
# @rbs @persistent_data_timeout: Integer
|
|
@@ -69,7 +70,8 @@ module Raptor
|
|
|
69
70
|
|
|
70
71
|
# Creates a new Reactor instance.
|
|
71
72
|
#
|
|
72
|
-
# @param
|
|
73
|
+
# @param http1_ractor_pool [RactorPool] ractor pool for HTTP/1.x parsing
|
|
74
|
+
# @param http2_ractor_pool [RactorPool] ractor pool for HTTP/2 parsing
|
|
73
75
|
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
74
76
|
# @param connection_options [Hash] per-connection timeout configuration
|
|
75
77
|
# @option connection_options [Integer] :first_data_timeout timeout for initial data
|
|
@@ -78,9 +80,10 @@ module Raptor
|
|
|
78
80
|
# @option http1_options [Integer] :persistent_data_timeout timeout for keep-alive idle connections
|
|
79
81
|
# @return [void]
|
|
80
82
|
#
|
|
81
|
-
# @rbs (untyped
|
|
82
|
-
def initialize(
|
|
83
|
-
@
|
|
83
|
+
# @rbs (untyped http1_ractor_pool, untyped http2_ractor_pool, untyped thread_pool, connection_options: Hash[Symbol, untyped], http1_options: Hash[Symbol, untyped]) -> void
|
|
84
|
+
def initialize(http1_ractor_pool, http2_ractor_pool, thread_pool, connection_options:, http1_options:)
|
|
85
|
+
@http1_ractor_pool = http1_ractor_pool
|
|
86
|
+
@http2_ractor_pool = http2_ractor_pool
|
|
84
87
|
@thread_pool = thread_pool
|
|
85
88
|
@first_data_timeout = connection_options[:first_data_timeout]
|
|
86
89
|
@chunk_data_timeout = connection_options[:chunk_data_timeout]
|
|
@@ -260,6 +263,40 @@ module Raptor
|
|
|
260
263
|
@id_to_flow_control[id]
|
|
261
264
|
end
|
|
262
265
|
|
|
266
|
+
# Stores an HTTP/2 connection's socket, state, writer, and flow
|
|
267
|
+
# controller in the reactor's per-connection maps.
|
|
268
|
+
#
|
|
269
|
+
# @param id [Integer] unique client identifier
|
|
270
|
+
# @param socket [TCPSocket] the connection socket
|
|
271
|
+
# @param state [Hash] initial connection state
|
|
272
|
+
# @param writer [Http2::Writer] per-connection frame writer
|
|
273
|
+
# @param flow_control [Http2::FlowControl] per-connection outbound flow controller
|
|
274
|
+
# @return [void]
|
|
275
|
+
#
|
|
276
|
+
# @rbs (id: Integer, socket: TCPSocket, state: Hash[Symbol, untyped], writer: untyped, flow_control: untyped) -> void
|
|
277
|
+
def attach_http2(id:, socket:, state:, writer:, flow_control:)
|
|
278
|
+
@id_to_socket[id] = socket
|
|
279
|
+
@socket_to_state[socket] = state
|
|
280
|
+
@id_to_writer[id] = writer
|
|
281
|
+
@id_to_flow_control[id] = flow_control
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Registers an attached socket for future reactor-driven reads.
|
|
285
|
+
#
|
|
286
|
+
# @param id [Integer] unique client identifier
|
|
287
|
+
# @return [void]
|
|
288
|
+
#
|
|
289
|
+
# @rbs (Integer id) -> void
|
|
290
|
+
def watch(id)
|
|
291
|
+
socket = @id_to_socket[id]
|
|
292
|
+
return unless socket
|
|
293
|
+
|
|
294
|
+
@queue << socket
|
|
295
|
+
@selector.wakeup
|
|
296
|
+
rescue ClosedQueueError
|
|
297
|
+
socket.close
|
|
298
|
+
end
|
|
299
|
+
|
|
263
300
|
# Updates connection state for an HTTP/2 connection and re-registers
|
|
264
301
|
# the socket for further reads.
|
|
265
302
|
#
|
|
@@ -383,7 +420,8 @@ module Raptor
|
|
|
383
420
|
end
|
|
384
421
|
|
|
385
422
|
state = state.frozen? ? state.merge(buffer: buffer) : state.merge!(buffer: buffer)
|
|
386
|
-
@
|
|
423
|
+
pool = state[:protocol] == :http2 ? @http2_ractor_pool : @http1_ractor_pool
|
|
424
|
+
pool << Ractor.make_shareable(state)
|
|
387
425
|
end
|
|
388
426
|
|
|
389
427
|
# Cleans up a client connection by removing it from tracking and closing the socket.
|
data/lib/raptor/server.rb
CHANGED
|
@@ -206,18 +206,14 @@ module Raptor
|
|
|
206
206
|
return unless perform_ssl_handshake(ssl_socket)
|
|
207
207
|
|
|
208
208
|
if ssl_socket.alpn_protocol == H2_PROTOCOL
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
remote_addr
|
|
215
|
-
|
|
216
|
-
protocol: :http2,
|
|
217
|
-
writer: @http2.create_writer,
|
|
218
|
-
flow_control: Http2::FlowControl.new
|
|
209
|
+
@http2.eager_accept(
|
|
210
|
+
ssl_socket,
|
|
211
|
+
ssl_socket.object_id,
|
|
212
|
+
@reactor,
|
|
213
|
+
@thread_pool,
|
|
214
|
+
remote_addr,
|
|
215
|
+
HTTPS_SCHEME
|
|
219
216
|
)
|
|
220
|
-
|
|
221
217
|
return
|
|
222
218
|
end
|
|
223
219
|
|
data/lib/raptor/version.rb
CHANGED
|
@@ -8,6 +8,10 @@ module Raptor
|
|
|
8
8
|
class Cluster
|
|
9
9
|
INHERITED_FDS_ENV: ::String
|
|
10
10
|
|
|
11
|
+
HTTP1_RACTOR_COUNT_CAP: ::Integer
|
|
12
|
+
|
|
13
|
+
HTTP2_RACTOR_COUNT_CAP: ::Integer
|
|
14
|
+
|
|
11
15
|
# Creates and runs a cluster with the given options.
|
|
12
16
|
#
|
|
13
17
|
# @param options [Hash] cluster configuration options
|
|
@@ -16,7 +20,27 @@ module Raptor
|
|
|
16
20
|
# @rbs (Hash[Symbol, untyped] options) -> void
|
|
17
21
|
def self.run: (Hash[Symbol, untyped] options) -> void
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
# Returns the default number of HTTP/1.1 pipeline ractors per
|
|
24
|
+
# worker for the given worker count, rounded from the available
|
|
25
|
+
# processor count per worker and clamped to
|
|
26
|
+
# `[1, HTTP1_RACTOR_COUNT_CAP]`.
|
|
27
|
+
#
|
|
28
|
+
# @param worker_count [Integer] the configured worker count
|
|
29
|
+
# @return [Integer]
|
|
30
|
+
#
|
|
31
|
+
# @rbs (Integer worker_count) -> Integer
|
|
32
|
+
def self.default_http1_ractor_count: (Integer worker_count) -> Integer
|
|
33
|
+
|
|
34
|
+
# Returns the default number of HTTP/2 pipeline ractors per
|
|
35
|
+
# worker for the given worker count, rounded from the available
|
|
36
|
+
# processor count per worker and clamped to
|
|
37
|
+
# `[1, HTTP2_RACTOR_COUNT_CAP]`.
|
|
38
|
+
#
|
|
39
|
+
# @param worker_count [Integer] the configured worker count
|
|
40
|
+
# @return [Integer]
|
|
41
|
+
#
|
|
42
|
+
# @rbs (Integer worker_count) -> Integer
|
|
43
|
+
def self.default_http2_ractor_count: (Integer worker_count) -> Integer
|
|
20
44
|
|
|
21
45
|
@server_port: Integer
|
|
22
46
|
|
|
@@ -56,6 +80,10 @@ module Raptor
|
|
|
56
80
|
|
|
57
81
|
@worker_boot_timeout: Integer
|
|
58
82
|
|
|
83
|
+
@http2_options: Hash[Symbol, untyped]
|
|
84
|
+
|
|
85
|
+
@bpf_active: bool
|
|
86
|
+
|
|
59
87
|
@resp_w: IO?
|
|
60
88
|
|
|
61
89
|
@resp_r: IO?
|
|
@@ -94,8 +122,6 @@ module Raptor
|
|
|
94
122
|
|
|
95
123
|
@app: untyped
|
|
96
124
|
|
|
97
|
-
@http2_options: Hash[Symbol, untyped]
|
|
98
|
-
|
|
99
125
|
@http1_options: Hash[Symbol, untyped]
|
|
100
126
|
|
|
101
127
|
@connection_options: Hash[Symbol, untyped]
|
|
@@ -104,7 +130,9 @@ module Raptor
|
|
|
104
130
|
|
|
105
131
|
@thread_count: Integer
|
|
106
132
|
|
|
107
|
-
@
|
|
133
|
+
@http2_ractor_count: Integer
|
|
134
|
+
|
|
135
|
+
@http1_ractor_count: Integer
|
|
108
136
|
|
|
109
137
|
@worker_count: Integer
|
|
110
138
|
|
|
@@ -117,7 +145,6 @@ module Raptor
|
|
|
117
145
|
# @option options [Integer] :socket_backlog kernel listen() queue depth for TCP/SSL listeners
|
|
118
146
|
# @option options [Boolean] :drain_accept_queue whether to drain the kernel accept queue on shutdown
|
|
119
147
|
# @option options [Integer] :workers number of worker processes
|
|
120
|
-
# @option options [Integer] :ractors number of ractors per worker process
|
|
121
148
|
# @option options [Integer] :threads number of threads per worker process
|
|
122
149
|
# @option options [#call] :app pre-built Rack application
|
|
123
150
|
# @option options [String] :rackup path to Rack configuration file
|
|
@@ -51,8 +51,8 @@ module Raptor
|
|
|
51
51
|
# @rbs (TCPSocket socket, Array[String] strings, ?timeout: Integer) -> void
|
|
52
52
|
def self.socket_writev: (TCPSocket socket, Array[String] strings, ?timeout: Integer) -> void
|
|
53
53
|
|
|
54
|
-
# Returns a Ractor-safe proc that
|
|
55
|
-
#
|
|
54
|
+
# Returns a Ractor-safe proc that parses an HTTP/1.x request from
|
|
55
|
+
# the state hash's buffered bytes.
|
|
56
56
|
#
|
|
57
57
|
# @param env_template [Hash] the Rack env template to seed each HTTP/1.x request with
|
|
58
58
|
# @param max_body_size [Integer, nil] byte limit for HTTP/1.x request bodies, or nil for no limit
|
|
@@ -192,6 +192,14 @@ module Raptor
|
|
|
192
192
|
# @rbs () -> Writer
|
|
193
193
|
def create_writer: () -> Writer
|
|
194
194
|
|
|
195
|
+
# Returns a Ractor-safe proc that parses HTTP/2 frames from the
|
|
196
|
+
# state hash's buffered bytes.
|
|
197
|
+
#
|
|
198
|
+
# @return [Proc]
|
|
199
|
+
#
|
|
200
|
+
# @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
201
|
+
def parser_worker: () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
202
|
+
|
|
195
203
|
# Advances HTTP/2 frame parsing from the connection buffer, returning
|
|
196
204
|
# updated connection state along with any outgoing protocol frames and
|
|
197
205
|
# completed stream requests.
|
|
@@ -235,12 +243,28 @@ module Raptor
|
|
|
235
243
|
# @rbs (Hash[Symbol, untyped] data, String buffer, Array[untyped] hpack_table, Hash[Integer, Hash[Symbol, untyped]] streams, Array[String] outgoing_frames, Array[Hash[Symbol, untyped]] completed_requests, Array[[Integer, Integer]] window_updates, Integer? peer_initial_window_size, Integer connection_window, bool preface_received, Integer last_client_stream_id, Hash[Symbol, untyped]? pending_headers, bool close_connection) -> Hash[Symbol, untyped]
|
|
236
244
|
def self.build_result: (Hash[Symbol, untyped] data, String buffer, Array[untyped] hpack_table, Hash[Integer, Hash[Symbol, untyped]] streams, Array[String] outgoing_frames, Array[Hash[Symbol, untyped]] completed_requests, Array[[ Integer, Integer ]] window_updates, Integer? peer_initial_window_size, Integer connection_window, bool preface_received, Integer last_client_stream_id, Hash[Symbol, untyped]? pending_headers, bool close_connection) -> Hash[Symbol, untyped]
|
|
237
245
|
|
|
246
|
+
# Sends the server SETTINGS frame on a freshly negotiated HTTP/2
|
|
247
|
+
# connection, then eagerly reads and parses the first client frame
|
|
248
|
+
# batch, dispatching completed streams directly to the thread pool.
|
|
249
|
+
# Falls back to the reactor when no initial data is ready.
|
|
250
|
+
#
|
|
251
|
+
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
252
|
+
# @param id [Integer] unique client identifier
|
|
253
|
+
# @param reactor [Reactor] the reactor managing the connection
|
|
254
|
+
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
255
|
+
# @param remote_addr [String] client IP address
|
|
256
|
+
# @param url_scheme [String] "https"
|
|
257
|
+
# @return [void]
|
|
258
|
+
#
|
|
259
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
|
|
260
|
+
def eager_accept: (OpenSSL::SSL::SSLSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
|
|
261
|
+
|
|
238
262
|
# Handles a parsed HTTP/2 result. Writes outgoing frames, dispatches
|
|
239
263
|
# completed stream requests to the thread pool, and eagerly consumes
|
|
240
264
|
# further buffered frame batches before returning control to the
|
|
241
265
|
# reactor.
|
|
242
266
|
#
|
|
243
|
-
# @param result [Hash] the parsed result
|
|
267
|
+
# @param result [Hash] the parsed result produced by {process_frames}
|
|
244
268
|
# @param reactor [Reactor] the reactor managing the connection
|
|
245
269
|
# @param thread_pool [AtomicThreadPool] thread pool for Rack app dispatch
|
|
246
270
|
# @return [void]
|
|
@@ -42,35 +42,38 @@ module Raptor
|
|
|
42
42
|
|
|
43
43
|
TIMEOUT_RESPONSE: ::String
|
|
44
44
|
|
|
45
|
-
@
|
|
45
|
+
@thread_pool: untyped
|
|
46
46
|
|
|
47
|
-
@
|
|
47
|
+
@http1_ractor_pool: untyped
|
|
48
48
|
|
|
49
|
-
@
|
|
49
|
+
@http2_ractor_pool: untyped
|
|
50
50
|
|
|
51
|
-
@
|
|
51
|
+
@first_data_timeout: Integer
|
|
52
52
|
|
|
53
|
-
@
|
|
53
|
+
@chunk_data_timeout: Integer
|
|
54
54
|
|
|
55
|
-
@
|
|
55
|
+
@persistent_data_timeout: Integer
|
|
56
|
+
|
|
57
|
+
@selector: NIO::Selector
|
|
56
58
|
|
|
57
59
|
@queue: Queue[TCPSocket]
|
|
58
60
|
|
|
59
|
-
@
|
|
61
|
+
@timeouts: RedBlackTree[TimeoutClient]
|
|
60
62
|
|
|
61
|
-
@
|
|
63
|
+
@id_to_socket: Hash[Integer, TCPSocket]
|
|
62
64
|
|
|
63
|
-
@
|
|
65
|
+
@socket_to_state: Hash[TCPSocket, Hash[Symbol, untyped]]
|
|
64
66
|
|
|
65
|
-
@
|
|
67
|
+
@id_to_timeout: Hash[Integer, TimeoutClient]
|
|
66
68
|
|
|
67
|
-
@
|
|
69
|
+
@id_to_writer: Hash[Integer, untyped]
|
|
68
70
|
|
|
69
|
-
@
|
|
71
|
+
@id_to_flow_control: Hash[Integer, untyped]
|
|
70
72
|
|
|
71
73
|
# Creates a new Reactor instance.
|
|
72
74
|
#
|
|
73
|
-
# @param
|
|
75
|
+
# @param http1_ractor_pool [RactorPool] ractor pool for HTTP/1.x parsing
|
|
76
|
+
# @param http2_ractor_pool [RactorPool] ractor pool for HTTP/2 parsing
|
|
74
77
|
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
75
78
|
# @param connection_options [Hash] per-connection timeout configuration
|
|
76
79
|
# @option connection_options [Integer] :first_data_timeout timeout for initial data
|
|
@@ -79,8 +82,8 @@ module Raptor
|
|
|
79
82
|
# @option http1_options [Integer] :persistent_data_timeout timeout for keep-alive idle connections
|
|
80
83
|
# @return [void]
|
|
81
84
|
#
|
|
82
|
-
# @rbs (untyped
|
|
83
|
-
def initialize: (untyped
|
|
85
|
+
# @rbs (untyped http1_ractor_pool, untyped http2_ractor_pool, untyped thread_pool, connection_options: Hash[Symbol, untyped], http1_options: Hash[Symbol, untyped]) -> void
|
|
86
|
+
def initialize: (untyped http1_ractor_pool, untyped http2_ractor_pool, untyped thread_pool, connection_options: Hash[Symbol, untyped], http1_options: Hash[Symbol, untyped]) -> void
|
|
84
87
|
|
|
85
88
|
# Starts the reactor's main event loop in a new thread. Runs until
|
|
86
89
|
# the registration queue is closed and drained.
|
|
@@ -159,6 +162,27 @@ module Raptor
|
|
|
159
162
|
# @rbs (Integer id) -> untyped?
|
|
160
163
|
def flow_control_for: (Integer id) -> untyped?
|
|
161
164
|
|
|
165
|
+
# Stores an HTTP/2 connection's socket, state, writer, and flow
|
|
166
|
+
# controller in the reactor's per-connection maps.
|
|
167
|
+
#
|
|
168
|
+
# @param id [Integer] unique client identifier
|
|
169
|
+
# @param socket [TCPSocket] the connection socket
|
|
170
|
+
# @param state [Hash] initial connection state
|
|
171
|
+
# @param writer [Http2::Writer] per-connection frame writer
|
|
172
|
+
# @param flow_control [Http2::FlowControl] per-connection outbound flow controller
|
|
173
|
+
# @return [void]
|
|
174
|
+
#
|
|
175
|
+
# @rbs (id: Integer, socket: TCPSocket, state: Hash[Symbol, untyped], writer: untyped, flow_control: untyped) -> void
|
|
176
|
+
def attach_http2: (id: Integer, socket: TCPSocket, state: Hash[Symbol, untyped], writer: untyped, flow_control: untyped) -> void
|
|
177
|
+
|
|
178
|
+
# Registers an attached socket for future reactor-driven reads.
|
|
179
|
+
#
|
|
180
|
+
# @param id [Integer] unique client identifier
|
|
181
|
+
# @return [void]
|
|
182
|
+
#
|
|
183
|
+
# @rbs (Integer id) -> void
|
|
184
|
+
def watch: (Integer id) -> void
|
|
185
|
+
|
|
162
186
|
# Updates connection state for an HTTP/2 connection and re-registers
|
|
163
187
|
# the socket for further reads.
|
|
164
188
|
#
|