ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
edge_cache.h
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file edge_cache.h
6 * @brief CDN edge-cache tier - pure engine (PC_ENABLE_EDGE_CACHE).
7 *
8 * The caching reverse-proxy edge that services/web/httpcache is the origin-side groundwork for. This
9 * header is the pure, host-testable core: the response header-field access and HTTP-date math that
10 * httpcache lacks, RFC 9111 freshness (lifetime + age), and the deterministic cache key + SHA-256
11 * digest + `Vary` secondary key. No sockets, no PC, no heap - the socket glue
12 * (edge_cache_proxy) and the L2 SD tier (edge_cache_sd) layer on top.
13 *
14 * @author Douglas Quigg (dstroy0)
15 * @date 2026
16 */
17
18#ifndef PROTOCORE_EDGE_CACHE_H
19#define PROTOCORE_EDGE_CACHE_H
20
21#include "protocore_config.h"
22
23#if PC_ENABLE_EDGE_CACHE
24
25#include "services/web/httpcache/httpcache.h" // pc_cache_control, cache_freshness_lifetime
26#include "shared_primitives/http_date.h" // PC_HTTP_DATE_MAX (the stored-date floor)
27#include <stddef.h>
28#include <stdint.h>
29
30// --- raw response header-block field access ------------------------------------------------------
31
32/**
33 * @brief Copy the value of header @p name from a raw HTTP response head (status line + CRLF headers)
34 * into @p out, OWS-trimmed and NUL-terminated.
35 *
36 * Case-insensitive name match; the first occurrence wins. Fails (returns false, @p out emptied) if the
37 * header is absent or its value would not fit @p out_cap (never truncates a validator).
38 */
39bool edge_header_value(const char *hdrs, size_t len, const char *name, char *out, size_t out_cap);
40
41// --- HTTP-date <-> epoch (RFC 9110 sec 5.6.7: IMF-fixdate, obsolete RFC 850, asctime) -------------
42
43/** @brief Parse an HTTP-date to epoch seconds (UTC), or return -1 if it does not parse. */
44int64_t edge_parse_http_date(const char *s, size_t len);
45
46// --- freshness (RFC 9111 sec 4.2) ----------------------------------------------------------------
47
48/**
49 * @brief Freshness lifetime in seconds, or -1 when none is explicit (caller applies a heuristic).
50 *
51 * Wraps ::cache_freshness_lifetime and computes its `Expires - Date` from the two response epochs
52 * locally (a difference of two origin-supplied times - valid with no local wall clock). @p date_epoch
53 * and @p expires_epoch are -1 when the header was absent.
54 */
55long edge_freshness_lifetime(const pc_cache_control *cc, bool shared, int64_t date_epoch, int64_t expires_epoch);
56
57/**
58 * @brief Heuristic freshness (RFC 9111 sec 4.2.2): 10% of (Date - Last-Modified), or -1 if either
59 * epoch is absent / not ordered.
60 */
61long edge_heuristic_lifetime(int64_t date_epoch, int64_t last_modified_epoch);
62
63/**
64 * @brief Corrected initial age at store time (RFC 9111 sec 4.2.3).
65 *
66 * @p response_time_epoch < 0 (no wall clock) falls back to @p age_hdr alone; @p age_hdr < 0 is 0.
67 */
68long edge_initial_age(int32_t age_hdr, int64_t date_epoch, int64_t response_time_epoch);
69
70/** @brief Current age = @p initial_age + resident time, taken from the monotonic clock (wrap-safe). */
71long edge_current_age(long initial_age, uint32_t insert_ms, uint32_t now_ms);
72
73/** @brief Fresh iff a lifetime is known (>= 0) and the current age has not reached it. */
74bool edge_is_fresh_at(long lifetime, long current_age);
75
76// --- cache key + digest + Vary -------------------------------------------------------------------
77
78/**
79 * @brief Build the canonical cache key `METHOD "\n" host "\n" path [ "\n" query ]` (host lowercased)
80 * into @p out.
81 *
82 * @return the key length (excluding NUL), or 0 if it would overflow @p out_cap (caller treats 0 as
83 * non-cacheable and fails open).
84 */
85size_t edge_key_canon(const char *method, const char *host, const char *path, const char *query, bool include_query,
86 char *out, size_t out_cap);
87
88/** @brief SHA-256 of the canonical key -> @p digest[32] (doubles as the L2 dbm key). */
89void edge_key_digest(const char *canon, size_t len, uint8_t digest[32]);
90
91/** @brief Request-header lookup used to build the Vary secondary key; return nullptr when absent. */
92using EdgeHdrLookup = const char *(*)(void *ctx, const char *name);
93
94/**
95 * @brief Serialize a request's values for each field-name in a response `Vary` header into @p out
96 * (a stable, order-preserving key for the Vary secondary match).
97 *
98 * Two requests select the same stored variant iff their serialized strings are equal. An empty / absent
99 * Vary writes "" and returns true. Returns false if Vary is "*" (uncacheable) or it would overflow.
100 */
101bool edge_vary_serialize(const char *vary_header, EdgeHdrLookup lookup, void *ctx, char *out, size_t out_cap);
102
103// --- L1 RAM store: entries, LRU, TTL, purge ------------------------------------------------------
104
105#define PC_EDGE_LRU_NONE 0xFFFFu
106
107/** @brief One cached object (fixed-size, zero-heap). */
108/* PC_EDGE_LASTMOD_MAX is a knob, but its floor is the protocol's: a stored date that
109 * cannot hold an IMF-fixdate is a date that silently vanishes. */
110#if PC_EDGE_LASTMOD_MAX < PC_HTTP_DATE_MAX
111#error "PC_EDGE_LASTMOD_MAX must be >= PC_HTTP_DATE_MAX (RFC 7231 IMF-fixdate + NUL)"
112#endif
113
114struct EdgeEntry
115{
116 bool used;
117 char key[PC_EDGE_KEY_MAX]; ///< canonical key (collision-safe exact compare)
118 uint8_t digest[32]; ///< pc_sha256(key) - the L2 dbm key
119 char vary_names[PC_EDGE_VARY_MAX]; ///< the response Vary header value (field-name list), "" if none
120 char vary_vals[PC_EDGE_VARY_MAX]; ///< serialized request Vary values at store time (secondary key)
121 int status; ///< stored response status (200)
122 char content_type[PC_EDGE_CTYPE_MAX]; ///< Content-Type to replay
123 char etag[PC_EDGE_ETAG_MAX]; ///< validator (quotes included), "" if none
124 char last_modified[PC_EDGE_LASTMOD_MAX]; ///< Last-Modified (RFC 1123), "" if none
125 char content_encoding[PC_EDGE_CENC_MAX]; ///< Content-Encoding to replay (e.g. gzip), "" if none
126 int64_t date_epoch; ///< origin Date (-1 absent)
127 int64_t expires_epoch; ///< origin Expires (-1 absent)
128 int32_t age_hdr; ///< origin Age at store (>=0)
129 long lifetime_s; ///< resolved freshness lifetime (always >=0)
130 long initial_age; ///< corrected initial age at store
131 uint32_t insert_ms; ///< monotonic store time (TTL/age base)
132 uint32_t last_used_ms; ///< recency
133 struct ///< intrusive LRU node (PC_EDGE_LRU_NONE = end)
134 {
135 uint16_t prev;
136 uint16_t next;
137 } lru;
138 uint16_t body_len;
139 uint8_t body[PC_EDGE_BODY_MAX];
140};
141
142/** @brief Cache observability counters. */
143struct EdgeCacheStats
144{
145 uint32_t hits;
146 uint32_t misses;
147 uint32_t revalidations_304;
148 uint32_t replaces_200;
149 uint32_t stores;
150 uint32_t evictions;
151 uint32_t purges;
152 uint32_t l2_spills;
153 uint32_t l2_promotes;
154 uint32_t mesh_hits; ///< sibling pulls served (PC_ENABLE_EDGE_MESH)
155 uint32_t mesh_misses; ///< peer queries that missed
156 uint64_t bytes_stored;
157};
158
159/**
160 * @brief Write-back hook fired with the LRU victim just before ::edge_store_alloc recycles its slot.
161 *
162 * Lets the L2 SD tier (edge_cache_sd) spill an evicted entry to persistent storage without the pure
163 * engine ever depending on dbm: the glue installs the callback, the engine only calls it. @p victim is
164 * still fully populated (not yet unlinked). Transient passthrough entries (empty key) are not offered.
165 */
166using EdgeEvictFn = void (*)(void *ctx, const EdgeEntry *victim);
167
168/** @brief The L1 store: a fixed pool of entries with an intrusive MRU..LRU list. */
169struct EdgeCacheStore
170{
171 EdgeEntry entries[PC_EDGE_CACHE_SLOTS];
172 uint16_t lru_head; ///< MRU end (PC_EDGE_LRU_NONE when empty)
173 uint16_t lru_tail; ///< LRU end (PC_EDGE_LRU_NONE when empty)
174 EdgeCacheStats stats;
175 EdgeEvictFn on_evict; ///< nullptr = no L2 write-back; else called with each evicted victim
176 void *evict_ctx; ///< opaque context passed to on_evict
177};
178
179/** @brief Reset a store to empty. */
180void edge_store_init(EdgeCacheStore *s);
181
182/**
183 * @brief Reserve a slot for @p canon / @p vary_key, evicting the LRU entry if the pool is full.
184 *
185 * The returned entry has its key/digest/vary set, is marked used, and is linked at the MRU end; the
186 * caller fills status/body/validators/freshness. Returns nullptr only if @p canon would not fit
187 * `PC_EDGE_KEY_MAX` (non-cacheable). Bumps `stores` (and `evictions` if it displaced an entry).
188 */
189EdgeEntry *edge_store_alloc(EdgeCacheStore *s, const char *canon, const char *vary_key);
190
191/**
192 * @brief Find the entry for @p canon whose stored Vary values equal @p vary_key; touch it to MRU.
193 * @return the entry, or nullptr on a miss. Freshness is the caller's decision (see ::edge_entry_fresh).
194 */
195EdgeEntry *edge_store_lookup(EdgeCacheStore *s, const char *canon, const char *vary_key, uint32_t now_ms);
196
197/**
198 * @brief Vary-aware lookup: find the entry for @p canon whose stored `Vary` field-name list, serialized
199 * against the current request (via @p lookup), matches the values seen at store time.
200 *
201 * This resolves the secondary key the caller cannot precompute (the Vary names come from the stored
202 * response). @return the matching variant (touched to MRU), or nullptr.
203 */
204EdgeEntry *edge_store_find(EdgeCacheStore *s, const char *canon, EdgeHdrLookup lookup, void *ctx, uint32_t now_ms);
205
206/** @brief Resolve and store an entry's freshness (lifetime with heuristic / default fallback + age). */
207void edge_entry_set_freshness(EdgeEntry *e, const pc_cache_control *cc, bool shared, int64_t date_epoch,
208 int64_t expires_epoch, int64_t last_modified_epoch, int32_t age_hdr,
209 int64_t response_time_epoch, uint32_t now_ms);
210
211/** @brief True if the entry carries a validator (ETag or Last-Modified) usable for revalidation. */
212bool edge_entry_has_validator(const EdgeEntry *e);
213
214/** @brief True if the entry is still fresh at @p now_ms. */
215bool edge_entry_fresh(const EdgeEntry *e, uint32_t now_ms);
216
217/**
218 * @brief Drop entries that are both stale AND unrevalidatable (no validator) - pure dead weight.
219 * @return the number evicted. Revalidatable stale entries are kept (they can still 304-refresh).
220 */
221uint32_t edge_store_sweep(EdgeCacheStore *s, uint32_t now_ms);
222
223/** @brief Purge every variant stored under the exact canonical key @p canon. @return count purged. */
224uint32_t edge_store_purge(EdgeCacheStore *s, const char *canon);
225
226/** @brief Purge every entry whose request path begins with @p prefix. @return count purged. */
227uint32_t edge_store_purge_prefix(EdgeCacheStore *s, const char *prefix);
228
229/** @brief Unlink @p e and free its slot (no stat bump). Used to release a transient passthrough entry. */
230void edge_store_free_entry(EdgeCacheStore *s, const EdgeEntry *e);
231
232// --- storeability (RFC 9111 sec 3) ---------------------------------------------------------------
233
234/**
235 * @brief May a response be stored? GET + 200 + not no-store/private + not `Vary: *` + body fits.
236 *
237 * @p vary_header may be nullptr. Authorization handling is the caller's (private requests bypass first).
238 */
239bool edge_is_storeable(int status, const char *method, const pc_cache_control *cc, const char *vary_header,
240 size_t body_len);
241
242// --- conditional revalidation (RFC 9111 sec 4.3) -------------------------------------------------
243
244/**
245 * @brief Build the conditional-request header lines for revalidating @p e (`If-None-Match` from its
246 * ETag and/or `If-Modified-Since` from its Last-Modified), into @p out.
247 * @return bytes written (0 if the entry carries no validator, or on overflow).
248 */
249size_t edge_build_conditional(const EdgeEntry *e, char *out, size_t cap);
250
251/**
252 * @brief Apply an origin `304 Not Modified` to a stored entry: recompute its freshness from the 304
253 * response headers and adopt any validators it carried, keeping the stored body (RFC 9111 4.3.4).
254 *
255 * @p new_hdrs / @p hdr_len are the 304 response head; @p response_time_epoch is when it arrived (-1 with
256 * no wall clock); @p now_ms is the monotonic clock.
257 */
258void edge_apply_304(EdgeEntry *e, const char *new_hdrs, size_t hdr_len, int64_t response_time_epoch, uint32_t now_ms);
259
260#endif // PC_ENABLE_EDGE_CACHE
261
262#endif // PROTOCORE_EDGE_CACHE_H
#define PC_EDGE_BODY_MAX
#define PC_EDGE_CACHE_SLOTS
Render a Unix epoch as an RFC 7231 IMF-fixdate.
HTTP Cache-Control directive builder + parser + freshness helper (RFC 9111), PC_ENABLE_HTTP_CACHE.
User-facing configuration for ProtoCore.
#define PC_EDGE_ETAG_MAX
Stored validator (ETag, quotes included).
#define PC_EDGE_KEY_MAX
#define PC_EDGE_VARY_MAX
#define PC_EDGE_CENC_MAX
Stored Content-Encoding to replay (e.g. gzip).
#define PC_EDGE_CTYPE_MAX
Stored Content-Type to replay.
#define PC_EDGE_LASTMOD_MAX
Stored Last-Modified (RFC 1123 date).