]> 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>
Fri, 18 Sep 2026 21:36:28 +0000 (17:36 -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 c6af1a34c622d0946df1008ba18c845e77b29f6d..de52de42654bdbb5eb519eb825cbe22e54dd1da6 100644 (file)
@@ -686,6 +686,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);
 }
 
@@ -1260,6 +1268,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,
@@ -2665,6 +2713,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;
@@ -2707,6 +2756,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))
@@ -2724,6 +2774,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 0f8e4c165d54ce6fc432bd83f31cd6814a413c55..8e9e3b31f26e6449959c568266e8830fc3ba3e2b 100644 (file)
@@ -300,6 +300,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;
        /*
@@ -480,6 +490,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 0c23fa3429dbe80462fe73a8eb48a1eef160a0a1..6ec021f23ed1a32b19b3a1724355d47a27b6bec0 100644 (file)
@@ -2091,6 +2091,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];
@@ -2206,7 +2213,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 8ff945d0e3fd004d7e238c4b8a5034bebff5b0e1..dbfc3b947f05944fa038391fec73ccaf3dab8418 100644 (file)
@@ -796,11 +796,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_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 e9a984a7af34e4e1e6abbcc113b0e2dfe85529fc..34d8491e2605f3aad4d712ee2c7e5eb10d15269b 100644 (file)
@@ -166,6 +166,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 6d5233e0ff5a13450e18f42d0a15082411e15863..f9809403d9efd4fcd3af3d51d7ef7807acf6a073 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_stranded);
 SPACE_INFO_ATTR(bytes_stripe_claimable);
 SPACE_INFO_ATTR(bytes_stripe_open);
 SPACE_INFO_ATTR(bytes_stripe_margin);
@@ -951,6 +952,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),