hyperion-rb 2.11.0 → 2.12.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 +566 -0
- data/README.md +102 -5
- data/ext/hyperion_http/extconf.rb +41 -0
- data/ext/hyperion_http/io_uring_loop.c +710 -0
- data/ext/hyperion_http/page_cache.c +1032 -0
- data/ext/hyperion_http/page_cache_internal.h +132 -0
- data/lib/hyperion/connection.rb +14 -0
- data/lib/hyperion/dispatch_mode.rb +19 -1
- data/lib/hyperion/http2_handler.rb +123 -5
- data/lib/hyperion/metrics.rb +38 -0
- data/lib/hyperion/prometheus_exporter.rb +76 -1
- data/lib/hyperion/server/connection_loop.rb +159 -0
- data/lib/hyperion/server.rb +183 -0
- data/lib/hyperion/thread_pool.rb +23 -7
- data/lib/hyperion/version.rb +1 -1
- metadata +4 -1
|
@@ -100,9 +100,20 @@
|
|
|
100
100
|
#include <fcntl.h>
|
|
101
101
|
#include <pthread.h>
|
|
102
102
|
#include <dirent.h>
|
|
103
|
+
#include <signal.h>
|
|
103
104
|
#include <sys/stat.h>
|
|
104
105
|
#include <sys/types.h>
|
|
105
106
|
#include <sys/time.h>
|
|
107
|
+
#include <sys/socket.h>
|
|
108
|
+
#include <netinet/in.h>
|
|
109
|
+
#include <netinet/tcp.h>
|
|
110
|
+
#include <arpa/inet.h>
|
|
111
|
+
|
|
112
|
+
/* Internal sharing surface — extern wrappers around the static helpers
|
|
113
|
+
* below, exposed to the 2.12-D io_uring sibling translation unit
|
|
114
|
+
* (`io_uring_loop.c`). The wrappers are defined in this file (next to
|
|
115
|
+
* the static helpers they wrap) and declared in `page_cache_internal.h`. */
|
|
116
|
+
#include "page_cache_internal.h"
|
|
106
117
|
|
|
107
118
|
/* Shared identifiers / refs. parser.c and sendfile.c each register their
|
|
108
119
|
* own copies of Hyperion / Hyperion::Http (lazy-define if missing); we
|
|
@@ -1061,12 +1072,992 @@ static VALUE rb_pc_auto_threshold(VALUE self) {
|
|
|
1061
1072
|
return INT2NUM(HYP_PC_AUTO_THRESHOLD);
|
|
1062
1073
|
}
|
|
1063
1074
|
|
|
1075
|
+
/* ============================================================
|
|
1076
|
+
* 2.12-C — Connection lifecycle in C.
|
|
1077
|
+
*
|
|
1078
|
+
* `run_static_accept_loop(listen_fd, idle_max_ms)` runs an accept ->
|
|
1079
|
+
* read-headers -> route-lookup -> write loop ENTIRELY in C for a
|
|
1080
|
+
* designated listening socket. Ruby is re-entered ONLY for:
|
|
1081
|
+
*
|
|
1082
|
+
* 1. Lifecycle hooks (when `lifecycle_hooks_active?` is true; gated
|
|
1083
|
+
* by a single `int` test so the no-hook hot path stays branch-
|
|
1084
|
+
* free) — one `rb_funcall` per request via the registered
|
|
1085
|
+
* `lifecycle_callback`.
|
|
1086
|
+
*
|
|
1087
|
+
* 2. Handoff: when the accepted connection's first request doesn't
|
|
1088
|
+
* match a `StaticEntry` (or the request is malformed / has a
|
|
1089
|
+
* body / is HTTP/1.0 close / requests an upgrade), the C loop
|
|
1090
|
+
* invokes the registered `handoff_callback` with `(fd_int,
|
|
1091
|
+
* partial_buffer_or_nil)` and continues to the next accept.
|
|
1092
|
+
* Ruby owns the fd from that point on.
|
|
1093
|
+
*
|
|
1094
|
+
* 3. Stop: when the listening fd returns EBADF / ECONNABORTED in a
|
|
1095
|
+
* way that suggests `Server#stop` closed it, the loop returns
|
|
1096
|
+
* the served-request count cleanly.
|
|
1097
|
+
*
|
|
1098
|
+
* Per-connection cost on a hit:
|
|
1099
|
+
* * 1 `accept` syscall (GVL released).
|
|
1100
|
+
* * 1 `setsockopt(TCP_NODELAY)` (mirrors 2.10-G; Nagle off so small
|
|
1101
|
+
* responses aren't waiting on the peer's delayed-ACK timer).
|
|
1102
|
+
* * 1 `recv` syscall to read the request headers (GVL released).
|
|
1103
|
+
* * 1 `write` syscall for the prebuilt response (GVL released).
|
|
1104
|
+
* * 0 Ruby allocations on the hot path past the served-count
|
|
1105
|
+
* accumulator (a Fixnum in the Ruby-visible return value).
|
|
1106
|
+
*
|
|
1107
|
+
* Wire format expectations:
|
|
1108
|
+
* * The request must be HTTP/1.1 (HTTP/1.0 is handed back to Ruby —
|
|
1109
|
+
* keep-alive defaulting differs and the existing connection.rb
|
|
1110
|
+
* already implements this correctly).
|
|
1111
|
+
* * No request body (`Content-Length` / `Transfer-Encoding` headers
|
|
1112
|
+
* trigger handoff). The whole point of `handle_static` is GETs/
|
|
1113
|
+
* HEADs without a body; anything else is operator misuse and the
|
|
1114
|
+
* Ruby path can produce a more diagnostic error.
|
|
1115
|
+
* * Method must be GET or HEAD (matches `serve_request`'s gate).
|
|
1116
|
+
*
|
|
1117
|
+
* Concurrency: this function MUST be called on a Ruby thread that
|
|
1118
|
+
* owns the GVL. The loop releases the GVL during the blocking
|
|
1119
|
+
* syscalls and re-acquires it for the lifecycle / handoff
|
|
1120
|
+
* callbacks; the C-side PageCache lock (`hyp_pc_lock`) is taken
|
|
1121
|
+
* only for the lookup snapshot and released before the write
|
|
1122
|
+
* (the same pattern `serve_request` already uses).
|
|
1123
|
+
* ============================================================ */
|
|
1124
|
+
|
|
1125
|
+
/* Per-process state for the lifecycle / handoff callbacks. The
|
|
1126
|
+
* callbacks themselves are mark-protected via `rb_gc_register_mark_object`
|
|
1127
|
+
* so they survive across the GC even though they're stored in
|
|
1128
|
+
* static globals. */
|
|
1129
|
+
static VALUE hyp_cl_lifecycle_callback = Qnil;
|
|
1130
|
+
static VALUE hyp_cl_handoff_callback = Qnil;
|
|
1131
|
+
static int hyp_cl_lifecycle_active = 0;
|
|
1132
|
+
|
|
1133
|
+
/* Stop flag — flipped from Ruby via `stop_accept_loop` when the
|
|
1134
|
+
* listener should drop out of the loop voluntarily (graceful
|
|
1135
|
+
* shutdown). The accept syscall is still blocking; the operator's
|
|
1136
|
+
* `Server#stop` close()s the listener, which races us out via
|
|
1137
|
+
* `accept` returning EBADF / EINVAL. The flag is the secondary
|
|
1138
|
+
* signal: between two requests on a keep-alive connection we
|
|
1139
|
+
* check it before the next read. */
|
|
1140
|
+
static volatile sig_atomic_t hyp_cl_stop = 0;
|
|
1141
|
+
|
|
1142
|
+
/* 2.12-E — per-process served-request counter, ticked once per request
|
|
1143
|
+
* served by `hyp_cl_serve_connection` (2.12-C accept4 loop) AND by the
|
|
1144
|
+
* 2.12-D io_uring loop (via the `pc_internal_*` shim). Read by Ruby at
|
|
1145
|
+
* scrape time via `Hyperion::Http::PageCache.c_loop_requests_total` —
|
|
1146
|
+
* the PrometheusExporter folds it into the
|
|
1147
|
+
* `hyperion_requests_dispatch_total{worker_id=PID}` series for the
|
|
1148
|
+
* current worker, so operators see one consistent per-worker number
|
|
1149
|
+
* regardless of which dispatch shape served the request.
|
|
1150
|
+
*
|
|
1151
|
+
* Atomicity: accessed via `__atomic_*` builtins (gcc/clang both
|
|
1152
|
+
* support these on every target this gem builds for). The hot-path
|
|
1153
|
+
* cost is one `lock add`-style instruction per request, well below
|
|
1154
|
+
* the ~10μs per-request budget at 134k r/s.
|
|
1155
|
+
*
|
|
1156
|
+
* Reset semantics: zeroed on `run_static_accept_loop` /
|
|
1157
|
+
* `run_static_io_uring_loop` entry so a previous loop's count from a
|
|
1158
|
+
* test-suite respawn doesn't leak into the new loop's snapshot.
|
|
1159
|
+
* Specs use `reset_c_loop_requests_total!` to assert from-zero
|
|
1160
|
+
* behaviour without driving a full loop.
|
|
1161
|
+
*
|
|
1162
|
+
* Type: `unsigned long long` to match the Integer marshalling
|
|
1163
|
+
* (`ULL2NUM`) on Ruby's side; signed wraparound is undefined behaviour
|
|
1164
|
+
* and we want defined unsigned-rollover semantics for the audit
|
|
1165
|
+
* counter (which would only matter at ~10^19 total requests).
|
|
1166
|
+
*/
|
|
1167
|
+
static volatile unsigned long long hyp_cl_requests_served_total = 0;
|
|
1168
|
+
|
|
1169
|
+
static inline void hyp_cl_tick_request(void) {
|
|
1170
|
+
__atomic_add_fetch(&hyp_cl_requests_served_total, 1ULL, __ATOMIC_RELAXED);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
static inline unsigned long long hyp_cl_load_requests_served(void) {
|
|
1174
|
+
return __atomic_load_n(&hyp_cl_requests_served_total, __ATOMIC_RELAXED);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
static inline void hyp_cl_reset_requests_served(void) {
|
|
1178
|
+
__atomic_store_n(&hyp_cl_requests_served_total, 0ULL, __ATOMIC_RELAXED);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/* Header-section size cap. Anything bigger is rejected: the request
|
|
1182
|
+
* is malformed or hostile, and either way Ruby's full parser is
|
|
1183
|
+
* the right place to produce an error response. Mirrors
|
|
1184
|
+
* `Connection::MAX_HEADER_BYTES` (64 KiB). */
|
|
1185
|
+
#define HYP_CL_MAX_HEADER_BYTES 65536
|
|
1186
|
+
/* Read chunk for header accumulation. 8 KiB matches the Ruby-side
|
|
1187
|
+
* `INBUF_INITIAL_CAPACITY` so a typical request fits in one recv. */
|
|
1188
|
+
#define HYP_CL_READ_CHUNK 8192
|
|
1189
|
+
|
|
1190
|
+
/* ---- accept ---- */
|
|
1191
|
+
typedef struct {
|
|
1192
|
+
int listen_fd;
|
|
1193
|
+
int client_fd;
|
|
1194
|
+
int err;
|
|
1195
|
+
} hyp_cl_accept_args_t;
|
|
1196
|
+
|
|
1197
|
+
static void *hyp_cl_accept_blocking(void *raw) {
|
|
1198
|
+
hyp_cl_accept_args_t *a = (hyp_cl_accept_args_t *)raw;
|
|
1199
|
+
a->client_fd = -1;
|
|
1200
|
+
a->err = 0;
|
|
1201
|
+
for (;;) {
|
|
1202
|
+
struct sockaddr_storage ss;
|
|
1203
|
+
socklen_t slen = (socklen_t)sizeof(ss);
|
|
1204
|
+
int fd = accept(a->listen_fd, (struct sockaddr *)&ss, &slen);
|
|
1205
|
+
if (fd >= 0) {
|
|
1206
|
+
a->client_fd = fd;
|
|
1207
|
+
return NULL;
|
|
1208
|
+
}
|
|
1209
|
+
if (errno == EINTR) {
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
1213
|
+
/* Listener fd was non-blocking despite our F_SETFL clear
|
|
1214
|
+
* (or someone else flipped it back). Park on select() so
|
|
1215
|
+
* we don't busy-loop. The stop flag is checked by the
|
|
1216
|
+
* outer Ruby caller between accepts. */
|
|
1217
|
+
fd_set rfds;
|
|
1218
|
+
FD_ZERO(&rfds);
|
|
1219
|
+
FD_SET(a->listen_fd, &rfds);
|
|
1220
|
+
struct timeval tv;
|
|
1221
|
+
tv.tv_sec = 1;
|
|
1222
|
+
tv.tv_usec = 0;
|
|
1223
|
+
int s = select(a->listen_fd + 1, &rfds, NULL, NULL, &tv);
|
|
1224
|
+
if (s < 0 && errno == EINTR) {
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
/* On timeout, return so the caller can check the stop
|
|
1228
|
+
* flag and re-enter. */
|
|
1229
|
+
if (s == 0) {
|
|
1230
|
+
a->err = EAGAIN;
|
|
1231
|
+
return NULL;
|
|
1232
|
+
}
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
a->err = errno;
|
|
1236
|
+
return NULL;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/* ---- recv ---- */
|
|
1241
|
+
typedef struct {
|
|
1242
|
+
int fd;
|
|
1243
|
+
char *buf;
|
|
1244
|
+
size_t cap;
|
|
1245
|
+
size_t off;
|
|
1246
|
+
int err;
|
|
1247
|
+
/* Set by the caller; signals that we should bail out of recv on
|
|
1248
|
+
* the first EAGAIN/EWOULDBLOCK rather than retrying. Used between
|
|
1249
|
+
* keep-alive requests so an idle conn doesn't stall the worker. */
|
|
1250
|
+
int nonblock_first;
|
|
1251
|
+
} hyp_cl_recv_args_t;
|
|
1252
|
+
|
|
1253
|
+
static void *hyp_cl_recv_blocking(void *raw) {
|
|
1254
|
+
hyp_cl_recv_args_t *a = (hyp_cl_recv_args_t *)raw;
|
|
1255
|
+
a->err = 0;
|
|
1256
|
+
for (;;) {
|
|
1257
|
+
if (a->off >= a->cap) {
|
|
1258
|
+
a->err = E2BIG;
|
|
1259
|
+
return NULL;
|
|
1260
|
+
}
|
|
1261
|
+
ssize_t n = recv(a->fd, a->buf + a->off, a->cap - a->off, 0);
|
|
1262
|
+
if (n > 0) {
|
|
1263
|
+
a->off += (size_t)n;
|
|
1264
|
+
/* Look for end-of-headers (\r\n\r\n). Bounded scan over
|
|
1265
|
+
* what we've buffered so far. */
|
|
1266
|
+
if (a->off >= 4) {
|
|
1267
|
+
/* Fast scan of the just-read window first; fall back to
|
|
1268
|
+
* a full scan if a CRLFCRLF straddled a recv boundary. */
|
|
1269
|
+
const char *base = a->buf;
|
|
1270
|
+
size_t scan_end = a->off;
|
|
1271
|
+
size_t i = (a->off - (size_t)n >= 3) ? a->off - (size_t)n - 3 : 0;
|
|
1272
|
+
while (i + 3 < scan_end) {
|
|
1273
|
+
if (base[i] == '\r' && base[i+1] == '\n' &&
|
|
1274
|
+
base[i+2] == '\r' && base[i+3] == '\n') {
|
|
1275
|
+
return NULL;
|
|
1276
|
+
}
|
|
1277
|
+
i++;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
if (n == 0) {
|
|
1283
|
+
/* Peer closed cleanly. */
|
|
1284
|
+
a->err = ECONNRESET;
|
|
1285
|
+
return NULL;
|
|
1286
|
+
}
|
|
1287
|
+
if (errno == EINTR) {
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
1291
|
+
/* The fd was unexpectedly non-blocking (Darwin's accept(2)
|
|
1292
|
+
* doesn't propagate O_NONBLOCK from the listener, but the
|
|
1293
|
+
* defensive branch keeps us correct on hosts where it
|
|
1294
|
+
* does — or where someone flipped the flag on the
|
|
1295
|
+
* accepted socket via setsockopt). Park on select(). */
|
|
1296
|
+
fd_set rfds;
|
|
1297
|
+
FD_ZERO(&rfds);
|
|
1298
|
+
FD_SET(a->fd, &rfds);
|
|
1299
|
+
struct timeval tv;
|
|
1300
|
+
tv.tv_sec = 30; /* generous; matches Connection's read timeout */
|
|
1301
|
+
tv.tv_usec = 0;
|
|
1302
|
+
int s = select(a->fd + 1, &rfds, NULL, NULL, &tv);
|
|
1303
|
+
if (s < 0 && errno == EINTR) {
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
if (s == 0) {
|
|
1307
|
+
a->err = ETIMEDOUT;
|
|
1308
|
+
return NULL;
|
|
1309
|
+
}
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
a->err = errno;
|
|
1313
|
+
return NULL;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/* Find end-of-headers in `buf` of length `len`. Returns the byte
|
|
1318
|
+
* offset PAST the trailing CRLFCRLF (i.e. where the body, if any,
|
|
1319
|
+
* would start), or -1 if not found. */
|
|
1320
|
+
static long hyp_cl_find_eoh(const char *buf, size_t len) {
|
|
1321
|
+
if (len < 4) {
|
|
1322
|
+
return -1;
|
|
1323
|
+
}
|
|
1324
|
+
for (size_t i = 0; i + 3 < len; i++) {
|
|
1325
|
+
if (buf[i] == '\r' && buf[i+1] == '\n' &&
|
|
1326
|
+
buf[i+2] == '\r' && buf[i+3] == '\n') {
|
|
1327
|
+
return (long)(i + 4);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return -1;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/* Parse the request line out of the headers section. On success,
|
|
1334
|
+
* fills *m_off, *m_len, *p_off, *p_len with the offsets/lengths of
|
|
1335
|
+
* METHOD and PATH inside `buf`, and returns the length of the
|
|
1336
|
+
* request line including the trailing CRLF. On malformed input
|
|
1337
|
+
* returns -1. The version (HTTP/1.1) is checked here too — anything
|
|
1338
|
+
* other than HTTP/1.1 returns -1 so the caller hands off to Ruby. */
|
|
1339
|
+
static long hyp_cl_parse_request_line(const char *buf, size_t len,
|
|
1340
|
+
size_t *m_off, size_t *m_len,
|
|
1341
|
+
size_t *p_off, size_t *p_len) {
|
|
1342
|
+
/* Find first SP — separates METHOD from PATH. */
|
|
1343
|
+
size_t i = 0;
|
|
1344
|
+
while (i < len && buf[i] != ' ' && buf[i] != '\r' && buf[i] != '\n') {
|
|
1345
|
+
i++;
|
|
1346
|
+
}
|
|
1347
|
+
if (i == 0 || i >= len || buf[i] != ' ') {
|
|
1348
|
+
return -1;
|
|
1349
|
+
}
|
|
1350
|
+
*m_off = 0;
|
|
1351
|
+
*m_len = i;
|
|
1352
|
+
i++;
|
|
1353
|
+
size_t p_start = i;
|
|
1354
|
+
while (i < len && buf[i] != ' ' && buf[i] != '\r' && buf[i] != '\n') {
|
|
1355
|
+
i++;
|
|
1356
|
+
}
|
|
1357
|
+
if (i >= len || buf[i] != ' ' || i == p_start) {
|
|
1358
|
+
return -1;
|
|
1359
|
+
}
|
|
1360
|
+
*p_off = p_start;
|
|
1361
|
+
*p_len = i - p_start;
|
|
1362
|
+
i++;
|
|
1363
|
+
/* Version: must be exactly "HTTP/1.1" followed by CRLF for the
|
|
1364
|
+
* C path. HTTP/1.0 has different keep-alive defaults; let Ruby
|
|
1365
|
+
* handle it. */
|
|
1366
|
+
if (i + 10 > len) {
|
|
1367
|
+
return -1;
|
|
1368
|
+
}
|
|
1369
|
+
if (memcmp(buf + i, "HTTP/1.1\r\n", 10) != 0) {
|
|
1370
|
+
return -1;
|
|
1371
|
+
}
|
|
1372
|
+
return (long)(i + 10);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/* Case-insensitive byte compare for header names. */
|
|
1376
|
+
static int hyp_cl_iequals(const char *a, size_t alen, const char *b, size_t blen) {
|
|
1377
|
+
if (alen != blen) {
|
|
1378
|
+
return 0;
|
|
1379
|
+
}
|
|
1380
|
+
for (size_t i = 0; i < alen; i++) {
|
|
1381
|
+
char ca = a[i];
|
|
1382
|
+
char cb = b[i];
|
|
1383
|
+
if (ca >= 'A' && ca <= 'Z') ca = (char)(ca + 32);
|
|
1384
|
+
if (cb >= 'A' && cb <= 'Z') cb = (char)(cb + 32);
|
|
1385
|
+
if (ca != cb) return 0;
|
|
1386
|
+
}
|
|
1387
|
+
return 1;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/* Inspect the header block (between request-line end and CRLFCRLF)
|
|
1391
|
+
* and report:
|
|
1392
|
+
* *connection_close: 1 if Connection: close was seen, 0 otherwise.
|
|
1393
|
+
* *has_body: 1 if Content-Length>0 or Transfer-Encoding
|
|
1394
|
+
* was seen (anything but CL:0).
|
|
1395
|
+
* *upgrade_seen: 1 if Upgrade or h2 settings header was seen.
|
|
1396
|
+
*
|
|
1397
|
+
* Returns 0 on success, -1 on malformed framing. */
|
|
1398
|
+
static int hyp_cl_scan_headers(const char *buf, size_t start, size_t end,
|
|
1399
|
+
int *connection_close, int *has_body,
|
|
1400
|
+
int *upgrade_seen) {
|
|
1401
|
+
*connection_close = 0;
|
|
1402
|
+
*has_body = 0;
|
|
1403
|
+
*upgrade_seen = 0;
|
|
1404
|
+
/* `end` points just past the closing CRLFCRLF; the last meaningful
|
|
1405
|
+
* header byte is at `end - 5` (the CR of the final header's CRLF
|
|
1406
|
+
* followed by the empty CRLF). The terminator we look for is
|
|
1407
|
+
* `\r\n` at positions [end-4, end-3]. */
|
|
1408
|
+
size_t i = start;
|
|
1409
|
+
while (i + 2 <= end) {
|
|
1410
|
+
if (i + 1 < end && buf[i] == '\r' && buf[i+1] == '\n') {
|
|
1411
|
+
/* Empty line — end of headers reached. */
|
|
1412
|
+
return 0;
|
|
1413
|
+
}
|
|
1414
|
+
size_t name_start = i;
|
|
1415
|
+
while (i < end && buf[i] != ':' && buf[i] != '\r') {
|
|
1416
|
+
i++;
|
|
1417
|
+
}
|
|
1418
|
+
if (i >= end || buf[i] != ':') {
|
|
1419
|
+
return -1;
|
|
1420
|
+
}
|
|
1421
|
+
size_t name_end = i;
|
|
1422
|
+
i++; /* past ':' */
|
|
1423
|
+
while (i < end && (buf[i] == ' ' || buf[i] == '\t')) {
|
|
1424
|
+
i++;
|
|
1425
|
+
}
|
|
1426
|
+
size_t val_start = i;
|
|
1427
|
+
while (i < end && buf[i] != '\r') {
|
|
1428
|
+
i++;
|
|
1429
|
+
}
|
|
1430
|
+
if (i + 1 >= end || buf[i+1] != '\n') {
|
|
1431
|
+
return -1;
|
|
1432
|
+
}
|
|
1433
|
+
size_t val_end = i;
|
|
1434
|
+
i += 2; /* past CRLF */
|
|
1435
|
+
|
|
1436
|
+
size_t nlen = name_end - name_start;
|
|
1437
|
+
size_t vlen = val_end - val_start;
|
|
1438
|
+
const char *nptr = buf + name_start;
|
|
1439
|
+
const char *vptr = buf + val_start;
|
|
1440
|
+
|
|
1441
|
+
if (hyp_cl_iequals(nptr, nlen, "connection", 10)) {
|
|
1442
|
+
/* Trim trailing whitespace. */
|
|
1443
|
+
while (vlen > 0 && (vptr[vlen - 1] == ' ' || vptr[vlen - 1] == '\t')) {
|
|
1444
|
+
vlen--;
|
|
1445
|
+
}
|
|
1446
|
+
if (hyp_cl_iequals(vptr, vlen, "close", 5)) {
|
|
1447
|
+
*connection_close = 1;
|
|
1448
|
+
} else if (hyp_cl_iequals(vptr, vlen, "upgrade", 7)) {
|
|
1449
|
+
*upgrade_seen = 1;
|
|
1450
|
+
}
|
|
1451
|
+
} else if (hyp_cl_iequals(nptr, nlen, "content-length", 14)) {
|
|
1452
|
+
/* Trim leading/trailing whitespace then parse. */
|
|
1453
|
+
while (vlen > 0 && (vptr[0] == ' ' || vptr[0] == '\t')) {
|
|
1454
|
+
vptr++; vlen--;
|
|
1455
|
+
}
|
|
1456
|
+
while (vlen > 0 && (vptr[vlen - 1] == ' ' || vptr[vlen - 1] == '\t')) {
|
|
1457
|
+
vlen--;
|
|
1458
|
+
}
|
|
1459
|
+
int cl_zero = (vlen == 1 && vptr[0] == '0');
|
|
1460
|
+
if (!cl_zero) {
|
|
1461
|
+
*has_body = 1;
|
|
1462
|
+
}
|
|
1463
|
+
} else if (hyp_cl_iequals(nptr, nlen, "transfer-encoding", 17)) {
|
|
1464
|
+
*has_body = 1;
|
|
1465
|
+
} else if (hyp_cl_iequals(nptr, nlen, "upgrade", 7)) {
|
|
1466
|
+
*upgrade_seen = 1;
|
|
1467
|
+
} else if (hyp_cl_iequals(nptr, nlen, "http2-settings", 14)) {
|
|
1468
|
+
*upgrade_seen = 1;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
return -1;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/* Lifecycle fire helper: rb_funcall into the registered callback
|
|
1475
|
+
* with (method_str, path_str). Wrapped in rb_protect so a misbehaving
|
|
1476
|
+
* Ruby hook can't take down the C loop. */
|
|
1477
|
+
typedef struct {
|
|
1478
|
+
VALUE callback;
|
|
1479
|
+
VALUE method_str;
|
|
1480
|
+
VALUE path_str;
|
|
1481
|
+
} hyp_cl_hook_args_t;
|
|
1482
|
+
|
|
1483
|
+
static VALUE hyp_cl_hook_invoke(VALUE raw) {
|
|
1484
|
+
hyp_cl_hook_args_t *a = (hyp_cl_hook_args_t *)raw;
|
|
1485
|
+
return rb_funcall(a->callback, rb_intern("call"), 2,
|
|
1486
|
+
a->method_str, a->path_str);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
static void hyp_cl_fire_lifecycle(const char *method, size_t mlen,
|
|
1490
|
+
const char *path, size_t plen) {
|
|
1491
|
+
if (!hyp_cl_lifecycle_active || NIL_P(hyp_cl_lifecycle_callback)) {
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
hyp_cl_hook_args_t a;
|
|
1495
|
+
a.callback = hyp_cl_lifecycle_callback;
|
|
1496
|
+
a.method_str = rb_str_new(method, (long)mlen);
|
|
1497
|
+
a.path_str = rb_str_new(path, (long)plen);
|
|
1498
|
+
int state = 0;
|
|
1499
|
+
rb_protect(hyp_cl_hook_invoke, (VALUE)&a, &state);
|
|
1500
|
+
/* Swallow the exception state — same contract as `Runtime#fire_*`:
|
|
1501
|
+
* a misbehaving observer must not break dispatch. The Ruby-side
|
|
1502
|
+
* callback already wraps individual hooks in their own rescues
|
|
1503
|
+
* and logs failures; this protect is belt-and-suspenders so the
|
|
1504
|
+
* C loop can't crash on a hook error either. */
|
|
1505
|
+
if (state) {
|
|
1506
|
+
rb_set_errinfo(Qnil);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/* Handoff: invoke the Ruby callback with (fd, partial_buffer_str_or_nil).
|
|
1511
|
+
* Ruby owns the fd from that point on — C must not close it. */
|
|
1512
|
+
typedef struct {
|
|
1513
|
+
VALUE callback;
|
|
1514
|
+
VALUE fd_int;
|
|
1515
|
+
VALUE buffer_str;
|
|
1516
|
+
} hyp_cl_handoff_args_t;
|
|
1517
|
+
|
|
1518
|
+
static VALUE hyp_cl_handoff_invoke(VALUE raw) {
|
|
1519
|
+
hyp_cl_handoff_args_t *a = (hyp_cl_handoff_args_t *)raw;
|
|
1520
|
+
return rb_funcall(a->callback, rb_intern("call"), 2,
|
|
1521
|
+
a->fd_int, a->buffer_str);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
static void hyp_cl_handoff(int client_fd, const char *partial, size_t partial_len) {
|
|
1525
|
+
if (NIL_P(hyp_cl_handoff_callback)) {
|
|
1526
|
+
/* No callback registered — close the fd ourselves rather than
|
|
1527
|
+
* leaking it. This branch is paranoia; the Ruby side always
|
|
1528
|
+
* registers a handoff callback before starting the loop. */
|
|
1529
|
+
close(client_fd);
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
hyp_cl_handoff_args_t a;
|
|
1533
|
+
a.callback = hyp_cl_handoff_callback;
|
|
1534
|
+
a.fd_int = INT2NUM(client_fd);
|
|
1535
|
+
a.buffer_str = (partial_len > 0) ? rb_str_new(partial, (long)partial_len) : Qnil;
|
|
1536
|
+
int state = 0;
|
|
1537
|
+
rb_protect(hyp_cl_handoff_invoke, (VALUE)&a, &state);
|
|
1538
|
+
if (state) {
|
|
1539
|
+
/* Handoff failed — swallow and close the fd; better to drop
|
|
1540
|
+
* one connection than crash the whole loop. */
|
|
1541
|
+
rb_set_errinfo(Qnil);
|
|
1542
|
+
close(client_fd);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/* Apply TCP_NODELAY to the accepted connection. Mirrors 2.10-G — Nagle
|
|
1547
|
+
* off so small responses aren't held by the peer's delayed-ACK timer.
|
|
1548
|
+
* Best-effort; failures are swallowed (some socket types don't honour
|
|
1549
|
+
* the option, or it was already set). */
|
|
1550
|
+
static void hyp_cl_apply_tcp_nodelay(int fd) {
|
|
1551
|
+
int one = 1;
|
|
1552
|
+
(void)setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/* Pre-built `404 Not Found` response, written to the socket when the
|
|
1556
|
+
* C loop sees a method/path it can answer with a definite negative
|
|
1557
|
+
* BUT can't hand off to Ruby (because the rest of the request looks
|
|
1558
|
+
* like a normal static request — peer expects an HTTP response, not
|
|
1559
|
+
* a TCP RST). Used only when handoff_callback is nil; otherwise we
|
|
1560
|
+
* always prefer to hand off to let Ruby produce the response.
|
|
1561
|
+
*
|
|
1562
|
+
* Currently unused — kept for future "C-only mode" where Ruby is
|
|
1563
|
+
* never re-entered. */
|
|
1564
|
+
/* static const char hyp_cl_404[] =
|
|
1565
|
+
* "HTTP/1.1 404 Not Found\r\n"
|
|
1566
|
+
* "content-type: text/plain\r\n"
|
|
1567
|
+
* "content-length: 9\r\n"
|
|
1568
|
+
* "connection: close\r\n"
|
|
1569
|
+
* "\r\n"
|
|
1570
|
+
* "not found"; */
|
|
1571
|
+
|
|
1572
|
+
/* Serve one connection on `client_fd`. Returns the count of requests
|
|
1573
|
+
* served on this connection; -1 if the connection ended in a way
|
|
1574
|
+
* that should not increment the served counter (handoff, peer
|
|
1575
|
+
* disconnect mid-request). The `*handed_off` flag distinguishes
|
|
1576
|
+
* "Ruby took ownership of this fd" (we must NOT close it) from
|
|
1577
|
+
* "peer closed and we should close" (we close locally).
|
|
1578
|
+
*
|
|
1579
|
+
* The full headers buffer is kept stack-local — at 8 KiB it fits
|
|
1580
|
+
* comfortably and we avoid per-connection malloc traffic. */
|
|
1581
|
+
static long hyp_cl_serve_connection(int client_fd, int *handed_off) {
|
|
1582
|
+
*handed_off = 0;
|
|
1583
|
+
long served = 0;
|
|
1584
|
+
char buf[HYP_CL_MAX_HEADER_BYTES];
|
|
1585
|
+
size_t buf_len = 0;
|
|
1586
|
+
|
|
1587
|
+
/* Apply TCP_NODELAY once per connection — it sticks for the
|
|
1588
|
+
* lifetime of the fd. */
|
|
1589
|
+
hyp_cl_apply_tcp_nodelay(client_fd);
|
|
1590
|
+
|
|
1591
|
+
for (;;) {
|
|
1592
|
+
if (hyp_cl_stop) {
|
|
1593
|
+
close(client_fd);
|
|
1594
|
+
return served;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/* If we have leftover bytes from the previous request (pipelined
|
|
1598
|
+
* input), they're already at the start of `buf`. Read more
|
|
1599
|
+
* until we have a full header section. */
|
|
1600
|
+
long eoh = hyp_cl_find_eoh(buf, buf_len);
|
|
1601
|
+
while (eoh < 0) {
|
|
1602
|
+
hyp_cl_recv_args_t r;
|
|
1603
|
+
r.fd = client_fd;
|
|
1604
|
+
r.buf = buf;
|
|
1605
|
+
r.cap = sizeof(buf);
|
|
1606
|
+
r.off = buf_len;
|
|
1607
|
+
r.err = 0;
|
|
1608
|
+
r.nonblock_first = 0;
|
|
1609
|
+
rb_thread_call_without_gvl(hyp_cl_recv_blocking, &r,
|
|
1610
|
+
RUBY_UBF_IO, NULL);
|
|
1611
|
+
buf_len = r.off;
|
|
1612
|
+
if (r.err == ECONNRESET || r.err == ECONNABORTED) {
|
|
1613
|
+
/* Peer hung up. Close locally; not a request. */
|
|
1614
|
+
close(client_fd);
|
|
1615
|
+
return served;
|
|
1616
|
+
}
|
|
1617
|
+
if (r.err != 0 && r.err != EINTR) {
|
|
1618
|
+
/* Unexpected read error — close locally. */
|
|
1619
|
+
close(client_fd);
|
|
1620
|
+
return served;
|
|
1621
|
+
}
|
|
1622
|
+
eoh = hyp_cl_find_eoh(buf, buf_len);
|
|
1623
|
+
if (eoh < 0 && buf_len >= sizeof(buf)) {
|
|
1624
|
+
/* Header section exceeds our cap — hand off to Ruby
|
|
1625
|
+
* (its parser produces a 400 with the right shape). */
|
|
1626
|
+
hyp_cl_handoff(client_fd, buf, buf_len);
|
|
1627
|
+
*handed_off = 1;
|
|
1628
|
+
return served;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
size_t method_off, method_len, path_off, path_len;
|
|
1633
|
+
long req_line_end = hyp_cl_parse_request_line(
|
|
1634
|
+
buf, (size_t)eoh, &method_off, &method_len, &path_off, &path_len);
|
|
1635
|
+
if (req_line_end < 0) {
|
|
1636
|
+
/* Malformed or HTTP/1.0 — let Ruby handle the response. */
|
|
1637
|
+
hyp_cl_handoff(client_fd, buf, buf_len);
|
|
1638
|
+
*handed_off = 1;
|
|
1639
|
+
return served;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
int connection_close = 0;
|
|
1643
|
+
int has_body = 0;
|
|
1644
|
+
int upgrade_seen = 0;
|
|
1645
|
+
int hdr_ok = hyp_cl_scan_headers(buf, (size_t)req_line_end, (size_t)eoh,
|
|
1646
|
+
&connection_close, &has_body, &upgrade_seen);
|
|
1647
|
+
if (hdr_ok != 0 || has_body || upgrade_seen) {
|
|
1648
|
+
hyp_cl_handoff(client_fd, buf, buf_len);
|
|
1649
|
+
*handed_off = 1;
|
|
1650
|
+
return served;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/* Method + path lookup against the page cache. Reuse the
|
|
1654
|
+
* existing classify + lookup helpers so the C-side cache state
|
|
1655
|
+
* is the single source of truth — `Server.handle_static`
|
|
1656
|
+
* registers entries via `register_prebuilt`, this loop reads
|
|
1657
|
+
* them via `lookup_locked`. */
|
|
1658
|
+
hyp_pc_method_t kind = hyp_pc_classify_method(buf + method_off, method_len);
|
|
1659
|
+
if (kind == HYP_PC_METHOD_OTHER) {
|
|
1660
|
+
hyp_cl_handoff(client_fd, buf, buf_len);
|
|
1661
|
+
*handed_off = 1;
|
|
1662
|
+
return served;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
pthread_mutex_lock(&hyp_pc_lock);
|
|
1666
|
+
int was_stale = 0;
|
|
1667
|
+
hyp_page_slot_t *slot = hyp_pc_lookup_locked(buf + path_off, path_len, &was_stale);
|
|
1668
|
+
if (slot == NULL) {
|
|
1669
|
+
pthread_mutex_unlock(&hyp_pc_lock);
|
|
1670
|
+
hyp_cl_handoff(client_fd, buf, buf_len);
|
|
1671
|
+
*handed_off = 1;
|
|
1672
|
+
return served;
|
|
1673
|
+
}
|
|
1674
|
+
size_t write_len = (kind == HYP_PC_METHOD_HEAD)
|
|
1675
|
+
? slot->page->headers_len
|
|
1676
|
+
: slot->page->response_len;
|
|
1677
|
+
char *snapshot = (char *)malloc(write_len);
|
|
1678
|
+
if (snapshot == NULL) {
|
|
1679
|
+
pthread_mutex_unlock(&hyp_pc_lock);
|
|
1680
|
+
/* OOM mid-loop — hand off so Ruby can return 500. */
|
|
1681
|
+
hyp_cl_handoff(client_fd, buf, buf_len);
|
|
1682
|
+
*handed_off = 1;
|
|
1683
|
+
return served;
|
|
1684
|
+
}
|
|
1685
|
+
memcpy(snapshot, slot->page->response_buf, write_len);
|
|
1686
|
+
pthread_mutex_unlock(&hyp_pc_lock);
|
|
1687
|
+
|
|
1688
|
+
hyp_pc_write_args_t wargs;
|
|
1689
|
+
wargs.fd = client_fd;
|
|
1690
|
+
wargs.buf = snapshot;
|
|
1691
|
+
wargs.len = write_len;
|
|
1692
|
+
wargs.total = 0;
|
|
1693
|
+
wargs.err = 0;
|
|
1694
|
+
rb_thread_call_without_gvl(hyp_pc_write_blocking, &wargs,
|
|
1695
|
+
RUBY_UBF_IO, NULL);
|
|
1696
|
+
free(snapshot);
|
|
1697
|
+
|
|
1698
|
+
if (wargs.err != 0 && wargs.total == 0) {
|
|
1699
|
+
/* Write failed — peer most likely gone. Close and exit. */
|
|
1700
|
+
close(client_fd);
|
|
1701
|
+
return served;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
served++;
|
|
1705
|
+
/* 2.12-E — per-process tick. Lock-free atomic so the SO_REUSEPORT
|
|
1706
|
+
* audit harness can scrape `c_loop_requests_total` mid-bench
|
|
1707
|
+
* without serialising on a Ruby-side mutex. */
|
|
1708
|
+
hyp_cl_tick_request();
|
|
1709
|
+
|
|
1710
|
+
/* Lifecycle hooks fire AFTER the wire write so observers see a
|
|
1711
|
+
* completed request. Keep this off the no-hook hot path via
|
|
1712
|
+
* the integer flag. */
|
|
1713
|
+
if (hyp_cl_lifecycle_active) {
|
|
1714
|
+
hyp_cl_fire_lifecycle(buf + method_off, method_len,
|
|
1715
|
+
buf + path_off, path_len);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/* Carry pipelined bytes forward into the same buffer. */
|
|
1719
|
+
size_t consumed = (size_t)eoh;
|
|
1720
|
+
if (consumed < buf_len) {
|
|
1721
|
+
memmove(buf, buf + consumed, buf_len - consumed);
|
|
1722
|
+
buf_len -= consumed;
|
|
1723
|
+
} else {
|
|
1724
|
+
buf_len = 0;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
if (connection_close) {
|
|
1728
|
+
/* Half-close, then briefly drain any inbound bytes so the
|
|
1729
|
+
* close() doesn't trigger an RST. Some platforms (notably
|
|
1730
|
+
* macOS) deliver RST when close() is called on a socket
|
|
1731
|
+
* with unread bytes in the receive queue — even the peer's
|
|
1732
|
+
* normal FIN-empty packet can race the close and surface
|
|
1733
|
+
* as ECONNRESET to the peer's last read(2). The drain is
|
|
1734
|
+
* bounded to a small absolute deadline so a misbehaving
|
|
1735
|
+
* peer can't stall us. */
|
|
1736
|
+
shutdown(client_fd, SHUT_WR);
|
|
1737
|
+
char drain[1024];
|
|
1738
|
+
for (int i = 0; i < 4; i++) {
|
|
1739
|
+
ssize_t n = recv(client_fd, drain, sizeof(drain), MSG_DONTWAIT);
|
|
1740
|
+
if (n <= 0) break;
|
|
1741
|
+
}
|
|
1742
|
+
close(client_fd);
|
|
1743
|
+
return served;
|
|
1744
|
+
}
|
|
1745
|
+
/* Keep-alive: loop back and read the next request. */
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
/* Args passed into the no-GVL blocking accept wrapper. */
|
|
1750
|
+
typedef struct {
|
|
1751
|
+
int listen_fd;
|
|
1752
|
+
/* On return: the served-request count for this loop invocation,
|
|
1753
|
+
* or -1 if the listener returned EBADF (graceful close). */
|
|
1754
|
+
long served_count;
|
|
1755
|
+
} hyp_cl_loop_args_t;
|
|
1756
|
+
|
|
1757
|
+
/* PageCache.run_static_accept_loop(listen_fd) -> Integer | :crashed
|
|
1758
|
+
*
|
|
1759
|
+
* Drives the accept-and-serve loop. Returns the count of requests
|
|
1760
|
+
* served when the loop exits cleanly (listener closed, stop flag
|
|
1761
|
+
* raised) or `:crashed` if an unrecoverable accept error happened. */
|
|
1762
|
+
static VALUE rb_pc_run_static_accept_loop(VALUE self, VALUE rb_listen_fd) {
|
|
1763
|
+
(void)self;
|
|
1764
|
+
int listen_fd = NUM2INT(rb_listen_fd);
|
|
1765
|
+
if (listen_fd < 0) {
|
|
1766
|
+
rb_raise(rb_eArgError, "listen_fd must be >= 0");
|
|
1767
|
+
}
|
|
1768
|
+
hyp_cl_stop = 0;
|
|
1769
|
+
/* 2.12-E — reset the per-process served-request counter on entry
|
|
1770
|
+
* so the audit metric reflects THIS loop's served count (not
|
|
1771
|
+
* leftovers from a prior loop in the same process — primarily a
|
|
1772
|
+
* test-suite concern; production has at most one loop per process
|
|
1773
|
+
* lifetime). */
|
|
1774
|
+
hyp_cl_reset_requests_served();
|
|
1775
|
+
|
|
1776
|
+
/* Ruby's `TCPServer.new` sets O_NONBLOCK on the listening fd so
|
|
1777
|
+
* `IO.select` + `accept_nonblock` works naturally on the Ruby
|
|
1778
|
+
* side. Our C accept loop wants a BLOCKING fd: we release the
|
|
1779
|
+
* GVL during the accept syscall and want the kernel to park us
|
|
1780
|
+
* there rather than burning CPU on EAGAIN. Clear O_NONBLOCK
|
|
1781
|
+
* unconditionally — the operator's existing accept-loop code
|
|
1782
|
+
* paths don't share this fd with us (we own it for the lifetime
|
|
1783
|
+
* of `run_static_accept_loop`). */
|
|
1784
|
+
int flags = fcntl(listen_fd, F_GETFL, 0);
|
|
1785
|
+
if (flags >= 0 && (flags & O_NONBLOCK)) {
|
|
1786
|
+
(void)fcntl(listen_fd, F_SETFL, flags & ~O_NONBLOCK);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
long served = 0;
|
|
1790
|
+
for (;;) {
|
|
1791
|
+
if (hyp_cl_stop) {
|
|
1792
|
+
break;
|
|
1793
|
+
}
|
|
1794
|
+
hyp_cl_accept_args_t a;
|
|
1795
|
+
a.listen_fd = listen_fd;
|
|
1796
|
+
a.client_fd = -1;
|
|
1797
|
+
a.err = 0;
|
|
1798
|
+
rb_thread_call_without_gvl(hyp_cl_accept_blocking, &a,
|
|
1799
|
+
RUBY_UBF_IO, NULL);
|
|
1800
|
+
if (a.client_fd < 0) {
|
|
1801
|
+
if (a.err == EBADF || a.err == EINVAL) {
|
|
1802
|
+
/* Listener was closed — graceful exit. */
|
|
1803
|
+
break;
|
|
1804
|
+
}
|
|
1805
|
+
if (a.err == ECONNABORTED || a.err == EAGAIN ||
|
|
1806
|
+
a.err == EWOULDBLOCK || a.err == EINTR) {
|
|
1807
|
+
/* Transient — re-check stop flag and re-enter accept. */
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
/* Unexpected accept error; surface as :crashed so Ruby can
|
|
1811
|
+
* fall back to its own accept loop. */
|
|
1812
|
+
return ID2SYM(rb_intern("crashed"));
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
int handed_off = 0;
|
|
1816
|
+
long n = hyp_cl_serve_connection(a.client_fd, &handed_off);
|
|
1817
|
+
if (n > 0) {
|
|
1818
|
+
served += n;
|
|
1819
|
+
}
|
|
1820
|
+
/* hyp_cl_serve_connection closes the fd itself unless it handed
|
|
1821
|
+
* off to Ruby. Either way our work for this connection is
|
|
1822
|
+
* done. */
|
|
1823
|
+
}
|
|
1824
|
+
return LONG2NUM(served);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/* PageCache.set_lifecycle_callback(callable_or_nil) -> callable_or_nil
|
|
1828
|
+
*
|
|
1829
|
+
* Registers (or clears) the per-request lifecycle callback. Called
|
|
1830
|
+
* once at server boot from `Hyperion::Server`'s accept-loop set-up.
|
|
1831
|
+
* The callback receives (method_str, path_str) once per request the
|
|
1832
|
+
* C loop served; the Ruby implementation builds a Request and fires
|
|
1833
|
+
* `Runtime#fire_request_start` + `fire_request_end`. */
|
|
1834
|
+
static VALUE rb_pc_set_lifecycle_callback(VALUE self, VALUE callback) {
|
|
1835
|
+
(void)self;
|
|
1836
|
+
if (!NIL_P(callback) && !rb_respond_to(callback, rb_intern("call"))) {
|
|
1837
|
+
rb_raise(rb_eArgError, "callback must respond to #call");
|
|
1838
|
+
}
|
|
1839
|
+
hyp_cl_lifecycle_callback = callback;
|
|
1840
|
+
return callback;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
/* PageCache.set_lifecycle_active(bool) -> bool
|
|
1844
|
+
*
|
|
1845
|
+
* Toggles the integer flag the C loop reads on every request to
|
|
1846
|
+
* decide whether to invoke the lifecycle callback. Decoupled from
|
|
1847
|
+
* the callback registration so Ruby can flip it cheaply when hooks
|
|
1848
|
+
* are added/removed at runtime, without re-registering the
|
|
1849
|
+
* callback object itself. */
|
|
1850
|
+
static VALUE rb_pc_set_lifecycle_active(VALUE self, VALUE flag) {
|
|
1851
|
+
(void)self;
|
|
1852
|
+
hyp_cl_lifecycle_active = RTEST(flag) ? 1 : 0;
|
|
1853
|
+
return flag;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/* PageCache.lifecycle_active? -> bool
|
|
1857
|
+
*
|
|
1858
|
+
* Spec/operator helper. */
|
|
1859
|
+
static VALUE rb_pc_lifecycle_active_p(VALUE self) {
|
|
1860
|
+
(void)self;
|
|
1861
|
+
return hyp_cl_lifecycle_active ? Qtrue : Qfalse;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
/* PageCache.set_handoff_callback(callable) -> callable
|
|
1865
|
+
*
|
|
1866
|
+
* Registers the callback the C loop invokes when a request can't
|
|
1867
|
+
* be served from the static cache. Receives (fd_int, partial_buffer_str_or_nil)
|
|
1868
|
+
* — Ruby owns the fd from that point on. The accept loop continues
|
|
1869
|
+
* to the next connection; a handoff is per-connection, not per-
|
|
1870
|
+
* accept-loop. */
|
|
1871
|
+
static VALUE rb_pc_set_handoff_callback(VALUE self, VALUE callback) {
|
|
1872
|
+
(void)self;
|
|
1873
|
+
if (!NIL_P(callback) && !rb_respond_to(callback, rb_intern("call"))) {
|
|
1874
|
+
rb_raise(rb_eArgError, "callback must respond to #call");
|
|
1875
|
+
}
|
|
1876
|
+
hyp_cl_handoff_callback = callback;
|
|
1877
|
+
return callback;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
/* PageCache.stop_accept_loop -> nil
|
|
1881
|
+
*
|
|
1882
|
+
* Flips the stop flag. The accept loop checks it between accepts and
|
|
1883
|
+
* (more importantly) between keep-alive requests on the same
|
|
1884
|
+
* connection; the `Server#stop` close()-on-listener is the primary
|
|
1885
|
+
* signal (it races us out via accept returning EBADF). */
|
|
1886
|
+
static VALUE rb_pc_stop_accept_loop(VALUE self) {
|
|
1887
|
+
(void)self;
|
|
1888
|
+
hyp_cl_stop = 1;
|
|
1889
|
+
return Qnil;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/* PageCache.c_loop_requests_total -> Integer
|
|
1893
|
+
*
|
|
1894
|
+
* 2.12-E — the running per-process count of requests served by either
|
|
1895
|
+
* the 2.12-C accept4 loop or the 2.12-D io_uring loop since the
|
|
1896
|
+
* loop-entry reset. Read at /-/metrics scrape time so the
|
|
1897
|
+
* `hyperion_requests_dispatch_total{worker_id=PID}` family reflects
|
|
1898
|
+
* C-loop-served requests in addition to Ruby-side ones. Lock-free
|
|
1899
|
+
* via the same atomic the loop bumps on each request. */
|
|
1900
|
+
static VALUE rb_pc_c_loop_requests_total(VALUE self) {
|
|
1901
|
+
(void)self;
|
|
1902
|
+
return ULL2NUM(hyp_cl_load_requests_served());
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/* PageCache.reset_c_loop_requests_total! -> 0
|
|
1906
|
+
*
|
|
1907
|
+
* 2.12-E — spec/operator escape hatch for clearing the per-process
|
|
1908
|
+
* counter between bench runs without restarting the worker. Production
|
|
1909
|
+
* has no need for this; the loop-entry path resets implicitly. */
|
|
1910
|
+
static VALUE rb_pc_reset_c_loop_requests_total_bang(VALUE self) {
|
|
1911
|
+
(void)self;
|
|
1912
|
+
hyp_cl_reset_requests_served();
|
|
1913
|
+
return INT2NUM(0);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
/* PageCache.bump_c_loop_requests_total_for_test!(n) -> Integer
|
|
1917
|
+
*
|
|
1918
|
+
* 2.12-E — spec-only counter primer. Lets the PrometheusExporter
|
|
1919
|
+
* fold-in test assert the merge logic without needing a live C loop
|
|
1920
|
+
* (which would tie the spec to listener bind + accept timing).
|
|
1921
|
+
* NOT documented as public surface; the name's `_for_test!` suffix
|
|
1922
|
+
* is the contract. */
|
|
1923
|
+
static VALUE rb_pc_bump_c_loop_requests_total_for_test_bang(VALUE self, VALUE rb_n) {
|
|
1924
|
+
(void)self;
|
|
1925
|
+
long n = NUM2LONG(rb_n);
|
|
1926
|
+
if (n < 0) {
|
|
1927
|
+
rb_raise(rb_eArgError, "n must be >= 0");
|
|
1928
|
+
}
|
|
1929
|
+
for (long i = 0; i < n; i++) {
|
|
1930
|
+
hyp_cl_tick_request();
|
|
1931
|
+
}
|
|
1932
|
+
return ULL2NUM(hyp_cl_load_requests_served());
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
/* PageCache.handoff_to_ruby(client_fd, _partial_buffer, _partial_len) -> Integer
|
|
1936
|
+
*
|
|
1937
|
+
* Echo helper — exposed for spec parity with the bench-time API
|
|
1938
|
+
* shape called out in the 2.12-C plan. The actual handoff happens
|
|
1939
|
+
* inside the C loop via the registered callback; this method exists
|
|
1940
|
+
* so callers can introspect the contract without engaging the
|
|
1941
|
+
* accept loop. */
|
|
1942
|
+
static VALUE rb_pc_handoff_to_ruby(VALUE self, VALUE rb_fd, VALUE rb_buf,
|
|
1943
|
+
VALUE rb_len) {
|
|
1944
|
+
(void)self; (void)rb_buf; (void)rb_len;
|
|
1945
|
+
return rb_fd;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1064
1948
|
/* PageCache.max_key_len -> Integer */
|
|
1065
1949
|
static VALUE rb_pc_max_key_len(VALUE self) {
|
|
1066
1950
|
(void)self;
|
|
1067
1951
|
return INT2NUM(HYP_PC_MAX_KEY_LEN);
|
|
1068
1952
|
}
|
|
1069
1953
|
|
|
1954
|
+
/* ============================================================
|
|
1955
|
+
* 2.12-D — sharing surface for io_uring_loop.c.
|
|
1956
|
+
*
|
|
1957
|
+
* Thin extern wrappers around the static helpers above. The io_uring
|
|
1958
|
+
* loop calls these once per request; the indirection cost is one
|
|
1959
|
+
* direct-call jump and is dominated by the syscall savings. Defined
|
|
1960
|
+
* here (rather than promoted-static) so the helpers' signatures stay
|
|
1961
|
+
* file-local and we don't accidentally widen the public surface of
|
|
1962
|
+
* the C ext. */
|
|
1963
|
+
|
|
1964
|
+
long pc_internal_find_eoh(const char *buf, size_t len) {
|
|
1965
|
+
return hyp_cl_find_eoh(buf, len);
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
long pc_internal_parse_request_line(const char *buf, size_t len,
|
|
1969
|
+
size_t *m_off, size_t *m_len,
|
|
1970
|
+
size_t *p_off, size_t *p_len) {
|
|
1971
|
+
return hyp_cl_parse_request_line(buf, len, m_off, m_len, p_off, p_len);
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
int pc_internal_scan_headers(const char *buf, size_t start, size_t end,
|
|
1975
|
+
int *connection_close, int *has_body,
|
|
1976
|
+
int *upgrade_seen) {
|
|
1977
|
+
return hyp_cl_scan_headers(buf, start, end, connection_close, has_body,
|
|
1978
|
+
upgrade_seen);
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
pc_internal_method_t pc_internal_classify_method(const char *m, size_t len) {
|
|
1982
|
+
hyp_pc_method_t k = hyp_pc_classify_method(m, len);
|
|
1983
|
+
switch (k) {
|
|
1984
|
+
case HYP_PC_METHOD_GET: return PC_INTERNAL_METHOD_GET;
|
|
1985
|
+
case HYP_PC_METHOD_HEAD: return PC_INTERNAL_METHOD_HEAD;
|
|
1986
|
+
default: return PC_INTERNAL_METHOD_OTHER;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
char *pc_internal_snapshot_response(const char *path, size_t path_len,
|
|
1991
|
+
pc_internal_method_t kind,
|
|
1992
|
+
size_t *out_len) {
|
|
1993
|
+
*out_len = 0;
|
|
1994
|
+
if (path_len == 0 || path_len > HYP_PC_MAX_KEY_LEN) {
|
|
1995
|
+
return NULL;
|
|
1996
|
+
}
|
|
1997
|
+
pthread_mutex_lock(&hyp_pc_lock);
|
|
1998
|
+
int was_stale = 0;
|
|
1999
|
+
hyp_page_slot_t *slot = hyp_pc_lookup_locked(path, path_len, &was_stale);
|
|
2000
|
+
if (slot == NULL) {
|
|
2001
|
+
pthread_mutex_unlock(&hyp_pc_lock);
|
|
2002
|
+
return NULL;
|
|
2003
|
+
}
|
|
2004
|
+
size_t write_len = (kind == PC_INTERNAL_METHOD_HEAD)
|
|
2005
|
+
? slot->page->headers_len
|
|
2006
|
+
: slot->page->response_len;
|
|
2007
|
+
char *snapshot = (char *)malloc(write_len);
|
|
2008
|
+
if (snapshot == NULL) {
|
|
2009
|
+
pthread_mutex_unlock(&hyp_pc_lock);
|
|
2010
|
+
return NULL;
|
|
2011
|
+
}
|
|
2012
|
+
memcpy(snapshot, slot->page->response_buf, write_len);
|
|
2013
|
+
pthread_mutex_unlock(&hyp_pc_lock);
|
|
2014
|
+
*out_len = write_len;
|
|
2015
|
+
return snapshot;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
void pc_internal_apply_tcp_nodelay(int fd) {
|
|
2019
|
+
hyp_cl_apply_tcp_nodelay(fd);
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
void pc_internal_fire_lifecycle(const char *method, size_t mlen,
|
|
2023
|
+
const char *path, size_t plen) {
|
|
2024
|
+
hyp_cl_fire_lifecycle(method, mlen, path, plen);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
int pc_internal_lifecycle_active(void) {
|
|
2028
|
+
return hyp_cl_lifecycle_active;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
void pc_internal_handoff(int client_fd, const char *partial, size_t partial_len) {
|
|
2032
|
+
hyp_cl_handoff(client_fd, partial, partial_len);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
int pc_internal_stop_requested(void) {
|
|
2036
|
+
return hyp_cl_stop ? 1 : 0;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
void pc_internal_reset_stop(void) {
|
|
2040
|
+
hyp_cl_stop = 0;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
/* 2.12-E — io_uring loop sibling tick / reset entry points. Forward to
|
|
2044
|
+
* the file-local helpers so the atomic stays a single-source-of-truth
|
|
2045
|
+
* for both loop variants. */
|
|
2046
|
+
void pc_internal_tick_request(void) {
|
|
2047
|
+
hyp_cl_tick_request();
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
void pc_internal_reset_requests_served(void) {
|
|
2051
|
+
hyp_cl_reset_requests_served();
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/* Belt-and-suspenders: keep the io_uring sibling's view of the header
|
|
2055
|
+
* cap in sync with this file's. Compile-time check via array sizing
|
|
2056
|
+
* (we deliberately avoid C11 `_Static_assert` for portability with the
|
|
2057
|
+
* older toolchains some linux distros still ship). */
|
|
2058
|
+
typedef int pc_internal_header_cap_check_t
|
|
2059
|
+
[(HYP_CL_MAX_HEADER_BYTES == PC_INTERNAL_MAX_HEADER_BYTES) ? 1 : -1];
|
|
2060
|
+
|
|
1070
2061
|
void Init_hyperion_page_cache(void) {
|
|
1071
2062
|
rb_mHyperion_pc = rb_const_get(rb_cObject, rb_intern("Hyperion"));
|
|
1072
2063
|
|
|
@@ -1109,6 +2100,38 @@ void Init_hyperion_page_cache(void) {
|
|
|
1109
2100
|
rb_pc_register_prebuilt, 3);
|
|
1110
2101
|
rb_define_singleton_method(rb_mHyperionHttpPageCache, "serve_request",
|
|
1111
2102
|
rb_pc_serve_request, 3);
|
|
2103
|
+
/* 2.12-C — connection lifecycle in C. */
|
|
2104
|
+
rb_define_singleton_method(rb_mHyperionHttpPageCache, "run_static_accept_loop",
|
|
2105
|
+
rb_pc_run_static_accept_loop, 1);
|
|
2106
|
+
rb_define_singleton_method(rb_mHyperionHttpPageCache, "stop_accept_loop",
|
|
2107
|
+
rb_pc_stop_accept_loop, 0);
|
|
2108
|
+
rb_define_singleton_method(rb_mHyperionHttpPageCache, "set_lifecycle_callback",
|
|
2109
|
+
rb_pc_set_lifecycle_callback, 1);
|
|
2110
|
+
rb_define_singleton_method(rb_mHyperionHttpPageCache, "set_lifecycle_active",
|
|
2111
|
+
rb_pc_set_lifecycle_active, 1);
|
|
2112
|
+
rb_define_singleton_method(rb_mHyperionHttpPageCache, "lifecycle_active?",
|
|
2113
|
+
rb_pc_lifecycle_active_p, 0);
|
|
2114
|
+
rb_define_singleton_method(rb_mHyperionHttpPageCache, "set_handoff_callback",
|
|
2115
|
+
rb_pc_set_handoff_callback, 1);
|
|
2116
|
+
rb_define_singleton_method(rb_mHyperionHttpPageCache, "handoff_to_ruby",
|
|
2117
|
+
rb_pc_handoff_to_ruby, 3);
|
|
2118
|
+
|
|
2119
|
+
/* 2.12-E — per-process served-request counter for the SO_REUSEPORT
|
|
2120
|
+
* load-balancing audit. Read at /-/metrics scrape time and folded
|
|
2121
|
+
* into `hyperion_requests_dispatch_total{worker_id=PID}` so
|
|
2122
|
+
* operators see a single per-worker number across every dispatch
|
|
2123
|
+
* shape (Rack via Connection, h2, the C loops). */
|
|
2124
|
+
rb_define_singleton_method(rb_mHyperionHttpPageCache, "c_loop_requests_total",
|
|
2125
|
+
rb_pc_c_loop_requests_total, 0);
|
|
2126
|
+
rb_define_singleton_method(rb_mHyperionHttpPageCache, "reset_c_loop_requests_total!",
|
|
2127
|
+
rb_pc_reset_c_loop_requests_total_bang, 0);
|
|
2128
|
+
rb_define_singleton_method(rb_mHyperionHttpPageCache, "bump_c_loop_requests_total_for_test!",
|
|
2129
|
+
rb_pc_bump_c_loop_requests_total_for_test_bang, 1);
|
|
2130
|
+
|
|
2131
|
+
/* Mark-protect the lifecycle / handoff callback slots so the GC
|
|
2132
|
+
* doesn't collect them while the C loop is running. */
|
|
2133
|
+
rb_gc_register_address(&hyp_cl_lifecycle_callback);
|
|
2134
|
+
rb_gc_register_address(&hyp_cl_handoff_callback);
|
|
1112
2135
|
|
|
1113
2136
|
id_fileno_pc = rb_intern("fileno");
|
|
1114
2137
|
id_to_io_pc = rb_intern("to_io");
|
|
@@ -1122,4 +2145,13 @@ void Init_hyperion_page_cache(void) {
|
|
|
1122
2145
|
rb_gc_register_mark_object(sym_stale_pc);
|
|
1123
2146
|
rb_gc_register_mark_object(sym_missing_pc);
|
|
1124
2147
|
rb_gc_register_mark_object(sym_miss_pc);
|
|
2148
|
+
|
|
2149
|
+
/* 2.12-D — register the io_uring sibling. The init defines the
|
|
2150
|
+
* `run_static_io_uring_loop` Ruby method on the same module
|
|
2151
|
+
* (`Hyperion::Http::PageCache`) and lazy-initialises any per-process
|
|
2152
|
+
* io_uring state. On non-Linux / no-liburing builds the registered
|
|
2153
|
+
* method returns the `:unavailable` symbol so the Ruby caller can
|
|
2154
|
+
* fall through to the 2.12-C accept4 path. */
|
|
2155
|
+
extern void Init_hyperion_io_uring_loop(VALUE mPageCache);
|
|
2156
|
+
Init_hyperion_io_uring_loop(rb_mHyperionHttpPageCache);
|
|
1125
2157
|
}
|