]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_alloc: count claimable whole-stripe supply directly
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Sun, 9 Aug 2026 20:04:21 +0000 (16:04 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Fri, 18 Sep 2026 21:36:24 +0000 (17:36 -0400)
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
fs/btrfs/block-group.c
fs/btrfs/block-group.h
fs/btrfs/free-space-cache.c
fs/btrfs/space-info.c
fs/btrfs/space-info.h
fs/btrfs/sysfs.c
include/trace/events/btrfs.h

index 8715dc51ec73a5d1d0a99696df3e18f39f710d79..a5b5a5cc1bde8ddef8b1eab0c1d16250b1cdf997 100644 (file)
@@ -2312,6 +2312,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))
@@ -2343,14 +2344,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
index 87d807639589d3c78a5889acc1421b039095f390..4c59b0ba0f2179cd2af2bc34d1a506cf6e0a0d3a 100644 (file)
@@ -292,6 +292,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;
 
        /*
index 80703492a56479f658cafb370032c04c6439d00b..8995f90fdebc28738ff82fd142e9acfe76d14484 100644 (file)
@@ -1350,6 +1350,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)
@@ -1404,6 +1408,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);
 
@@ -1483,6 +1500,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.
@@ -1609,6 +1668,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)
@@ -2022,7 +2094,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))
@@ -2043,14 +2115,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);
        }
 }
@@ -2067,6 +2144,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;
@@ -2077,13 +2155,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);
        }
 }
@@ -2111,8 +2193,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);
        /*
@@ -2551,6 +2652,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;
@@ -2581,6 +2683,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;
index 6603214a14381361bbb763c3871fc720354b3d3d..d89a80be360095b6125f30bf0f08772dfff1a287 100644 (file)
@@ -721,11 +721,12 @@ static void __btrfs_dump_space_info(const struct btrfs_space_info *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);
 }
 
index 39e32ed85636edec318009dd58ce188c4452c674..65c976e84fcc3147841e3d2c1f9f3e14dfc16fc7 100644 (file)
@@ -158,6 +158,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;
@@ -309,6 +313,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");
 
 static inline u64 btrfs_space_info_used(const struct btrfs_space_info *s_info,
                                        bool may_use_included)
index 9b98cf58b366c9bfc43ad4051e8508aecc50dfda..57e8cb5f89d8c9eb0405b8989bdfe16ea77240ef 100644 (file)
@@ -818,6 +818,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);
@@ -949,6 +950,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),
index db541d70e964098279fec6e51b1fe6cbcf186117..e95f1b3a8a0c56fad0a33866dcd0a16179e41e2f 100644 (file)
@@ -3299,6 +3299,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,