raptor 0.16.0 → 0.17.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/.buildkite/pipeline.yml +4 -4
- data/CHANGELOG.md +13 -1
- data/Dockerfile +0 -2
- data/README.md +16 -16
- data/docs/brisrails-talk.md +1 -1
- data/docs/raptor-vs-puma.md +2 -2
- data/ext/raptor_bpf/reuseport_select.bpf.c +19 -28
- data/ext/raptor_http/raptor_http.c +213 -0
- data/ext/raptor_http2/raptor_http2.c +60 -0
- data/lib/rackup/handler/raptor.rb +0 -1
- data/lib/raptor/cluster.rb +6 -6
- data/lib/raptor/http1.rb +37 -158
- data/lib/raptor/http2.rb +1 -16
- data/lib/raptor/reuseport_bpf.rb +20 -3
- data/lib/raptor/server.rb +5 -2
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/cluster.rbs +6 -4
- data/sig/generated/raptor/http1.rbs +18 -77
- data/sig/generated/raptor/http2.rbs +0 -4
- data/sig/generated/raptor/reuseport_bpf.rbs +12 -2
- data/sig/generated/raptor/server.rbs +3 -2
- 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: fbd24155ed44e8b8faf1f5b11a6078a34ab58bf43c290f3ffcf2a4a582c74d16
|
|
4
|
+
data.tar.gz: 7d6a0f38240c4ae3e723650fcd7a9888f2b59b69f3145018a918524565324972
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 811414a1f6a2469ef38c20935564ba204efdc5988437be5d0fa8a167d85a42204abe2a743115d090b24cf2cc8489e8dffdf57a93c1313936dcfa04a7d2213768
|
|
7
|
+
data.tar.gz: 29a62a959b22ca99c55eea39919d8fe3618fdb5061054718c38503c658c803d77a3a2b40cc9b8089da917faf4041569545073c24c468f6e8552f0d113ee0d469
|
data/.buildkite/pipeline.yml
CHANGED
|
@@ -41,7 +41,7 @@ steps:
|
|
|
41
41
|
soft_fail: true
|
|
42
42
|
image: "rubylang/ruby:master-dev"
|
|
43
43
|
commands:
|
|
44
|
-
- apt-get update && apt-get install -y libyaml-dev
|
|
44
|
+
- apt-get update && apt-get install -y libyaml-dev libssl-dev
|
|
45
45
|
- ruby -v
|
|
46
46
|
- bundle install
|
|
47
47
|
- bundle exec rake ci
|
|
@@ -52,7 +52,7 @@ steps:
|
|
|
52
52
|
env:
|
|
53
53
|
RUBY_YJIT_ENABLE: "1"
|
|
54
54
|
commands:
|
|
55
|
-
- apt-get update && apt-get install -y libyaml-dev
|
|
55
|
+
- apt-get update && apt-get install -y libyaml-dev libssl-dev
|
|
56
56
|
- ruby -v
|
|
57
57
|
- bundle install
|
|
58
58
|
- bundle exec rake ci
|
|
@@ -63,7 +63,7 @@ steps:
|
|
|
63
63
|
env:
|
|
64
64
|
RUBY_GC_STRESS: "1"
|
|
65
65
|
commands:
|
|
66
|
-
- apt-get update && apt-get install -y libyaml-dev
|
|
66
|
+
- apt-get update && apt-get install -y libyaml-dev libssl-dev
|
|
67
67
|
- ruby -v
|
|
68
68
|
- bundle install
|
|
69
69
|
- bundle exec rake ci
|
|
@@ -75,7 +75,7 @@ steps:
|
|
|
75
75
|
RUBY_YJIT_ENABLE: "1"
|
|
76
76
|
RUBY_GC_STRESS: "1"
|
|
77
77
|
commands:
|
|
78
|
-
- apt-get update && apt-get install -y libyaml-dev
|
|
78
|
+
- apt-get update && apt-get install -y libyaml-dev libssl-dev
|
|
79
79
|
- ruby -v
|
|
80
80
|
- bundle install
|
|
81
81
|
- bundle exec rake ci
|
data/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.17.0] - 2026-08-29
|
|
4
|
+
|
|
5
|
+
- Reserve BPF-routed connections before accept
|
|
6
|
+
- Skip unnecessary HTTP/1.1 response work
|
|
7
|
+
- Assemble HTTP/2 response headers in C
|
|
8
|
+
- Emit HTTP/1.1 response headers and chunked bodies in C
|
|
9
|
+
- Decode chunked request bodies in C
|
|
10
|
+
|
|
11
|
+
## [0.16.1] - 2026-08-23
|
|
12
|
+
|
|
13
|
+
- Drop the stale `Ractors=NUM` option from the Rackup handler
|
|
14
|
+
|
|
3
15
|
## [0.16.0] - 2026-08-22
|
|
4
16
|
|
|
5
17
|
- Parse HTTP/2 requests in a dedicated ractor pool
|
|
6
18
|
- Parse the first HTTP/2 frame batch inline on accept
|
|
7
|
-
- Nest the `ractors` config under `http1` and `http2
|
|
19
|
+
- Nest the `ractors` config under `http1` and `http2`
|
|
8
20
|
|
|
9
21
|
## [0.15.1] - 2026-08-16
|
|
10
22
|
|
data/Dockerfile
CHANGED
|
@@ -18,8 +18,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
18
18
|
&& rm -rf /var/lib/apt/lists/*
|
|
19
19
|
|
|
20
20
|
COPY --from=hey-builder /go/bin/hey /usr/local/bin/hey
|
|
21
|
-
COPY --from=hey-builder /usr/local/go /usr/local/go
|
|
22
|
-
ENV PATH=/usr/local/go/bin:${PATH}
|
|
23
21
|
|
|
24
22
|
ENV BUNDLE_PATH=/workspace/.bundle
|
|
25
23
|
ENV BUNDLE_APP_CONFIG=/workspace/.bundle
|
data/README.md
CHANGED
|
@@ -37,7 +37,7 @@ run proc { |_env| [200, { "content-type" => "text/plain" }, ["Hello, World!"]] }
|
|
|
37
37
|
```
|
|
38
38
|
> bundle exec raptor -w 10 -t 3 hello_world.ru
|
|
39
39
|
[Raptor 83654|Main|Main] Cluster initializing:
|
|
40
|
-
[Raptor 83654|Main|Main] ├─ Version: 0.
|
|
40
|
+
[Raptor 83654|Main|Main] ├─ Version: 0.17.0
|
|
41
41
|
[Raptor 83654|Main|Main] ├─ Ruby Version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
|
|
42
42
|
[Raptor 83654|Main|Main] ├─ Environment: development
|
|
43
43
|
[Raptor 83654|Main|Main] ├─ Master PID: 83654
|
|
@@ -201,28 +201,28 @@ Worker 1 (phase 0): pid=91351, requests=1199, busy=1/3, backlog=0, booted, last_
|
|
|
201
201
|
|
|
202
202
|
## (Micro) Benchmarks
|
|
203
203
|
|
|
204
|
-
Raptor 0.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
204
|
+
Raptor 0.17.0 vs Puma 8.0.2 vs Falcon 0.57.0 across two workload profiles. **IO-bound** is a GET endpoint that
|
|
205
|
+
interleaves 5-10 short sleeps (total 2.5-15ms) with small CPU work, simulating a read path that makes several DB or
|
|
206
|
+
cache calls. **CPU-bound** is a POST endpoint that accepts a small JSON body, interleaves 3-5 chunks of JSON item
|
|
207
|
+
building (total 450-1500 items) with sub-100µs sleeps, and returns the built array, simulating a write path that does
|
|
208
|
+
most of its work in Ruby with a few near-zero-cost cache hits.
|
|
209
209
|
|
|
210
210
|
Each cell reports the median throughput and median p95 latency independently across 5 runs, so the two numbers in a row
|
|
211
211
|
may come from different runs. Every run starts a fresh server process so the samples are independent of each other;
|
|
212
212
|
state accumulated in a previous run cannot bias the next. Across the whole table, the widest spread
|
|
213
|
-
((max - min) / 2 / median) between runs of a single cell was ±
|
|
213
|
+
((max - min) / 2 / median) between runs of a single cell was ±22.4% for throughput and ±37.4% for p95.
|
|
214
214
|
|
|
215
|
-
| Protocol | Workload | Raptor req/s | Raptor p95 | Puma req/s | Puma p95 | vs Puma req/s | vs Puma p95 |
|
|
216
|
-
| --------------------- | -------- | ------------ | ---------- | ----------- | --------- | ------------- | ------------ |
|
|
217
|
-
| HTTP/1.1 | IO | 3.
|
|
218
|
-
| HTTP/1.1 | CPU | 8.
|
|
219
|
-
| HTTP/1.1 (keep-alive) | IO |
|
|
220
|
-
| HTTP/1.1 (keep-alive) | CPU | 8.
|
|
221
|
-
| HTTP/2 | IO | 0.
|
|
222
|
-
| HTTP/2 | CPU | 6.
|
|
215
|
+
| Protocol | Workload | Raptor req/s | Raptor p95 | Puma req/s | Puma p95 | vs Puma req/s | vs Puma p95 | Falcon req/s | Falcon p95 | vs Falcon req/s | vs Falcon p95 |
|
|
216
|
+
| --------------------- | -------- | ------------ | ---------- | ----------- | --------- | ------------- | ------------ | ------------ | ---------- | --------------- | ------------- |
|
|
217
|
+
| HTTP/1.1 | IO | 3.35k req/s | 59.60 ms | 1.51k req/s | 126.70 ms | 121.9% higher | 53.0% lower | 12.19k req/s | 14.10 ms | 72.6% lower | 322.7% higher |
|
|
218
|
+
| HTTP/1.1 | CPU | 8.31k req/s | 25.00 ms | 8.63k req/s | 21.10 ms | 3.7% lower | 18.5% higher | 6.57k req/s | 27.40 ms | 26.4% higher | 8.8% lower |
|
|
219
|
+
| HTTP/1.1 (keep-alive) | IO | 3.22k req/s | 46.90 ms | 1.47k req/s | 113.80 ms | 119.9% higher | 58.8% lower | 6.23k req/s | 28.20 ms | 48.2% lower | 66.3% higher |
|
|
220
|
+
| HTTP/1.1 (keep-alive) | CPU | 8.30k req/s | 23.30 ms | 8.41k req/s | 22.10 ms | 1.3% lower | 5.4% higher | 6.95k req/s | 32.30 ms | 19.5% higher | 27.9% lower |
|
|
221
|
+
| HTTP/2 | IO | 0.98k req/s | 196.28 ms | N/A | N/A | - | - | 6.55k req/s | 27.71 ms | 85.1% lower | 608.4% higher |
|
|
222
|
+
| HTTP/2 | CPU | 6.23k req/s | 33.11 ms | N/A | N/A | - | - | 6.92k req/s | 42.94 ms | 10.0% lower | 22.9% lower |
|
|
223
223
|
|
|
224
224
|
> ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [aarch64-linux]
|
|
225
|
-
> 10 worker processes; Raptor
|
|
225
|
+
> 10 worker processes; Raptor and Puma run 3 threads per worker, Falcon runs unbounded fibers per worker;
|
|
226
226
|
> 120 concurrent HTTP/1.1 client connections; 40 concurrent HTTP/2 client connections × 3 streams each
|
|
227
227
|
|
|
228
228
|
See [bin/benchmark](bin/benchmark) for more details.
|
data/docs/brisrails-talk.md
CHANGED
|
@@ -37,7 +37,7 @@ _[luma.com/t3l24s8h](https://luma.com/t3l24s8h)_
|
|
|
37
37
|
- <big>I built a Ruby web server over the last few months</big>
|
|
38
38
|
- <big>It's called Raptor</big>
|
|
39
39
|
- <big>It runs Rack apps, like Puma and Falcon do</big>
|
|
40
|
-
- <big>It's built around
|
|
40
|
+
- <big>It's built around a Ractor-parallel HTTP parser</big>
|
|
41
41
|
- <big>It's fast enough that the numbers are interesting</big>
|
|
42
42
|
- <big>It's not production-ready. Nobody's Rails app should be behind it yet.</big>
|
|
43
43
|
|
data/docs/raptor-vs-puma.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Raptor vs Puma: A Design Comparison
|
|
2
2
|
|
|
3
|
-
Raptor is a Ruby web server designed around
|
|
3
|
+
Raptor is a Ruby web server designed around a Ractor-parallel HTTP parser. On CPU-bound HTTP/1.1 it holds a real edge over the latest Puma release on the same hardware regardless of keep-alive; on IO-heavy work fibers still win, and Falcon is the server to compare to there rather than Raptor or Puma. Raptor also speaks HTTP/2 natively, which Puma does not. This document is a systems-design walkthrough of how it manages both.
|
|
4
4
|
|
|
5
5
|
## Why this document exists
|
|
6
6
|
|
|
@@ -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 servers run one worker process per available CPU; Raptor
|
|
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 and Puma 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
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
#include <bpf/bpf_helpers.h>
|
|
3
3
|
|
|
4
4
|
#define MAX_WORKERS 64
|
|
5
|
-
#define LOAD_TIE_TOLERANCE 1
|
|
6
5
|
|
|
7
6
|
// Per-worker listening sockets, keyed by worker index.
|
|
8
7
|
struct {
|
|
@@ -20,14 +19,12 @@ struct {
|
|
|
20
19
|
__uint(max_entries, MAX_WORKERS + 1);
|
|
21
20
|
} loads SEC(".maps");
|
|
22
21
|
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// instead of clustering on whichever worker most recently reported the
|
|
28
|
-
// lowest load.
|
|
22
|
+
// Uses the power-of-two-choices strategy and reserves the selected worker's
|
|
23
|
+
// slot before returning. Sampling two hash-selected workers avoids
|
|
24
|
+
// concentrating variable-cost requests, while the reservation prevents a
|
|
25
|
+
// burst from repeatedly selecting the same reported load.
|
|
29
26
|
SEC("sk_reuseport")
|
|
30
|
-
int
|
|
27
|
+
int select_less_loaded(struct sk_reuseport_md *ctx) {
|
|
31
28
|
__u32 count_key = 0;
|
|
32
29
|
__u32 *count_ptr = bpf_map_lookup_elem(&loads, &count_key);
|
|
33
30
|
if (!count_ptr || *count_ptr == 0) {
|
|
@@ -38,29 +35,23 @@ int select_least_loaded(struct sk_reuseport_md *ctx) {
|
|
|
38
35
|
num_workers = MAX_WORKERS;
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
__u32
|
|
42
|
-
__u32
|
|
43
|
-
__u32
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (!load_ptr) {
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (*load_ptr < min_load) {
|
|
55
|
-
min_load = *load_ptr;
|
|
56
|
-
min_idx = worker_idx;
|
|
57
|
-
}
|
|
58
|
-
if (*load_ptr > max_load) {
|
|
59
|
-
max_load = *load_ptr;
|
|
38
|
+
__u32 chosen_idx = ctx->hash % num_workers;
|
|
39
|
+
__u32 chosen_key = chosen_idx + 1;
|
|
40
|
+
__u32 *chosen_load = bpf_map_lookup_elem(&loads, &chosen_key);
|
|
41
|
+
if (num_workers > 1) {
|
|
42
|
+
__u32 alternate_idx = (chosen_idx + 1 + ((ctx->hash >> 16) % (num_workers - 1))) % num_workers;
|
|
43
|
+
__u32 alternate_key = alternate_idx + 1;
|
|
44
|
+
__u32 *alternate_load = bpf_map_lookup_elem(&loads, &alternate_key);
|
|
45
|
+
if (chosen_load && alternate_load && *alternate_load < *chosen_load) {
|
|
46
|
+
chosen_idx = alternate_idx;
|
|
47
|
+
chosen_load = alternate_load;
|
|
60
48
|
}
|
|
61
49
|
}
|
|
62
50
|
|
|
63
|
-
|
|
51
|
+
if (chosen_load) {
|
|
52
|
+
__sync_fetch_and_add(chosen_load, 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
64
55
|
bpf_sk_select_reuseport(ctx, &socks, &chosen_idx, 0);
|
|
65
56
|
return SK_PASS;
|
|
66
57
|
}
|
|
@@ -37,6 +37,8 @@ typedef struct raptor_parser {
|
|
|
37
37
|
#define FLAG_HAS_BODY 0x2
|
|
38
38
|
#define FLAG_FINISHED 0x4
|
|
39
39
|
|
|
40
|
+
#define MAX_CHUNK_OVERHEAD (16 * 1024)
|
|
41
|
+
|
|
40
42
|
static VALUE eHttpParserError;
|
|
41
43
|
static VALUE global_request_method;
|
|
42
44
|
static VALUE global_request_uri;
|
|
@@ -44,6 +46,10 @@ static VALUE global_query_string;
|
|
|
44
46
|
static VALUE global_server_protocol;
|
|
45
47
|
static VALUE global_request_path;
|
|
46
48
|
static VALUE global_fragment;
|
|
49
|
+
static ID id_complete;
|
|
50
|
+
static ID id_incomplete;
|
|
51
|
+
static ID id_malformed;
|
|
52
|
+
static ID id_too_large;
|
|
47
53
|
|
|
48
54
|
struct common_field {
|
|
49
55
|
const char *name;
|
|
@@ -1316,6 +1322,200 @@ static VALUE parser_body(VALUE self) {
|
|
|
1316
1322
|
return parser->body;
|
|
1317
1323
|
}
|
|
1318
1324
|
|
|
1325
|
+
static const char *find_crlf(const char *buf, size_t len) {
|
|
1326
|
+
if (len < 2) return NULL;
|
|
1327
|
+
const char *end = buf + len - 1;
|
|
1328
|
+
for (const char *p = buf; p < end; p++) {
|
|
1329
|
+
if (*p == '\r' && *(p + 1) == '\n') return p;
|
|
1330
|
+
}
|
|
1331
|
+
return NULL;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
static VALUE parser_decode_chunked(int argc, VALUE *argv, VALUE self) {
|
|
1335
|
+
(void)self;
|
|
1336
|
+
VALUE buffer, max_size;
|
|
1337
|
+
rb_scan_args(argc, argv, "11", &buffer, &max_size);
|
|
1338
|
+
Check_Type(buffer, T_STRING);
|
|
1339
|
+
|
|
1340
|
+
const char *buf = RSTRING_PTR(buffer);
|
|
1341
|
+
size_t len = (size_t)RSTRING_LEN(buffer);
|
|
1342
|
+
size_t offset = 0;
|
|
1343
|
+
size_t overhead = 0;
|
|
1344
|
+
int has_max = !NIL_P(max_size);
|
|
1345
|
+
size_t max_bytes = has_max ? NUM2SIZET(max_size) : 0;
|
|
1346
|
+
|
|
1347
|
+
VALUE decoded = rb_str_buf_new((long)len);
|
|
1348
|
+
|
|
1349
|
+
while (offset < len) {
|
|
1350
|
+
const char *crlf = find_crlf(buf + offset, len - offset);
|
|
1351
|
+
if (!crlf) return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
|
|
1352
|
+
|
|
1353
|
+
size_t line_len = (size_t)(crlf - (buf + offset));
|
|
1354
|
+
size_t size_len = line_len;
|
|
1355
|
+
for (size_t i = 0; i < line_len; i++) {
|
|
1356
|
+
if (buf[offset + i] == ';') { size_len = i; break; }
|
|
1357
|
+
}
|
|
1358
|
+
if (size_len == 0) return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
|
|
1359
|
+
|
|
1360
|
+
uint64_t chunk_size = 0;
|
|
1361
|
+
for (size_t i = 0; i < size_len; i++) {
|
|
1362
|
+
char c = buf[offset + i];
|
|
1363
|
+
uint64_t digit;
|
|
1364
|
+
if (c >= '0' && c <= '9') digit = (uint64_t)(c - '0');
|
|
1365
|
+
else if (c >= 'a' && c <= 'f') digit = (uint64_t)(c - 'a' + 10);
|
|
1366
|
+
else if (c >= 'A' && c <= 'F') digit = (uint64_t)(c - 'A' + 10);
|
|
1367
|
+
else return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
|
|
1368
|
+
if (chunk_size > (UINT64_MAX >> 4)) return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
|
|
1369
|
+
chunk_size = (chunk_size << 4) | digit;
|
|
1370
|
+
}
|
|
1371
|
+
if (chunk_size > (uint64_t)(SIZE_MAX / 2)) return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
|
|
1372
|
+
|
|
1373
|
+
size_t crlf_offset = offset + line_len;
|
|
1374
|
+
|
|
1375
|
+
if (chunk_size == 0) {
|
|
1376
|
+
size_t trailer_offset = crlf_offset + 2;
|
|
1377
|
+
while (1) {
|
|
1378
|
+
if (trailer_offset >= len) return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
|
|
1379
|
+
const char *tcrlf = find_crlf(buf + trailer_offset, len - trailer_offset);
|
|
1380
|
+
if (!tcrlf) return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
|
|
1381
|
+
if (tcrlf == buf + trailer_offset) return rb_ary_new_from_args(2, decoded, ID2SYM(id_complete));
|
|
1382
|
+
trailer_offset = (size_t)(tcrlf - buf) + 2;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
size_t decoded_len = (size_t)RSTRING_LEN(decoded);
|
|
1387
|
+
if (has_max && (decoded_len + (size_t)chunk_size) > max_bytes) {
|
|
1388
|
+
return rb_ary_new_from_args(2, decoded, ID2SYM(id_too_large));
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
overhead += line_len + 4;
|
|
1392
|
+
if (overhead > (decoded_len + (size_t)chunk_size + MAX_CHUNK_OVERHEAD)) {
|
|
1393
|
+
return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
size_t data_offset = crlf_offset + 2;
|
|
1397
|
+
size_t available = len > data_offset ? len - data_offset : 0;
|
|
1398
|
+
size_t bytes_to_copy = (size_t)chunk_size < available ? (size_t)chunk_size : available;
|
|
1399
|
+
rb_str_cat(decoded, buf + data_offset, (long)bytes_to_copy);
|
|
1400
|
+
|
|
1401
|
+
offset = data_offset + (size_t)chunk_size + 2;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
static uint8_t illegal_header_key_bitmap[32];
|
|
1408
|
+
static uint8_t illegal_header_value_bitmap[32];
|
|
1409
|
+
|
|
1410
|
+
static void bitmap_set(uint8_t *bitmap, unsigned char byte) {
|
|
1411
|
+
bitmap[byte >> 3] |= (uint8_t)(1u << (byte & 7));
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
static int contains_illegal_byte(const uint8_t *bitmap, const char *str, size_t len) {
|
|
1415
|
+
for (size_t i = 0; i < len; i++) {
|
|
1416
|
+
unsigned char byte = (unsigned char)str[i];
|
|
1417
|
+
if ((bitmap[byte >> 3] >> (byte & 7)) & 1) return 1;
|
|
1418
|
+
}
|
|
1419
|
+
return 0;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
static VALUE parser_illegal_header_key_p(VALUE self, VALUE key) {
|
|
1423
|
+
(void)self;
|
|
1424
|
+
Check_Type(key, T_STRING);
|
|
1425
|
+
return contains_illegal_byte(illegal_header_key_bitmap, RSTRING_PTR(key), (size_t)RSTRING_LEN(key)) ? Qtrue : Qfalse;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
static VALUE parser_illegal_header_value_p(VALUE self, VALUE value) {
|
|
1429
|
+
(void)self;
|
|
1430
|
+
Check_Type(value, T_STRING);
|
|
1431
|
+
return contains_illegal_byte(illegal_header_value_bitmap, RSTRING_PTR(value), (size_t)RSTRING_LEN(value)) ? Qtrue : Qfalse;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
static void emit_header_line(VALUE buffer, const char *name, long name_len,
|
|
1435
|
+
const char *value, long value_len) {
|
|
1436
|
+
if (value_len == 0) return;
|
|
1437
|
+
if (contains_illegal_byte(illegal_header_value_bitmap, value, (size_t)value_len)) return;
|
|
1438
|
+
rb_str_cat(buffer, name, name_len);
|
|
1439
|
+
rb_str_cat(buffer, ": ", 2);
|
|
1440
|
+
rb_str_cat(buffer, value, value_len);
|
|
1441
|
+
rb_str_cat(buffer, "\r\n", 2);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
static void emit_header(VALUE buffer, const char *name, long name_len, VALUE value) {
|
|
1445
|
+
VALUE str = RB_TYPE_P(value, T_STRING) ? value : rb_funcall(value, rb_intern("to_s"), 0);
|
|
1446
|
+
const char *ptr = RSTRING_PTR(str);
|
|
1447
|
+
long len = RSTRING_LEN(str);
|
|
1448
|
+
if (len == 0) return;
|
|
1449
|
+
|
|
1450
|
+
if (!memchr(ptr, '\n', (size_t)len)) {
|
|
1451
|
+
emit_header_line(buffer, name, name_len, ptr, len);
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
long line_start = 0;
|
|
1456
|
+
for (long i = 0; i < len; i++) {
|
|
1457
|
+
if (ptr[i] == '\n') {
|
|
1458
|
+
emit_header_line(buffer, name, name_len, ptr + line_start, i - line_start);
|
|
1459
|
+
line_start = i + 1;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (line_start < len) {
|
|
1463
|
+
emit_header_line(buffer, name, name_len, ptr + line_start, len - line_start);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
static int format_header_iter(VALUE key, VALUE value, VALUE data) {
|
|
1468
|
+
VALUE buffer = (VALUE)data;
|
|
1469
|
+
if (!RB_TYPE_P(key, T_STRING)) return ST_CONTINUE;
|
|
1470
|
+
const char *name = RSTRING_PTR(key);
|
|
1471
|
+
long name_len = RSTRING_LEN(key);
|
|
1472
|
+
if (contains_illegal_byte(illegal_header_key_bitmap, name, (size_t)name_len)) return ST_CONTINUE;
|
|
1473
|
+
|
|
1474
|
+
if (RB_TYPE_P(value, T_ARRAY)) {
|
|
1475
|
+
long entries = RARRAY_LEN(value);
|
|
1476
|
+
for (long i = 0; i < entries; i++) {
|
|
1477
|
+
emit_header(buffer, name, name_len, RARRAY_AREF(value, i));
|
|
1478
|
+
}
|
|
1479
|
+
} else {
|
|
1480
|
+
emit_header(buffer, name, name_len, value);
|
|
1481
|
+
}
|
|
1482
|
+
return ST_CONTINUE;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
static VALUE parser_format_headers(VALUE self, VALUE buffer, VALUE headers) {
|
|
1486
|
+
(void)self;
|
|
1487
|
+
Check_Type(buffer, T_STRING);
|
|
1488
|
+
Check_Type(headers, T_HASH);
|
|
1489
|
+
rb_hash_foreach(headers, format_header_iter, buffer);
|
|
1490
|
+
return buffer;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
static VALUE parser_chunked_encode(VALUE self, VALUE buffer, VALUE chunk) {
|
|
1494
|
+
(void)self;
|
|
1495
|
+
Check_Type(buffer, T_STRING);
|
|
1496
|
+
Check_Type(chunk, T_STRING);
|
|
1497
|
+
long chunk_len = RSTRING_LEN(chunk);
|
|
1498
|
+
if (chunk_len == 0) return buffer;
|
|
1499
|
+
|
|
1500
|
+
char hex_buf[17];
|
|
1501
|
+
int hex_len = snprintf(hex_buf, sizeof(hex_buf), "%lx", (unsigned long)chunk_len);
|
|
1502
|
+
rb_str_cat(buffer, hex_buf, hex_len);
|
|
1503
|
+
rb_str_cat(buffer, "\r\n", 2);
|
|
1504
|
+
rb_str_cat(buffer, RSTRING_PTR(chunk), chunk_len);
|
|
1505
|
+
rb_str_cat(buffer, "\r\n", 2);
|
|
1506
|
+
return buffer;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
static void init_illegal_header_bitmaps(void) {
|
|
1510
|
+
for (int byte = 0x00; byte <= 0x20; byte++) bitmap_set(illegal_header_key_bitmap, (unsigned char)byte);
|
|
1511
|
+
const char *key_specials = "()<>@,;:\\\"/[]?={}";
|
|
1512
|
+
for (const char *p = key_specials; *p; p++) bitmap_set(illegal_header_key_bitmap, (unsigned char)*p);
|
|
1513
|
+
bitmap_set(illegal_header_key_bitmap, 0x7F);
|
|
1514
|
+
|
|
1515
|
+
for (int byte = 0x00; byte <= 0x08; byte++) bitmap_set(illegal_header_value_bitmap, (unsigned char)byte);
|
|
1516
|
+
for (int byte = 0x0A; byte <= 0x1F; byte++) bitmap_set(illegal_header_value_bitmap, (unsigned char)byte);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1319
1519
|
RUBY_FUNC_EXPORTED void Init_raptor_http(void) {
|
|
1320
1520
|
rb_ext_ractor_safe(true);
|
|
1321
1521
|
|
|
@@ -1337,6 +1537,13 @@ RUBY_FUNC_EXPORTED void Init_raptor_http(void) {
|
|
|
1337
1537
|
global_request_path = rb_str_new2("PATH_INFO");
|
|
1338
1538
|
global_fragment = rb_str_new2("FRAGMENT");
|
|
1339
1539
|
|
|
1540
|
+
id_complete = rb_intern("complete");
|
|
1541
|
+
id_incomplete = rb_intern("incomplete");
|
|
1542
|
+
id_malformed = rb_intern("malformed");
|
|
1543
|
+
id_too_large = rb_intern("too_large");
|
|
1544
|
+
|
|
1545
|
+
init_illegal_header_bitmaps();
|
|
1546
|
+
|
|
1340
1547
|
for (size_t i = 0; i < NUM_COMMON_FIELDS; i++) {
|
|
1341
1548
|
common_fields[i].interned = rb_enc_interned_str(common_fields[i].name, common_fields[i].len, rb_utf8_encoding());
|
|
1342
1549
|
rb_global_variable(&common_fields[i].interned);
|
|
@@ -1351,4 +1558,10 @@ RUBY_FUNC_EXPORTED void Init_raptor_http(void) {
|
|
|
1351
1558
|
rb_define_method(cHttpParser, "nread", parser_nread, 0);
|
|
1352
1559
|
rb_define_method(cHttpParser, "reset", parser_reset, 0);
|
|
1353
1560
|
rb_define_method(cHttpParser, "body", parser_body, 0);
|
|
1561
|
+
rb_define_singleton_method(cHttpParser, "decode_chunked", parser_decode_chunked, -1);
|
|
1562
|
+
rb_define_singleton_method(cHttpParser, "illegal_header_key?", parser_illegal_header_key_p, 1);
|
|
1563
|
+
rb_define_singleton_method(cHttpParser, "illegal_header_value?", parser_illegal_header_value_p, 1);
|
|
1564
|
+
rb_define_singleton_method(cHttpParser, "format_headers", parser_format_headers, 2);
|
|
1565
|
+
rb_define_singleton_method(cHttpParser, "chunked_encode", parser_chunked_encode, 2);
|
|
1566
|
+
rb_define_const(cHttpParser, "MAX_CHUNK_OVERHEAD", INT2FIX(MAX_CHUNK_OVERHEAD));
|
|
1354
1567
|
}
|
|
@@ -599,6 +599,65 @@ static VALUE h2_encode_headers(VALUE self, VALUE headers) {
|
|
|
599
599
|
return hpack_encode_header_block(headers);
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
+
static const char *const HOP_BY_HOP_HEADERS[] = {
|
|
603
|
+
"connection", "transfer-encoding", "keep-alive", "upgrade", "proxy-connection"
|
|
604
|
+
};
|
|
605
|
+
#define NUM_HOP_BY_HOP_HEADERS 5
|
|
606
|
+
|
|
607
|
+
static int is_hop_by_hop(const char *name, long name_len) {
|
|
608
|
+
for (size_t i = 0; i < NUM_HOP_BY_HOP_HEADERS; i++) {
|
|
609
|
+
size_t hbh_len = strlen(HOP_BY_HOP_HEADERS[i]);
|
|
610
|
+
if ((long)hbh_len == name_len && memcmp(name, HOP_BY_HOP_HEADERS[i], hbh_len) == 0) return 1;
|
|
611
|
+
}
|
|
612
|
+
return 0;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
static VALUE ascii_lowercase_str(VALUE str) {
|
|
616
|
+
long len = RSTRING_LEN(str);
|
|
617
|
+
VALUE lower = rb_str_new(RSTRING_PTR(str), len);
|
|
618
|
+
char *ptr = RSTRING_PTR(lower);
|
|
619
|
+
for (long i = 0; i < len; i++) {
|
|
620
|
+
if (ptr[i] >= 'A' && ptr[i] <= 'Z') ptr[i] = (char)(ptr[i] | 0x20);
|
|
621
|
+
}
|
|
622
|
+
return lower;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
static int response_headers_iter(VALUE key, VALUE value, VALUE data) {
|
|
626
|
+
VALUE pairs = data;
|
|
627
|
+
if (!RB_TYPE_P(key, T_STRING)) return ST_CONTINUE;
|
|
628
|
+
|
|
629
|
+
VALUE lowered = ascii_lowercase_str(key);
|
|
630
|
+
const char *lname = RSTRING_PTR(lowered);
|
|
631
|
+
long lname_len = RSTRING_LEN(lowered);
|
|
632
|
+
|
|
633
|
+
if (lname_len >= 5 && memcmp(lname, "rack.", 5) == 0) return ST_CONTINUE;
|
|
634
|
+
if (is_hop_by_hop(lname, lname_len)) return ST_CONTINUE;
|
|
635
|
+
|
|
636
|
+
if (RB_TYPE_P(value, T_ARRAY)) {
|
|
637
|
+
long entries = RARRAY_LEN(value);
|
|
638
|
+
for (long i = 0; i < entries; i++) {
|
|
639
|
+
VALUE entry = RARRAY_AREF(value, i);
|
|
640
|
+
VALUE str = RB_TYPE_P(entry, T_STRING) ? entry : rb_obj_as_string(entry);
|
|
641
|
+
rb_ary_push(pairs, rb_ary_new_from_args(2, lowered, str));
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
VALUE str = RB_TYPE_P(value, T_STRING) ? value : rb_obj_as_string(value);
|
|
645
|
+
rb_ary_push(pairs, rb_ary_new_from_args(2, lowered, str));
|
|
646
|
+
}
|
|
647
|
+
return ST_CONTINUE;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
static VALUE h2_encode_response_headers(VALUE self, VALUE status, VALUE headers) {
|
|
651
|
+
(void)self;
|
|
652
|
+
Check_Type(headers, T_HASH);
|
|
653
|
+
|
|
654
|
+
VALUE pairs = rb_ary_new();
|
|
655
|
+
rb_ary_push(pairs, rb_ary_new_from_args(2, rb_str_new_lit(":status"), rb_obj_as_string(status)));
|
|
656
|
+
rb_hash_foreach(headers, response_headers_iter, pairs);
|
|
657
|
+
|
|
658
|
+
return hpack_encode_header_block(pairs);
|
|
659
|
+
}
|
|
660
|
+
|
|
602
661
|
static VALUE h2_parse_frame(VALUE self, VALUE buffer) {
|
|
603
662
|
(void)self;
|
|
604
663
|
Check_Type(buffer, T_STRING);
|
|
@@ -765,6 +824,7 @@ RUBY_FUNC_EXPORTED void Init_raptor_http2(void) {
|
|
|
765
824
|
rb_define_method(cHttp2Parser, "parse_frame", h2_parse_frame, 1);
|
|
766
825
|
rb_define_method(cHttp2Parser, "parse_headers", h2_parse_headers, 2);
|
|
767
826
|
rb_define_method(cHttp2Parser, "encode_headers", h2_encode_headers, 1);
|
|
827
|
+
rb_define_method(cHttp2Parser, "encode_response_headers", h2_encode_response_headers, 2);
|
|
768
828
|
rb_define_method(cHttp2Parser, "parse_settings", h2_parse_settings, 1);
|
|
769
829
|
rb_define_method(cHttp2Parser, "build_settings", h2_build_settings, 1);
|
|
770
830
|
rb_define_method(cHttp2Parser, "build_frame", h2_build_frame, 4);
|
|
@@ -43,7 +43,6 @@ module Rackup
|
|
|
43
43
|
"Host=HOST" => "Hostname to listen on (default: #{DEFAULT_OPTIONS[:Host]})",
|
|
44
44
|
"Port=PORT" => "Port to listen on (default: #{DEFAULT_OPTIONS[:Port]})",
|
|
45
45
|
"Workers=NUM" => "Number of worker processes (default: available processor count)",
|
|
46
|
-
"Ractors=NUM" => "Number of pipeline ractors per worker (default: 1)",
|
|
47
46
|
"Threads=NUM" => "Number of threads per worker (default: 3)",
|
|
48
47
|
"Config=PATH" => "Load additional configuration from PATH"
|
|
49
48
|
}
|
data/lib/raptor/cluster.rb
CHANGED
|
@@ -46,11 +46,11 @@ module Raptor
|
|
|
46
46
|
# `[1, HTTP1_RACTOR_COUNT_CAP]`.
|
|
47
47
|
#
|
|
48
48
|
# @param worker_count [Integer] the configured worker count
|
|
49
|
+
# @param cores [Integer] the available processor count
|
|
49
50
|
# @return [Integer]
|
|
50
51
|
#
|
|
51
|
-
# @rbs (Integer worker_count) -> Integer
|
|
52
|
-
def self.default_http1_ractor_count(worker_count)
|
|
53
|
-
cores = Integer(Concurrent.available_processor_count)
|
|
52
|
+
# @rbs (Integer worker_count, ?cores: Integer) -> Integer
|
|
53
|
+
def self.default_http1_ractor_count(worker_count, cores: Integer(Concurrent.available_processor_count))
|
|
54
54
|
(cores.to_f / worker_count).round.clamp(1, HTTP1_RACTOR_COUNT_CAP)
|
|
55
55
|
end
|
|
56
56
|
|
|
@@ -60,11 +60,11 @@ module Raptor
|
|
|
60
60
|
# `[1, HTTP2_RACTOR_COUNT_CAP]`.
|
|
61
61
|
#
|
|
62
62
|
# @param worker_count [Integer] the configured worker count
|
|
63
|
+
# @param cores [Integer] the available processor count
|
|
63
64
|
# @return [Integer]
|
|
64
65
|
#
|
|
65
|
-
# @rbs (Integer worker_count) -> Integer
|
|
66
|
-
def self.default_http2_ractor_count(worker_count)
|
|
67
|
-
cores = Integer(Concurrent.available_processor_count)
|
|
66
|
+
# @rbs (Integer worker_count, ?cores: Integer) -> Integer
|
|
67
|
+
def self.default_http2_ractor_count(worker_count, cores: Integer(Concurrent.available_processor_count))
|
|
68
68
|
(cores.to_f / worker_count).round.clamp(1, HTTP2_RACTOR_COUNT_CAP)
|
|
69
69
|
end
|
|
70
70
|
|