]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_alloc: pessimistic data reservation margin
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Mon, 3 Aug 2026 07:22:52 +0000 (03:22 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Wed, 16 Sep 2026 21:40:02 +0000 (17:40 -0400)
A byte-counted data reservation holds no collateral against the stripe
claim rule.  Between admission at write() time and the allocation at
writeback, the trapped-space picture keeps moving: commits close open
stripe runs and trap their sub-stripe tails, and free space returning
mid-transaction (unpinned deletions, same-transaction frees, drained
run tails, freed reservations) lands in the free space cache as
fragments the claim rule can never hand out but that no counter yet
reflects.  The counters were honest at every instant for NEW
admissions while OLD admissions were left holding air; delalloc
writeback gets no second chance, so the pages were dropped.  Measured
at the fill edge: ~19 MiB of orphaned bytes_may_use with accounting
and allocator in perfect agreement, and in a later round the admitted
may_use+margin exactly equaled the phantom mid-transaction fragments.

Be pessimistic at reservation time and optimistic at allocation time,
the same shape metadata reservations already use:

- bytes_stripe_margin: one full stripe width (fs_info->
  stripe_margin_unit, the widest raid56 data full stripe) per
  outstanding delalloc extent, charged and released inside
  btrfs_mod_outstanding_extents() so it stays exact across delalloc
  merges and splits -- including async compression, where the io-tree
  split hook grows the margin to one stripe per compressed piece,
  matching the true worst case.  Counted in btrfs_space_info_used()
  so it holds back admissions.  The margin exists only for in-flight
  dirty data; steady-state capacity and statfs are untouched.

- Admission probes len + margin through the ticketed FLUSH_DATA path
  (then immediately re-releases the probe, which the delalloc hooks
  re-charge), so a writer that cannot be covered waits for the
  flusher -- commits convert pinned deletions into claimable whole
  stripes -- and receives an honest ENOSPC at write(2) if flushing
  cannot produce cover.

- Every btrfs_add_free_space() into an armed group counts the added
  range's sub-stripe head and tail fragments as trapped immediately;
  whole stripes fully inside the range are trivially fully free and
  stay claimable.  A bounded overcount in the safe direction, never
  an undercount; the commit rescan remains the authority and
  reconciles (and now grants tickets when it lowers the counter,
  since admission waiters may be blocked on exactly that headroom).
  This closes the retire-to-rescan window the previous patch left
  open, and closes it in the one place every returning range passes
  through: counting at the individual close/drain call sites instead
  misses paths (the drain returns have two) and gets the arithmetic
  wrong (round_up() on the non-power-of-two 448K stripe width).

- When a margin-backed data allocation must split across free space
  fragments, every non-final piece is kept a whole-stripe multiple
  (rounddown; the width is not a power of two), so at most one piece
  per delalloc extent can strand a tail -- the margin pre-paid
  exactly one.

- A data writeback allocation returning ENOSPC anyway is loudly
  warned: the margin should make it impossible, and the pages are
  dropped.

With this, the fill-to-ENOSPC harness goes fully clean for the first
time: zero writeback drops, zero warnings, zero allocation failures,
margin drains to zero at idle, and fill capacity is unchanged.  A
writeback ENOSPC retry loop that this replaces is dropped entirely.

Known bounds, accepted: compressed writes are probed one unit per
128M range but charged per 128K piece (probe optimistic, charge
honest); direct IO is probed but carries no margin (its reserve-to-
allocate window is one syscall); a remount toggling stripe_alloc
with dirty delalloc drains the counter (charges are gated on the
mount option, releases are not, and the counter clamps at zero).

Assisted-by: Claude:claude-fable-5
fs/btrfs/block-group.c
fs/btrfs/btrfs_inode.h
fs/btrfs/delalloc-space.c
fs/btrfs/extent-tree.c
fs/btrfs/free-space-cache.c
fs/btrfs/fs.h
fs/btrfs/space-info.c
fs/btrfs/space-info.h
fs/btrfs/sysfs.c

index 7749c7767ae1127f936b3d8bc11e3547fc6b64ee..753c5cf34575de25a3caebdaa3bfaa4143a879d1 100644 (file)
@@ -2065,6 +2065,12 @@ void btrfs_scan_stripe_unusable(struct btrfs_fs_info *fs_info, bool force)
 
                spin_lock(&sinfo->lock);
                sinfo->bytes_stripe_unusable = total;
+               /*
+                * The rescan usually lowers the counter (mid-transaction
+                * incremental adds overcount conservatively); admission
+                * tickets may be waiting on exactly that headroom.
+                */
+               btrfs_try_granting_tickets(sinfo);
                spin_unlock(&sinfo->lock);
        }
 }
index 58164328b7f596749c2de7904fac8bbbad5a9e31..ae017cf3b4fbc80cb9ca04de105217452aaee839 100644 (file)
@@ -488,6 +488,8 @@ static inline bool is_data_inode(const struct btrfs_inode *inode)
        return btrfs_ino(inode) != BTRFS_BTREE_INODE_OBJECTID;
 }
 
+void btrfs_stripe_margin_mod(struct btrfs_inode *inode, int mod);
+
 static inline void btrfs_mod_outstanding_extents(struct btrfs_inode *inode,
                                                 int mod)
 {
@@ -497,6 +499,7 @@ static inline void btrfs_mod_outstanding_extents(struct btrfs_inode *inode,
                return;
        trace_btrfs_inode_mod_outstanding_extents(inode->root, btrfs_ino(inode),
                                                  mod, inode->outstanding_extents);
+       btrfs_stripe_margin_mod(inode, mod);
 }
 
 /*
index 288e1776c02da3f14aed1e105e0a28bbbaadfa16..e8a08de2421e995b4c7a5b4a9bfe4eea462bdf7b 100644 (file)
@@ -144,6 +144,7 @@ int btrfs_check_data_free_space(struct btrfs_inode *inode,
 {
        struct btrfs_fs_info *fs_info = inode->root->fs_info;
        enum btrfs_reserve_flush_enum flush = BTRFS_RESERVE_FLUSH_DATA;
+       u64 stripe_margin = 0;
        int ret;
 
        /* align the range */
@@ -156,9 +157,31 @@ int btrfs_check_data_free_space(struct btrfs_inode *inode,
        else if (btrfs_is_free_space_inode(inode))
                flush = BTRFS_RESERVE_FLUSH_FREE_SPACE_INODE;
 
-       ret = btrfs_reserve_data_bytes(data_sinfo_for_inode(inode), len, flush);
+       /*
+        * stripe_alloc: probe for the pessimistic reservation margin on top
+        * of the bytes themselves, so the ticketed flush below has to
+        * produce enough claimable whole stripes to cover this write's
+        * worst case, and a writer that cannot be covered gets an honest
+        * ENOSPC here rather than a dropped writeback later.  The margin is
+        * released again immediately: the moment the range becomes delalloc,
+        * btrfs_mod_outstanding_extents() re-charges the same worst case
+        * into bytes_stripe_margin, which it then carries until writeback.
+        */
+       if (flush == BTRFS_RESERVE_FLUSH_DATA &&
+           btrfs_test_opt(fs_info, STRIPE_ALLOC)) {
+               const u64 unit = READ_ONCE(fs_info->stripe_margin_unit);
+
+               if (unit)
+                       stripe_margin = (u64)count_max_extents(fs_info, len) * unit;
+       }
+
+       ret = btrfs_reserve_data_bytes(data_sinfo_for_inode(inode),
+                                      len + stripe_margin, flush);
        if (ret < 0)
                return ret;
+       if (stripe_margin)
+               btrfs_space_info_free_bytes_may_use(data_sinfo_for_inode(inode),
+                                                   stripe_margin);
 
        /* Use new btrfs_qgroup_reserve_data to reserve precious data space. */
        ret = btrfs_qgroup_reserve_data(inode, reserved, start, len);
index a9c3db29da57b9c829d7f9a1e79345d6cc20278e..d5606991e0ae2f697846fac389e0dc9ae93b5982 100644 (file)
@@ -4869,6 +4869,22 @@ again:
                        num_bytes = min(num_bytes >> 1, ins->offset);
                        num_bytes = round_down(num_bytes,
                                               fs_info->sectorsize);
+                       /*
+                        * stripe_alloc: when a data allocation must be split
+                        * across free space fragments, keep every non-final
+                        * piece a whole-stripe multiple so at most one piece
+                        * per delalloc extent can strand a sub-stripe tail.
+                        * The reservation margin pre-paid exactly one tail;
+                        * splitting must not mint more.
+                        */
+                       if (is_data && btrfs_test_opt(fs_info, STRIPE_ALLOC)) {
+                               const u64 unit =
+                                       READ_ONCE(fs_info->stripe_margin_unit);
+
+                               /* Not a power of two: rounddown(), not round_down(). */
+                               if (unit && num_bytes > unit)
+                                       num_bytes = rounddown(num_bytes, unit);
+                       }
                        num_bytes = max(num_bytes, min_alloc_size);
                        ram_bytes = num_bytes;
                        if (num_bytes == min_alloc_size)
@@ -4885,6 +4901,16 @@ again:
                                btrfs_dump_space_info(fs_info, sinfo,
                                                      num_bytes, 1);
                }
+               /*
+                * The pessimistic reservation margin should make a data
+                * writeback allocation failure impossible: admission held
+                * back a full stripe per delalloc extent.  If it happens
+                * anyway the pages are dropped, so say so loudly.
+                */
+               if (is_data && delalloc && btrfs_test_opt(fs_info, STRIPE_ALLOC))
+                       btrfs_warn_rl(fs_info,
+       "stripe_alloc: data writeback allocation of %llu bytes returned ENOSPC despite reservation margin; buffered data in this range will be dropped",
+                                     num_bytes);
        }
 
        return ret;
index e4562b64f9ca8c821efe28d5cecf3b1baf870f51..c55507906593c0801a777229239d8013ab0433c3 100644 (file)
@@ -2756,6 +2756,51 @@ 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: count the sub-stripe fragments of a returning free range
+ * as trapped immediately, without waiting for the commit-time rescan.
+ *
+ * The rescan remains the authority, but reservations (which now carry the
+ * pessimistic per-extent margin) read bytes_stripe_unusable in the middle
+ * of a transaction, and any mid-transaction return of free space -- an
+ * unpinned deletion, a same-transaction free, a drained run tail, a freed
+ * reservation -- otherwise leaves its fragments backing admissions the
+ * claim rule can never honor.  Whole stripes lying fully inside the
+ * returned range are trivially fully free (hence claimable) and are not
+ * counted; the partial head and tail fragments are counted trapped even
+ * though neighbouring free space may complete their stripes.  That is a
+ * bounded overcount in the safe direction (early ENOSPC at admission,
+ * reconciled by the next rescan), never an undercount.
+ */
+static void stripe_unusable_account_add(struct btrfs_block_group *bg,
+                                       u64 bytenr, u64 size)
+{
+       struct btrfs_space_info *sinfo = bg->space_info;
+       const u64 fsl = bg->full_stripe_len;
+       u64 rel_start, rel_end, aligned_start, aligned_end, frag;
+
+       if (!READ_ONCE(bg->stripe_unusable_ready) || !sinfo)
+               return;
+       if (!btrfs_is_stripe_alloc_bg(bg))
+               return;
+
+       rel_start = bytenr - bg->start;
+       rel_end = rel_start + size;
+       aligned_start = div64_u64(rel_start + fsl - 1, fsl) * fsl;
+       aligned_end = div64_u64(rel_end, fsl) * fsl;
+
+       if (aligned_end > aligned_start)
+               frag = size - (aligned_end - aligned_start);
+       else
+               frag = size;
+       if (!frag)
+               return;
+
+       spin_lock(&sinfo->lock);
+       sinfo->bytes_stripe_unusable += frag;
+       spin_unlock(&sinfo->lock);
+}
+
 int btrfs_add_free_space(struct btrfs_block_group *block_group,
                         u64 bytenr, u64 size)
 {
@@ -2769,6 +2814,7 @@ int btrfs_add_free_space(struct btrfs_block_group *block_group,
                trim_state = BTRFS_TRIM_STATE_TRIMMED;
 
        stripe_unusable_mark_dirty(block_group);
+       stripe_unusable_account_add(block_group, bytenr, size);
        return __btrfs_add_free_space(block_group, bytenr, size, trim_state);
 }
 
index 59c369d0fb9e8a3b2a34837a3a7f8a773a5a5ffe..8166f5eaff1ce6534775494d45b49dddee06f2f4 100644 (file)
@@ -882,6 +882,14 @@ struct btrfs_fs_info {
         */
        u64 max_extent_size;
 
+       /*
+        * Widest full-stripe width among raid56 DATA block groups; the unit
+        * of the stripe_alloc pessimistic reservation margin (one unit per
+        * outstanding delalloc extent).  Zero when the filesystem has no
+        * raid56 data.  Grows monotonically as block groups appear.
+        */
+       u64 stripe_margin_unit;
+
        /* Block groups and devices containing active swapfiles. */
        spinlock_t swapfile_pins_lock;
        struct rb_root swapfile_pins;
index d30cef95364df875e99b625eaa0b4ca7e7ca37c4..bf3f9c12042d6555a0dfc0c6f28b22fc0dedd787 100644 (file)
@@ -16,6 +16,7 @@
 #include "extent-tree.h"
 #include "zoned.h"
 #include "delayed-inode.h"
+#include "btrfs_inode.h"
 
 /*
  * HOW DOES SPACE RESERVATION WORK
@@ -181,6 +182,7 @@ u64 __pure btrfs_space_info_used(const struct btrfs_space_info *s_info,
                s_info->bytes_pinned + s_info->bytes_readonly +
                s_info->bytes_zone_unusable +
                s_info->bytes_stripe_unusable + s_info->bytes_stripe_open +
+               s_info->bytes_stripe_margin +
                (may_use_included ? s_info->bytes_may_use : 0);
 }
 
@@ -368,6 +370,18 @@ void btrfs_add_bg_to_space_info(struct btrfs_fs_info *info,
 
        factor = btrfs_bg_type_to_factor(block_group->flags);
 
+       /*
+        * Track the widest raid56 data full stripe as the unit of the
+        * stripe_alloc pessimistic reservation margin.  Pure geometry;
+        * whether a margin is actually charged is decided per reservation
+        * by the mount option.
+        */
+       if ((block_group->flags & BTRFS_BLOCK_GROUP_DATA) &&
+           (block_group->flags & BTRFS_BLOCK_GROUP_RAID56_MASK) &&
+           block_group->full_stripe_len > READ_ONCE(info->stripe_margin_unit))
+               WRITE_ONCE(info->stripe_margin_unit,
+                          block_group->full_stripe_len);
+
        spin_lock(&space_info->lock);
        space_info->total_bytes += block_group->length;
        space_info->disk_total += block_group->length * factor;
@@ -388,6 +402,59 @@ void btrfs_add_bg_to_space_info(struct btrfs_fs_info *info,
        up_write(&space_info->groups_sem);
 }
 
+/*
+ * stripe_alloc pessimistic reservation margin.
+ *
+ * A byte-counted data reservation holds no collateral against the stripe
+ * claim rule: between admission at write() time and the allocation at
+ * writeback, every transaction commit closes open stripe runs and traps
+ * their sub-stripe tails, quietly converting bytes the reservation was
+ * counting on into bytes_stripe_unusable.  The reservation cannot fail at
+ * that point -- delalloc writeback gets no second chance -- so pre-pay the
+ * worst case instead: charge one full stripe width for every outstanding
+ * delalloc extent, and release it when the extent is allocated (or its
+ * delalloc range dropped).  The actual allocation is optimistic -- extents
+ * pack into shared runs and usually trap nothing -- and the unused margin
+ * returns the moment the extent completes.
+ *
+ * The charge rides btrfs_mod_outstanding_extents(), the same bookkeeping
+ * that sizes metadata reservations, so it stays exact across delalloc
+ * merges and splits.  Charges are gated on the mount option but releases
+ * are not (and clamp at zero), so toggling stripe_alloc across a remount
+ * with dirty delalloc drains the counter instead of stranding it.
+ */
+void btrfs_stripe_margin_mod(struct btrfs_inode *inode, int mod)
+{
+       struct btrfs_fs_info *fs_info = inode->root->fs_info;
+       struct btrfs_space_info *sinfo = fs_info->data_sinfo;
+       const u64 unit = READ_ONCE(fs_info->stripe_margin_unit);
+       u64 bytes;
+
+       if (!unit || !sinfo || mod == 0)
+               return;
+       if (btrfs_test_opt(fs_info, STRIPE_ALLOC)) {
+               /* charges and releases both live */
+       } else if (mod > 0 || READ_ONCE(sinfo->bytes_stripe_margin) == 0) {
+               /*
+                * Option off: never charge, and skip the lock once the
+                * counter has drained (with no charges it only falls, so
+                * reading zero is stable).
+                */
+               return;
+       }
+
+       bytes = (u64)abs(mod) * unit;
+       spin_lock(&sinfo->lock);
+       if (mod > 0) {
+               sinfo->bytes_stripe_margin += bytes;
+       } else {
+               sinfo->bytes_stripe_margin -= min(bytes, sinfo->bytes_stripe_margin);
+               btrfs_try_granting_tickets(sinfo);
+       }
+       spin_unlock(&sinfo->lock);
+}
+
+
 struct btrfs_space_info *btrfs_find_space_info(struct btrfs_fs_info *info,
                                               u64 flags)
 {
@@ -611,10 +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",
+"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",
                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_readonly, info->bytes_zone_unusable,
+               info->bytes_stripe_unusable, info->bytes_stripe_open,
+               info->bytes_stripe_margin);
 }
 
 void btrfs_dump_space_info(struct btrfs_fs_info *fs_info,
index 9534e6b99efaa45bdafb5c2fad9f9fed195e2323..a626edc963b5b525f6373c6abc818c5c05b67253 100644 (file)
@@ -141,6 +141,16 @@ struct btrfs_space_info {
                                           reservations must not count on it
                                           either.  Synced from the block groups'
                                           stripe_open_remainder. */
+       u64 bytes_stripe_margin;        /* stripe_alloc pessimistic reservation
+                                          margin: one full stripe width per
+                                          outstanding delalloc extent.  Pre-pays
+                                          the worst-case sub-stripe tail each
+                                          future extent can strand when its run
+                                          closes, so commits between write() and
+                                          writeback can never trap space out
+                                          from under an admitted reservation.
+                                          Charged and released in lockstep with
+                                          the inodes' outstanding_extents. */
 
        u64 max_extent_size;    /* This will hold the maximum extent size of
                                   the space info if we had an ENOSPC in the
@@ -284,7 +294,10 @@ DECLARE_SPACE_INFO_UPDATE(bytes_zone_unusable, "zone_unusable");
 DECLARE_SPACE_INFO_UPDATE(bytes_stripe_unusable, "stripe_unusable");
 
 
+struct btrfs_inode;
+
 int btrfs_init_space_info(struct btrfs_fs_info *fs_info);
+void btrfs_stripe_margin_mod(struct btrfs_inode *inode, int mod);
 void btrfs_add_bg_to_space_info(struct btrfs_fs_info *info,
                                struct btrfs_block_group *block_group);
 void btrfs_update_space_info_chunk_size(struct btrfs_space_info *space_info,
index f2389424ea1e77b618cbdad177faf215325472c0..8460311f4b82005b240a933b2b86d9921a26cb76 100644 (file)
@@ -928,6 +928,8 @@ 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_open);
+SPACE_INFO_ATTR(bytes_stripe_margin);
 SPACE_INFO_ATTR(disk_used);
 SPACE_INFO_ATTR(disk_total);
 SPACE_INFO_ATTR(reclaim_count);
@@ -1057,6 +1059,8 @@ 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_open),
+       BTRFS_ATTR_PTR(space_info, bytes_stripe_margin),
        BTRFS_ATTR_PTR(space_info, disk_used),
        BTRFS_ATTR_PTR(space_info, disk_total),
        BTRFS_ATTR_PTR(space_info, bg_reclaim_threshold),