From: Maxime Henrion Date: Fri, 26 Jun 2026 20:40:26 +0000 (-0400) Subject: MINOR: shctx: allow consumers to customize eviction strategy X-Git-Url: http://git.kaiwu.me/web/doc/static/gitweb.js?a=commitdiff_plain;h=731b71363f51c03b4517059dcb3945f667a82042;p=haproxy.git MINOR: shctx: allow consumers to customize eviction strategy This introduces a make_room() callback that consumers can provide in order to implement more specific eviction strategies. To be used in the HTTP cache code. This goes in hand with a new shctx_row_truncate() function that allows consumers to truncate a row, which can be used in the make_room() callback. --- diff --git a/include/haproxy/shctx-t.h b/include/haproxy/shctx-t.h index 493024af8..835475c0c 100644 --- a/include/haproxy/shctx-t.h +++ b/include/haproxy/shctx-t.h @@ -50,6 +50,15 @@ struct shared_context { struct list avail; /* list for active and free blocks */ unsigned int nbav; /* number of available blocks */ unsigned int max_obj_size; /* maximum object size (in bytes). */ + /* Optional callback invoked when shctx is about to take a cold row's + * blocks. The consumer is expected to free blocks using + * shctx_row_truncate(). It must return 1 only if it freed at least one + * block, or 0 otherwise: shctx_row_reserve_hot() re-invokes it as long + * as it returns 1, so returning 1 without freeing anything loops + * forever under the write lock. Called with the shctx write lock held. + * If not specified, the default eviction strategy is used. + */ + int (*make_room)(struct shared_context *shctx); void (*free_block)(struct shared_block *first, void *data); void (*reserve_finish)(struct shared_context *shctx); void *cb_data; diff --git a/include/haproxy/shctx.h b/include/haproxy/shctx.h index 01bb09d32..c6ef675db 100644 --- a/include/haproxy/shctx.h +++ b/include/haproxy/shctx.h @@ -24,6 +24,8 @@ int shctx_init(struct shared_context **orig_shctx, int extra, __maybe_unused const char *name); struct shared_block *shctx_row_reserve_hot(struct shared_context *shctx, struct shared_block *last, int data_len); +void shctx_row_truncate(struct shared_context *shctx, struct shared_block *first, + int new_len); void shctx_row_detach(struct shared_context *shctx, struct shared_block *first); void shctx_row_reattach(struct shared_context *shctx, struct shared_block *first); int shctx_row_data_append(struct shared_context *shctx, diff --git a/src/shctx.c b/src/shctx.c index 2d443133e..10940cb43 100644 --- a/src/shctx.c +++ b/src/shctx.c @@ -28,7 +28,7 @@ struct shared_block *shctx_row_reserve_hot(struct shared_context *shctx, struct shared_block *first, int data_len) { - struct shared_block *last = NULL, *block, *sblock; + struct shared_block *last = NULL, *block; struct shared_block *ret = first; int remain = 1; @@ -76,7 +76,19 @@ struct shared_block *shctx_row_reserve_hot(struct shared_context *shctx, goto out; } - list_for_each_entry_safe(block, sblock, &shctx->avail, list) { + while (data_len > 0) { + block = LIST_NEXT(&shctx->avail, struct shared_block *, list); + if (&block->list == &shctx->avail) + break; + + /* Head of avail is a cold row: give the consumer a chance to + * free room first. If make_room succeeds, freed blocks are + * now at the head of avail and we re-read the head next loop. + */ + if (block->len && shctx->make_room) { + if (shctx->make_room(shctx)) + continue; + } /* release callback */ if (block->len && shctx->free_block) @@ -98,13 +110,9 @@ struct shared_block *shctx_row_reserve_hot(struct shared_context *shctx, } ++ret->block_count; + ret->last_reserved = block; data_len -= shctx->block_size; - - if (data_len <= 0) { - ret->last_reserved = block; - break; - } } shctx_wrunlock(shctx); @@ -116,6 +124,70 @@ out: return ret; } +/* + * Truncate a cold row to bytes of content. The row keeps the minimum + * number of blocks needed to hold bytes; tail blocks beyond that + * become free blocks. If is 0, the row no longer exists and all of + * its blocks become free. Freed blocks are moved to the head of the avail + * list so the next reservation finds them immediately. + * + * The row must be cold (refcount 0, thus in the avail list) and the shctx + * write lock must be held by the caller. + */ +void shctx_row_truncate(struct shared_context *shctx, struct shared_block *first, + int new_len) +{ + struct shared_block *new_last = first; + struct shared_block *freed_first = NULL; + int new_block_count = 1; + int i; + + BUG_ON(new_len < 0 || new_len > first->len); + BUG_ON(first->refcount > 0); + + if (new_len == 0) { + if (shctx->free_block) + shctx->free_block(first, shctx->cb_data); + freed_first = first; + } else { + new_block_count = (new_len + shctx->block_size - 1) / shctx->block_size; + for (i = 1; i < new_block_count; i++) + new_last = LIST_NEXT(&new_last->list, struct shared_block *, list); + + if (new_block_count < first->block_count) + freed_first = LIST_NEXT(&new_last->list, + struct shared_block *, list); + } + + if (freed_first) { + /* Move the freed chain freed_first..first->last_reserved to the + * head of avail. + */ + struct shared_block *freed_last = first->last_reserved; + struct list *prev = freed_first->list.p; + struct list *next = freed_last->list.n; + + prev->n = next; + next->p = prev; + freed_first->list.p = &shctx->avail; + freed_last->list.n = shctx->avail.n; + shctx->avail.n->p = &freed_last->list; + shctx->avail.n = &freed_first->list; + } + + first->len = new_len; + first->block_count = new_block_count; + first->last_reserved = new_last; + + /* Appending must resume in the partially filled tail block. When + * is a multiple of the block size (or 0) there is no such + * block; a NULL is correct then because the next + * shctx_row_reserve_hot() call will set it to the first newly + * reserved block (see its "!remain" handling). + */ + first->last_append = (new_len % shctx->block_size) ? new_last : NULL; +} + /* * if the refcount is 0 move the row to the hot list. Increment the refcount */