From: Zygo Blaxell Date: Sun, 9 Aug 2026 22:05:44 +0000 (-0400) Subject: btrfs: stripe_alloc: count claimable whole-stripe supply directly X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b63bb6dfaae33e419d09e42b5b41bf7905e3bbea;p=linux btrfs: stripe_alloc: count claimable whole-stripe supply directly Data reservations are admitted against arithmetic -- free space minus trapped fragments minus open-run remainders minus a per-extent margin -- and under sustained near-full churn the arithmetic and the claim rule disagree for long enough that admitted buffered writes reach writeback with nothing claimable left: measured, ~5.2-5.5k reserved writebacks dropped per fsstress churn run, silently for anything not waiting on fsync. Stock refuses the same write()s up front. Nothing between "durable by fsync" and "refused by write()" is acceptable when an operator accident fills the disk. First step, accounting only: measure the constraint instead of deriving it. bg->stripe_claimable counts bytes of fully free whole stripes -- exactly what btrfs_claim_free_stripe_run() can take, the complement of stripe_unusable within each stripe. The commit rescan derives it from the same pass that computes trapped bytes (free minus trapped) and is the sole upward correction; incremental maintenance under ctl->tree_lock only ever DEBITS -- removals round their decrement OUT to every touched stripe, claims subtract exactly what they took. The counter can therefore only under-count between commits, never over-count. Crediting freed whole stripes incrementally on the add side is deliberately not done, having been tried and dropped. Several add paths re-add free space that a low-level remove never de-credited -- the async discard trim (unlink_free_space / bitmap_clear_bits, then do_trimming's re-add through __btrfs_add_free_space) and btrfs_remove_free_space's middle-split tail re-add -- so crediting on add double-counts and drives the counter ABOVE the authoritative scan. That is the dangerous direction: once admission gates on this counter, an over-count admits reservations against phantom supply that writeback then drops, while an under-count is an early, clean write()-time ENOSPC. Debit-only makes over-counting structurally impossible, whatever a future re-add path forgets to de-credit. (Reproduced with compress+autodefrag+discard=async on a legacy-converted raid5 filesystem, incremental ~= 2x scanned; credit_return attribution confirmed the phantom entered through do_trimming's re-add, not the open-stripe allocator returns.) A WARN_RATELIMIT at the rescan catches the dangerous direction anyway (incremental above scanned), which under a debit-only rule means a missed consumption site, and localizes it; the clamp to the scanned value keeps every rescan authoritative regardless. Rate limited rather than _ONCE because such a gap recurs on every commit, and reporting only the first hit hides that it is ongoing. The space_info aggregate bytes_stripe_claimable follows the stripe_unusable pattern (incremental between commits, re-totaled from armed groups at the rescan), shows in the ENOSPC dump and in sysfs. No admission change yet; that comes once the counter proves accurate under the fill, churn, balance and reclaim suites. The by-size fast-fail in the claim keeps its early exit: the point of measuring is to make such inputs trustworthy, not to search harder around them. Data block groups only for now; raid56 metadata and mixed block groups join when the data counter has settled. Assisted-by: Claude:claude-fable-5 --- diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index 1f4e9709c4b10..8e15181d99b3d 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -2279,6 +2279,7 @@ void btrfs_scan_stripe_unusable(struct btrfs_fs_info *fs_info, bool force) list_for_each_entry(sinfo, &fs_info->space_info, list) { u64 total = 0; + u64 total_claimable = 0; int raid; if (!(sinfo->flags & BTRFS_BLOCK_GROUP_DATA)) @@ -2310,14 +2311,18 @@ void btrfs_scan_stripe_unusable(struct btrfs_fs_info *fs_info, bool force) * groups are excluded; their free space is already * accounted as read-only. */ - if (READ_ONCE(bg->stripe_unusable_ready)) + if (READ_ONCE(bg->stripe_unusable_ready)) { total += READ_ONCE(bg->stripe_unusable); + total_claimable += + READ_ONCE(bg->stripe_claimable); + } } } up_read(&sinfo->groups_sem); spin_lock(&sinfo->lock); sinfo->bytes_stripe_unusable = total; + sinfo->bytes_stripe_claimable = total_claimable; /* * The rescan usually lowers the counter (mid-transaction * incremental adds overcount conservatively); admission diff --git a/fs/btrfs/block-group.h b/fs/btrfs/block-group.h index 0041f5132764f..38c14cbd08acf 100644 --- a/fs/btrfs/block-group.h +++ b/fs/btrfs/block-group.h @@ -284,6 +284,14 @@ struct btrfs_block_group { * (honest f_bavail); the reservation layer is deliberately unchanged. */ u64 stripe_unusable; + /* + * Directly measured claimable supply: bytes of fully free, aligned + * whole stripes in the free space cache -- exactly what + * btrfs_claim_free_stripe_run() can take. Maintained incrementally + * under ctl->tree_lock by rules that only ever err LOW (early + * write()-time ENOSPC), corrected upward by the commit rescan. + */ + u64 stripe_claimable; bool stripe_unusable_ready; /* diff --git a/fs/btrfs/free-space-cache.c b/fs/btrfs/free-space-cache.c index 265aa66469363..c6011fdbfccd0 100644 --- a/fs/btrfs/free-space-cache.c +++ b/fs/btrfs/free-space-cache.c @@ -2623,6 +2623,10 @@ static void steal_from_bitmap(struct btrfs_free_space_ctl *ctl, } } +static void stripe_claimable_mod(struct btrfs_block_group *bg, s64 delta); +static u64 stripe_touched_bytes(const struct btrfs_block_group *bg, + u64 bytenr, u64 size); + static int __btrfs_add_free_space(struct btrfs_block_group *block_group, u64 offset, u64 bytes, enum btrfs_trim_state trim_state) @@ -2677,6 +2681,19 @@ link: if (ret) kmem_cache_free(btrfs_free_space_cachep, info); out: + /* + * Do NOT incrementally credit newly-freed whole stripes here. Add paths + * re-add space that a low-level remove never de-credited -- the async + * discard trim (unlink_free_space / bitmap_clear_bits + do_trimming's + * re-add) and btrfs_remove_free_space's middle-split tail -- so crediting + * on add double-counts and stripe_claimable drifts ABOVE the authoritative + * scan. That is the dangerous direction: once admission gates on the + * counter, an over-count admits reservations against phantom supply that + * writeback then drops. The incremental rules now only ever DEBIT + * (removes/claims, which err low); the per-commit rescan is the sole + * upward correction, so the counter can only under-count (early, clean + * write()-time ENOSPC) -- never over-count. + */ btrfs_discard_update_discardable(block_group); spin_unlock(&ctl->tree_lock); @@ -2756,6 +2773,48 @@ static int __btrfs_add_free_space_zoned(struct btrfs_block_group *block_group, static void stripe_unusable_mark_dirty(struct btrfs_block_group *bg); +/* + * stripe_alloc: keep the directly measured claimable whole-stripe supply + * in step with free space mutations. Positive deltas come from returned + * ranges' interior whole stripes, negative from claims and removals. The + * blind rules only ever err by counting claimable LOW (early write()-time + * ENOSPC, corrected upward by the commit rescan), never HIGH: a + * reservation admitted against phantom supply fails at writeback and + * drops buffered data. Caller holds ctl->tree_lock. + */ +static void stripe_claimable_mod(struct btrfs_block_group *bg, s64 delta) +{ + struct btrfs_space_info *sinfo = bg->space_info; + + if (!READ_ONCE(bg->stripe_unusable_ready) || !delta) + return; + if (delta < 0 && bg->stripe_claimable < (u64)-delta) + delta = -(s64)bg->stripe_claimable; + if (!delta) + return; + bg->stripe_claimable += delta; + if (!sinfo) + return; + spin_lock(&sinfo->lock); + if (delta < 0 && sinfo->bytes_stripe_claimable < (u64)-delta) + sinfo->bytes_stripe_claimable = 0; + else + btrfs_space_info_update_bytes_stripe_claimable(sinfo, delta); + spin_unlock(&sinfo->lock); +} + +/* The whole-stripe span [bytenr, bytenr + size) touches at all. */ +static u64 stripe_touched_bytes(const struct btrfs_block_group *bg, + u64 bytenr, u64 size) +{ + const u64 fsl = bg->full_stripe_len; + const u64 rel = bytenr - bg->start; + const u64 astart = div64_u64(rel, fsl) * fsl; + const u64 aend = div64_u64(rel + size + fsl - 1, fsl) * fsl; + + return aend - astart; +} + /* * stripe_alloc: count the sub-stripe fragments of a returning free range * as trapped immediately, without waiting for the commit-time rescan. @@ -2879,6 +2938,19 @@ int btrfs_remove_free_space(struct btrfs_block_group *block_group, spin_lock(&ctl->tree_lock); + /* + * The removed bytes leave free space; every whole stripe the range + * touches may lose wholeness (a boundary fragment removal breaks a + * fully free stripe as surely as an interior one empties it). + * Rounding the decrement OUT to touched stripes errs claimable-low. + * The trapped counter is deliberately not decremented for removed + * fragments: leaving it high is the safe direction, and the rescan + * settles both. + */ + if (btrfs_is_stripe_alloc_bg(block_group)) + stripe_claimable_mod(block_group, + -(s64)stripe_touched_bytes(block_group, offset, bytes)); + again: ret = 0; if (!bytes) @@ -3293,7 +3365,7 @@ static u64 stripe_unusable_scan(struct btrfs_block_group *bg, u32 *freep, void btrfs_block_group_init_stripe_unusable(struct btrfs_block_group *bg) { struct btrfs_free_space_ctl *ctl = bg->free_space_ctl; - u64 trapped, nstripes; + u64 trapped, claimable, nstripes; u32 *freep; if (!btrfs_is_stripe_alloc_bg(bg)) @@ -3314,14 +3386,19 @@ void btrfs_block_group_init_stripe_unusable(struct btrfs_block_group *bg) } trapped = stripe_unusable_scan(bg, freep, nstripes); bg->stripe_unusable = trapped; + /* Every free byte is claimable or trapped; the scan split them. */ + bg->stripe_claimable = ctl->free_space - trapped; + claimable = bg->stripe_claimable; bg->stripe_unusable_ready = true; spin_unlock(&ctl->tree_lock); kvfree(freep); - if (trapped) { + if (trapped || claimable) { spin_lock(&bg->space_info->lock); btrfs_space_info_update_bytes_stripe_unusable(bg->space_info, trapped); + btrfs_space_info_update_bytes_stripe_claimable(bg->space_info, + claimable); spin_unlock(&bg->space_info->lock); } } @@ -3338,6 +3415,7 @@ void btrfs_block_group_disarm_stripe_unusable(struct btrfs_block_group *bg) { struct btrfs_free_space_ctl *ctl = bg->free_space_ctl; u64 trapped; + u64 claimable; if (!btrfs_is_stripe_alloc_bg(bg)) return; @@ -3348,13 +3426,17 @@ void btrfs_block_group_disarm_stripe_unusable(struct btrfs_block_group *bg) return; } trapped = bg->stripe_unusable; + claimable = bg->stripe_claimable; + bg->stripe_claimable = 0; bg->stripe_unusable_ready = false; spin_unlock(&ctl->tree_lock); - if (trapped) { + if (trapped || claimable) { spin_lock(&bg->space_info->lock); btrfs_space_info_update_bytes_stripe_unusable(bg->space_info, -(s64)trapped); + btrfs_space_info_update_bytes_stripe_claimable(bg->space_info, + -(s64)claimable); spin_unlock(&bg->space_info->lock); } } @@ -3382,8 +3464,27 @@ void btrfs_block_group_rescan_stripe_unusable(struct btrfs_block_group *bg) return; spin_lock(&ctl->tree_lock); - if (bg->stripe_unusable_ready) /* skip if disarmed (read-only) */ + if (bg->stripe_unusable_ready) { /* skip if disarmed (read-only) */ + u64 scanned; + bg->stripe_unusable = stripe_unusable_scan(bg, freep, nstripes); + scanned = ctl->free_space - bg->stripe_unusable; + /* + * The incremental rules may only under-count claimable + * supply; counting more than the scan finds means a missed + * consumption site, the direction that admits reservations + * writeback cannot honor. Rate-limited rather than _ONCE: a + * persistent gap -- e.g. a filesystem carrying extents from the + * legacy allocator, a layout the incremental rules were not + * shaped for -- recurs every commit, and hiding all but the + * first hit loses that signal in the field. The clamp below + * keeps each rescan authoritative regardless. + */ + WARN_RATELIMIT(bg->stripe_claimable > scanned, + "bg %llu incremental stripe_claimable %llu above scanned %llu", + bg->start, bg->stripe_claimable, scanned); + bg->stripe_claimable = scanned; + } spin_unlock(&ctl->tree_lock); kvfree(freep); /* @@ -3822,6 +3923,7 @@ int btrfs_claim_free_stripe_run(struct btrfs_block_group *block_group, goto out; spare_used = claim_stripe_run_pieces(block_group, run_start, run_len, spare); + stripe_claimable_mod(block_group, -(s64)run_len); *start = run_start; *len = run_len; ret = 0; @@ -3852,6 +3954,7 @@ int btrfs_claim_free_stripe_run(struct btrfs_block_group *block_group, kmem_cache_free(btrfs_free_space_cachep, entry); } } + stripe_claimable_mod(block_group, -(s64)run_len); *start = run_start; *len = run_len; ret = 0; diff --git a/fs/btrfs/space-info.c b/fs/btrfs/space-info.c index bf3f9c12042d6..570d45556b5e0 100644 --- a/fs/btrfs/space-info.c +++ b/fs/btrfs/space-info.c @@ -678,11 +678,12 @@ static void __btrfs_dump_space_info(const struct btrfs_fs_info *fs_info, (s64)(info->total_bytes - btrfs_space_info_used(info, true)), info->full ? "" : "not "); btrfs_info(fs_info, -"space_info total=%llu, used=%llu, pinned=%llu, reserved=%llu, may_use=%llu, readonly=%llu zone_unusable=%llu stripe_unusable=%llu stripe_open=%llu stripe_margin=%llu", +"space_info total=%llu, used=%llu, pinned=%llu, reserved=%llu, may_use=%llu, readonly=%llu zone_unusable=%llu stripe_unusable=%llu stripe_open=%llu stripe_margin=%llu stripe_claimable=%llu", info->total_bytes, info->bytes_used, info->bytes_pinned, info->bytes_reserved, info->bytes_may_use, info->bytes_readonly, info->bytes_zone_unusable, info->bytes_stripe_unusable, info->bytes_stripe_open, + info->bytes_stripe_claimable, info->bytes_stripe_margin); } diff --git a/fs/btrfs/space-info.h b/fs/btrfs/space-info.h index a626edc963b5b..ae85c4208b81d 100644 --- a/fs/btrfs/space-info.h +++ b/fs/btrfs/space-info.h @@ -133,6 +133,10 @@ struct btrfs_space_info { cannot admit writes against it, and subtracted in statfs (from a different base -- the free space cache walk). */ + u64 bytes_stripe_claimable; /* sum of armed groups' directly + measured claimable whole-stripe + bytes; see stripe_claimable in + struct btrfs_block_group */ u64 bytes_stripe_open; /* sum of open stripe runs' unallocated remainders. Claimed out of the free space cache but not yet allocated; @@ -292,6 +296,7 @@ DECLARE_SPACE_INFO_UPDATE(bytes_may_use, "space_info"); DECLARE_SPACE_INFO_UPDATE(bytes_pinned, "pinned"); DECLARE_SPACE_INFO_UPDATE(bytes_zone_unusable, "zone_unusable"); DECLARE_SPACE_INFO_UPDATE(bytes_stripe_unusable, "stripe_unusable"); +DECLARE_SPACE_INFO_UPDATE(bytes_stripe_claimable, "stripe_claimable"); struct btrfs_inode; diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index cd511f4556e5a..bf38732b4c650 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -928,6 +928,7 @@ SPACE_INFO_ATTR(bytes_may_use); SPACE_INFO_ATTR(bytes_readonly); SPACE_INFO_ATTR(bytes_zone_unusable); SPACE_INFO_ATTR(bytes_stripe_unusable); +SPACE_INFO_ATTR(bytes_stripe_claimable); SPACE_INFO_ATTR(bytes_stripe_open); SPACE_INFO_ATTR(bytes_stripe_margin); SPACE_INFO_ATTR(disk_used); @@ -1059,6 +1060,7 @@ static struct attribute *space_info_attrs[] = { BTRFS_ATTR_PTR(space_info, bytes_readonly), BTRFS_ATTR_PTR(space_info, bytes_zone_unusable), BTRFS_ATTR_PTR(space_info, bytes_stripe_unusable), + BTRFS_ATTR_PTR(space_info, bytes_stripe_claimable), BTRFS_ATTR_PTR(space_info, bytes_stripe_open), BTRFS_ATTR_PTR(space_info, bytes_stripe_margin), BTRFS_ATTR_PTR(space_info, disk_used), diff --git a/include/trace/events/btrfs.h b/include/trace/events/btrfs.h index 0bf8d88285332..f84af3b65182c 100644 --- a/include/trace/events/btrfs.h +++ b/include/trace/events/btrfs.h @@ -2393,6 +2393,14 @@ DEFINE_EVENT(btrfs__space_info_update, update_bytes_stripe_unusable, TP_ARGS(fs_info, sinfo, old, diff) ); +DEFINE_EVENT(btrfs__space_info_update, update_bytes_stripe_claimable, + + TP_PROTO(const struct btrfs_fs_info *fs_info, + const struct btrfs_space_info *sinfo, u64 old, s64 diff), + + TP_ARGS(fs_info, sinfo, old, diff) +); + TRACE_EVENT(btrfs_stripe_pad_decline, TP_PROTO(const struct btrfs_fs_info *fs_info, u64 full_stripe,