]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_alloc: persist nocow runs across commits and remounts
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Fri, 31 Jul 2026 03:51:51 +0000 (23:51 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Mon, 3 Aug 2026 07:21:56 +0000 (03:21 -0400)
A nodatacow inode's private stripe run used to close at every
transaction commit like all runs, so a slowly appended nocow file burned
a fresh stripe per commit -- and after a remount its partial stripe's
free tail was abandoned outright.  Neither cost buys anything: the run
machinery's commit-time closing exists for invariant I2, and a nocow
stripe holds only the owner's write-hole-waived data, so there is
nothing for I2 to protect.

Keep NOCOW-class runs open across commits: the commit-time retirement
and its drain predicate skip them (their extents insert without the
window-sequence deferral, which is fine -- the data is on disk when the
ordered extent finishes, and later same-stripe writes can tear only the
owner's own data).  They still close on forced quiescing (read-only,
removal, unmount) and now on the owning inode's eviction, so a cached
but idle inode cannot pin a claimed tail forever.

Across remounts, re-adopt instead: when a nocow allocation's hint
points into a partial stripe that the committed extent tree proves is
wholly owned by the allocating inode, claim exactly the stripe's free
tail (a new exact-range claim that verifies every byte is free before
removing; nothing else can consume free space inside a partially used
stripe, so verify-then-remove cannot race) and continue the run at the
old frontier.  Appends to a nocow file then pack sequentially through
commits, evictions and remounts alike.

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

index bd55c1ba4f72920217d43a94734102f8e5cfb844..ce0df686800e1a7e3a186dccf768e7bff9701ef8 100644 (file)
@@ -487,6 +487,12 @@ void btrfs_wait_block_group_reservations(struct btrfs_block_group *bg)
  * group has no runs left.
  */
 
+static bool stripe_extents_owned_by(struct btrfs_fs_info *fs_info,
+                                   u64 root_id, u64 ino, u64 start, u64 len,
+                                   u64 *frontier);
+static bool stripe_range_run_free(struct btrfs_block_group *bg, u64 start,
+                                 u64 len);
+
 struct btrfs_open_stripe_run {
        struct list_head list;          /* bg->open_stripe_runs */
        struct btrfs_block_group *bg;
@@ -792,13 +798,15 @@ out:
  * Returns 0 and sets *ret_offset, -ENOSPC if no fully-free stripe run in
  * this block group can satisfy the allocation, or -ENOMEM.
  */
-int btrfs_alloc_from_inode_stripe_run(struct btrfs_block_group *bg, u64 ino,
+int btrfs_alloc_from_inode_stripe_run(struct btrfs_block_group *bg,
+                                     struct btrfs_inode *inode,
                                      enum btrfs_stripe_run_class class,
-                                     u64 num_bytes, u64 *ret_offset,
-                                     u64 *available)
+                                     u64 num_bytes, u64 readopt_hint,
+                                     u64 *ret_offset, u64 *available)
 {
        struct btrfs_fs_info *fs_info = bg->fs_info;
        const u64 fsl = bg->full_stripe_len;
+       const u64 ino = btrfs_ino(inode);
        struct btrfs_open_stripe_run *new_run;
        struct btrfs_open_stripe_run *run;
        unsigned long flags;
@@ -835,6 +843,42 @@ int btrfs_alloc_from_inode_stripe_run(struct btrfs_block_group *bg, u64 ino,
        if (!new_run)
                return -ENOMEM;
 
+       /*
+        * Re-adoption (nocow only): after a remount (or eviction) the
+        * inode's last partial stripe has no run, and a fresh claim would
+        * abandon its free tail forever.  When the allocation hint points
+        * into a stripe wholly owned by this inode, adopt the tail as the
+        * private run instead: every byte below the frontier is provably
+        * the inode's own committed data, and nothing else can consume a
+        * partial stripe's free space (see btrfs_claim_stripe_tail()).
+        */
+       if (class == BTRFS_STRIPE_RUN_NOCOW && readopt_hint &&
+           readopt_hint >= bg->start &&
+           readopt_hint < bg->start + bg->length) {
+               u64 stripe = bg->start +
+                            round_down(readopt_hint - bg->start, fsl);
+               u64 frontier = 0;
+               u64 tlen;
+
+               if (stripe_extents_owned_by(fs_info, btrfs_root_id(inode->root),
+                                           ino, stripe, fsl, &frontier) &&
+                   frontier > stripe && frontier < stripe + fsl &&
+                   num_bytes <= stripe + fsl - frontier) {
+                       tlen = stripe + fsl - frontier;
+                       /*
+                        * btrfs_stripe_run_range_usable() is for whole-stripe
+                        * claims (it rejects anything shorter than a full
+                        * stripe); a tail needs only the run-overlap check.
+                        */
+                       if (stripe_range_run_free(bg, frontier, tlen) &&
+                           !btrfs_claim_stripe_tail(bg, frontier, tlen)) {
+                               start = frontier;
+                               len = tlen;
+                               goto install;
+                       }
+               }
+       }
+
        ret = btrfs_claim_free_stripe_run(bg,
                        div64_u64(num_bytes + fsl - 1, fsl) * fsl,
                        &start, &len);
@@ -848,6 +892,7 @@ int btrfs_alloc_from_inode_stripe_run(struct btrfs_block_group *bg, u64 ino,
                goto out;
        }
 
+install:
        /* See the installation comment in btrfs_alloc_from_open_stripe(). */
        spin_lock(&fs_info->open_stripe_lock);
        if (list_empty(&bg->open_stripe_bg_list)) {
@@ -981,7 +1026,8 @@ void btrfs_show_stripe_rmw(struct seq_file *seq, u32 mask)
  * the whole stripe frees and commits -- which the extent tree shows.
  */
 static bool stripe_extents_owned_by(struct btrfs_fs_info *fs_info,
-                                   u64 root_id, u64 ino, u64 start, u64 len)
+                                   u64 root_id, u64 ino, u64 start, u64 len,
+                                   u64 *frontier)
 {
        struct btrfs_root *extent_root = btrfs_extent_root(fs_info, start);
        struct btrfs_path *path;
@@ -1082,6 +1128,8 @@ static bool stripe_extents_owned_by(struct btrfs_fs_info *fs_info,
                        ret = false;
                        goto out;
                }
+               if (frontier && key.objectid + key.offset > *frontier)
+                       *frontier = min(key.objectid + key.offset, start + len);
                path->slots[0]++;
        }
 out:
@@ -1148,7 +1196,8 @@ bool btrfs_stripe_nocow_writable(struct btrfs_inode *inode, u64 start,
                spin_unlock_irqrestore(&bg->stripe_run_lock, flags);
 
                if (verdict == 0 &&
-                   !stripe_extents_owned_by(fs_info, root_id, ino, cur, fsl))
+                   !stripe_extents_owned_by(fs_info, root_id, ino, cur, fsl,
+                                            NULL))
                        verdict = -1;
                if (verdict < 0) {
                        ret = false;
@@ -1278,6 +1327,110 @@ static void log_carry_record(struct btrfs_inode *inode, u64 file_start,
        carry->nr_ranges++;
 }
 
+/*
+ * Disk end of @inode's last regular file extent, or 0: the re-adoption
+ * candidate for a nocow allocation arriving with no allocation hint --
+ * after a remount or eviction the extent maps are gone, so the normal
+ * em-based hint is empty exactly when re-adoption matters most.
+ */
+u64 btrfs_nocow_alloc_hint(struct btrfs_inode *inode)
+{
+       struct btrfs_root *root = inode->root;
+       struct btrfs_path *path;
+       struct btrfs_key key;
+       struct btrfs_file_extent_item *fi;
+       u64 hint = 0;
+       int ret;
+
+       path = btrfs_alloc_path();
+       if (!path)
+               return 0;
+       key.objectid = btrfs_ino(inode);
+       key.type = BTRFS_EXTENT_DATA_KEY;
+       key.offset = (u64)-1;
+       ret = btrfs_search_slot(NULL, root, &key, path, 0, 0);
+       if (ret < 0)
+               goto out;
+       ret = btrfs_previous_item(root, path, key.objectid,
+                                 BTRFS_EXTENT_DATA_KEY);
+       if (ret)
+               goto out;
+       btrfs_item_key_to_cpu(path->nodes[0], &key, path->slots[0]);
+       fi = btrfs_item_ptr(path->nodes[0], path->slots[0],
+                           struct btrfs_file_extent_item);
+       if (btrfs_file_extent_type(path->nodes[0], fi) ==
+                                               BTRFS_FILE_EXTENT_INLINE)
+               goto out;
+       if (btrfs_file_extent_disk_bytenr(path->nodes[0], fi))
+               hint = btrfs_file_extent_disk_bytenr(path->nodes[0], fi) +
+                      btrfs_file_extent_disk_num_bytes(path->nodes[0], fi);
+out:
+       btrfs_free_path(path);
+       return hint;
+}
+
+/* Does no run object (open or draining) overlap [start, start + len)? */
+static bool stripe_range_run_free(struct btrfs_block_group *bg, u64 start,
+                                 u64 len)
+{
+       struct btrfs_open_stripe_run *run;
+       unsigned long flags;
+       bool ret = true;
+
+       spin_lock_irqsave(&bg->stripe_run_lock, flags);
+       list_for_each_entry(run, &bg->open_stripe_runs, list) {
+               if (run->start < start + len && run->end > start) {
+                       ret = false;
+                       break;
+               }
+       }
+       spin_unlock_irqrestore(&bg->stripe_run_lock, flags);
+       return ret;
+}
+
+/*
+ * Close every private stripe run owned by @inode, of any class: called at
+ * inode eviction so a persistent nocow run's claimed tail does not stay
+ * unusable for the rest of the mount (log-class runs would close at the
+ * next commit anyway; closing them here is merely tidy).  The runs' tails
+ * return to the free space cache.
+ */
+void btrfs_close_inode_stripe_runs(struct btrfs_inode *inode)
+{
+       struct btrfs_fs_info *fs_info = inode->root->fs_info;
+       const u64 ino = btrfs_ino(inode);
+       struct btrfs_block_group *bg;
+
+       if (list_empty_careful(&fs_info->open_stripe_bgs))
+               return;
+restart:
+       spin_lock(&fs_info->open_stripe_lock);
+       list_for_each_entry(bg, &fs_info->open_stripe_bgs,
+                           open_stripe_bg_list) {
+               struct btrfs_open_stripe_run *run;
+               unsigned long flags;
+               u64 tail_start = 0;
+               u64 tail_len = 0;
+
+               spin_lock_irqsave(&bg->stripe_run_lock, flags);
+               list_for_each_entry(run, &bg->open_stripe_runs, list) {
+                       if (!run->open || run->owner != ino)
+                               continue;
+                       tail_len = close_open_stripe_run(bg, run, &tail_start);
+                       break;
+               }
+               spin_unlock_irqrestore(&bg->stripe_run_lock, flags);
+               if (tail_len) {
+                       btrfs_get_block_group(bg);
+                       spin_unlock(&fs_info->open_stripe_lock);
+                       btrfs_add_free_space(bg, tail_start, tail_len);
+                       btrfs_put_block_group(bg);
+                       goto restart;
+               }
+       }
+       spin_unlock(&fs_info->open_stripe_lock);
+}
+
 /*
  * A log commit is about to make the extent at [bytenr, bytenr + num_bytes)
  * durable.  Once the log super lands, no later write may extend or RMW the
@@ -1641,6 +1794,17 @@ restart:
 
                if (!run->open || run->open_seq >= seq)
                        continue;
+               /*
+                * A nodatacow inode's private run survives transaction
+                * commits: its stripes hold only that inode's expendable
+                * (write-hole-waived) data, so invariant I2 has nothing to
+                * protect there, and closing would burn a stripe per commit
+                * for a slowly appended nocow file.  Forced quiescing
+                * (read-only, removal, unmount: seq == U64_MAX) still closes
+                * them, as does the owning inode's eviction.
+                */
+               if (run->class == BTRFS_STRIPE_RUN_NOCOW && seq != U64_MAX)
+                       continue;
                for (class = 0; class < BTRFS_STRIPE_RUN_NR_CLASSES; class++)
                        for (band = 0; band < BTRFS_STRIPE_RUN_NR_BANDS; band++)
                                if (bg->open_stripe[class][band] == run)
@@ -1678,6 +1842,9 @@ static bool bg_open_stripes_settled(struct btrfs_block_group *bg, u64 seq)
 
        spin_lock_irqsave(&bg->stripe_run_lock, flags);
        list_for_each_entry(run, &bg->open_stripe_runs, list) {
+               /* Persistent nocow runs are not part of any commit window. */
+               if (run->class == BTRFS_STRIPE_RUN_NOCOW && seq != U64_MAX)
+                       continue;
                if (run->open_seq < seq) {
                        ret = false;
                        break;
index bec46f1597d0497b0e6e6087fd1930eab4ffdfbd..6f3fd7982a69261009d50507a6987296fac18eea 100644 (file)
@@ -427,10 +427,13 @@ void btrfs_wait_block_group_reservations(struct btrfs_block_group *bg);
 int btrfs_alloc_from_open_stripe(struct btrfs_block_group *bg, u64 num_bytes,
                                 enum btrfs_stripe_run_class class,
                                 u64 *ret_offset, u64 *available);
-int btrfs_alloc_from_inode_stripe_run(struct btrfs_block_group *bg, u64 ino,
+int btrfs_alloc_from_inode_stripe_run(struct btrfs_block_group *bg,
+                                     struct btrfs_inode *inode,
                                      enum btrfs_stripe_run_class class,
-                                     u64 num_bytes, u64 *ret_offset,
-                                     u64 *available);
+                                     u64 num_bytes, u64 readopt_hint,
+                                     u64 *ret_offset, u64 *available);
+void btrfs_close_inode_stripe_runs(struct btrfs_inode *inode);
+u64 btrfs_nocow_alloc_hint(struct btrfs_inode *inode);
 void btrfs_open_stripe_write_done(struct btrfs_block_group *bg, u64 start,
                                  u64 num_bytes);
 void btrfs_open_stripe_write_done_run(struct btrfs_open_stripe_run *run,
index 00cfac52a0438d93988027011c6eaeb198b5360c..409068647edd2beda1cc3c414803edbb998fa348 100644 (file)
@@ -4260,8 +4260,9 @@ static int do_allocation_stripe(struct btrfs_block_group *block_group,
                struct btrfs_inode *steer_inode = ffe_ctl->steer_inode;
 
                ret = btrfs_alloc_from_inode_stripe_run(block_group,
-                               btrfs_ino(steer_inode), ffe_ctl->steer_class,
-                               ffe_ctl->num_bytes, &offset, &available);
+                               steer_inode, ffe_ctl->steer_class,
+                               ffe_ctl->num_bytes, ffe_ctl->hint_byte,
+                               &offset, &available);
                if (ret == -ENOMEM)
                        return ret;
                if (!ret) {
@@ -5040,6 +5041,9 @@ int btrfs_reserve_extent(struct btrfs_root *root, struct btrfs_inode *inode,
 
                        if (run_hint)
                                hint_byte = run_hint;
+                       else if (steer_class == BTRFS_STRIPE_RUN_NOCOW &&
+                                !hint_byte)
+                               hint_byte = btrfs_nocow_alloc_hint(inode);
                }
        }
 
index 6dada3c02314eac0d52af4d100c16b3fa01d01dc..d8f2274ee8ecb0f98a35135622ae7a613c710d21 100644 (file)
@@ -3426,6 +3426,68 @@ static bool find_stripe_run_in_bitmap(struct btrfs_free_space_ctl *ctl,
  * Returns 0 on success, -ENOSPC if no aligned fully-free stripe is present
  * in this block group.
  */
+/*
+ * Claim exactly [start, start + len) from the free space cache, failing
+ * (-ENOSPC) unless every byte of it is currently free.  Used to re-adopt
+ * the free tail of a partial stripe wholly owned by one nodatacow inode:
+ * nothing else can consume free space inside a partially used stripe of a
+ * stripe_alloc block group (allocations come only from claimed runs, and
+ * runs claim only fully-free stripes), so the verify-then-remove sequence
+ * cannot race with another consumer -- except a concurrent re-adoption by
+ * the same inode, which this function's recount safely turns into one
+ * winner and one -ENOSPC.
+ */
+int btrfs_claim_stripe_tail(struct btrfs_block_group *block_group,
+                           u64 start, u64 len)
+{
+       struct btrfs_free_space_ctl *ctl = block_group->free_space_ctl;
+       const u64 end = start + len;
+       u64 found = 0;
+       struct rb_node *n;
+
+       spin_lock(&ctl->tree_lock);
+       for (n = rb_first(&ctl->free_space_offset); n; n = rb_next(n)) {
+               struct btrfs_free_space *e =
+                       rb_entry(n, struct btrfs_free_space, offset_index);
+               u64 e_start = e->offset;
+               u64 e_end;
+
+               if (e->bitmap) {
+                       const u64 unit = block_group->fs_info->sectorsize;
+                       unsigned long b = 0;
+
+                       e_end = e_start + (u64)BITS_PER_BITMAP * unit;
+                       if (e_end <= start || e_start >= end)
+                               continue;
+                       while (b < BITS_PER_BITMAP) {
+                               unsigned long z;
+                               u64 f_start, f_end;
+
+                               b = find_next_bit(e->bitmap, BITS_PER_BITMAP, b);
+                               if (b >= BITS_PER_BITMAP)
+                                       break;
+                               z = find_next_zero_bit(e->bitmap,
+                                                      BITS_PER_BITMAP, b);
+                               f_start = max(start, e_start + (u64)b * unit);
+                               f_end = min(end, e_start + (u64)z * unit);
+                               if (f_end > f_start)
+                                       found += f_end - f_start;
+                               b = z;
+                       }
+                       continue;
+               }
+               e_end = e_start + e->bytes;
+               if (e_end <= start || e_start >= end)
+                       continue;
+               found += min(end, e_end) - max(start, e_start);
+       }
+       spin_unlock(&ctl->tree_lock);
+
+       if (found != len)
+               return -ENOSPC;
+       return btrfs_remove_free_space(block_group, start, len);
+}
+
 int btrfs_claim_free_stripe_run(struct btrfs_block_group *block_group,
                                u64 want_bytes, u64 *start, u64 *len)
 {
index 815638b724ef2fb22bd8904b797b6b9ffb2d3da3..c5afb92dcc5d2859edf8784a95991b28ec04da12 100644 (file)
@@ -138,6 +138,8 @@ bool btrfs_is_free_space_trimmed(struct btrfs_block_group *block_group);
 u64 btrfs_find_space_for_alloc(struct btrfs_block_group *block_group,
                               u64 offset, u64 bytes, u64 empty_size,
                               u64 *max_extent_size);
+int btrfs_claim_stripe_tail(struct btrfs_block_group *block_group,
+                           u64 start, u64 len);
 int btrfs_claim_free_stripe_run(struct btrfs_block_group *block_group,
                                u64 want_bytes, u64 *start, u64 *len);
 void btrfs_block_group_init_stripe_unusable(struct btrfs_block_group *block_group);
index a87dc3df8cab45ba56c84d92deefc6a543458b96..7aa7d020fc2d65aac3d4ff686fc8f7900c58b7ac 100644 (file)
@@ -5513,6 +5513,7 @@ void btrfs_evict_inode(struct inode *inode)
                goto clear_inode;
 
        fs_info = inode_to_fs_info(inode);
+       btrfs_close_inode_stripe_runs(BTRFS_I(inode));
        evict_inode_truncate_pages(inode);
 
        if (inode->i_nlink &&