hyperion-rb 2.16.4 → 2.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/CHANGELOG.md +80 -0
- data/ext/hyperion_http/page_cache.c +183 -6
- data/ext/hyperion_http/parser.c +127 -0
- data/lib/hyperion/connection.rb +33 -7
- data/lib/hyperion/server/route_table.rb +22 -1
- data/lib/hyperion/server.rb +61 -7
- data/lib/hyperion/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3cd6a998da3319e4fa653a2802c6d2114946cfca36dba3c6806f48ada9467e78
|
|
4
|
+
data.tar.gz: 317fb337740577fddd5c1203d2b41f8f8409d8e3998944c13595ea5db06bd1da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff99796e54e33fcbae1c8e61e30bbde780a8531db28601f86375a2babfe4c7c7edf546a1b7512523524e57ec49ec88487a7c350e6a1c890772474e2dd9b11e70
|
|
7
|
+
data.tar.gz: '08c49dbb4fe093a9ed4901e841f5504e374faef1fb4b3ff522f698e26afadbea58200f0546a273355113c6d62e3b4353660248f2fc8300d1959547d48460c715'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.17.0 — 2026-05-08
|
|
4
|
+
|
|
5
|
+
### Hot-path bucket: parser value intern + RFC-compliant handle_static prebuilt
|
|
6
|
+
|
|
7
|
+
Two hot-path optimisations land together. Both are correctness-positive
|
|
8
|
+
(no behavioural change for compliant clients; the second one moves
|
|
9
|
+
`handle_static` from non-RFC-compliant headers to RFC-compliant ones).
|
|
10
|
+
|
|
11
|
+
#### Parser header-value intern table
|
|
12
|
+
|
|
13
|
+
`Hyperion::CParser` (`ext/hyperion_http/parser.c`) gains a 64-slot,
|
|
14
|
+
33-seed open-addressed FNV-1a intern table for the highest-frequency
|
|
15
|
+
HTTP/1.1 header *values* (Connection: keep-alive, Accept-Encoding combos,
|
|
16
|
+
common User-Agent strings, localhost / 127.0.0.1, etc.). On every
|
|
17
|
+
`stash_pending_header` call, the materialised value String is hashed and
|
|
18
|
+
probed against the table; on hit, the freshly-allocated `rb_str_new`
|
|
19
|
+
String becomes garbage and the env Hash holds the pre-frozen interned
|
|
20
|
+
VALUE. Net effect: lower GC retention under burst, faster downstream
|
|
21
|
+
identity-based string compares, reduced promote-to-old-gen pressure on
|
|
22
|
+
long-running workers.
|
|
23
|
+
|
|
24
|
+
- `bench/hello_handle_block.ru` (Row 3): **8727 → 9008 rps median** in
|
|
25
|
+
isolation (+3.2%), within noise but trending positive.
|
|
26
|
+
- Static path (Row 1) is parser-bypass — intern doesn't apply.
|
|
27
|
+
|
|
28
|
+
#### `Server.handle_static` — prebuilt wire bytes with Date splice
|
|
29
|
+
|
|
30
|
+
`handle_static` previously emitted lowercase headers and **no Date
|
|
31
|
+
header**, in violation of RFC 7231 §7.1.1.2 ("An origin server with a
|
|
32
|
+
clock MUST generate a Date header field in all 2xx, 3xx, and 4xx
|
|
33
|
+
responses"). 2.17.0 fixes that:
|
|
34
|
+
|
|
35
|
+
- The full HTTP/1.1 wire response is built ONCE at registration
|
|
36
|
+
(status line + `Server: Hyperion` + `Content-Type` + `Content-Length`
|
|
37
|
+
+ `Connection: keep-alive` + 29-byte `Date:` placeholder + body),
|
|
38
|
+
frozen, and stored on the `StaticEntry` struct alongside the legacy
|
|
39
|
+
`buffer` field (preserved for back-compat).
|
|
40
|
+
- A new per-second-cached imf-fixdate cache lives in
|
|
41
|
+
`ext/hyperion_http/page_cache.c` (hand-rolled formatter — `strftime`
|
|
42
|
+
is locale-dependent and RFC 7231 mandates ASCII English month/day
|
|
43
|
+
names). Every snapshot site memcpy's the frozen response into a
|
|
44
|
+
per-write scratch buffer and splices the 29-byte cached date into
|
|
45
|
+
the placeholder slot AFTER releasing the page lock. The frozen
|
|
46
|
+
Ruby String is never mutated.
|
|
47
|
+
- `PageCache.register_prebuilt` grows from arity 3 to arity -1 (3 or 4
|
|
48
|
+
args; the optional 4th is `date_offset`). Pre-2.17 callers (3 args
|
|
49
|
+
or 4-with-zero) fall through to the unchanged un-spliced fast path.
|
|
50
|
+
- `Connection#serve_static_entry`'s Ruby fallback (used when the C ext
|
|
51
|
+
is absent — JRuby / TruffleRuby / build failure) mirrors the C path:
|
|
52
|
+
`prebuilt_keepalive_bytes.dup` + `[]= Time.now.httpdate`.
|
|
53
|
+
|
|
54
|
+
Bench (3-trial median on `openclaw-vm`, Linux 6.8 / Ruby 3.3.3):
|
|
55
|
+
|
|
56
|
+
| Row | Workload | 2.16.4 | 2.17.0 | Δ |
|
|
57
|
+
|----:|-----------------------------------------|-------:|-------:|--:|
|
|
58
|
+
| 1 | `handle_static` + io_uring (peak) | 133211 | 127377 | -4.4% (Δ ≈ 80 extra RFC-mandated bytes per response; trial spread 122k–146k = ~19%) |
|
|
59
|
+
| 3 | `Server.handle` block (dynamic) | 8727 | 9458 | **+8.4%** |
|
|
60
|
+
|
|
61
|
+
The Row 1 delta is the cost of writing the new mandatory Date / Server /
|
|
62
|
+
Connection bytes. Trade is throughput-for-compliance — Hyperion's pre-2.17
|
|
63
|
+
`handle_static` wasn't a valid HTTP/1.1 origin server.
|
|
64
|
+
|
|
65
|
+
Captured artefacts:
|
|
66
|
+
- `docs/BENCH_HOTPATH_2026_05_08_before.csv`
|
|
67
|
+
- `docs/BENCH_HOTPATH_2026_05_08_after.csv`
|
|
68
|
+
|
|
69
|
+
#### Tests
|
|
70
|
+
|
|
71
|
+
- New: `spec/hyperion/parser_value_intern_spec.rb` (4 examples — intern
|
|
72
|
+
hits, miss fallback, encoding preservation, wrk User-Agent identity).
|
|
73
|
+
- New: `spec/hyperion/handle_static_prebuilt_spec.rb` (7 examples —
|
|
74
|
+
wire shape, Date offset, custom content-type, back-compat with
|
|
75
|
+
legacy `buffer`, end-to-end real-socket request).
|
|
76
|
+
- Modified: `spec/hyperion/direct_route_spec.rb` (two wire-output
|
|
77
|
+
assertions updated for the new capital-cased headers + Date).
|
|
78
|
+
|
|
79
|
+
`bin/check` clean (mode=quick, 81 examples, 0 failures); full suite
|
|
80
|
+
1266 examples, 0 failures, 16 pre-existing Linux-only / liburing
|
|
81
|
+
pending on macOS.
|
|
82
|
+
|
|
3
83
|
## 2.16.4 — 2026-05-07
|
|
4
84
|
|
|
5
85
|
### io_uring hotpath accept fiber: row-19 BOOT-FAIL fix
|
|
@@ -160,6 +160,15 @@ typedef struct hyp_page_s {
|
|
|
160
160
|
* (no on-disk file backing — never re-stat,
|
|
161
161
|
* never invalidate on missing file). */
|
|
162
162
|
char *content_type; /* heap-owned, picked at insert time */
|
|
163
|
+
/* 2.17-A (Hot Path Task 2) — byte offset within `response_buf`
|
|
164
|
+
* of the first byte of a 29-byte HTTP `Date:` placeholder ('X' run).
|
|
165
|
+
* Zero means "no placeholder; do not splice". When non-zero, every
|
|
166
|
+
* snapshot site overwrites the 29 bytes at this offset in the
|
|
167
|
+
* stack/heap snapshot copy with the per-second-cached imf-fixdate
|
|
168
|
+
* string before the kernel write fires. The `response_buf` itself
|
|
169
|
+
* is never mutated (it is shared across concurrent reads under the
|
|
170
|
+
* page-cache rwlock). */
|
|
171
|
+
size_t date_offset;
|
|
163
172
|
} hyp_page_t;
|
|
164
173
|
|
|
165
174
|
typedef struct hyp_page_slot_s {
|
|
@@ -254,6 +263,117 @@ static double hyp_pc_now(void) {
|
|
|
254
263
|
return (double)tv.tv_sec + (double)tv.tv_usec / 1.0e6;
|
|
255
264
|
}
|
|
256
265
|
|
|
266
|
+
/* ============================================================
|
|
267
|
+
* 2.17-A (Hot Path Task 2) — per-second-cached HTTP `Date:` header.
|
|
268
|
+
*
|
|
269
|
+
* The wire format is RFC 7231 imf-fixdate:
|
|
270
|
+
* "Sun, 06 Nov 1994 08:49:37 GMT"
|
|
271
|
+
* Always exactly 29 bytes, ASCII, no trailing NUL written into the
|
|
272
|
+
* splice slot — the slot lives in the middle of a frozen response
|
|
273
|
+
* buffer and the surrounding bytes (CRLF + remaining headers + body)
|
|
274
|
+
* are already correct.
|
|
275
|
+
*
|
|
276
|
+
* Refresh strategy: lazy, tied to wall-clock seconds. Every call to
|
|
277
|
+
* `hyp_pc_cached_date` checks whether the cached `time_t` matches
|
|
278
|
+
* `time(NULL)`; on miss, the cache is rebuilt from `gmtime_r` under a
|
|
279
|
+
* dedicated mutex (NOT `hyp_pc_lock` — splice happens AFTER the page
|
|
280
|
+
* lock has been released, so we mustn't reacquire the page lock for
|
|
281
|
+
* the date refresh). Concurrent rebuilds race to write the same 29
|
|
282
|
+
* bytes; the result is bit-identical so the race is benign — readers
|
|
283
|
+
* either see the previous second's value (already-stale-by-one-second
|
|
284
|
+
* is RFC-acceptable) or the new one.
|
|
285
|
+
*
|
|
286
|
+
* The cached buffer is sized 32 to leave room for an aligned read on
|
|
287
|
+
* older toolchains that might over-read past `[28]`; we hand out only
|
|
288
|
+
* the first 29 bytes via `hyp_pc_cached_date_copy`. */
|
|
289
|
+
#define HYP_PC_DATE_LEN 29
|
|
290
|
+
|
|
291
|
+
static char hyp_pc_cached_date[32];
|
|
292
|
+
static time_t hyp_pc_cached_date_sec = 0;
|
|
293
|
+
static pthread_mutex_t hyp_pc_date_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
294
|
+
|
|
295
|
+
/* Day-of-week and month tables match RFC 7231 §7.1.1.1 imf-fixdate. */
|
|
296
|
+
static const char *const hyp_pc_dow[7] = {
|
|
297
|
+
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
|
|
298
|
+
};
|
|
299
|
+
static const char *const hyp_pc_mon[12] = {
|
|
300
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
301
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/* Format `t` as RFC 7231 imf-fixdate into `dst` (29 bytes, no NUL).
|
|
305
|
+
* Hand-rolled to avoid `strftime`'s locale-dependent output (locale-
|
|
306
|
+
* aware libcs would spell the month / day in non-ASCII for some
|
|
307
|
+
* locales — RFC 7231 mandates English). Branch-free except for the
|
|
308
|
+
* gmtime_r itself. */
|
|
309
|
+
static void hyp_pc_format_imf_fixdate(time_t t, char *dst) {
|
|
310
|
+
struct tm tm;
|
|
311
|
+
if (gmtime_r(&t, &tm) == NULL) {
|
|
312
|
+
/* Fall back to a known-good zero date; should never happen
|
|
313
|
+
* for a sane wall clock. */
|
|
314
|
+
memcpy(dst, "Thu, 01 Jan 1970 00:00:00 GMT", HYP_PC_DATE_LEN);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
int year = tm.tm_year + 1900;
|
|
318
|
+
int dow = tm.tm_wday & 7; /* defensive: mask to [0,7) */
|
|
319
|
+
int mon = tm.tm_mon % 12; /* defensive */
|
|
320
|
+
if (dow >= 7) dow = 0;
|
|
321
|
+
if (mon < 0) mon = 0;
|
|
322
|
+
|
|
323
|
+
/* "Sun, 06 Nov 1994 08:49:37 GMT" */
|
|
324
|
+
memcpy(dst, hyp_pc_dow[dow], 3);
|
|
325
|
+
dst[3] = ',';
|
|
326
|
+
dst[4] = ' ';
|
|
327
|
+
dst[5] = (char)('0' + (tm.tm_mday / 10));
|
|
328
|
+
dst[6] = (char)('0' + (tm.tm_mday % 10));
|
|
329
|
+
dst[7] = ' ';
|
|
330
|
+
memcpy(dst + 8, hyp_pc_mon[mon], 3);
|
|
331
|
+
dst[11] = ' ';
|
|
332
|
+
dst[12] = (char)('0' + ((year / 1000) % 10));
|
|
333
|
+
dst[13] = (char)('0' + ((year / 100) % 10));
|
|
334
|
+
dst[14] = (char)('0' + ((year / 10) % 10));
|
|
335
|
+
dst[15] = (char)('0' + (year % 10));
|
|
336
|
+
dst[16] = ' ';
|
|
337
|
+
dst[17] = (char)('0' + (tm.tm_hour / 10));
|
|
338
|
+
dst[18] = (char)('0' + (tm.tm_hour % 10));
|
|
339
|
+
dst[19] = ':';
|
|
340
|
+
dst[20] = (char)('0' + (tm.tm_min / 10));
|
|
341
|
+
dst[21] = (char)('0' + (tm.tm_min % 10));
|
|
342
|
+
dst[22] = ':';
|
|
343
|
+
dst[23] = (char)('0' + (tm.tm_sec / 10));
|
|
344
|
+
dst[24] = (char)('0' + (tm.tm_sec % 10));
|
|
345
|
+
dst[25] = ' ';
|
|
346
|
+
dst[26] = 'G';
|
|
347
|
+
dst[27] = 'M';
|
|
348
|
+
dst[28] = 'T';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* Copy the per-second-cached imf-fixdate into `dst` (29 bytes).
|
|
352
|
+
* Refreshes the cache iff the wall-clock second has advanced since the
|
|
353
|
+
* last refresh. Safe to call without `hyp_pc_lock`. */
|
|
354
|
+
static void hyp_pc_cached_date_copy(char *dst) {
|
|
355
|
+
time_t now = time(NULL);
|
|
356
|
+
pthread_mutex_lock(&hyp_pc_date_lock);
|
|
357
|
+
if (now != hyp_pc_cached_date_sec) {
|
|
358
|
+
hyp_pc_format_imf_fixdate(now, hyp_pc_cached_date);
|
|
359
|
+
hyp_pc_cached_date_sec = now;
|
|
360
|
+
}
|
|
361
|
+
memcpy(dst, hyp_pc_cached_date, HYP_PC_DATE_LEN);
|
|
362
|
+
pthread_mutex_unlock(&hyp_pc_date_lock);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* Splice the per-second-cached imf-fixdate into `snapshot` at
|
|
366
|
+
* `date_offset`. No-op when `date_offset == 0` (entry has no
|
|
367
|
+
* placeholder — pre-2.17 register_prebuilt callers). Bounds-checks
|
|
368
|
+
* against `snap_len` so a corrupted header layout never overruns the
|
|
369
|
+
* snapshot buffer. */
|
|
370
|
+
static inline void hyp_pc_splice_date(char *snapshot, size_t snap_len,
|
|
371
|
+
size_t date_offset) {
|
|
372
|
+
if (date_offset == 0) return;
|
|
373
|
+
if (date_offset + HYP_PC_DATE_LEN > snap_len) return;
|
|
374
|
+
hyp_pc_cached_date_copy(snapshot + date_offset);
|
|
375
|
+
}
|
|
376
|
+
|
|
257
377
|
/* FNV-1a 64-bit. Stable, cheap, branchless on the hot path; not a
|
|
258
378
|
* cryptographic hash but the cache only stores trusted operator paths. */
|
|
259
379
|
static uint64_t hyp_pc_hash(const char *key, size_t len) {
|
|
@@ -694,6 +814,7 @@ static VALUE rb_pc_write_to(VALUE self, VALUE socket_io, VALUE rb_path) {
|
|
|
694
814
|
return sym_missing_pc;
|
|
695
815
|
}
|
|
696
816
|
size_t resp_len = slot->page->response_len;
|
|
817
|
+
size_t date_off = slot->page->date_offset;
|
|
697
818
|
char *snapshot = (char *)malloc(resp_len);
|
|
698
819
|
if (snapshot == NULL) {
|
|
699
820
|
pthread_mutex_unlock(&hyp_pc_lock);
|
|
@@ -702,6 +823,11 @@ static VALUE rb_pc_write_to(VALUE self, VALUE socket_io, VALUE rb_path) {
|
|
|
702
823
|
}
|
|
703
824
|
memcpy(snapshot, slot->page->response_buf, resp_len);
|
|
704
825
|
pthread_mutex_unlock(&hyp_pc_lock);
|
|
826
|
+
/* 2.17-A — overwrite the 29-byte Date placeholder in the snapshot
|
|
827
|
+
* (NEVER the page's frozen response_buf) with the per-second-cached
|
|
828
|
+
* imf-fixdate string. No-op when the entry was registered without
|
|
829
|
+
* a placeholder (date_off == 0). */
|
|
830
|
+
hyp_pc_splice_date(snapshot, resp_len, date_off);
|
|
705
831
|
|
|
706
832
|
hyp_pc_write_args_t args;
|
|
707
833
|
args.fd = fd;
|
|
@@ -721,7 +847,7 @@ static VALUE rb_pc_write_to(VALUE self, VALUE socket_io, VALUE rb_path) {
|
|
|
721
847
|
return SSIZET2NUM(args.total);
|
|
722
848
|
}
|
|
723
849
|
|
|
724
|
-
/* PageCache.register_prebuilt(path, response_bytes, body_len) -> Integer
|
|
850
|
+
/* PageCache.register_prebuilt(path, response_bytes, body_len, date_offset = 0) -> Integer
|
|
725
851
|
*
|
|
726
852
|
* 2.10-F — register a fully prebuilt HTTP response under a route path
|
|
727
853
|
* (e.g. `/health`). Unlike `cache_file`, the entry has NO on-disk
|
|
@@ -730,16 +856,37 @@ static VALUE rb_pc_write_to(VALUE self, VALUE socket_io, VALUE rb_path) {
|
|
|
730
856
|
* starts inside `response_bytes` so HEAD requests can write the
|
|
731
857
|
* headers-only prefix.
|
|
732
858
|
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
859
|
+
* 2.17-A (Hot Path Task 2) — `date_offset` is an OPTIONAL fourth
|
|
860
|
+
* argument: when non-zero, it is the byte offset within
|
|
861
|
+
* `response_bytes` of a 29-byte `XXX...` placeholder reserved for
|
|
862
|
+
* the HTTP `Date:` header. The C splice helper overwrites those 29
|
|
863
|
+
* bytes in a per-write SCRATCH copy with the per-second-cached
|
|
864
|
+
* imf-fixdate string before each kernel write — the registered
|
|
865
|
+
* `response_bytes` is never mutated. Pre-2.17 callers that pass
|
|
866
|
+
* three arguments (or pass 0 for the fourth) get the un-spliced
|
|
867
|
+
* behaviour they had before.
|
|
868
|
+
*
|
|
869
|
+
* `response_bytes.bytesize` MUST be >= `body_len`. When
|
|
870
|
+
* `date_offset != 0` it must satisfy
|
|
871
|
+
* `date_offset + 29 <= response_bytes.bytesize` so the splice never
|
|
872
|
+
* runs off the end of the buffer. Returns the stored response
|
|
873
|
+
* byte count on success.
|
|
735
874
|
*
|
|
736
875
|
* Used by `Hyperion::Server.handle_static` to fold the prebuilt
|
|
737
876
|
* static-route response into the C fast path so the request hot
|
|
738
877
|
* path is one hash lookup + one `write()` syscall, fully outside
|
|
739
878
|
* Ruby method dispatch. */
|
|
740
|
-
static VALUE rb_pc_register_prebuilt(VALUE
|
|
741
|
-
VALUE rb_response, VALUE rb_body_len) {
|
|
879
|
+
static VALUE rb_pc_register_prebuilt(int argc, VALUE *argv, VALUE self) {
|
|
742
880
|
(void)self;
|
|
881
|
+
if (argc < 3 || argc > 4) {
|
|
882
|
+
rb_raise(rb_eArgError,
|
|
883
|
+
"wrong number of arguments (given %d, expected 3..4)", argc);
|
|
884
|
+
}
|
|
885
|
+
VALUE rb_path = argv[0];
|
|
886
|
+
VALUE rb_response = argv[1];
|
|
887
|
+
VALUE rb_body_len = argv[2];
|
|
888
|
+
VALUE rb_date_off = (argc == 4) ? argv[3] : INT2FIX(0);
|
|
889
|
+
|
|
743
890
|
Check_Type(rb_path, T_STRING);
|
|
744
891
|
Check_Type(rb_response, T_STRING);
|
|
745
892
|
|
|
@@ -761,6 +908,16 @@ static VALUE rb_pc_register_prebuilt(VALUE self, VALUE rb_path,
|
|
|
761
908
|
"body_len (%zu) must be <= response_bytes.bytesize (%zu)",
|
|
762
909
|
body_len, resp_len);
|
|
763
910
|
}
|
|
911
|
+
long date_off_signed = NUM2LONG(rb_date_off);
|
|
912
|
+
if (date_off_signed < 0) {
|
|
913
|
+
rb_raise(rb_eArgError, "date_offset must be >= 0");
|
|
914
|
+
}
|
|
915
|
+
size_t date_off = (size_t)date_off_signed;
|
|
916
|
+
if (date_off != 0 && date_off + HYP_PC_DATE_LEN > resp_len) {
|
|
917
|
+
rb_raise(rb_eArgError,
|
|
918
|
+
"date_offset (%zu) + 29 must be <= response_bytes.bytesize (%zu)",
|
|
919
|
+
date_off, resp_len);
|
|
920
|
+
}
|
|
764
921
|
|
|
765
922
|
hyp_page_t *page = (hyp_page_t *)calloc(1, sizeof(*page));
|
|
766
923
|
if (page == NULL) {
|
|
@@ -802,6 +959,7 @@ static VALUE rb_pc_register_prebuilt(VALUE self, VALUE rb_path,
|
|
|
802
959
|
page->last_check = hyp_pc_now();
|
|
803
960
|
page->immutable = 1;
|
|
804
961
|
page->prebuilt = 1;
|
|
962
|
+
page->date_offset = date_off;
|
|
805
963
|
|
|
806
964
|
uint64_t h = hyp_pc_hash(path, path_len);
|
|
807
965
|
pthread_mutex_lock(&hyp_pc_lock);
|
|
@@ -887,6 +1045,7 @@ static VALUE rb_pc_serve_request(VALUE self, VALUE socket_io,
|
|
|
887
1045
|
size_t write_len = (kind == HYP_PC_METHOD_HEAD)
|
|
888
1046
|
? slot->page->headers_len
|
|
889
1047
|
: slot->page->response_len;
|
|
1048
|
+
size_t date_off = slot->page->date_offset;
|
|
890
1049
|
char *snapshot = (char *)malloc(write_len);
|
|
891
1050
|
if (snapshot == NULL) {
|
|
892
1051
|
pthread_mutex_unlock(&hyp_pc_lock);
|
|
@@ -895,6 +1054,11 @@ static VALUE rb_pc_serve_request(VALUE self, VALUE socket_io,
|
|
|
895
1054
|
}
|
|
896
1055
|
memcpy(snapshot, slot->page->response_buf, write_len);
|
|
897
1056
|
pthread_mutex_unlock(&hyp_pc_lock);
|
|
1057
|
+
/* 2.17-A — splice the per-second-cached imf-fixdate into the
|
|
1058
|
+
* snapshot's Date placeholder (when the entry has one). HEAD
|
|
1059
|
+
* write_len equals headers_len, so a Date placeholder placed
|
|
1060
|
+
* inside the headers span is still in range. */
|
|
1061
|
+
hyp_pc_splice_date(snapshot, write_len, date_off);
|
|
898
1062
|
|
|
899
1063
|
hyp_pc_write_args_t args;
|
|
900
1064
|
args.fd = fd;
|
|
@@ -1920,6 +2084,7 @@ static long hyp_cl_serve_connection(int client_fd, int *handed_off) {
|
|
|
1920
2084
|
size_t write_len = (kind == HYP_PC_METHOD_HEAD)
|
|
1921
2085
|
? slot->page->headers_len
|
|
1922
2086
|
: slot->page->response_len;
|
|
2087
|
+
size_t date_off = slot->page->date_offset;
|
|
1923
2088
|
char *snapshot = (char *)malloc(write_len);
|
|
1924
2089
|
if (snapshot == NULL) {
|
|
1925
2090
|
pthread_mutex_unlock(&hyp_pc_lock);
|
|
@@ -1930,6 +2095,10 @@ static long hyp_cl_serve_connection(int client_fd, int *handed_off) {
|
|
|
1930
2095
|
}
|
|
1931
2096
|
memcpy(snapshot, slot->page->response_buf, write_len);
|
|
1932
2097
|
pthread_mutex_unlock(&hyp_pc_lock);
|
|
2098
|
+
/* 2.17-A — splice the per-second-cached imf-fixdate into
|
|
2099
|
+
* the snapshot's Date placeholder before the kernel write.
|
|
2100
|
+
* No-op for entries registered without a placeholder. */
|
|
2101
|
+
hyp_pc_splice_date(snapshot, write_len, date_off);
|
|
1933
2102
|
|
|
1934
2103
|
hyp_pc_write_args_t wargs;
|
|
1935
2104
|
wargs.fd = client_fd;
|
|
@@ -2484,6 +2653,7 @@ char *pc_internal_snapshot_response(const char *path, size_t path_len,
|
|
|
2484
2653
|
size_t write_len = (kind == PC_INTERNAL_METHOD_HEAD)
|
|
2485
2654
|
? slot->page->headers_len
|
|
2486
2655
|
: slot->page->response_len;
|
|
2656
|
+
size_t date_off = slot->page->date_offset;
|
|
2487
2657
|
char *snapshot = (char *)malloc(write_len);
|
|
2488
2658
|
if (snapshot == NULL) {
|
|
2489
2659
|
pthread_mutex_unlock(&hyp_pc_lock);
|
|
@@ -2491,6 +2661,11 @@ char *pc_internal_snapshot_response(const char *path, size_t path_len,
|
|
|
2491
2661
|
}
|
|
2492
2662
|
memcpy(snapshot, slot->page->response_buf, write_len);
|
|
2493
2663
|
pthread_mutex_unlock(&hyp_pc_lock);
|
|
2664
|
+
/* 2.17-A — splice the per-second-cached imf-fixdate into the
|
|
2665
|
+
* Date placeholder before handing the snapshot to io_uring (the
|
|
2666
|
+
* kernel reads the bytes asynchronously, so the splice MUST land
|
|
2667
|
+
* before the SQE is submitted). */
|
|
2668
|
+
hyp_pc_splice_date(snapshot, write_len, date_off);
|
|
2494
2669
|
*out_len = write_len;
|
|
2495
2670
|
return snapshot;
|
|
2496
2671
|
}
|
|
@@ -2576,8 +2751,10 @@ void Init_hyperion_page_cache(void) {
|
|
|
2576
2751
|
rb_pc_auto_threshold, 0);
|
|
2577
2752
|
rb_define_singleton_method(rb_mHyperionHttpPageCache, "max_key_len",
|
|
2578
2753
|
rb_pc_max_key_len, 0);
|
|
2754
|
+
/* 2.17-A — variable arity: 3-arg legacy form, 4-arg form adds
|
|
2755
|
+
* `date_offset` for the imf-fixdate splice slot. */
|
|
2579
2756
|
rb_define_singleton_method(rb_mHyperionHttpPageCache, "register_prebuilt",
|
|
2580
|
-
rb_pc_register_prebuilt,
|
|
2757
|
+
rb_pc_register_prebuilt, -1);
|
|
2581
2758
|
rb_define_singleton_method(rb_mHyperionHttpPageCache, "serve_request",
|
|
2582
2759
|
rb_pc_serve_request, 3);
|
|
2583
2760
|
/* 2.12-C — connection lifecycle in C. */
|
data/ext/hyperion_http/parser.c
CHANGED
|
@@ -153,6 +153,117 @@ typedef struct {
|
|
|
153
153
|
static VALUE rb_kEMPTY_STR; /* frozen empty ASCII-8BIT String */
|
|
154
154
|
static VALUE rb_kHTTP_1_1; /* frozen "HTTP/1.1" String */
|
|
155
155
|
|
|
156
|
+
/* Phase HotPath-1 (2.17.0) — intern table for the highest-frequency HTTP/1.1
|
|
157
|
+
* *value* tokens (not header names — those are interned via the
|
|
158
|
+
* PREINTERNED_HEADERS table above). On a typical wrk-shaped benchmark, the
|
|
159
|
+
* VALUE side of every header (Host, User-Agent, Accept, Connection,
|
|
160
|
+
* Accept-Encoding, etc.) is identical across every request on a keep-alive
|
|
161
|
+
* connection. Without interning, each request rb_str_new's a fresh String
|
|
162
|
+
* for every value — 6–10 allocations per request, all destined for the
|
|
163
|
+
* young GC. With interning, stash_pending_header's hash check returns the
|
|
164
|
+
* pre-frozen VALUE for known values; the freshly-allocated String becomes
|
|
165
|
+
* unreferenced garbage (collected at next minor GC) and the env Hash holds
|
|
166
|
+
* a single shared identity across all requests. Net wins: lower GC retention
|
|
167
|
+
* under burst, faster string compares downstream (identity), reduced
|
|
168
|
+
* promote-to-old-gen pressure on long-running workers.
|
|
169
|
+
*
|
|
170
|
+
* Layout: open-addressed table sized to 64 slots (power of 2; load factor
|
|
171
|
+
* <0.5 with 33 seeds). Probing capped at 8 to keep cache-friendly.
|
|
172
|
+
* Lookup is FNV-1a on the byte range; on hit we return the frozen VALUE,
|
|
173
|
+
* on miss we return Qundef and the caller falls through to the original
|
|
174
|
+
* (unmodified) String. The table is seeded once at extension init from a
|
|
175
|
+
* static literal list — no growth, no eviction, no thread synchronisation.
|
|
176
|
+
*/
|
|
177
|
+
#define HYP_VALUE_INTERN_TABLE_SIZE 64u /* power of 2 */
|
|
178
|
+
#define HYP_VALUE_INTERN_PROBE_MAX 8
|
|
179
|
+
#define HYP_VALUE_INTERN_MAX_LEN 96u /* skip lookup for values > this */
|
|
180
|
+
|
|
181
|
+
typedef struct {
|
|
182
|
+
uint64_t fnv1a;
|
|
183
|
+
uint32_t len;
|
|
184
|
+
VALUE str;
|
|
185
|
+
} hyp_value_intern_entry_t;
|
|
186
|
+
|
|
187
|
+
static hyp_value_intern_entry_t hyp_value_intern_tbl[HYP_VALUE_INTERN_TABLE_SIZE];
|
|
188
|
+
|
|
189
|
+
static const char *const hyp_value_intern_seed[] = {
|
|
190
|
+
/* Connection */
|
|
191
|
+
"keep-alive", "close", "Keep-Alive", "Upgrade", "upgrade",
|
|
192
|
+
/* Accept-Encoding */
|
|
193
|
+
"gzip, deflate", "gzip, deflate, br", "gzip, deflate, br, zstd",
|
|
194
|
+
"identity", "gzip", "deflate", "br", "*",
|
|
195
|
+
/* Accept */
|
|
196
|
+
"*/*", "text/html", "application/json", "text/plain",
|
|
197
|
+
"application/x-www-form-urlencoded", "text/event-stream",
|
|
198
|
+
/* Accept-Language */
|
|
199
|
+
"en-US,en;q=0.9", "en-US", "en",
|
|
200
|
+
/* User-Agent — synthetic / bench clients */
|
|
201
|
+
"Mozilla/5.0 (compatible; wrk/4.2.0)",
|
|
202
|
+
"wrk/4.2.0",
|
|
203
|
+
"curl/8.4.0", "curl/8.5.0", "curl/8.6.0",
|
|
204
|
+
/* Host — local bench */
|
|
205
|
+
"localhost", "127.0.0.1", "0.0.0.0",
|
|
206
|
+
/* Cache-Control */
|
|
207
|
+
"no-cache", "no-store", "max-age=0",
|
|
208
|
+
/* Misc */
|
|
209
|
+
"trailers", "websocket", "h2c",
|
|
210
|
+
};
|
|
211
|
+
#define HYP_VALUE_INTERN_SEED_N \
|
|
212
|
+
(sizeof(hyp_value_intern_seed) / sizeof(hyp_value_intern_seed[0]))
|
|
213
|
+
|
|
214
|
+
static inline uint64_t hyp_fnv1a(const char *bytes, uint32_t len) {
|
|
215
|
+
uint64_t h = 0xcbf29ce484222325ULL;
|
|
216
|
+
for (uint32_t i = 0; i < len; i++) {
|
|
217
|
+
h ^= (uint8_t)bytes[i];
|
|
218
|
+
h *= 0x100000001b3ULL;
|
|
219
|
+
}
|
|
220
|
+
return h;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
static inline VALUE hyp_intern_lookup(const char *bytes, uint32_t len) {
|
|
224
|
+
if (len == 0 || len > HYP_VALUE_INTERN_MAX_LEN) return Qundef;
|
|
225
|
+
uint64_t h = hyp_fnv1a(bytes, len);
|
|
226
|
+
uint32_t mask = HYP_VALUE_INTERN_TABLE_SIZE - 1u;
|
|
227
|
+
uint32_t idx = (uint32_t)h & mask;
|
|
228
|
+
for (int probe = 0; probe < HYP_VALUE_INTERN_PROBE_MAX; probe++) {
|
|
229
|
+
hyp_value_intern_entry_t *e =
|
|
230
|
+
&hyp_value_intern_tbl[(idx + (uint32_t)probe) & mask];
|
|
231
|
+
if (e->str == 0) return Qundef;
|
|
232
|
+
if (e->fnv1a == h && e->len == len &&
|
|
233
|
+
memcmp(RSTRING_PTR(e->str), bytes, len) == 0) {
|
|
234
|
+
return e->str;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return Qundef;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
static void hyp_value_intern_seed_all(void) {
|
|
241
|
+
memset(hyp_value_intern_tbl, 0, sizeof(hyp_value_intern_tbl));
|
|
242
|
+
uint32_t mask = HYP_VALUE_INTERN_TABLE_SIZE - 1u;
|
|
243
|
+
for (size_t i = 0; i < HYP_VALUE_INTERN_SEED_N; i++) {
|
|
244
|
+
const char *s = hyp_value_intern_seed[i];
|
|
245
|
+
uint32_t len = (uint32_t)strlen(s);
|
|
246
|
+
if (len == 0 || len > HYP_VALUE_INTERN_MAX_LEN) continue;
|
|
247
|
+
uint64_t h = hyp_fnv1a(s, len);
|
|
248
|
+
uint32_t idx = (uint32_t)h & mask;
|
|
249
|
+
for (int probe = 0; probe < HYP_VALUE_INTERN_PROBE_MAX; probe++) {
|
|
250
|
+
hyp_value_intern_entry_t *e =
|
|
251
|
+
&hyp_value_intern_tbl[(idx + (uint32_t)probe) & mask];
|
|
252
|
+
if (e->str == 0) {
|
|
253
|
+
/* Match the encoding of values produced by rb_str_new
|
|
254
|
+
* (ASCII-8BIT) so identity comparisons downstream don't
|
|
255
|
+
* fail on encoding mismatch. */
|
|
256
|
+
VALUE v = rb_obj_freeze(rb_str_new(s, (long)len));
|
|
257
|
+
rb_global_variable(&e->str); /* GC root */
|
|
258
|
+
e->str = v;
|
|
259
|
+
e->fnv1a = h;
|
|
260
|
+
e->len = len;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
156
267
|
static void state_init(parser_state_t *s) {
|
|
157
268
|
s->method = Qnil;
|
|
158
269
|
s->path = Qnil; /* allocated in on_url first call */
|
|
@@ -216,6 +327,15 @@ static void stash_pending_header(parser_state_t *s) {
|
|
|
216
327
|
key = rb_funcall(s->current_header_name, id_downcase, 0);
|
|
217
328
|
}
|
|
218
329
|
VALUE val = NIL_P(s->current_header_value) ? rb_kEMPTY_STR : s->current_header_value;
|
|
330
|
+
/* Phase HotPath-1 (2.17.0): intern common header values so identical
|
|
331
|
+
* values across keep-alive requests share one frozen VALUE. The
|
|
332
|
+
* freshly-allocated `s->current_header_value` becomes garbage on
|
|
333
|
+
* intern hit. */
|
|
334
|
+
if (val != rb_kEMPTY_STR) {
|
|
335
|
+
long val_len = RSTRING_LEN(val);
|
|
336
|
+
VALUE interned = hyp_intern_lookup(RSTRING_PTR(val), (uint32_t)val_len);
|
|
337
|
+
if (interned != Qundef) val = interned;
|
|
338
|
+
}
|
|
219
339
|
rb_hash_aset(s->headers, key, val);
|
|
220
340
|
s->current_header_name = Qnil;
|
|
221
341
|
s->current_header_value = Qnil;
|
|
@@ -1670,6 +1790,13 @@ void Init_hyperion_http(void) {
|
|
|
1670
1790
|
rb_obj_freeze(rb_aHeaderTable);
|
|
1671
1791
|
rb_define_const(rb_cCParser, "PREINTERNED_HEADERS", rb_aHeaderTable);
|
|
1672
1792
|
|
|
1793
|
+
/* Phase HotPath-1 (2.17.0): seed the header-value intern table. See
|
|
1794
|
+
* the long comment at the table declaration. Must run after the
|
|
1795
|
+
* encoding registry is up (Init_hyperion_parser is called by
|
|
1796
|
+
* Init_hyperion_http after rb_define_module) so rb_str_new finds
|
|
1797
|
+
* ASCII-8BIT. */
|
|
1798
|
+
hyp_value_intern_seed_all();
|
|
1799
|
+
|
|
1673
1800
|
/* 2.13-B — status-line, header-key, header-line caches used by
|
|
1674
1801
|
* cbuild_response_head. The status-line table is fixed-size (no GC
|
|
1675
1802
|
* concerns; bytes are .rodata). The two header caches are
|
data/lib/hyperion/connection.rb
CHANGED
|
@@ -670,20 +670,46 @@ module Hyperion
|
|
|
670
670
|
|
|
671
671
|
# 2.10-F — call into the C ext when available, else fall back to
|
|
672
672
|
# the 2.10-D Ruby `socket.write` path. Returns bytes written.
|
|
673
|
+
#
|
|
674
|
+
# 2.17-A — when the C ext is unavailable (JRuby / TruffleRuby /
|
|
675
|
+
# build failure) AND the entry carries the new keep-alive prebuilt
|
|
676
|
+
# bytes, splice the per-second-cached httpdate into a per-write
|
|
677
|
+
# scratch String and write THAT — so the wire output shape is
|
|
678
|
+
# identical to the C-loop path even on the slow Ruby fallback.
|
|
679
|
+
# The frozen `prebuilt_keepalive_bytes` itself is never mutated;
|
|
680
|
+
# `String#dup` copies it onto the heap so `[]=` is safe.
|
|
673
681
|
def serve_static_entry(socket, request, entry)
|
|
674
682
|
if defined?(::Hyperion::Http::PageCache) &&
|
|
675
683
|
::Hyperion::Http::PageCache.respond_to?(:serve_request)
|
|
676
684
|
result = ::Hyperion::Http::PageCache.serve_request(socket, request.method, entry.path)
|
|
677
685
|
return result.last if result.is_a?(Array) && result.first == :ok
|
|
678
686
|
end
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
687
|
+
socket.write(static_entry_fallback_bytes(request, entry))
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
# 2.17-A — build the wire bytes for the Ruby fallback path. When
|
|
691
|
+
# the entry carries `prebuilt_keepalive_bytes` + `prebuilt_date_offset`
|
|
692
|
+
# (registered post-2.17), use those bytes with the date spliced in.
|
|
693
|
+
# Otherwise fall through to the legacy `entry.buffer` (lowercase
|
|
694
|
+
# headers, no Date) — this path keeps pre-2.17 callers that
|
|
695
|
+
# constructed StaticEntry directly working unchanged.
|
|
696
|
+
def static_entry_fallback_bytes(request, entry)
|
|
697
|
+
if entry.prebuilt_keepalive_bytes && entry.prebuilt_date_offset.to_i.positive?
|
|
698
|
+
bytes = entry.prebuilt_keepalive_bytes.dup
|
|
699
|
+
bytes[entry.prebuilt_date_offset, 29] = Time.now.httpdate
|
|
700
|
+
return bytes if request.method != 'HEAD'
|
|
701
|
+
|
|
702
|
+
head_end = bytes.index("\r\n\r\n")
|
|
703
|
+
return head_end ? bytes.byteslice(0, head_end + 4) : bytes
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
if request.method == 'HEAD' && entry.headers_bytesize < entry.buffer.bytesize
|
|
707
|
+
entry.buffer.byteslice(0, entry.headers_bytesize)
|
|
708
|
+
else
|
|
709
|
+
entry.buffer
|
|
710
|
+
end
|
|
686
711
|
end
|
|
712
|
+
private :static_entry_fallback_bytes
|
|
687
713
|
|
|
688
714
|
# 2.10-D — write a direct-route response. Returns the status
|
|
689
715
|
# code that was written (so `dispatch_direct!` can bump the
|
|
@@ -59,7 +59,28 @@ module Hyperion
|
|
|
59
59
|
# on HEAD too, which is RFC-correct (HEAD MAY include the body
|
|
60
60
|
# so long as Content-Length matches; the spec only forbids the
|
|
61
61
|
# SERVER from sending body bytes the client didn't ask for).
|
|
62
|
-
|
|
62
|
+
#
|
|
63
|
+
# 2.17-A (Hot Path Task 2) adds two more fields so the C-loop
|
|
64
|
+
# writer can mem-splice a per-second-cached HTTP `Date:` header
|
|
65
|
+
# into a pre-built keep-alive response without rebuilding it
|
|
66
|
+
# from scratch:
|
|
67
|
+
# * `prebuilt_keepalive_bytes` — frozen ASCII-8BIT String of
|
|
68
|
+
# the full HTTP/1.1 wire response (status line + Server +
|
|
69
|
+
# Content-Type + Content-Length + Connection: keep-alive +
|
|
70
|
+
# Date placeholder + body) with a 29-byte 'X' run reserved
|
|
71
|
+
# at `prebuilt_date_offset`. The placeholder is overwritten
|
|
72
|
+
# in a per-write scratch buffer (NEVER in this frozen
|
|
73
|
+
# String) by the C splice helper before the syscall fires.
|
|
74
|
+
# * `prebuilt_date_offset` — Integer byte offset of the first
|
|
75
|
+
# placeholder byte within `prebuilt_keepalive_bytes`. Zero
|
|
76
|
+
# means "no Date placeholder; do not splice". 29 bytes is
|
|
77
|
+
# the canonical RFC 7231 imf-fixdate length.
|
|
78
|
+
# Existing callers that construct StaticEntry with 3 or 4 args
|
|
79
|
+
# see nil for these new fields and the C side falls through to
|
|
80
|
+
# the un-spliced fast path it has used since 2.10-F.
|
|
81
|
+
StaticEntry = Struct.new(:method, :path, :buffer, :headers_len,
|
|
82
|
+
:prebuilt_keepalive_bytes,
|
|
83
|
+
:prebuilt_date_offset) do
|
|
63
84
|
# Returns the pre-built response bytes ready for one
|
|
64
85
|
# `socket.write` call. Always frozen.
|
|
65
86
|
def response_bytes
|
data/lib/hyperion/server.rb
CHANGED
|
@@ -136,10 +136,23 @@ module Hyperion
|
|
|
136
136
|
head.force_encoding(Encoding::ASCII_8BIT)
|
|
137
137
|
buffer = (head + body).freeze
|
|
138
138
|
|
|
139
|
+
# 2.17-A (Hot Path Task 2) — pre-build the keep-alive wire bytes
|
|
140
|
+
# with a 29-byte Date placeholder so the C-loop writer can splice
|
|
141
|
+
# the per-second-cached httpdate string in without rebuilding the
|
|
142
|
+
# head from scratch every request. Returned as `[bytes, offset]`
|
|
143
|
+
# — `offset` is the byte index of the first placeholder byte.
|
|
144
|
+
prebuilt_ka_bytes, date_offset =
|
|
145
|
+
build_static_wire_bytes(body, content_type: content_type, server_string: 'Hyperion')
|
|
146
|
+
|
|
139
147
|
method_key = method_sym.to_s.upcase.to_sym
|
|
140
148
|
# 2.10-F — record the headers prefix length on the StaticEntry
|
|
141
149
|
# struct so HEAD-method writes can serve a headers-only prefix.
|
|
142
|
-
|
|
150
|
+
# 2.17-A — also stash the keep-alive prebuilt bytes + Date offset
|
|
151
|
+
# so the C splice helper (or the Ruby fallback in
|
|
152
|
+
# Connection#serve_static_entry) can stamp the cached date in
|
|
153
|
+
# before each write.
|
|
154
|
+
entry = RouteTable::StaticEntry.new(method_key, path.dup.freeze, buffer, head.bytesize,
|
|
155
|
+
prebuilt_ka_bytes, date_offset).freeze
|
|
143
156
|
# 2.10-F — register the entry DIRECTLY (StaticEntry responds to
|
|
144
157
|
# `#call`) instead of wrapping it in a closure, so the dispatch
|
|
145
158
|
# path can branch on `is_a?(StaticEntry)` BEFORE invoking the
|
|
@@ -152,17 +165,58 @@ module Hyperion
|
|
|
152
165
|
# MUST also answer HEAD with the same headers). No-op on a
|
|
153
166
|
# POST/PUT/etc. registration — those don't get a HEAD twin.
|
|
154
167
|
route_table.register(:HEAD, path, entry) if method_key == :GET
|
|
155
|
-
# 2.
|
|
156
|
-
# `PageCache.serve_request`
|
|
157
|
-
#
|
|
158
|
-
# (
|
|
159
|
-
# the
|
|
168
|
+
# 2.17-A — fold the keep-alive prebuilt bytes (with Date placeholder)
|
|
169
|
+
# into the C-side PageCache so `PageCache.serve_request` and the
|
|
170
|
+
# C accept loop both serve the new shape. The 4-arg
|
|
171
|
+
# `register_prebuilt` form (introduced in 2.17-A) records the
|
|
172
|
+
# Date offset on the C-side `hyp_page_t` so every snapshot
|
|
173
|
+
# site splices the cached date before writing. Best-effort:
|
|
174
|
+
# the C ext may be absent on JRuby / TruffleRuby — the
|
|
175
|
+
# dispatcher silently falls back to the Ruby `socket.write`
|
|
176
|
+
# path that's been there since 2.10-D.
|
|
160
177
|
if defined?(::Hyperion::Http::PageCache) && ::Hyperion::Http::PageCache.respond_to?(:register_prebuilt)
|
|
161
|
-
::Hyperion::Http::PageCache.register_prebuilt(path,
|
|
178
|
+
::Hyperion::Http::PageCache.register_prebuilt(path, prebuilt_ka_bytes, body.bytesize, date_offset)
|
|
162
179
|
end
|
|
163
180
|
entry
|
|
164
181
|
end
|
|
165
182
|
|
|
183
|
+
# 2.17-A (Hot Path Task 2) — assemble the prebuilt keep-alive
|
|
184
|
+
# wire bytes for a static route registered via `handle_static`.
|
|
185
|
+
# Returns `[frozen_bytes, date_offset]`:
|
|
186
|
+
# * `frozen_bytes` — ASCII-8BIT, frozen String of the full
|
|
187
|
+
# HTTP/1.1 response (status line + Server + Content-Type +
|
|
188
|
+
# Content-Length + Connection: keep-alive + Date placeholder
|
|
189
|
+
# + CRLFCRLF + body). The Date placeholder is a 29-byte 'X'
|
|
190
|
+
# run that the C splice helper overwrites in a per-write
|
|
191
|
+
# scratch buffer (the frozen String itself is NEVER mutated).
|
|
192
|
+
# * `date_offset` — Integer index of the first placeholder
|
|
193
|
+
# byte within `frozen_bytes`. 29 bytes is the canonical RFC
|
|
194
|
+
# 7231 imf-fixdate length (`Sun, 06 Nov 1994 08:49:37 GMT`).
|
|
195
|
+
#
|
|
196
|
+
# Header order is fixed on purpose — the C splice helper relies
|
|
197
|
+
# on the offset being stable across requests; placing Date last
|
|
198
|
+
# keeps the offset arithmetic trivial regardless of body length
|
|
199
|
+
# variations between routes. Headers are capitalized in the
|
|
200
|
+
# canonical RFC 7230 §3.2 form (case-insensitive but
|
|
201
|
+
# conventionally capitalized) so the wire output matches what
|
|
202
|
+
# CDN / proxy logs expect.
|
|
203
|
+
def self.build_static_wire_bytes(body, content_type:, server_string:)
|
|
204
|
+
placeholder = 'X' * 29
|
|
205
|
+
head_prefix = +"HTTP/1.1 200 OK\r\n" \
|
|
206
|
+
"Server: #{server_string}\r\n" \
|
|
207
|
+
"Content-Type: #{content_type}\r\n" \
|
|
208
|
+
"Content-Length: #{body.bytesize}\r\n" \
|
|
209
|
+
"Connection: keep-alive\r\n" \
|
|
210
|
+
"Date: "
|
|
211
|
+
head_prefix.force_encoding(Encoding::ASCII_8BIT)
|
|
212
|
+
date_offset = head_prefix.bytesize
|
|
213
|
+
head = head_prefix + placeholder + "\r\n\r\n"
|
|
214
|
+
head.force_encoding(Encoding::ASCII_8BIT)
|
|
215
|
+
bytes = (head + body).b.freeze
|
|
216
|
+
[bytes, date_offset]
|
|
217
|
+
end
|
|
218
|
+
private_class_method :build_static_wire_bytes
|
|
219
|
+
|
|
166
220
|
# 1.7.0 added kwargs (all default to current behaviour):
|
|
167
221
|
# * `runtime:` — `Hyperion::Runtime` instance (default
|
|
168
222
|
# `Runtime.default`). Threaded through to
|
data/lib/hyperion/version.rb
CHANGED