From 25fcee5b0c72006c682a36c1b1925384bd892275 Mon Sep 17 00:00:00 2001 From: Maxime Henrion Date: Fri, 26 Jun 2026 16:44:50 -0400 Subject: [PATCH] MEDIUM: cache: emit early hints if configured to do so The HTTP cache now supports sending 103 Early Hints responses if enabled, without the need for explicit per-rule configuration. Two paths feed the 103 response depending on the matched entry. A stripped entry (CACHE_EF_STRIPPED) carries its Link records pre-extracted alongside the cache entry header and is copied out of the row as-is. A complete entry only carries the cached HTX; the Link headers are walked on the fly at request time, deliberately avoiding the extra storage cost of duplicating them alongside the body. The on-the-fly path has a cost: a workload where many hits land on expired complete entries re-parses the cached response on each hit. To bound this, the first such hit also strips the entry in place when conditions allow (the hints pool is below its cap, and the row is not currently being read by another request); subsequent hits for the same URL then take the cheap pre-extracted path. To let the cache-use lookup reach that logic, get_entry() now returns expired entries to callers passing delete_expired=0; such callers are responsible for checking entry->expire themselves. --- src/cache.c | 102 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 10 deletions(-) diff --git a/src/cache.c b/src/cache.c index 46f641103..bc3d3d43a 100644 --- a/src/cache.c +++ b/src/cache.c @@ -255,11 +255,11 @@ static inline void release_entry_locked(struct cache_tree *cache, struct cache_e static inline void release_entry_unlocked(struct cache_tree *cache, struct cache_entry *entry); /* - * Find a cache_entry in the 's tree that has the hash . - * If is 0 then the entry is left untouched if it is found but - * is already expired, and NULL is returned. Otherwise, the expired entry is - * removed from the tree and NULL is returned. - * Returns a valid (not expired) cache_tree pointer. + * Find a cache_entry in that has the hash . If + * is non-zero and the entry is expired, it is removed from + * the tree and NULL is returned. Otherwise the entry is returned as-is, + * including expired entries when is 0 - the caller is then + * responsible for inspecting entry->expire. * The returned entry is not retained, it should be explicitly retained only * when necessary. * @@ -281,12 +281,11 @@ struct cache_entry *get_entry(struct cache_tree *cache_tree, char *hash, int del if (memcmp(entry->hash, hash, sizeof(entry->hash))) return NULL; - if (entry->expire > date.tv_sec) { - return entry; - } else if (delete_expired) { + if (delete_expired && entry->expire <= date.tv_sec) { release_entry_locked(cache_tree, entry); + return NULL; } - return NULL; + return entry; } /* @@ -2438,6 +2437,46 @@ static int should_send_notmodified_response(struct cache *cache, struct htx *htx return retval; } +/* + * Emit an HTTP 103 Early Hints response built from the cached of + * length . The format matches what http_action_store_cache + * writes: concatenated records of {uint16_t length, char value[length]}, each + * value being a Link header value. Skipped for HTTP/1.0 clients. + */ +static void cache_emit_early_hints(struct stream *s, const char *hint_data, + unsigned int hint_data_len) +{ + struct htx *htx; + const char *p = hint_data; + const char *end = hint_data + hint_data_len; + + if (!(s->txn.http->req.flags & HTTP_MSGF_VER_11)) + return; + + htx = http_early_hint_start(s); + if (!htx) + goto error; + + while (p + sizeof(uint16_t) <= end) { + uint16_t vlen = read_u16(p); + + p += sizeof(vlen); + if (p + vlen > end) + break; + if (!htx_add_header(htx, ist("link"), ist2(p, vlen))) + goto error; + p += vlen; + } + + if (!http_early_hint_end(s)) + goto error; + return; + + error: + channel_htx_truncate(&s->res, htxbuf(&s->res.buf)); + s->txn.http->status = 0; +} + enum act_return http_action_req_cache_use(struct act_rule *rule, struct proxy *px, struct session *sess, struct stream *s, int flags) { @@ -2448,6 +2487,7 @@ enum act_return http_action_req_cache_use(struct act_rule *rule, struct proxy *p struct cache *cache = cconf->c.cache; struct shared_context *shctx = shctx_ptr(cache); struct shared_block *entry_block; + struct buffer *hint_buf = NULL; struct cache_tree *cache_tree = NULL; @@ -2497,16 +2537,58 @@ enum act_return http_action_req_cache_use(struct act_rule *rule, struct proxy *p entry_block = block_ptr(res); shctx_wrlock(shctx); - if ((res->flags & CACHE_EF_COMPLETE) && !(res->flags & CACHE_EF_STRIPPED)) { + if (res->expire > date.tv_sec && + (res->flags & CACHE_EF_COMPLETE) && !(res->flags & CACHE_EF_STRIPPED)) { shctx_row_detach(shctx, entry_block); detached = 1; } else { + if ((cache->flags & CACHE_CF_EARLY_HINTS) && + (res->flags & (CACHE_EF_STRIPPED | CACHE_EF_COMPLETE))) { + /* The emitted hints are best-effort and may not exactly + * match the response finally served: with Vary they may + * come from a different variant (resolved below), and a + * stripped entry keeps emitting after expiry until it is + * re-cached or evicted. This only affects which resources + * are preloaded, never the response itself, so it is + * accepted here. + * + * The trash chunk in hint_buf is consumed only after the + * unlocks below. As get_trash_chunk() rotates between two + * buffers, reusing it more than once before then would + * cycle back onto hint_buf and clobber it. + */ + hint_buf = get_trash_chunk(); + if (res->flags & CACHE_EF_STRIPPED) { + unsigned int len = + entry_block->len - sizeof(struct cache_entry); + + if (len > 0 && len <= b_size(hint_buf) && + shctx_row_data_get(shctx, entry_block, + (unsigned char *)b_orig(hint_buf), + sizeof(struct cache_entry), + len) == 0) + hint_buf->data = len; + + /* Refresh this entry's position in the hints LRU. */ + LIST_DELETE(&res->lru); + LIST_APPEND(&cache->hints_lru, &res->lru); + } else { + /* CACHE_EF_COMPLETE */ + cache_extract_hints(shctx, entry_block, hint_buf); + if (b_data(hint_buf) > 0 && entry_block->refcount == 0 && + cache->hints_blocks < CACHE_HINTS_CAP(cache)) + cache_strip_entry(shctx, res, hint_buf); + } + } release_entry(cache_tree, res, 0); res = NULL; } shctx_wrunlock(shctx); cache_rdunlock(cache_tree); + if (hint_buf && b_data(hint_buf) > 0) + cache_emit_early_hints(s, b_orig(hint_buf), b_data(hint_buf)); + /* In case of Vary, we could have multiple entries with the same * primary hash. We need to calculate the secondary hash in order * to find the actual entry we want (if it exists). */ -- 2.47.3