From a87c317931a14ddf47e3a027589d5911f4cebdd6 Mon Sep 17 00:00:00 2001 From: Auke Kok Date: Tue, 16 Jun 2026 21:55:03 -0700 Subject: [PATCH] Account for the pending freed-head rotation in the commit room gates The server commit deadlocks when its freed allocator list head block fills to near capacity. Both gates that decide whether a transaction has room measure it as free slots in the clean freed head block: - hold_commit() admits a holder only if scoutfs_alloc_meta_remaining() reports enough freed room (2 * COMMIT_HOLD_ALLOC_BUDGET slots). - empty_list()/fill_list()'s list_has_blocks() proceed only if the head has extent_mod_blocks() slots. The clean head is not what a transaction gets: the first dirtying allocation runs dirty_alloc_blocks(), which rotates in a fresh head block when the current head is under EMPTY_FREED_THRESH. A clean, nearly-full head has a full block's worth of room as soon as it's touched. With the gates refusing on the clean full head, no holder is admitted and the drains never start, so the rotation in dirty_alloc_blocks() is never reached. The server spins applying empty commits and the filesystem can't mount or recover (observed at ~11k empty commits/sec; freed head first_nr 8148 of an 8184 capacity). Fix the accounting in scoutfs_alloc_meta_remaining(): when the freed list isn't dirtied yet and the clean head is under EMPTY_FREED_THRESH, report the room the pending rotation will give, SCOUTFS_ALLOC_LIST_MAX_BLOCKS - 2 (a fresh block, less the old avail and freed head blocks the rotation frees into it). list_has_blocks() routes through the same function so fill_list()/empty_list() use identical accounting; otherwise an avail-low, freed-full commit could still wedge because fill_list() couldn't refill avail past the clean full freed head. hold_commit() then admits a holder (or a drain starts) and the first allocation rotates the full head. The avail gate and the meta_low() loop-stop are left conservative, so genuine ENOSPC still fails and freeing loops still commit before overflowing a head. Signed-off-by: Auke Kok --- kmod/src/alloc.c | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/kmod/src/alloc.c b/kmod/src/alloc.c index 0ceaf3b8..f6a5be0c 100644 --- a/kmod/src/alloc.c +++ b/kmod/src/alloc.c @@ -1150,13 +1150,20 @@ static bool list_has_blocks(struct super_block *sb, struct scoutfs_alloc *alloc, { u32 tree_blocks = extent_mod_blocks(root->root.height) * extents; u32 most = 1 + tree_blocks + addl_blocks; + u32 avail; + u32 freed; - if (le32_to_cpu(alloc->avail.first_nr) < most) { + /* use the same room accounting as the commit hold gate, including the + * pending freed-head rotation, so a clean nearly-full freed head can't + * stop fill_list()/empty_list() from making progress */ + scoutfs_alloc_meta_remaining(alloc, &avail, &freed); + + if (avail < most) { scoutfs_inc_counter(sb, alloc_list_avail_lo); return false; } - if (list_block_space(alloc->freed.first_nr) < most) { + if (freed < most) { scoutfs_inc_counter(sb, alloc_list_freed_hi); return false; } @@ -1384,14 +1391,35 @@ bool scoutfs_alloc_meta_low(struct super_block *sb, return lo; } +/* + * Report the metadata allocator room a transaction will actually have. + * + * If the freed list hasn't been dirtied yet, the first dirtying allocation + * rotates in a fresh head block when the current head is under + * EMPTY_FREED_THRESH (see dirty_alloc_blocks()). A clean but nearly-full + * head then has a fresh block's worth of room as soon as it's touched, so + * report that; otherwise the commit hold gate and the fill/empty drains read + * it as no room and refuse to make progress. The rotation frees the old + * avail and freed head blocks into the fresh block, so the room it leaves is + * MAX - 2. + * + * dirty_freed_bl isn't covered by the seqlock, but it only transitions + * NULL->set on a transaction's first allocation and back to NULL at + * prepare_commit; a stale read predicts the rotation one allocation early or + * late, which still gates correctly. + */ void scoutfs_alloc_meta_remaining(struct scoutfs_alloc *alloc, u32 *avail_total, u32 *freed_space) { unsigned int seq; + u32 fr; do { seq = read_seqbegin(&alloc->seqlock); *avail_total = le32_to_cpu(alloc->avail.first_nr); - *freed_space = list_block_space(alloc->freed.first_nr); + fr = list_block_space(alloc->freed.first_nr); + if (!alloc->dirty_freed_bl && fr < EMPTY_FREED_THRESH) + fr = SCOUTFS_ALLOC_LIST_MAX_BLOCKS - 2; + *freed_space = fr; } while (read_seqretry(&alloc->seqlock, seq)); }