hyperion-rb 2.13.0 → 2.15.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 +675 -0
- data/README.md +131 -904
- data/ext/hyperion_http/page_cache.c +538 -43
- data/lib/hyperion/adapter/rack.rb +285 -0
- data/lib/hyperion/server/connection_loop.rb +104 -6
- data/lib/hyperion/server/route_table.rb +64 -0
- data/lib/hyperion/server.rb +234 -2
- data/lib/hyperion/version.rb +1 -1
- metadata +1 -1
|
@@ -1130,6 +1130,57 @@ static VALUE hyp_cl_lifecycle_callback = Qnil;
|
|
|
1130
1130
|
static VALUE hyp_cl_handoff_callback = Qnil;
|
|
1131
1131
|
static int hyp_cl_lifecycle_active = 0;
|
|
1132
1132
|
|
|
1133
|
+
/* 2.14-A — dynamic block registry. Path → Ruby Proc for routes
|
|
1134
|
+
* registered via `Server.handle(:GET, '/path') { |env| ... }`. The
|
|
1135
|
+
* C accept loop's `hyp_cl_serve_connection` checks this list AFTER
|
|
1136
|
+
* the static page cache lookup misses; on hit, it invokes
|
|
1137
|
+
* `hyp_dyn_dispatch_callback` (registered once at server boot) with
|
|
1138
|
+
* the request shape data + the registered block, gets back the
|
|
1139
|
+
* fully-formed HTTP/1.1 response bytes, and writes them outside the
|
|
1140
|
+
* GVL.
|
|
1141
|
+
*
|
|
1142
|
+
* The registry is small (16 entries default, capped at 256) — apps
|
|
1143
|
+
* with hundreds of dynamic routes belong on a Rails router, not on
|
|
1144
|
+
* the C accept loop's exact-match dispatch. The fixed-size array is
|
|
1145
|
+
* walked linearly because (a) modern CPUs blast through 256 cache
|
|
1146
|
+
* lines in tens of nanoseconds and (b) avoiding a hash table keeps
|
|
1147
|
+
* the C surface tiny. */
|
|
1148
|
+
#define HYP_DYN_MAX_ROUTES 256
|
|
1149
|
+
|
|
1150
|
+
typedef struct {
|
|
1151
|
+
char *path; /* heap-owned, NUL-terminated */
|
|
1152
|
+
size_t path_len;
|
|
1153
|
+
VALUE block; /* the Ruby Proc; gc-protected via the
|
|
1154
|
+
* registry-wide rb_gc_register_address slot
|
|
1155
|
+
* `hyp_dyn_routes_mark_anchor` (see below). */
|
|
1156
|
+
int method; /* PC_INTERNAL_METHOD_GET / HEAD; 2.14-A
|
|
1157
|
+
* accepts only GET (HEAD support is a future
|
|
1158
|
+
* extension — apps rarely need a custom HEAD
|
|
1159
|
+
* handler distinct from GET). */
|
|
1160
|
+
} hyp_dyn_route_t;
|
|
1161
|
+
|
|
1162
|
+
static hyp_dyn_route_t hyp_dyn_routes[HYP_DYN_MAX_ROUTES];
|
|
1163
|
+
static size_t hyp_dyn_route_count = 0;
|
|
1164
|
+
static pthread_mutex_t hyp_dyn_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
1165
|
+
|
|
1166
|
+
/* The dispatch callback is the single bridge between the C loop and
|
|
1167
|
+
* Ruby for dynamic-block requests. Set once via
|
|
1168
|
+
* `set_dynamic_dispatch_callback`; cleared at server stop. The C loop
|
|
1169
|
+
* passes the registered block as the 7th positional arg so the
|
|
1170
|
+
* callback can call into the right Proc without re-doing the path
|
|
1171
|
+
* lookup on the Ruby side. */
|
|
1172
|
+
static VALUE hyp_dyn_dispatch_callback = Qnil;
|
|
1173
|
+
|
|
1174
|
+
/* 2.14-A — GC anchor for every Block VALUE we hold in the registry.
|
|
1175
|
+
* `rb_gc_register_address(&hyp_dyn_routes_mark_anchor)` keeps the
|
|
1176
|
+
* anchor alive; the registry's `mark` is performed via the
|
|
1177
|
+
* `hyp_dyn_routes_mark` GC mark function we wire into the
|
|
1178
|
+
* Hyperion::Http::PageCache module via `rb_define_module` plus a
|
|
1179
|
+
* small mark wrapper class. The simpler alternative — call
|
|
1180
|
+
* `rb_gc_register_address` once per registered block — works too;
|
|
1181
|
+
* we use that approach in `register_dynamic_block` below. */
|
|
1182
|
+
static VALUE hyp_dyn_routes_mark_anchor = Qnil;
|
|
1183
|
+
|
|
1133
1184
|
/* Stop flag — flipped from Ruby via `stop_accept_loop` when the
|
|
1134
1185
|
* listener should drop out of the loop voluntarily (graceful
|
|
1135
1186
|
* shutdown). The accept syscall is still blocking; the operator's
|
|
@@ -1471,6 +1522,57 @@ static int hyp_cl_scan_headers(const char *buf, size_t start, size_t end,
|
|
|
1471
1522
|
return -1;
|
|
1472
1523
|
}
|
|
1473
1524
|
|
|
1525
|
+
/* 2.14-A — extract the `Host:` header value from a parsed header
|
|
1526
|
+
* block. Returns 0 on success and writes `*h_off`/`*h_len` (offsets
|
|
1527
|
+
* relative to `buf`); -1 if not found. The implementation mirrors
|
|
1528
|
+
* `hyp_cl_scan_headers` (case-insensitive name compare, trim
|
|
1529
|
+
* whitespace), but only matches Host. The C loop calls this only on
|
|
1530
|
+
* a dynamic-block hit — the static-fast-path doesn't read Host. */
|
|
1531
|
+
static int hyp_cl_extract_host(const char *buf, size_t start, size_t end,
|
|
1532
|
+
size_t *h_off, size_t *h_len) {
|
|
1533
|
+
*h_off = 0;
|
|
1534
|
+
*h_len = 0;
|
|
1535
|
+
size_t i = start;
|
|
1536
|
+
while (i + 2 <= end) {
|
|
1537
|
+
if (i + 1 < end && buf[i] == '\r' && buf[i+1] == '\n') {
|
|
1538
|
+
return -1; /* end of headers; not found */
|
|
1539
|
+
}
|
|
1540
|
+
size_t name_start = i;
|
|
1541
|
+
while (i < end && buf[i] != ':' && buf[i] != '\r') {
|
|
1542
|
+
i++;
|
|
1543
|
+
}
|
|
1544
|
+
if (i >= end || buf[i] != ':') {
|
|
1545
|
+
return -1;
|
|
1546
|
+
}
|
|
1547
|
+
size_t name_end = i;
|
|
1548
|
+
i++;
|
|
1549
|
+
while (i < end && (buf[i] == ' ' || buf[i] == '\t')) {
|
|
1550
|
+
i++;
|
|
1551
|
+
}
|
|
1552
|
+
size_t val_start = i;
|
|
1553
|
+
while (i < end && buf[i] != '\r') {
|
|
1554
|
+
i++;
|
|
1555
|
+
}
|
|
1556
|
+
if (i + 1 >= end || buf[i+1] != '\n') {
|
|
1557
|
+
return -1;
|
|
1558
|
+
}
|
|
1559
|
+
size_t val_end = i;
|
|
1560
|
+
i += 2;
|
|
1561
|
+
|
|
1562
|
+
size_t nlen = name_end - name_start;
|
|
1563
|
+
if (hyp_cl_iequals(buf + name_start, nlen, "host", 4)) {
|
|
1564
|
+
/* Trim trailing whitespace. */
|
|
1565
|
+
while (val_end > val_start && (buf[val_end - 1] == ' ' || buf[val_end - 1] == '\t')) {
|
|
1566
|
+
val_end--;
|
|
1567
|
+
}
|
|
1568
|
+
*h_off = val_start;
|
|
1569
|
+
*h_len = val_end - val_start;
|
|
1570
|
+
return 0;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return -1;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1474
1576
|
/* Lifecycle fire helper: rb_funcall into the registered callback
|
|
1475
1577
|
* with (method_str, path_str). Wrapped in rb_protect so a misbehaving
|
|
1476
1578
|
* Ruby hook can't take down the C loop. */
|
|
@@ -1552,6 +1654,140 @@ static void hyp_cl_apply_tcp_nodelay(int fd) {
|
|
|
1552
1654
|
(void)setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
|
|
1553
1655
|
}
|
|
1554
1656
|
|
|
1657
|
+
/* ============================================================
|
|
1658
|
+
* 2.14-A — Dynamic block dispatch from inside the C accept loop.
|
|
1659
|
+
* ============================================================
|
|
1660
|
+
*
|
|
1661
|
+
* `hyp_dyn_lookup_block(path, plen, method)` walks the registered
|
|
1662
|
+
* dynamic-block table and returns the registered block VALUE on
|
|
1663
|
+
* exact-match hit, or `Qnil` on miss. Linear scan (capped at
|
|
1664
|
+
* HYP_DYN_MAX_ROUTES = 256 entries); the alternative — a hash map
|
|
1665
|
+
* inside C — would push more state into the C side without buying
|
|
1666
|
+
* meaningful throughput at the route counts the C accept loop
|
|
1667
|
+
* targets.
|
|
1668
|
+
*
|
|
1669
|
+
* The lookup grabs a lightweight pthread mutex; the registry is
|
|
1670
|
+
* almost always read-only after server boot, so contention is
|
|
1671
|
+
* negligible (one cmpxchg-style instruction per request). */
|
|
1672
|
+
static VALUE hyp_dyn_lookup_block(const char *path, size_t plen,
|
|
1673
|
+
hyp_pc_method_t method) {
|
|
1674
|
+
if (plen == 0 || method == HYP_PC_METHOD_OTHER) {
|
|
1675
|
+
return Qnil;
|
|
1676
|
+
}
|
|
1677
|
+
pthread_mutex_lock(&hyp_dyn_lock);
|
|
1678
|
+
VALUE found = Qnil;
|
|
1679
|
+
for (size_t i = 0; i < hyp_dyn_route_count; i++) {
|
|
1680
|
+
hyp_dyn_route_t *r = &hyp_dyn_routes[i];
|
|
1681
|
+
if (r->path_len == plen && memcmp(r->path, path, plen) == 0) {
|
|
1682
|
+
/* GET-registered entries also serve HEAD (the dispatcher
|
|
1683
|
+
* could trim the body; for 2.14-A we just call the block
|
|
1684
|
+
* and let it return whatever it returns — operators that
|
|
1685
|
+
* want a HEAD-specific shape register a separate route). */
|
|
1686
|
+
if ((int)method == r->method ||
|
|
1687
|
+
((int)method == PC_INTERNAL_METHOD_HEAD && r->method == PC_INTERNAL_METHOD_GET)) {
|
|
1688
|
+
found = r->block;
|
|
1689
|
+
break;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
pthread_mutex_unlock(&hyp_dyn_lock);
|
|
1694
|
+
return found;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
/* Args for the rb_protect-wrapped Ruby dispatch call. The 8-arg
|
|
1698
|
+
* Ruby callable is invoked under the GVL (we re-acquire it via
|
|
1699
|
+
* rb_thread_call_with_gvl from the no-GVL outer frame); the result
|
|
1700
|
+
* is a single binary String — the fully-formed HTTP/1.1 response. */
|
|
1701
|
+
typedef struct {
|
|
1702
|
+
VALUE callback;
|
|
1703
|
+
VALUE method_str;
|
|
1704
|
+
VALUE path_str;
|
|
1705
|
+
VALUE query_str;
|
|
1706
|
+
VALUE host_str;
|
|
1707
|
+
VALUE headers_blob;
|
|
1708
|
+
VALUE remote_addr;
|
|
1709
|
+
VALUE block;
|
|
1710
|
+
VALUE keep_alive;
|
|
1711
|
+
} hyp_dyn_dispatch_args_t;
|
|
1712
|
+
|
|
1713
|
+
static VALUE hyp_dyn_dispatch_invoke(VALUE raw) {
|
|
1714
|
+
hyp_dyn_dispatch_args_t *a = (hyp_dyn_dispatch_args_t *)raw;
|
|
1715
|
+
VALUE argv[8];
|
|
1716
|
+
argv[0] = a->method_str;
|
|
1717
|
+
argv[1] = a->path_str;
|
|
1718
|
+
argv[2] = a->query_str;
|
|
1719
|
+
argv[3] = a->host_str;
|
|
1720
|
+
argv[4] = a->headers_blob;
|
|
1721
|
+
argv[5] = a->remote_addr;
|
|
1722
|
+
argv[6] = a->block;
|
|
1723
|
+
argv[7] = a->keep_alive;
|
|
1724
|
+
return rb_funcallv(a->callback, rb_intern("call"), 8, argv);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/* Drive one dynamic-block request through the registered Ruby
|
|
1728
|
+
* dispatch helper. On success, returns a freshly malloc'd byte
|
|
1729
|
+
* buffer containing the full HTTP/1.1 response (caller must free()
|
|
1730
|
+
* it) and writes the byte length into *out_len. On failure (no
|
|
1731
|
+
* callback, exception, return-shape error), returns NULL — the
|
|
1732
|
+
* caller will hand off to Ruby so the connection still gets a
|
|
1733
|
+
* response.
|
|
1734
|
+
*
|
|
1735
|
+
* MUST be called under the GVL. */
|
|
1736
|
+
static char *hyp_dyn_dispatch_under_gvl(const char *method, size_t mlen,
|
|
1737
|
+
const char *path, size_t plen,
|
|
1738
|
+
const char *query, size_t qlen,
|
|
1739
|
+
const char *host, size_t hlen,
|
|
1740
|
+
const char *hdrs, size_t hdrs_len,
|
|
1741
|
+
const char *peer, size_t peer_len,
|
|
1742
|
+
VALUE block, int keep_alive,
|
|
1743
|
+
size_t *out_len) {
|
|
1744
|
+
*out_len = 0;
|
|
1745
|
+
if (NIL_P(hyp_dyn_dispatch_callback) || NIL_P(block)) {
|
|
1746
|
+
return NULL;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
hyp_dyn_dispatch_args_t a;
|
|
1750
|
+
a.callback = hyp_dyn_dispatch_callback;
|
|
1751
|
+
a.method_str = rb_str_new(method, (long)mlen);
|
|
1752
|
+
a.path_str = rb_str_new(path, (long)plen);
|
|
1753
|
+
a.query_str = (qlen > 0) ? rb_str_new(query, (long)qlen) : rb_str_new("", 0);
|
|
1754
|
+
a.host_str = (hlen > 0) ? rb_str_new(host, (long)hlen) : rb_str_new("", 0);
|
|
1755
|
+
a.headers_blob = (hdrs_len > 0) ? rb_str_new(hdrs, (long)hdrs_len) : rb_str_new("", 0);
|
|
1756
|
+
a.remote_addr = (peer_len > 0) ? rb_str_new(peer, (long)peer_len) : rb_str_new("", 0);
|
|
1757
|
+
a.block = block;
|
|
1758
|
+
a.keep_alive = keep_alive ? Qtrue : Qfalse;
|
|
1759
|
+
|
|
1760
|
+
int state = 0;
|
|
1761
|
+
VALUE result = rb_protect(hyp_dyn_dispatch_invoke, (VALUE)&a, &state);
|
|
1762
|
+
if (state) {
|
|
1763
|
+
rb_set_errinfo(Qnil);
|
|
1764
|
+
return NULL;
|
|
1765
|
+
}
|
|
1766
|
+
if (!RB_TYPE_P(result, T_STRING)) {
|
|
1767
|
+
return NULL;
|
|
1768
|
+
}
|
|
1769
|
+
long len = RSTRING_LEN(result);
|
|
1770
|
+
if (len <= 0) {
|
|
1771
|
+
return NULL;
|
|
1772
|
+
}
|
|
1773
|
+
char *snapshot = (char *)malloc((size_t)len);
|
|
1774
|
+
if (snapshot == NULL) {
|
|
1775
|
+
return NULL;
|
|
1776
|
+
}
|
|
1777
|
+
memcpy(snapshot, RSTRING_PTR(result), (size_t)len);
|
|
1778
|
+
*out_len = (size_t)len;
|
|
1779
|
+
return snapshot;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
/* The accept-loop thread holds the GVL by default; only releases it
|
|
1783
|
+
* around the blocking syscalls (accept, recv, write). The dynamic
|
|
1784
|
+
* dispatch sits between the recv (which restored the GVL on return)
|
|
1785
|
+
* and the write (which drops it again), so no rb_thread_call_with_gvl
|
|
1786
|
+
* round-trip is needed — `hyp_dyn_dispatch_under_gvl` is called with
|
|
1787
|
+
* the GVL already held. The `_under_gvl` suffix is documentation:
|
|
1788
|
+
* the function MUST NOT be called from a `rb_thread_call_without_gvl`
|
|
1789
|
+
* frame (no Ruby VM access without the GVL). */
|
|
1790
|
+
|
|
1555
1791
|
/* Pre-built `404 Not Found` response, written to the socket when the
|
|
1556
1792
|
* C loop sees a method/path it can answer with a definite negative
|
|
1557
1793
|
* BUT can't hand off to Ruby (because the rest of the request looks
|
|
@@ -1662,57 +1898,166 @@ static long hyp_cl_serve_connection(int client_fd, int *handed_off) {
|
|
|
1662
1898
|
return served;
|
|
1663
1899
|
}
|
|
1664
1900
|
|
|
1901
|
+
/* 2.14-A — split request-target into path + query. The path
|
|
1902
|
+
* registry uses the path-only key so an entry registered as
|
|
1903
|
+
* `/echo` matches `/echo?x=1`. */
|
|
1904
|
+
size_t path_only_len = path_len;
|
|
1905
|
+
size_t query_off = 0;
|
|
1906
|
+
size_t query_len = 0;
|
|
1907
|
+
for (size_t qi = 0; qi < path_len; qi++) {
|
|
1908
|
+
if (buf[path_off + qi] == '?') {
|
|
1909
|
+
path_only_len = qi;
|
|
1910
|
+
query_off = path_off + qi + 1;
|
|
1911
|
+
query_len = path_len - qi - 1;
|
|
1912
|
+
break;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1665
1916
|
pthread_mutex_lock(&hyp_pc_lock);
|
|
1666
1917
|
int was_stale = 0;
|
|
1667
|
-
hyp_page_slot_t *slot = hyp_pc_lookup_locked(buf + path_off,
|
|
1668
|
-
if (slot
|
|
1918
|
+
hyp_page_slot_t *slot = hyp_pc_lookup_locked(buf + path_off, path_only_len, &was_stale);
|
|
1919
|
+
if (slot != NULL) {
|
|
1920
|
+
size_t write_len = (kind == HYP_PC_METHOD_HEAD)
|
|
1921
|
+
? slot->page->headers_len
|
|
1922
|
+
: slot->page->response_len;
|
|
1923
|
+
char *snapshot = (char *)malloc(write_len);
|
|
1924
|
+
if (snapshot == NULL) {
|
|
1925
|
+
pthread_mutex_unlock(&hyp_pc_lock);
|
|
1926
|
+
/* OOM mid-loop — hand off so Ruby can return 500. */
|
|
1927
|
+
hyp_cl_handoff(client_fd, buf, buf_len);
|
|
1928
|
+
*handed_off = 1;
|
|
1929
|
+
return served;
|
|
1930
|
+
}
|
|
1931
|
+
memcpy(snapshot, slot->page->response_buf, write_len);
|
|
1669
1932
|
pthread_mutex_unlock(&hyp_pc_lock);
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1933
|
+
|
|
1934
|
+
hyp_pc_write_args_t wargs;
|
|
1935
|
+
wargs.fd = client_fd;
|
|
1936
|
+
wargs.buf = snapshot;
|
|
1937
|
+
wargs.len = write_len;
|
|
1938
|
+
wargs.total = 0;
|
|
1939
|
+
wargs.err = 0;
|
|
1940
|
+
rb_thread_call_without_gvl(hyp_pc_write_blocking, &wargs,
|
|
1941
|
+
RUBY_UBF_IO, NULL);
|
|
1942
|
+
free(snapshot);
|
|
1943
|
+
|
|
1944
|
+
if (wargs.err != 0 && wargs.total == 0) {
|
|
1945
|
+
/* Write failed — peer most likely gone. Close and exit. */
|
|
1946
|
+
close(client_fd);
|
|
1947
|
+
return served;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
served++;
|
|
1951
|
+
/* 2.12-E — per-process tick. Lock-free atomic so the SO_REUSEPORT
|
|
1952
|
+
* audit harness can scrape `c_loop_requests_total` mid-bench
|
|
1953
|
+
* without serialising on a Ruby-side mutex. */
|
|
1954
|
+
hyp_cl_tick_request();
|
|
1955
|
+
|
|
1956
|
+
/* Lifecycle hooks fire AFTER the wire write so observers see a
|
|
1957
|
+
* completed request. Keep this off the no-hook hot path via
|
|
1958
|
+
* the integer flag. */
|
|
1959
|
+
if (hyp_cl_lifecycle_active) {
|
|
1960
|
+
hyp_cl_fire_lifecycle(buf + method_off, method_len,
|
|
1961
|
+
buf + path_off, path_only_len);
|
|
1962
|
+
}
|
|
1963
|
+
} else {
|
|
1679
1964
|
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
1965
|
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1966
|
+
/* 2.14-A — try the dynamic-block registry. On hit, dispatch
|
|
1967
|
+
* through the registered Ruby callback under the GVL (we
|
|
1968
|
+
* already hold it — the recv() returned and we haven't yet
|
|
1969
|
+
* released for write). The callback returns a fully-formed
|
|
1970
|
+
* HTTP/1.1 response String; we copy to a heap buffer, then
|
|
1971
|
+
* release the GVL for the write so the next accept can
|
|
1972
|
+
* proceed concurrently on another thread. */
|
|
1973
|
+
VALUE block = hyp_dyn_lookup_block(buf + path_off, path_only_len, kind);
|
|
1974
|
+
if (NIL_P(block)) {
|
|
1975
|
+
/* No static, no dynamic — hand off to Ruby. */
|
|
1976
|
+
hyp_cl_handoff(client_fd, buf, buf_len);
|
|
1977
|
+
*handed_off = 1;
|
|
1978
|
+
return served;
|
|
1979
|
+
}
|
|
1697
1980
|
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1981
|
+
/* Pull the Host header for env['HTTP_HOST'] / SERVER_NAME. */
|
|
1982
|
+
size_t host_hdr_off = 0;
|
|
1983
|
+
size_t host_hdr_len = 0;
|
|
1984
|
+
(void)hyp_cl_extract_host(buf, (size_t)req_line_end, (size_t)eoh,
|
|
1985
|
+
&host_hdr_off, &host_hdr_len);
|
|
1986
|
+
|
|
1987
|
+
/* Pull peer addr for env['REMOTE_ADDR']. Best-effort; if
|
|
1988
|
+
* getpeername fails (rare on a freshly-accepted fd) we
|
|
1989
|
+
* pass an empty String and let the Ruby helper substitute
|
|
1990
|
+
* 127.0.0.1. */
|
|
1991
|
+
char peer_buf[64];
|
|
1992
|
+
peer_buf[0] = '\0';
|
|
1993
|
+
size_t peer_len = 0;
|
|
1994
|
+
{
|
|
1995
|
+
struct sockaddr_storage ss;
|
|
1996
|
+
socklen_t slen = (socklen_t)sizeof(ss);
|
|
1997
|
+
if (getpeername(client_fd, (struct sockaddr *)&ss, &slen) == 0) {
|
|
1998
|
+
if (ss.ss_family == AF_INET) {
|
|
1999
|
+
struct sockaddr_in *sin = (struct sockaddr_in *)&ss;
|
|
2000
|
+
if (inet_ntop(AF_INET, &sin->sin_addr, peer_buf, sizeof(peer_buf))) {
|
|
2001
|
+
peer_len = strlen(peer_buf);
|
|
2002
|
+
}
|
|
2003
|
+
} else if (ss.ss_family == AF_INET6) {
|
|
2004
|
+
struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)&ss;
|
|
2005
|
+
if (inet_ntop(AF_INET6, &sin6->sin6_addr, peer_buf, sizeof(peer_buf))) {
|
|
2006
|
+
peer_len = strlen(peer_buf);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
1703
2011
|
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
2012
|
+
/* Header blob: from req_line_end to eoh-2 (strips the
|
|
2013
|
+
* trailing CRLF that delimits headers from the empty
|
|
2014
|
+
* line). The Ruby helper tolerates an extra trailing
|
|
2015
|
+
* CRLF, so a few bytes of slack are safe. */
|
|
2016
|
+
size_t hdrs_blob_off = (size_t)req_line_end;
|
|
2017
|
+
size_t hdrs_blob_len = ((size_t)eoh > hdrs_blob_off + 2)
|
|
2018
|
+
? ((size_t)eoh - hdrs_blob_off - 2)
|
|
2019
|
+
: 0;
|
|
2020
|
+
|
|
2021
|
+
int keep_alive = !connection_close;
|
|
2022
|
+
size_t resp_len = 0;
|
|
2023
|
+
char *resp_buf = hyp_dyn_dispatch_under_gvl(
|
|
2024
|
+
buf + method_off, method_len,
|
|
2025
|
+
buf + path_off, path_only_len,
|
|
2026
|
+
(query_len > 0) ? buf + query_off : "", query_len,
|
|
2027
|
+
(host_hdr_len > 0) ? buf + host_hdr_off : "", host_hdr_len,
|
|
2028
|
+
(hdrs_blob_len > 0) ? buf + hdrs_blob_off : "", hdrs_blob_len,
|
|
2029
|
+
peer_buf, peer_len,
|
|
2030
|
+
block, keep_alive, &resp_len);
|
|
2031
|
+
if (resp_buf == NULL) {
|
|
2032
|
+
/* Dispatch failed (no callback / exception / shape error).
|
|
2033
|
+
* Hand off to Ruby so the connection still gets a
|
|
2034
|
+
* response. */
|
|
2035
|
+
hyp_cl_handoff(client_fd, buf, buf_len);
|
|
2036
|
+
*handed_off = 1;
|
|
2037
|
+
return served;
|
|
2038
|
+
}
|
|
1709
2039
|
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
2040
|
+
hyp_pc_write_args_t wargs;
|
|
2041
|
+
wargs.fd = client_fd;
|
|
2042
|
+
wargs.buf = resp_buf;
|
|
2043
|
+
wargs.len = resp_len;
|
|
2044
|
+
wargs.total = 0;
|
|
2045
|
+
wargs.err = 0;
|
|
2046
|
+
rb_thread_call_without_gvl(hyp_pc_write_blocking, &wargs,
|
|
2047
|
+
RUBY_UBF_IO, NULL);
|
|
2048
|
+
free(resp_buf);
|
|
2049
|
+
|
|
2050
|
+
if (wargs.err != 0 && wargs.total == 0) {
|
|
2051
|
+
close(client_fd);
|
|
2052
|
+
return served;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
served++;
|
|
2056
|
+
hyp_cl_tick_request();
|
|
2057
|
+
/* Lifecycle hooks for the dynamic-block path fire INSIDE the
|
|
2058
|
+
* Ruby dispatch helper (it has the env hash in scope), not
|
|
2059
|
+
* via the C-side lifecycle callback. The C-side flag stays
|
|
2060
|
+
* the static-path's hook contract. */
|
|
1716
2061
|
}
|
|
1717
2062
|
|
|
1718
2063
|
/* Carry pipelined bytes forward into the same buffer. */
|
|
@@ -1951,6 +2296,141 @@ static VALUE rb_pc_max_key_len(VALUE self) {
|
|
|
1951
2296
|
return INT2NUM(HYP_PC_MAX_KEY_LEN);
|
|
1952
2297
|
}
|
|
1953
2298
|
|
|
2299
|
+
/* ============================================================
|
|
2300
|
+
* 2.14-A — dynamic block registry: Ruby-callable surface.
|
|
2301
|
+
* ============================================================
|
|
2302
|
+
*
|
|
2303
|
+
* `register_dynamic_block(path, method, block)` adds an entry to the
|
|
2304
|
+
* C-side dynamic-block table. Called once per registered route at
|
|
2305
|
+
* server boot from `Server#run_c_accept_loop`. Returns the new entry
|
|
2306
|
+
* count.
|
|
2307
|
+
*
|
|
2308
|
+
* `clear_dynamic_blocks!` empties the table — used by specs and on
|
|
2309
|
+
* server stop so a subsequent boot doesn't see stale entries.
|
|
2310
|
+
*
|
|
2311
|
+
* `set_dynamic_dispatch_callback(callable)` registers the single
|
|
2312
|
+
* Ruby callable the C loop invokes for every dynamic-block hit;
|
|
2313
|
+
* typically a small closure built by the server that captures the
|
|
2314
|
+
* runtime + delegates to `Adapter::Rack.dispatch_for_c_loop`.
|
|
2315
|
+
*/
|
|
2316
|
+
static VALUE rb_pc_register_dynamic_block(VALUE self, VALUE rb_path,
|
|
2317
|
+
VALUE rb_method, VALUE rb_block) {
|
|
2318
|
+
(void)self;
|
|
2319
|
+
Check_Type(rb_path, T_STRING);
|
|
2320
|
+
if (NIL_P(rb_block) || !rb_respond_to(rb_block, rb_intern("call"))) {
|
|
2321
|
+
rb_raise(rb_eArgError, "block must respond to #call");
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
const char *path = RSTRING_PTR(rb_path);
|
|
2325
|
+
size_t path_len = (size_t)RSTRING_LEN(rb_path);
|
|
2326
|
+
if (path_len == 0 || path_len > HYP_PC_MAX_KEY_LEN) {
|
|
2327
|
+
rb_raise(rb_eArgError, "path must be 1..%d bytes", HYP_PC_MAX_KEY_LEN);
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
int method_kind = PC_INTERNAL_METHOD_GET;
|
|
2331
|
+
if (SYMBOL_P(rb_method)) {
|
|
2332
|
+
ID m = SYM2ID(rb_method);
|
|
2333
|
+
if (m == rb_intern("HEAD")) method_kind = PC_INTERNAL_METHOD_HEAD;
|
|
2334
|
+
else if (m == rb_intern("GET")) method_kind = PC_INTERNAL_METHOD_GET;
|
|
2335
|
+
else method_kind = PC_INTERNAL_METHOD_OTHER;
|
|
2336
|
+
} else if (RB_TYPE_P(rb_method, T_STRING)) {
|
|
2337
|
+
const char *ms = RSTRING_PTR(rb_method);
|
|
2338
|
+
long mslen = RSTRING_LEN(rb_method);
|
|
2339
|
+
if (mslen == 4 && (ms[0] == 'H' || ms[0] == 'h')) method_kind = PC_INTERNAL_METHOD_HEAD;
|
|
2340
|
+
else if (mslen == 3 && (ms[0] == 'G' || ms[0] == 'g')) method_kind = PC_INTERNAL_METHOD_GET;
|
|
2341
|
+
else method_kind = PC_INTERNAL_METHOD_OTHER;
|
|
2342
|
+
}
|
|
2343
|
+
if (method_kind == PC_INTERNAL_METHOD_OTHER) {
|
|
2344
|
+
rb_raise(rb_eArgError, "2.14-A only supports :GET / :HEAD dynamic blocks");
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
pthread_mutex_lock(&hyp_dyn_lock);
|
|
2348
|
+
if (hyp_dyn_route_count >= HYP_DYN_MAX_ROUTES) {
|
|
2349
|
+
pthread_mutex_unlock(&hyp_dyn_lock);
|
|
2350
|
+
rb_raise(rb_eRuntimeError,
|
|
2351
|
+
"dynamic-block registry full (max %d entries)",
|
|
2352
|
+
HYP_DYN_MAX_ROUTES);
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
/* If a registration for the same (path, method) already exists,
|
|
2356
|
+
* replace it — last writer wins, mirrors RouteTable's contract. */
|
|
2357
|
+
int slot_idx = -1;
|
|
2358
|
+
for (size_t i = 0; i < hyp_dyn_route_count; i++) {
|
|
2359
|
+
hyp_dyn_route_t *r = &hyp_dyn_routes[i];
|
|
2360
|
+
if (r->path_len == path_len && r->method == method_kind &&
|
|
2361
|
+
memcmp(r->path, path, path_len) == 0) {
|
|
2362
|
+
slot_idx = (int)i;
|
|
2363
|
+
break;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
if (slot_idx < 0) {
|
|
2367
|
+
slot_idx = (int)hyp_dyn_route_count;
|
|
2368
|
+
hyp_dyn_routes[slot_idx].path = (char *)malloc(path_len + 1);
|
|
2369
|
+
if (hyp_dyn_routes[slot_idx].path == NULL) {
|
|
2370
|
+
pthread_mutex_unlock(&hyp_dyn_lock);
|
|
2371
|
+
rb_raise(rb_eNoMemError, "register_dynamic_block: path alloc");
|
|
2372
|
+
}
|
|
2373
|
+
memcpy(hyp_dyn_routes[slot_idx].path, path, path_len);
|
|
2374
|
+
hyp_dyn_routes[slot_idx].path[path_len] = '\0';
|
|
2375
|
+
hyp_dyn_routes[slot_idx].path_len = path_len;
|
|
2376
|
+
hyp_dyn_route_count++;
|
|
2377
|
+
/* Anchor the block VALUE against GC. We register the slot's
|
|
2378
|
+
* address once per slot; subsequent writes to the same slot
|
|
2379
|
+
* (replacement on duplicate registration) update the VALUE
|
|
2380
|
+
* in place — the address itself stays anchored. */
|
|
2381
|
+
rb_gc_register_address(&hyp_dyn_routes[slot_idx].block);
|
|
2382
|
+
}
|
|
2383
|
+
hyp_dyn_routes[slot_idx].block = rb_block;
|
|
2384
|
+
hyp_dyn_routes[slot_idx].method = method_kind;
|
|
2385
|
+
pthread_mutex_unlock(&hyp_dyn_lock);
|
|
2386
|
+
|
|
2387
|
+
/* Touch the shared anchor so the registration is observable from
|
|
2388
|
+
* Ruby debugging tools (`ObjectSpace.dump` walks the registered
|
|
2389
|
+
* addresses; a non-nil value here proves at least one block is
|
|
2390
|
+
* pinned by the registry). */
|
|
2391
|
+
hyp_dyn_routes_mark_anchor = rb_block;
|
|
2392
|
+
|
|
2393
|
+
return SIZET2NUM(hyp_dyn_route_count);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
static VALUE rb_pc_clear_dynamic_blocks_bang(VALUE self) {
|
|
2397
|
+
(void)self;
|
|
2398
|
+
pthread_mutex_lock(&hyp_dyn_lock);
|
|
2399
|
+
for (size_t i = 0; i < hyp_dyn_route_count; i++) {
|
|
2400
|
+
free(hyp_dyn_routes[i].path);
|
|
2401
|
+
hyp_dyn_routes[i].path = NULL;
|
|
2402
|
+
hyp_dyn_routes[i].path_len = 0;
|
|
2403
|
+
hyp_dyn_routes[i].block = Qnil;
|
|
2404
|
+
hyp_dyn_routes[i].method = 0;
|
|
2405
|
+
/* We deliberately do NOT call rb_gc_unregister_address —
|
|
2406
|
+
* the address slots stay registered so they can be reused
|
|
2407
|
+
* by a subsequent register_dynamic_block call. The Qnil
|
|
2408
|
+
* write here is enough to drop the GC pin on the prior
|
|
2409
|
+
* block VALUE; Qnil is statically rooted. */
|
|
2410
|
+
}
|
|
2411
|
+
hyp_dyn_route_count = 0;
|
|
2412
|
+
hyp_dyn_routes_mark_anchor = Qnil;
|
|
2413
|
+
pthread_mutex_unlock(&hyp_dyn_lock);
|
|
2414
|
+
return Qnil;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
static VALUE rb_pc_dynamic_block_count(VALUE self) {
|
|
2418
|
+
(void)self;
|
|
2419
|
+
pthread_mutex_lock(&hyp_dyn_lock);
|
|
2420
|
+
size_t n = hyp_dyn_route_count;
|
|
2421
|
+
pthread_mutex_unlock(&hyp_dyn_lock);
|
|
2422
|
+
return SIZET2NUM(n);
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
static VALUE rb_pc_set_dynamic_dispatch_callback(VALUE self, VALUE callback) {
|
|
2426
|
+
(void)self;
|
|
2427
|
+
if (!NIL_P(callback) && !rb_respond_to(callback, rb_intern("call"))) {
|
|
2428
|
+
rb_raise(rb_eArgError, "callback must respond to #call");
|
|
2429
|
+
}
|
|
2430
|
+
hyp_dyn_dispatch_callback = callback;
|
|
2431
|
+
return callback;
|
|
2432
|
+
}
|
|
2433
|
+
|
|
1954
2434
|
/* ============================================================
|
|
1955
2435
|
* 2.12-D — sharing surface for io_uring_loop.c.
|
|
1956
2436
|
*
|
|
@@ -2128,10 +2608,25 @@ void Init_hyperion_page_cache(void) {
|
|
|
2128
2608
|
rb_define_singleton_method(rb_mHyperionHttpPageCache, "bump_c_loop_requests_total_for_test!",
|
|
2129
2609
|
rb_pc_bump_c_loop_requests_total_for_test_bang, 1);
|
|
2130
2610
|
|
|
2611
|
+
/* 2.14-A — dynamic block dispatch surface. */
|
|
2612
|
+
rb_define_singleton_method(rb_mHyperionHttpPageCache, "register_dynamic_block",
|
|
2613
|
+
rb_pc_register_dynamic_block, 3);
|
|
2614
|
+
rb_define_singleton_method(rb_mHyperionHttpPageCache, "clear_dynamic_blocks!",
|
|
2615
|
+
rb_pc_clear_dynamic_blocks_bang, 0);
|
|
2616
|
+
rb_define_singleton_method(rb_mHyperionHttpPageCache, "dynamic_block_count",
|
|
2617
|
+
rb_pc_dynamic_block_count, 0);
|
|
2618
|
+
rb_define_singleton_method(rb_mHyperionHttpPageCache, "set_dynamic_dispatch_callback",
|
|
2619
|
+
rb_pc_set_dynamic_dispatch_callback, 1);
|
|
2620
|
+
|
|
2131
2621
|
/* Mark-protect the lifecycle / handoff callback slots so the GC
|
|
2132
2622
|
* doesn't collect them while the C loop is running. */
|
|
2133
2623
|
rb_gc_register_address(&hyp_cl_lifecycle_callback);
|
|
2134
2624
|
rb_gc_register_address(&hyp_cl_handoff_callback);
|
|
2625
|
+
/* 2.14-A — anchor the dynamic dispatch callback + per-route block
|
|
2626
|
+
* VALUEs (those are anchored on first registration; this is the
|
|
2627
|
+
* shared marker the dispatch callback hides behind). */
|
|
2628
|
+
rb_gc_register_address(&hyp_dyn_dispatch_callback);
|
|
2629
|
+
rb_gc_register_address(&hyp_dyn_routes_mark_anchor);
|
|
2135
2630
|
|
|
2136
2631
|
id_fileno_pc = rb_intern("fileno");
|
|
2137
2632
|
id_to_io_pc = rb_intern("to_io");
|