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