]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_alloc: count whole stripes stranded behind live runs as trapped
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Fri, 11 Sep 2026 12:59:09 +0000 (08:59 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Wed, 16 Sep 2026 21:40:06 +0000 (17:40 -0400)
The data admission gate admits a reservation while the whole-stripe
supply, bytes_stripe_claimable, covers it, and the per-commit scan
measures that supply as the block group's wholly free full stripes.
But a wholly free stripe inside a live stripe run's range cannot be
claimed: btrfs_stripe_run_range_usable() rejects any claim overlapping
a run object, because a second run over the same range would make the
range-to-run lookups ambiguous.  The run was claimed over the stripe,
the extents allocated there were freed again, and the run has not been
freed yet.  For a COW run that window closes at the next commit; a
nodatacow inode's private run deliberately survives commits and lives
until the inode is evicted.

On a raid56 filesystem filled to ENOSPC by fsstress (thousands of
fallocated files, so thousands of cached inodes each holding a private
NOCOW run) a live walk of the free space cache showed the accounting
exact -- the wholly free stripes summed to bytes_stripe_claimable to the
byte -- and 99% of them inside the ranges of open NOCOW runs with no
inflight IO: 22.8 MiB of 22.8 MiB behind 387 runs in one group, 73.4 MiB
of 73.4 MiB behind 976 runs in another.  Every claim failed in O(1) on
the "no claimable run" verdict, every write() admitted against the 99 MiB
of phantom supply failed at writeback, and the data was dropped ("data
writeback allocation ... returned ENOSPC despite reservation margin"),
about 100,000 delalloc ranges per run of the degraded acceptance suite
where the stock allocator drops none.

Have the scan take such stripes out of the claimable supply: count them
in stripe_unusable, which btrfs_space_info_used() already charges so the
gate cannot admit against them, and record them separately as
stripe_stranded so that btrfs_stripe_bg_wants_reclaim() does not ask
relocation to recover space that comes back by itself.  When a run is
freed, mark its group for rescan so the next commit credits the stripes
back.  The space_info total is exposed as bytes_stripe_stranded in sysfs
and in the space info dump.

This makes the gate honest -- write() gets the ENOSPC instead of
writeback dropping the data -- but does not return the stranded space
while the runs live; that is the run lifecycle's business.

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

index b808f6128c65be3c6144ed98fedf6fc15694d11c..f2256fee2531eb2afd0ace89dffe8199d411fe5e 100644 (file)
@@ -658,6 +658,14 @@ static void free_open_stripe_run(struct btrfs_block_group *bg,
        list_del(&run->list);
        bitmap_free(run->live);
        kfree(run);
+       /*
+        * Wholly free stripes inside this run's range were counted as trapped
+        * by the last stripe_unusable scan (btrfs_stripe_run_stranded_stripes());
+        * with the run gone they are claimable again, so have the next commit
+        * rescan the group and credit them back.
+        */
+       if (READ_ONCE(bg->stripe_unusable_ready))
+               set_bit(BLOCK_GROUP_FLAG_STRIPE_UNUSABLE_DIRTY, &bg->runtime_flags);
        wake_up_var(&bg->open_stripe_runs);
 }
 
@@ -1232,6 +1240,46 @@ bool btrfs_stripe_run_range_usable(struct btrfs_block_group *bg,
        return true;
 }
 
+/*
+ * For the stripe_unusable scan: wholly free stripes that overlap a live
+ * stripe run's range cannot be claimed while the run object exists (see
+ * btrfs_stripe_run_range_usable()) -- the run was claimed over them, the
+ * extents allocated there were freed again, and the run has not been freed
+ * yet; a nodatacow inode's private run lives until the inode is evicted.
+ * Counted as claimable, they let the admission gate admit writes the claim
+ * path then refuses, and writeback drops the data.  Take them out of @freep
+ * (so the caller counts them neither as claimable nor as partial) and return
+ * their total; free_open_stripe_run() marks the group for rescan so they
+ * are credited back when the run dies.  Caller holds ctl->tree_lock;
+ * stripe_run_lock nests inside it.
+ */
+u64 btrfs_stripe_run_stranded_stripes(struct btrfs_block_group *bg, u32 *freep,
+                                     u64 nstripes)
+{
+       const u64 fsl = bg->full_stripe_len;
+       struct btrfs_open_stripe_run *run;
+       unsigned long flags;
+       u64 stranded = 0;
+
+       spin_lock_irqsave(&bg->stripe_run_lock, flags);
+       list_for_each_entry(run, &bg->open_stripe_runs, list) {
+               u64 idx, last;
+
+               if (run->end <= run->start)
+                       continue;
+               idx = div64_u64(run->start - bg->start, fsl);
+               last = div64_u64(run->end - 1 - bg->start, fsl);
+               for (; idx <= last && idx < nstripes; idx++) {
+                       if (freep[idx] != fsl)
+                               continue;
+                       freep[idx] = 0;
+                       stranded += fsl;
+               }
+       }
+       spin_unlock_irqrestore(&bg->stripe_run_lock, flags);
+       return stranded;
+}
+
 /*
  * Parse a stripe_alloc_allow_rmw word list ("nodatacow",
  * "prealloc", "fsync") into its mask.  Words are separated by comma,
@@ -2632,6 +2680,7 @@ void btrfs_scan_stripe_unusable(struct btrfs_fs_info *fs_info, bool force)
                u64 total = 0;
                u64 total_claimable = 0;
                u64 total_claimable_reloc = 0;
+               u64 total_stranded = 0;
                u64 largest = 0;
                u64 unit = 0;
                u32 nr_rw = 0;
@@ -2674,6 +2723,7 @@ void btrfs_scan_stripe_unusable(struct btrfs_fs_info *fs_info, bool force)
                                 */
                                if (READ_ONCE(bg->stripe_unusable_ready)) {
                                        total += READ_ONCE(bg->stripe_unusable);
+                                       total_stranded += READ_ONCE(bg->stripe_stranded);
                                        total_claimable +=
                                                READ_ONCE(bg->stripe_claimable);
                                        if (bg->start == READ_ONCE(fs_info->data_reloc_bg))
@@ -2691,6 +2741,7 @@ void btrfs_scan_stripe_unusable(struct btrfs_fs_info *fs_info, bool force)
                sinfo->bytes_stripe_unusable = total;
                sinfo->bytes_stripe_claimable = total_claimable;
                sinfo->bytes_stripe_claimable_reloc = total_claimable_reloc;
+               sinfo->bytes_stripe_stranded = total_stranded;
                if (!(sinfo->flags & BTRFS_BLOCK_GROUP_DATA)) {
                        u64 reserve = 0;
 
index 34355efe3d699e829857a29ba2f272b546a94ee9..70a059a9a8a35e72ed657745320db0af74e4ac47 100644 (file)
@@ -292,6 +292,16 @@ struct btrfs_block_group {
         * (honest f_bavail); the reservation layer is deliberately unchanged.
         */
        u64 stripe_unusable;
+       /*
+        * The part of stripe_unusable that is wholly free stripes lying inside
+        * live stripe-run ranges (stripe_alloc): the run was claimed over them,
+        * the extents allocated there were freed again, and the run object
+        * still exists, so btrfs_stripe_run_range_usable() refuses to claim
+        * them.  Trapped for admission, but they come back by themselves when
+        * the run is freed, so reclaim leaves them out.  Set by the same scan,
+        * under the same lock.
+        */
+       u64 stripe_stranded;
        /* jiffies when the reclaim trigger last queued this group (stripe_alloc) */
        unsigned long stripe_reclaim_jiffies;
        /*
@@ -470,6 +480,8 @@ bool btrfs_defer_stripe_meta_write(struct btrfs_fs_info *fs_info,
                                   const struct writeback_control *wbc);
 bool btrfs_stripe_open_run_class(struct btrfs_fs_info *fs_info, u64 logical,
                                 enum btrfs_stripe_run_class *class);
+u64 btrfs_stripe_run_stranded_stripes(struct btrfs_block_group *bg, u32 *freep,
+                                     u64 nstripes);
 bool btrfs_stripe_run_range_usable(struct btrfs_block_group *bg,
                                   u64 run_start, u64 *run_len);
 /*
index 19cb04300550d66903c05eebb9f3c18be11b2994..5cd32e6fb472afc7923d896b1e62200051d1ce58 100644 (file)
@@ -3362,6 +3362,13 @@ static u64 stripe_unusable_scan(struct btrfs_block_group *bg, u32 *freep,
               "stripe_unusable_scan counted=%llu != free_space=%llu",
               counted, ctl->free_space);
 
+       /*
+        * Whole stripes stranded behind live stripe runs are free but not
+        * claimable until their run is freed: trapped, like a partial stripe's
+        * free bytes, so the admission gate does not admit writes against them.
+        */
+       bg->stripe_stranded = btrfs_stripe_run_stranded_stripes(bg, freep, nstripes);
+       trapped = bg->stripe_stranded;
        for (i = 0; i < nstripes; i++)
                if (freep[i] && freep[i] < fsl)
                        trapped += freep[i];
@@ -3477,7 +3484,13 @@ bool btrfs_stripe_bg_wants_reclaim(struct btrfs_block_group *bg)
        struct btrfs_space_info *sinfo = bg->space_info;
        const int thresh = READ_ONCE(sinfo->bg_reclaim_threshold);
        const u64 used = READ_ONCE(bg->used);
-       const u64 trapped = READ_ONCE(bg->stripe_unusable);
+       const u64 unusable = READ_ONCE(bg->stripe_unusable);
+       /*
+        * Stripes stranded behind live runs are trapped for admission but not
+        * for reclaim: they come back by themselves when the run is freed, and
+        * relocating the group would only force that.
+        */
+       const u64 trapped = unusable - min(unusable, READ_ONCE(bg->stripe_stranded));
        u64 elsewhere;
        u64 promised;
 
index 6bf657efe988c2c63149e120b2123e989692d45a..419c90b2ffebde7c7b1758e4906240ff2c24221f 100644 (file)
@@ -750,11 +750,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_claimable=%llu stripe_margin=%llu stripe_reserve=%llu",
+"space_info total=%llu, used=%llu, pinned=%llu, reserved=%llu, may_use=%llu, readonly=%llu zone_unusable=%llu stripe_unusable=%llu stripe_stranded=%llu stripe_open=%llu stripe_claimable=%llu stripe_margin=%llu stripe_reserve=%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_unusable, info->bytes_stripe_stranded,
+               info->bytes_stripe_open,
                info->bytes_stripe_claimable,
                info->bytes_stripe_margin, info->bytes_stripe_reserve);
 }
index 1bdab3a6dfd619054deb0795c458caa6ada9aff3..b17547c0874015e7cdeb252ded6765df33ac1819 100644 (file)
@@ -141,6 +141,11 @@ 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_stranded;      /* the part of bytes_stripe_unusable that
+                                          is whole stripes stranded behind live
+                                          stripe runs (see the block group's
+                                          stripe_stranded); back to claimable when
+                                          the runs are freed */
        u64 bytes_stripe_claimable_reloc;       /* the part of bytes_stripe_claimable
                                           in the group dedicated to data
                                           relocation (fs_info->data_reloc_bg):
index 3ce0fe46640b47c2062e74618f164539a4a48178..96cc8b0d76f6e702e50f598419e1c465c7748f49 100644 (file)
@@ -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_stranded);
 SPACE_INFO_ATTR(bytes_stripe_claimable);
 SPACE_INFO_ATTR(bytes_stripe_open);
 SPACE_INFO_ATTR(bytes_stripe_margin);
@@ -1061,6 +1062,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_stranded),
        BTRFS_ATTR_PTR(space_info, bytes_stripe_claimable),
        BTRFS_ATTR_PTR(space_info, bytes_stripe_open),
        BTRFS_ATTR_PTR(space_info, bytes_stripe_margin),