mirror of
https://github.com/haproxy/haproxy.git
synced 2026-07-14 11:35:16 -04:00
MEDIUM: cache: early hints-aware eviction code
We now use the make_room callback from shctx to customize the eviction procedure. We attempt to balance eviction between hints and full-blown entries by capping the number of blocks used by hints entries to 25% of the size of the cache. Upon eviction of a full entry, we first try to strip it to a hints-only entry if possible. A Link header's value (which may carry multiple comma-separated links) is limited to 1024 bytes for hint extraction. Longer values are skipped entirely and contribute no hint records.
This commit is contained in:
parent
b089325f0e
commit
77c4de1ec2
1 changed files with 206 additions and 1 deletions
207
src/cache.c
207
src/cache.c
|
|
@ -49,6 +49,9 @@
|
|||
#define CACHE_CF_VARY_PROCESSING 0x00000001 /* manage Vary header (disabled by default) */
|
||||
#define CACHE_CF_EARLY_HINTS 0x00000002 /* enable HTTP 103 Early Hints (disabled by default) */
|
||||
|
||||
/* Soft cap on the number of cache blocks that may be held by hints entries. */
|
||||
#define CACHE_HINTS_CAP(cache) ((cache)->maxblocks / 4)
|
||||
|
||||
static uint64_t cache_hash_seed = 0;
|
||||
|
||||
const char *cache_store_flt_id = "cache store filter";
|
||||
|
|
@ -237,6 +240,9 @@ struct cache_entry {
|
|||
#define CACHE_BLOCKSIZE 1024
|
||||
#define CACHE_ENTRY_MAX_AGE 2147483648U
|
||||
|
||||
/* Maximum size of a single Link header value for hint extraction. */
|
||||
#define CACHE_MAX_HINT_LINK_VAL 1024
|
||||
|
||||
static struct list caches = LIST_HEAD_INIT(caches);
|
||||
static struct list caches_config = LIST_HEAD_INIT(caches_config); /* cache config to init */
|
||||
static struct cache *tmp_cache_config = NULL;
|
||||
|
|
@ -1079,7 +1085,7 @@ static int rel_is_hint(const struct ist rel)
|
|||
* Returns true if the value of the Link header contains at least one rel attribute
|
||||
* worth sending in a 103 Early Hint response.
|
||||
*/
|
||||
static int __maybe_unused link_is_hint(struct ist val)
|
||||
static int link_is_hint(struct ist val)
|
||||
{
|
||||
const char *p = istptr(val), *end = istend(val);
|
||||
struct ist params, pname, pval;
|
||||
|
|
@ -1158,6 +1164,204 @@ static void cache_free_blocks(struct shared_block *first, void *data)
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Walk the HTX header blocks and accumulate Link values relevant for early
|
||||
* hints into <hint_buf>. Returns the number of bytes written to <hint_buf>.
|
||||
*/
|
||||
static int cache_extract_hints(struct shared_context *shctx,
|
||||
struct shared_block *first,
|
||||
struct buffer *hint_buf)
|
||||
{
|
||||
unsigned int offset = sizeof(struct cache_entry);
|
||||
char value_buf[CACHE_MAX_HINT_LINK_VAL];
|
||||
|
||||
while (offset + sizeof(uint32_t) <= first->len) {
|
||||
uint32_t info;
|
||||
enum htx_blk_type type;
|
||||
size_t name_len, value_len;
|
||||
|
||||
if (shctx_row_data_get(shctx, first, (unsigned char *)&info,
|
||||
offset, sizeof(info)) != 0)
|
||||
break;
|
||||
type = __htx_blkinfo_type(info);
|
||||
if (type == HTX_BLK_EOH)
|
||||
break;
|
||||
if (type != HTX_BLK_HDR) {
|
||||
offset += sizeof(info) + (info & 0xfffffff);
|
||||
continue;
|
||||
}
|
||||
|
||||
name_len = info & 0xff;
|
||||
value_len = (info >> 8) & 0xfffff;
|
||||
if (offset + sizeof(info) + name_len + value_len > first->len)
|
||||
break;
|
||||
/* value_buf linearizes the value out of the row; the HTX
|
||||
* extractor reads it in place and needs no such cap.
|
||||
*/
|
||||
if (value_len <= sizeof(value_buf) && name_len == 4) {
|
||||
char name[4];
|
||||
|
||||
shctx_row_data_get(shctx, first, (unsigned char *)name,
|
||||
offset + sizeof(info), 4);
|
||||
if (memcmp(name, "link", 4) == 0) {
|
||||
struct ist iter, lv;
|
||||
size_t hdr_start = b_data(hint_buf);
|
||||
uint16_t hdr_len = 0;
|
||||
|
||||
shctx_row_data_get(shctx, first,
|
||||
(unsigned char *)value_buf,
|
||||
offset + sizeof(info) + name_len,
|
||||
value_len);
|
||||
iter = ist2(value_buf, value_len);
|
||||
|
||||
if (b_data(hint_buf) + sizeof(hdr_len) > b_size(hint_buf))
|
||||
break;
|
||||
hint_buf->data += sizeof(hdr_len);
|
||||
|
||||
while (http_next_hdr_value(&iter, &lv)) {
|
||||
size_t needed = lv.len;
|
||||
|
||||
if (!link_is_hint(lv))
|
||||
continue;
|
||||
if (hdr_len > 0)
|
||||
needed += 2;
|
||||
if (hdr_len + needed > UINT16_MAX)
|
||||
continue;
|
||||
if (b_data(hint_buf) + needed > b_size(hint_buf))
|
||||
continue;
|
||||
|
||||
if (hdr_len > 0) {
|
||||
chunk_memcat(hint_buf, ", ", 2);
|
||||
hdr_len += 2;
|
||||
}
|
||||
chunk_memcat(hint_buf, lv.ptr, lv.len);
|
||||
hdr_len += lv.len;
|
||||
}
|
||||
|
||||
if (hdr_len == 0)
|
||||
hint_buf->data -= sizeof(hdr_len);
|
||||
else
|
||||
memcpy(b_orig(hint_buf) + hdr_start,
|
||||
&hdr_len, sizeof(hdr_len));
|
||||
}
|
||||
}
|
||||
offset += sizeof(info) + name_len + value_len;
|
||||
}
|
||||
|
||||
return b_data(hint_buf);
|
||||
}
|
||||
|
||||
/*
|
||||
* Strip a full cache entry in place: persist <hint_buf>'s content past
|
||||
* sizeof(cache_entry), truncate the row to match, mark the entry as
|
||||
* CACHE_EF_STRIPPED, and move it to the hints LRU. Must be called under
|
||||
* the shctx wrlock and with the row's refcount at 0.
|
||||
*
|
||||
* The extracted hint records may end up larger than the original entry
|
||||
* (each value is re-joined with ", " and carries a length prefix), so the
|
||||
* stripped layout is not guaranteed to be smaller. Only strip when it frees
|
||||
* at least one block: this makes room, keeps the copy below within the row,
|
||||
* and guarantees forward progress for the make_room() caller. Returns 1 if
|
||||
* the entry was stripped, 0 if it was left untouched.
|
||||
*/
|
||||
static int cache_strip_entry(struct shared_context *shctx,
|
||||
struct cache_entry *entry,
|
||||
const struct buffer *hint_buf)
|
||||
{
|
||||
struct cache *cache = (struct cache *)shctx->data;
|
||||
struct shared_block *first = block_ptr(entry);
|
||||
struct shared_block *block = first;
|
||||
const char *src = b_head(hint_buf);
|
||||
unsigned int new_len = sizeof(struct cache_entry) + b_data(hint_buf);
|
||||
unsigned int off = sizeof(struct cache_entry);
|
||||
unsigned int left = b_data(hint_buf);
|
||||
|
||||
if ((new_len + shctx->block_size - 1) / shctx->block_size >= first->block_count)
|
||||
return 0;
|
||||
|
||||
while (off >= shctx->block_size) {
|
||||
block = LIST_NEXT(&block->list, struct shared_block *, list);
|
||||
off -= shctx->block_size;
|
||||
}
|
||||
|
||||
while (left > 0) {
|
||||
unsigned int chunk = shctx->block_size - off;
|
||||
|
||||
if (chunk > left)
|
||||
chunk = left;
|
||||
memcpy(block->data + off, src, chunk);
|
||||
src += chunk;
|
||||
left -= chunk;
|
||||
off = 0;
|
||||
block = LIST_NEXT(&block->list, struct shared_block *, list);
|
||||
}
|
||||
|
||||
shctx_row_truncate(shctx, first, new_len);
|
||||
entry->flags |= CACHE_EF_STRIPPED;
|
||||
LIST_DELETE(&entry->lru);
|
||||
LIST_APPEND(&cache->hints_lru, &entry->lru);
|
||||
cache->hints_blocks += first->block_count;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Free one entry's blocks. If the hints pool is at or above the limit
|
||||
* (set to 25%), first try to evict the oldest hint entry. Otherwise,
|
||||
* pop the oldest full entry and try to strip it. If the entry doesn't
|
||||
* contain the relevant Link headers, or if we are past the limit for
|
||||
* hints blocks, evict it entirely.
|
||||
* Returns 1 on success, 0 if nothing could be freed.
|
||||
*/
|
||||
static int cache_make_room(struct shared_context *shctx)
|
||||
{
|
||||
struct cache *cache = (struct cache *)shctx->data;
|
||||
struct cache_entry *entry, *back;
|
||||
int can_strip = (cache->hints_blocks < CACHE_HINTS_CAP(cache));
|
||||
|
||||
if (!can_strip) {
|
||||
list_for_each_entry_safe(entry, back, &cache->hints_lru, lru) {
|
||||
struct shared_block *first = block_ptr(entry);
|
||||
|
||||
if (first->refcount == 0) {
|
||||
shctx_row_truncate(shctx, first, 0);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list_for_each_entry_safe(entry, back, &cache->full_lru, lru) {
|
||||
struct shared_block *first = block_ptr(entry);
|
||||
struct buffer *hint_buf = NULL;
|
||||
int hint_len = 0;
|
||||
|
||||
if (first->refcount > 0)
|
||||
continue;
|
||||
|
||||
/* A key of 0 means the entry is no longer in the tree; its
|
||||
* blocks are dead and can be reclaimed right away.
|
||||
*/
|
||||
if (!entry->eb.key) {
|
||||
shctx_row_truncate(shctx, first, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (can_strip) {
|
||||
hint_buf = get_trash_chunk();
|
||||
hint_len = cache_extract_hints(shctx, first, hint_buf);
|
||||
}
|
||||
|
||||
/* Strip the entry to keep its hints; fall back to evicting it
|
||||
* whole if it has no hints or stripping would free no block.
|
||||
*/
|
||||
if (hint_len <= 0 || !cache_strip_entry(shctx, entry, hint_buf))
|
||||
shctx_row_truncate(shctx, first, 0);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void cache_reserve_finish(struct shared_context *shctx)
|
||||
{
|
||||
struct cache_entry *object, *back;
|
||||
|
|
@ -2661,6 +2865,7 @@ int post_check_cache()
|
|||
}
|
||||
shctx->free_block = cache_free_blocks;
|
||||
shctx->reserve_finish = cache_reserve_finish;
|
||||
shctx->make_room = cache_make_room;
|
||||
shctx->cb_data = (void*)shctx->data;
|
||||
/* the cache structure is stored in the shctx and added to the
|
||||
* caches list, we can remove the entry from the caches_config
|
||||
|
|
|
|||
Loading…
Reference in a new issue