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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d2048ec0cf41ced4050d0b53949d96c71cfab42e921c05bd4db16e03099c441d
4
- data.tar.gz: 5c0eacc1154ea6e8ed72ccd29dee4770347dbf773f8087895b9de2be1b9c699b
3
+ metadata.gz: 02fe742ea31e44d742de9f0ed5b94cc53901d39e25b054fe0bd4affd14e169c1
4
+ data.tar.gz: 4279672bfd532f9bcadfb5ce81d85b8ba4921b8072e302bfc33bf74ce4609224
5
5
  SHA512:
6
- metadata.gz: 3844c94223e342793ce75e329f4c86dab40ce353a6d7041e06d4c01f26569102df7a26bba0324f829558363d254e7521862fbe2f846b0de4c1af55f2a1f59062
7
- data.tar.gz: d595b6670b26bc19a8805c48d17bd44ebc5a1e075a8b8cc44db811a0b5f79052862ad160514d109758271010161c5fd9b516e71ef9a2637a7fff25e64aafac2b
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 4 -t 3 hello_world.ru
39
- [Raptor 33061|Main|Main] Cluster initializing:
40
- [Raptor 33061|Main|Main] ├─ Version: 0.15.1
41
- [Raptor 33061|Main|Main] ├─ Ruby Version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
42
- [Raptor 33061|Main|Main] ├─ Environment: development
43
- [Raptor 33061|Main|Main] ├─ Master PID: 33061
44
- [Raptor 33061|Main|Main] │ └─ 4 worker processes
45
- [Raptor 33061|Main|Main] │ ├─ 1 server thread
46
- [Raptor 33061|Main|Main] │ ├─ 1 reactor thread
47
- [Raptor 33061|Main|Main] │ ├─ 1 pipeline ractor
48
- [Raptor 33061|Main|Main] │ ├─ 1 pipeline collector thread
49
- [Raptor 33061|Main|Main] │ ├─ 3 worker threads
50
- [Raptor 33061|Main|Main] │ └─ 1 stats thread
51
- [Raptor 33061|Main|Main] └─ Listening on 0.0.0.0:9292
52
- [Raptor 33064|Main|Main] Worker 0 booted
53
- [Raptor 33065|Main|Main] Worker 1 booted
54
- [Raptor 33066|Main|Main] Worker 2 booted
55
- [Raptor 33067|Main|Main] Worker 3 booted
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.15.1 vs Puma 8.0.2 vs PumaPlus 0.1.0 vs Falcon 0.57.0 across two workload profiles. **IO-bound** is a GET
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 ±29.5% for throughput and ±42.2% for p95.
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.28k req/s | 59.90 ms | 1.44k req/s | 132.20 ms | 127.7% higher | 54.7% lower | 1.55k req/s | 91.30 ms | 111.3% higher | 34.4% lower | 12.50k req/s | 13.80 ms | 73.7% lower | 334.1% higher |
210
- | HTTP/1.1 | CPU | 8.49k req/s | 32.40 ms | 9.08k req/s | 19.40 ms | 6.5% lower | 67.0% higher | 8.79k req/s | 17.40 ms | 3.4% lower | 86.2% higher | 6.89k req/s | 25.50 ms | 23.2% higher | 27.1% higher |
211
- | HTTP/1.1 (keep-alive) | IO | 2.08k req/s | 54.60 ms | 1.40k req/s | 112.80 ms | 48.0% higher | 51.6% lower | 1.53k req/s | 91.10 ms | 35.8% higher | 40.1% lower | 6.17k req/s | 28.70 ms | 66.3% lower | 90.2% higher |
212
- | HTTP/1.1 (keep-alive) | CPU | 9.04k req/s | 17.40 ms | 8.92k req/s | 21.30 ms | 1.3% higher | 18.3% lower | 9.04k req/s | 17.00 ms | 0.1% higher | 2.4% higher | 7.32k req/s | 29.90 ms | 23.5% higher | 41.8% lower |
213
- | HTTP/2 | IO | 0.97k req/s | 201.32 ms | N/A | N/A | - | - | 1.49k req/s | 92.51 ms | 35.0% lower | 117.6% higher | 6.61k req/s | 27.95 ms | 85.4% lower | 620.4% higher |
214
- | HTTP/2 | CPU | 5.42k req/s | 35.22 ms | N/A | N/A | - | - | 8.61k req/s | 18.18 ms | 37.0% lower | 93.8% higher | 7.32k req/s | 29.95 ms | 25.9% lower | 17.6% higher |
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;
@@ -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 RP["Ractor pool"]
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
- COL["Collector thread<br/>drains Ractor::Port"]
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 --> RP
592
- RP --> COL
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 pool
629
+ ## The Ractor pools
624
630
 
625
- The parser. This is where the parallelism actually kicks in.
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 thread<br/>drains Ractor::Port"]
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 one of the 48 client connections in flight at once. Thread-based servers cap out at their pool size.</big>
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>
@@ -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 three servers run four worker processes; Raptor and Puma use three threads per worker, and Falcon uses unbounded fibers per worker. Load generators use 48 concurrent HTTP/1.1 client connections, and 16 h2 connections × 3 streams each for the h2 rows.
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 five kinds of concurrent activity:
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` of M pipeline Ractors** doing HTTP parsing in parallel. Default M is 1, but the interesting part is that this can be scaled up.
220
- 4. **A collector thread** that receives parsed results from the pipeline Ractors via a `Ractor::Port`.
221
- 5. **An `AtomicThreadPool` of T app threads** running the Rack app and writing responses.
222
- 6. Plus one stats thread that writes the shared-memory slot every second.
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 pool actually works.** Raptor uses the `ractor-pool` gem, which is another one of my libraries. The 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; this is the default because a single Ractor already parses in parallel with the app threads and adds enough headroom for typical workloads.
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 check right before `accept`:
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 * 1.2).ceil, MIN_BACKPRESSURE_THRESHOLD].max
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
- where `@reactor.backlog` is `thread_pool.queue_size + thread_pool.active_count`. If the total load on the thread pool is at 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.
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 (or, for HTTP/2, straight to the parser). The reactor does not try to parse; it does the I/O and hands off the raw buffer.
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 is written and the socket is registered in the reactor with `protocol: :http2` plus a fresh `Writer` and `FlowControl` for the connection.
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 RP["RactorPool"]
354
- CRD["Coordinator Ractor<br/>coordinator-dispatch"]
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
- COL["Collector thread<br/>Ractor::Port receive<br/>dispatches to thread pool<br/>or back to reactor"]
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 -->|"data not ready or incomplete, reactor.add"| RCT
377
- RCT -->|"Ractor.make_shareable state"| RP
378
- RP -->|"result via Ractor::Port"| COL
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 RP parse
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 two things at once on two CPU cores. The 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.
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 formula, read every iteration of the accept loop: `if backlog >= max(pool_size * 1.2, 8), skip accept`. 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.
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 their 12 total threads (4 workers × 3), so with 48 client connections the extra 36 sit in the pool queue at any given moment. Falcon spawns a fiber per connection and cooperatively yields on every sleep, so all 48 requests can be in flight simultaneously. That gap shows up as roughly 3-4x throughput and much lower p95 for Falcon.
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 16 physical connections spread across four workers, 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.
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
@@ -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 @ractor_count: Integer
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
- @ractor_count = options[:ractors]
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
- ractor_pool = RactorPool.new(
740
- size: @ractor_count,
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
- if parsed_result[:protocol] == :http2
745
- http2.handle_parsed_request(parsed_result, reactor, thread_pool)
746
- else
747
- http1.handle_parsed_request(parsed_result, reactor, thread_pool)
748
- end
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
- ractor_pool,
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
- ractor_pool.shutdown
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 "│ ├─ #{@ractor_count} pipeline ractor#{"s" if @ractor_count > 1}"
935
- Log.info "│ ├─ 1 pipeline collector thread"
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 routes each pipeline task to the
97
- # HTTP/1.x or HTTP/2 handler based on the state hash's `:protocol` key.
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
- if data[:protocol] == :http2
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 from the ractor pool
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]
@@ -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 @ractor_pool: untyped
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 ractor_pool [RactorPool] ractor pool for HTTP parsing
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 ractor_pool, untyped thread_pool, connection_options: Hash[Symbol, untyped], http1_options: Hash[Symbol, untyped]) -> void
82
- def initialize(ractor_pool, thread_pool, connection_options:, http1_options:)
83
- @ractor_pool = ractor_pool
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
- @ractor_pool << Ractor.make_shareable(state)
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
- ssl_socket.write(@http2.initial_settings_frame) rescue nil
210
-
211
- @reactor.add(
212
- id: ssl_socket.object_id,
213
- socket: ssl_socket,
214
- remote_addr: remote_addr,
215
- url_scheme: HTTPS_SCHEME,
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
 
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Raptor
5
- VERSION = "0.15.1"
5
+ VERSION = "0.16.0"
6
6
  end
@@ -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
- @bpf_active: bool
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
- @ractor_count: Integer
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 routes each pipeline task to the
55
- # HTTP/1.x or HTTP/2 handler based on the state hash's `:protocol` key.
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 from the ractor pool
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
- @id_to_flow_control: Hash[Integer, untyped]
45
+ @thread_pool: untyped
46
46
 
47
- @id_to_writer: Hash[Integer, untyped]
47
+ @http1_ractor_pool: untyped
48
48
 
49
- @id_to_timeout: Hash[Integer, TimeoutClient]
49
+ @http2_ractor_pool: untyped
50
50
 
51
- @socket_to_state: Hash[TCPSocket, Hash[Symbol, untyped]]
51
+ @first_data_timeout: Integer
52
52
 
53
- @id_to_socket: Hash[Integer, TCPSocket]
53
+ @chunk_data_timeout: Integer
54
54
 
55
- @timeouts: RedBlackTree[TimeoutClient]
55
+ @persistent_data_timeout: Integer
56
+
57
+ @selector: NIO::Selector
56
58
 
57
59
  @queue: Queue[TCPSocket]
58
60
 
59
- @selector: NIO::Selector
61
+ @timeouts: RedBlackTree[TimeoutClient]
60
62
 
61
- @persistent_data_timeout: Integer
63
+ @id_to_socket: Hash[Integer, TCPSocket]
62
64
 
63
- @chunk_data_timeout: Integer
65
+ @socket_to_state: Hash[TCPSocket, Hash[Symbol, untyped]]
64
66
 
65
- @first_data_timeout: Integer
67
+ @id_to_timeout: Hash[Integer, TimeoutClient]
66
68
 
67
- @ractor_pool: untyped
69
+ @id_to_writer: Hash[Integer, untyped]
68
70
 
69
- @thread_pool: untyped
71
+ @id_to_flow_control: Hash[Integer, untyped]
70
72
 
71
73
  # Creates a new Reactor instance.
72
74
  #
73
- # @param ractor_pool [RactorPool] ractor pool for HTTP parsing
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 ractor_pool, untyped thread_pool, connection_options: Hash[Symbol, untyped], http1_options: Hash[Symbol, untyped]) -> void
83
- def initialize: (untyped ractor_pool, untyped thread_pool, connection_options: Hash[Symbol, untyped], http1_options: Hash[Symbol, untyped]) -> void
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
  #
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raptor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.1
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young