]> git.kaiwu.me - haproxy.git/commitdiff
MINOR: shctx: allow consumers to customize eviction strategy
authorMaxime Henrion <mhenrion@haproxy.com>
Fri, 26 Jun 2026 20:40:26 +0000 (16:40 -0400)
committerWilly Tarreau <w@1wt.eu>
Fri, 10 Jul 2026 14:42:35 +0000 (16:42 +0200)
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.

include/haproxy/shctx-t.h
include/haproxy/shctx.h
src/shctx.c

index 493024af81cdf0509f0c37be728bb146606cf8a3..835475c0cb6f134384afbd81763ac9588aac8e4e 100644 (file)
@@ -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;
index 01bb09d3274d0c4b186bef90980e52b9e076133b..c6ef675db2ecc33b30d97d9e3e4e2a154a1f3cea 100644 (file)
@@ -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,
index 2d443133e319eb1c325a50cca3dc91753289463f..10940cb4383f8ad80762f42a4d07fc487b4e06f2 100644 (file)
@@ -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 <new_len> bytes of content. The row keeps the minimum
+ * number of blocks needed to hold <new_len> bytes; tail blocks beyond that
+ * become free blocks. If <new_len> 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
+        * <new_len> is a multiple of the block size (or 0) there is no such
+        * block; a NULL <last_append> 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
  */