]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_alloc: carry an inode's log tail forward at fsync
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Thu, 30 Jul 2026 07:28:14 +0000 (03:28 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Fri, 4 Sep 2026 17:16:56 +0000 (13:16 -0400)
Closing a log-active inode's private stripe run at each log commit
traps the run's final partial stripe tail every time the inode fsyncs:
a steady fsync stream burns one stripe per log commit until the stripes
free or balance runs.

Reclaim the tails by carrying the partial stripe's live data forward.
At each log commit, the settling walk now computes the closed run's
final partial stripe, and the logging paths record the file ranges of
the logged extents that live inside it on a small per-inode table
(bounded; overflow just means an extent is not carried).  At the
inode's next fsync, before its delalloc flush, each recorded range that
still maps to the recorded disk bytenr is re-dirtied, defrag style
(reserve, reset delalloc state, mark the folios dirty; absent folios
are read back, which is a plain read of settled data).  The flush then
COWs the carried ranges together with the new data into the inode's
current private run, and the old partial stripe empties and frees
whole.

Everything downstream is the ordinary COW pipeline: new extent maps,
ordered extents, checksums, file extent items and delayed refs, and
the log's modified-extents snapshot -- taken inside btrfs_log_inode
after the flush -- picks up the new addresses by itself.  Reflinked
ranges need no special handling: foreign referents keep the old extent
alive in its closed (never again written) stripe, and only the space
reclaim degrades.  A range that was rewritten, truncated, punched or
compressed simply fails the mapping check and stays put.

Crash safety does not regress.  A carried extent's old copy is dropped
through the normal paths: if it was committed, the free pins until the
transaction commits; if it was logged but never committed, its ADD and
DROP delayed refs cancel in cleanup_ref_head(), which also pins
must_insert_reserved heads -- either way the old blocks cannot be
reallocated before the log that references them is superseded.

The settling walk also learned to report the partial stripe through a
widened btrfs_log_settle_stripes() signature (inode and file range
instead of fs_info); compressed extents pass a zero file length to opt
out of carrying while still settling.

The re-dirty reserves with NO_FLUSH.  carry_one_range() takes the folio
locks and the extent range lock before reserving, and a flushing
(ticketed) reservation can sleep in wait_reserve_ticket() until the
flusher makes progress -- but every way forward needs the locks we
hold: FLUSH_DELALLOC has to write back the carried range, which blocks
in __folio_lock() on our locked folio, and a transaction commit (with
flushoncommit) waits for the same writeback.  The whole filesystem then
wedges behind the stuck commit.  Carrying is best effort by design and
the carried range's data is already durable in its old stripe, so on
ENOSPC just skip the carry and let the old stripe free the slow way.

Assisted-by: Claude:claude-fable-5
fs/btrfs/block-group.c
fs/btrfs/block-group.h
fs/btrfs/btrfs_inode.h
fs/btrfs/defrag.c
fs/btrfs/defrag.h
fs/btrfs/file.c
fs/btrfs/inode.c
fs/btrfs/tree-log.c

index 9dffa5b8bc9466a4626c91b269f2261ab92458ca..acaae540403d3ccab4e1f77044935a972dd48ef8 100644 (file)
@@ -909,6 +909,63 @@ static bool stripe_log_range_settled(struct btrfs_block_group *bg, u64 bytenr)
        return settled;
 }
 
+/*
+ * Remember, on the logging inode, a logged extent that lives in the final
+ * partial stripe of one of the inode's recently closed runs, so that the
+ * next fsync can re-dirty its file range and carry the data forward into
+ * the then-current run -- letting the old partial stripe free whole.  A
+ * zero @file_len means the caller does not want this extent carried
+ * (e.g. compressed extents, which would recompress).  @ps_len is nonzero
+ * when the settling walk could see the run and computed its final partial
+ * stripe.  Every table here is small and bounded; on overflow the extent
+ * simply is not carried and its stripe frees the slow way.  Caller holds
+ * the inode's log_mutex, which serializes against the fsync-entry consumer.
+ */
+static void log_carry_record(struct btrfs_inode *inode, u64 file_start,
+                            u64 file_len, u64 bytenr, u64 num_bytes,
+                            u64 ps_start, u64 ps_len)
+{
+       struct btrfs_log_carry *carry = inode->log_carry;
+       int n, i;
+
+       if (!carry) {
+               carry = kzalloc(sizeof(*carry), GFP_NOFS);
+               if (!carry)
+                       return;
+               inode->log_carry = carry;
+       }
+       if (ps_len) {
+               n = min_t(int, carry->nr_ps, BTRFS_LOG_CARRY_NR_PS);
+               for (i = 0; i < n; i++)
+                       if (carry->ps_start[i] == ps_start)
+                               break;
+               if (i == n) {
+                       int slot = carry->nr_ps % BTRFS_LOG_CARRY_NR_PS;
+
+                       carry->ps_start[slot] = ps_start;
+                       carry->ps_len[slot] = ps_len;
+                       carry->nr_ps++;
+               }
+       }
+       if (!file_len || carry->nr_ranges >= BTRFS_LOG_CARRY_NR_RANGES)
+               return;
+       n = min_t(int, carry->nr_ps, BTRFS_LOG_CARRY_NR_PS);
+       for (i = 0; i < n; i++)
+               if (bytenr < carry->ps_start[i] + carry->ps_len[i] &&
+                   bytenr + num_bytes > carry->ps_start[i])
+                       break;
+       if (i == n)
+               return;
+       for (i = 0; i < carry->nr_ranges; i++)
+               if (carry->range[i].start == file_start &&
+                   carry->range[i].len == file_len)
+                       return;
+       carry->range[carry->nr_ranges].start = file_start;
+       carry->range[carry->nr_ranges].len = file_len;
+       carry->range[carry->nr_ranges].disk_bytenr = bytenr;
+       carry->nr_ranges++;
+}
+
 /*
  * 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
@@ -923,10 +980,15 @@ static bool stripe_log_range_settled(struct btrfs_block_group *bg, u64 bytenr)
  * the wait is bio flight time plus the parked-write deadline that the flush
  * below short-circuits.  It is a pure data-IO wait (no transaction joins),
  * safe under the inode log mutex and a running transaction handle.
+ *
+ * [file_start, file_start + file_len) is the extent's file range, recorded
+ * for carry-forward when the extent sits in the run's final partial stripe;
+ * file_len == 0 opts the extent out of carrying (settling still happens).
  */
-void btrfs_log_settle_stripes(struct btrfs_fs_info *fs_info, u64 bytenr,
-                             u64 num_bytes)
+void btrfs_log_settle_stripes(struct btrfs_inode *inode, u64 file_start,
+                             u64 file_len, u64 bytenr, u64 num_bytes)
 {
+       struct btrfs_fs_info *fs_info = inode->root->fs_info;
        struct btrfs_block_group *bg;
        struct btrfs_open_stripe_run *run;
        unsigned long flags;
@@ -934,6 +996,8 @@ void btrfs_log_settle_stripes(struct btrfs_fs_info *fs_info, u64 bytenr,
        u64 tail_len = 0;
        u64 flush_start = 0;
        u64 flush_len = 0;
+       u64 ps_start = 0;
+       u64 ps_len = 0;
 
        if (list_empty_careful(&fs_info->open_stripe_bgs))
                return;
@@ -943,10 +1007,24 @@ void btrfs_log_settle_stripes(struct btrfs_fs_info *fs_info, u64 bytenr,
 
        spin_lock_irqsave(&bg->stripe_run_lock, flags);
        list_for_each_entry(run, &bg->open_stripe_runs, list) {
+               u64 filled;
+
                if (bytenr < run->start || bytenr >= run->offset)
                        continue;
                flush_start = run->start;
                flush_len = run->end - run->start;
+               /*
+                * The run's final partial stripe, whether this call closes
+                * the run or an earlier settle already did (offset froze at
+                * close).  Runs start stripe-aligned, so alignment relative
+                * to run->start is stripe alignment.
+                */
+               filled = run->offset - run->start;
+               if (!IS_ALIGNED(filled, bg->full_stripe_len)) {
+                       ps_start = run->start +
+                               round_down(filled, bg->full_stripe_len);
+                       ps_len = bg->full_stripe_len;
+               }
                if (run->open) {
                        int class;
                        int band;
@@ -961,6 +1039,9 @@ void btrfs_log_settle_stripes(struct btrfs_fs_info *fs_info, u64 bytenr,
        }
        spin_unlock_irqrestore(&bg->stripe_run_lock, flags);
 
+       log_carry_record(inode, file_start, file_len, bytenr, num_bytes,
+                        ps_start, ps_len);
+
        if (tail_len)
                btrfs_add_free_space(bg, tail_start, tail_len);
        if (flush_len) {
index 466abd3f2fa154ecf89c56c546fd024d8430cfff..6343635470425256b09113b5b008dcd56e7a2866 100644 (file)
@@ -405,8 +405,8 @@ void btrfs_retire_block_group_stripes(struct btrfs_block_group *bg);
 bool btrfs_stripe_in_open_run(struct btrfs_fs_info *fs_info, u64 logical);
 bool btrfs_stripe_run_range_usable(struct btrfs_block_group *bg,
                                   u64 run_start, u64 *run_len);
-void btrfs_log_settle_stripes(struct btrfs_fs_info *fs_info, u64 bytenr,
-                             u64 num_bytes);
+void btrfs_log_settle_stripes(struct btrfs_inode *inode, u64 file_start,
+                             u64 file_len, u64 bytenr, u64 num_bytes);
 int btrfs_stripe_alloc_check_support(struct btrfs_fs_info *fs_info);
 int btrfs_enable_stripe_alloc(struct btrfs_fs_info *fs_info);
 void btrfs_disable_stripe_alloc(struct btrfs_fs_info *fs_info);
index 9797d85d77844ff2b5b46d02f58da51c6aa8dff9..58164328b7f596749c2de7904fac8bbbad5a9e31 100644 (file)
@@ -135,6 +135,30 @@ enum {
 };
 
 /* in memory btrfs inode */
+/*
+ * Carry-forward bookkeeping for a log-active inode under stripe-exclusive
+ * allocation.  ps_start/ps_len remember the final partial stripes of the
+ * inode's recently closed private runs (a small ring; older entries are
+ * overwritten).  range[] records the file ranges of logged extents living
+ * in those partial stripes, for the next fsync to re-dirty; disk_bytenr
+ * lets the consumer verify the range still maps there before carrying.
+ * Bounded loss: when the ring or the range table overflows, the extra
+ * extents simply stay put and their stripes free the slow way.
+ */
+#define BTRFS_LOG_CARRY_NR_PS 4
+#define BTRFS_LOG_CARRY_NR_RANGES 48
+struct btrfs_log_carry {
+       int nr_ps;
+       u64 ps_start[BTRFS_LOG_CARRY_NR_PS];
+       u64 ps_len[BTRFS_LOG_CARRY_NR_PS];
+       int nr_ranges;
+       struct {
+               u64 start;
+               u64 len;
+               u64 disk_bytenr;
+       } range[BTRFS_LOG_CARRY_NR_RANGES];
+};
+
 struct btrfs_inode {
        /* which subvolume this inode belongs to */
        struct btrfs_root *root;
@@ -332,6 +356,17 @@ struct btrfs_inode {
                u64 ref_root_id;
        };
 
+       /*
+        * Carry-forward state for a log-active inode (see
+        * BTRFS_INODE_LOG_ALLOC): the file ranges of this inode's extents
+        * that were left in the final partial stripe of a private stripe run
+        * closed at a log commit.  The next fsync re-dirties them so they
+        * COW forward into the current run, letting the old partial stripe
+        * free whole.  Lazily allocated; both recording (during logging) and
+        * consuming (at fsync entry) run under log_mutex.
+        */
+       struct btrfs_log_carry *log_carry;
+
        /*
         * Start of the last extent allocated from this inode's private
         * LOG-class stripe run (see BTRFS_INODE_LOG_ALLOC).  Advisory only:
index 2e3c011d410a6f3b36ae37bceb84427149d38434..f0b5adba66a04cb08de1b3cbba4765ca79d5d180 100644 (file)
@@ -1269,6 +1269,157 @@ free_folios:
        return ret;
 }
 
+/*
+ * Carry-forward for log-active inodes under stripe-exclusive allocation
+ * (see struct btrfs_log_carry).  Re-dirty a file range recorded at the
+ * last log commit so the coming writeback COWs it into the inode's
+ * current private stripe run and the old partial stripe can free whole.
+ * The range is carried only while it still fully maps to the recorded
+ * disk bytenr; a range that was rewritten, truncated, hole-punched or
+ * already carried fails that check and is skipped -- skipping only costs
+ * the space optimization, never correctness.  Modeled on
+ * defrag_one_range().
+ */
+static int carry_one_range(struct btrfs_inode *inode, u64 start, u64 len,
+                          u64 disk_bytenr)
+{
+       struct btrfs_fs_info *fs_info = inode->root->fs_info;
+       struct extent_changeset *data_reserved = NULL;
+       struct extent_state *cached_state = NULL;
+       struct extent_map *em;
+       struct folio **folios;
+       const unsigned int nr_pages = ((start + len - 1) >> PAGE_SHIFT) -
+                                     (start >> PAGE_SHIFT) + 1;
+       u64 cur;
+       int ret = 0;
+
+       ASSERT(IS_ALIGNED(start, fs_info->sectorsize) &&
+              IS_ALIGNED(len, fs_info->sectorsize));
+
+       folios = kzalloc_objs(struct folio *, nr_pages, GFP_NOFS);
+       if (!folios)
+               return -ENOMEM;
+
+       cur = start;
+       for (int i = 0; cur < start + len && i < nr_pages; i++) {
+               folios[i] = defrag_prepare_one_folio(inode, cur >> PAGE_SHIFT);
+               if (IS_ERR(folios[i])) {
+                       ret = PTR_ERR(folios[i]);
+                       folios[i] = NULL;
+                       goto free_folios;
+               }
+               cur = folio_pos(folios[i]) + folio_size(folios[i]);
+       }
+       for (int i = 0; i < nr_pages && folios[i]; i++)
+               folio_wait_writeback(folios[i]);
+
+       ASSERT(folios[0]);
+       btrfs_lock_extent(&inode->io_tree, folio_pos(folios[0]), cur - 1,
+                         &cached_state);
+       /* Still fully mapped to the recorded location? */
+       em = btrfs_get_extent(inode, NULL, start, len);
+       if (IS_ERR(em)) {
+               ret = PTR_ERR(em);
+               goto unlock_extent;
+       }
+       if (em->disk_bytenr >= EXTENT_MAP_LAST_BYTE ||
+           (em->flags & EXTENT_FLAG_PREALLOC) ||
+           btrfs_extent_map_compression(em) != BTRFS_COMPRESS_NONE ||
+           em->start > start ||
+           em->start + em->len < start + len ||
+           btrfs_extent_map_block_start(em) + (start - em->start) !=
+               disk_bytenr) {
+               btrfs_free_extent_map(em);
+               goto unlock_extent;
+       }
+       btrfs_free_extent_map(em);
+
+       /*
+        * We hold the folio locks and the extent lock, so a flushing
+        * (ticketed) reservation can deadlock: satisfying the ticket needs
+        * delalloc writeback -- or a transaction commit, which with
+        * flushoncommit waits for that writeback -- and writeback of this
+        * range blocks in __folio_lock on the folios held here.  Reserve
+        * with NO_FLUSH; carrying is optional, so on ENOSPC the tail just
+        * stays in its old stripe (see btrfs_carry_log_tail).
+        */
+       ret = btrfs_check_data_free_space(inode, &data_reserved, start, len,
+                                         true);
+       if (ret < 0)
+               goto unlock_extent;
+       ret = btrfs_delalloc_reserve_metadata(inode, len, len, true);
+       if (ret < 0) {
+               btrfs_free_reserved_data_space(inode, data_reserved, start,
+                                              len);
+               extent_changeset_free(data_reserved);
+               goto unlock_extent;
+       }
+       btrfs_clear_extent_bit(&inode->io_tree, start, start + len - 1,
+                              EXTENT_DELALLOC | EXTENT_DO_ACCOUNTING |
+                              EXTENT_DEFRAG, &cached_state);
+       btrfs_set_extent_bit(&inode->io_tree, start, start + len - 1,
+                            EXTENT_DELALLOC, &cached_state);
+       for (int i = 0; i < nr_pages && folios[i]; i++) {
+               struct folio *folio = folios[i];
+
+               if (start >= folio_pos(folio) + folio_size(folio) ||
+                   start + len <= folio_pos(folio))
+                       continue;
+               btrfs_folio_clamp_set_dirty(fs_info, folio, start, len);
+       }
+       btrfs_delalloc_release_extents(inode, len);
+       extent_changeset_free(data_reserved);
+unlock_extent:
+       btrfs_unlock_extent(&inode->io_tree, folio_pos(folios[0]), cur - 1,
+                           &cached_state);
+free_folios:
+       for (int i = 0; i < nr_pages; i++) {
+               if (!folios[i])
+                       break;
+               folio_unlock(folios[i]);
+               folio_put(folios[i]);
+       }
+       kfree(folios);
+       return ret;
+}
+
+/*
+ * Consume the inode's carry table (see struct btrfs_log_carry): re-dirty
+ * every recorded range so the caller's upcoming writeback carries them
+ * into the inode's current private stripe run.  Called at fsync entry
+ * with the inode locked; takes log_mutex only to detach the table, so
+ * the logging side never records into a half-consumed list.  All
+ * failures are soft: an uncarried range just leaves its old stripe to
+ * free the slow way.
+ */
+void btrfs_carry_log_tail(struct btrfs_inode *inode)
+{
+       struct btrfs_log_carry *copy;
+
+       if (!btrfs_test_opt(inode->root->fs_info, STRIPE_ALLOC))
+               return;
+       if (!READ_ONCE(inode->log_carry))
+               return;
+       copy = kmalloc(sizeof(*copy), GFP_NOFS);
+       if (!copy)
+               return;
+       mutex_lock(&inode->log_mutex);
+       if (!inode->log_carry || !inode->log_carry->nr_ranges) {
+               mutex_unlock(&inode->log_mutex);
+               kfree(copy);
+               return;
+       }
+       memcpy(copy, inode->log_carry, sizeof(*copy));
+       inode->log_carry->nr_ranges = 0;
+       mutex_unlock(&inode->log_mutex);
+
+       for (int i = 0; i < copy->nr_ranges; i++)
+               carry_one_range(inode, copy->range[i].start,
+                               copy->range[i].len,
+                               copy->range[i].disk_bytenr);
+       kfree(copy);
+}
+
 static int defrag_one_cluster(struct btrfs_inode *inode,
                              struct file_ra_state *ra,
                              u64 start, u32 len, u32 extent_thresh,
index a7f917a38dbfb75ed9d5452d95dbcbd1be3c0d9f..0b2a1241518931854d29664bba5ffc7e57f284a8 100644 (file)
@@ -16,6 +16,7 @@ struct btrfs_ioctl_defrag_range_args;
 int btrfs_defrag_file(struct btrfs_inode *inode, struct file_ra_state *ra,
                      struct btrfs_ioctl_defrag_range_args *range,
                      u64 newer_than, unsigned long max_to_defrag);
+void btrfs_carry_log_tail(struct btrfs_inode *inode);
 int __init btrfs_auto_defrag_init(void);
 void __cold btrfs_auto_defrag_exit(void);
 void btrfs_add_inode_defrag(struct btrfs_inode *inode, u32 extent_thresh);
index 3c8b5fdb63f4231349693c7b494fafd21d35710d..804c4de613c391ffbd7e45db96f63a5b1b8a96cb 100644 (file)
@@ -34,6 +34,7 @@
 #include "extent-tree.h"
 #include "file-item.h"
 #include "ioctl.h"
+#include "defrag.h"
 #include "file.h"
 #include "super.h"
 #include "print-tree.h"
@@ -1610,6 +1611,15 @@ int btrfs_sync_file(struct file *file, loff_t start, loff_t end, int datasync)
 
        atomic_inc(&root->log_batch);
 
+       /*
+        * Re-dirty the tail ranges recorded at the last log commit so the
+        * writeback below carries them into the inode's current private
+        * stripe run, letting the old partial stripe free whole (see
+        * struct btrfs_log_carry).  Purely a space optimization: a range
+        * that cannot be carried is simply skipped.
+        */
+       btrfs_carry_log_tail(inode);
+
        /*
         * Before we acquired the inode's lock and the mmap lock, someone may
         * have dirtied more pages in the target range. We need to make sure
index d775564a235e8feb27cbd0ea9ee30a276d893c17..98ee8ea075232d0e52d85df9186c919367b1f16c 100644 (file)
@@ -8042,6 +8042,7 @@ struct inode *btrfs_alloc_inode(struct super_block *sb)
        ei->last_reflink_trans = 0;
        ei->last_log_commit = 0;
        ei->log_run_hint = 0;
+       ei->log_carry = NULL;
 
        spin_lock_init(&ei->lock);
        ei->outstanding_extents = 0;
@@ -8081,6 +8082,7 @@ struct inode *btrfs_alloc_inode(struct super_block *sb)
 void btrfs_test_destroy_inode(struct inode *inode)
 {
        btrfs_drop_extent_map_range(BTRFS_I(inode), 0, (u64)-1, false);
+       kfree(BTRFS_I(inode)->log_carry);
        kfree(BTRFS_I(inode)->file_extent_tree);
        kmem_cache_free(btrfs_inode_cachep, BTRFS_I(inode));
 }
@@ -8088,6 +8090,7 @@ void btrfs_test_destroy_inode(struct inode *inode)
 
 void btrfs_free_inode(struct inode *inode)
 {
+       kfree(BTRFS_I(inode)->log_carry);
        kfree(BTRFS_I(inode)->file_extent_tree);
        kmem_cache_free(btrfs_inode_cachep, BTRFS_I(inode));
 }
index 1e73caf58e171e19134437715ca4e5a33a599826..e9c24ab6b330f49afbfd73118bf1de1b34d337d0 100644 (file)
@@ -4877,9 +4877,12 @@ static noinline int copy_items(struct btrfs_trans_handle *trans,
                        disk_num_bytes =
                                btrfs_file_extent_disk_num_bytes(src, extent);
                        if (disk_bytenr)
-                               btrfs_log_settle_stripes(trans->fs_info,
-                                                        disk_bytenr,
-                                                        disk_num_bytes);
+                               btrfs_log_settle_stripes(inode,
+                                       ins_keys[dst_index].offset,
+                                       btrfs_file_extent_compression(src, extent) ?
+                                               0 :
+                                               btrfs_file_extent_num_bytes(src, extent),
+                                       disk_bytenr, disk_num_bytes);
                }
 
                if (skip_csum)
@@ -5165,8 +5168,10 @@ static int log_one_extent(struct btrfs_trans_handle *trans,
         */
        if (!(em->flags & EXTENT_FLAG_PREALLOC) &&
            em->disk_bytenr < EXTENT_MAP_LAST_BYTE)
-               btrfs_log_settle_stripes(inode->root->fs_info, em->disk_bytenr,
-                                        em->disk_num_bytes);
+               btrfs_log_settle_stripes(inode, em->start,
+                                        compress_type == BTRFS_COMPRESS_NONE ?
+                                               em->len : 0,
+                                        em->disk_bytenr, em->disk_num_bytes);
 
        if (compress_type != BTRFS_COMPRESS_NONE) {
                btrfs_set_stack_file_extent_disk_bytenr(&fi, block_start);