]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_alloc: settle a logged extent's stripes at log commit
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Thu, 30 Jul 2026 00:51:24 +0000 (20:51 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Fri, 18 Sep 2026 21:36:21 +0000 (17:36 -0400)
Stripe-exclusive allocation bounds degraded-crash damage to the current
transaction, but within that window two log commits can still share a
stripe: fsync 1 writes the head of a stripe, the open run keeps filling
it, and a later write's read-modify-write rewrites the parity that
protects fsync 1's data.  A degraded crash during the second write tears
the first -- the write hole's shape, confined to the log window, and the
reason the fsync guarantee has so far been "loss is detectable" rather
than "completed fsyncs survive".

Close it with a case analysis.  A logged extent in a full stripe is
already safe: nothing ever writes a full stripe again, since COW never
overwrites and there are no free sectors left to allocate.  A logged
extent in a partial stripe is exposed only to future writes into that
stripe's remaining sectors -- so at log commit, close the stripe's open
run (nothing further allocates into it), kick any parked partial writes
for it, and wait for its in-flight data IO before the log super is
written.  This is the log-window analogue of invariant I2, which the
commit-time retirement provides for full commits.  Both log paths are
hooked: the fast path per extent map in log_one_extent(), and the
full-sync path where copy_items() walks new data extents (old-transaction
extents are skipped there, and are exactly the ones already settled by
their own commit).

The drain is bounded and join-free: run inflight is counted from
allocation, which happens during writeback with submission following in
the same pass, so the wait is bio flight time plus the parked-write
deadline that the flush short-circuits; write_done reporting needs no
transaction join, so waiting under the inode log mutex and a running
transaction handle is safe.

The cost is spatial: every fsync that logs an extent in a partial stripe
retires that stripe early, trapping its unwritten tail like any other
partially filled stripe until it frees or balance reclaims it.
Fsync-heavy workloads therefore burn a stripe tail per touched stripe
per fsync; the follow-up per-inode log runs and copy-forward relocation
exist to reclaim exactly that cost, and are optimizations on top of the
guarantee this patch completes.

Assisted-by: Claude:claude-fable-5
(cherry picked from commit 53c44fdbca58bd1fa267a5648e236ed4238d99ed)

fs/btrfs/block-group.c
fs/btrfs/block-group.h
fs/btrfs/tree-log.c

index 426cae94ad8b16929eeb1a3f75ed41f76fbd2745..619377af36b2c81e3f1a1be3783f31285e80ea30 100644 (file)
@@ -804,6 +804,86 @@ bool btrfs_stripe_run_range_usable(struct btrfs_block_group *bg,
        return true;
 }
 
+/* No open or draining run covers @bytenr any more. */
+static bool stripe_log_range_settled(struct btrfs_block_group *bg, u64 bytenr)
+{
+       struct btrfs_open_stripe_run *run;
+       unsigned long flags;
+       bool settled = true;
+
+       spin_lock_irqsave(&bg->stripe_run_lock, flags);
+       list_for_each_entry(run, &bg->open_stripe_runs, list) {
+               if (bytenr >= run->start && bytenr < run->offset) {
+                       settled = false;
+                       break;
+               }
+       }
+       spin_unlock_irqrestore(&bg->stripe_run_lock, flags);
+       return settled;
+}
+
+/*
+ * 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
+ * extent's stripes -- the log-window analogue of invariant I2, which the
+ * commit-time retirement provides for full commits.  Close the open run
+ * covering the extent so nothing further allocates into its stripes, kick
+ * any parked partial writes for it, and wait for all data IO into it to
+ * finish.  Extents in full or already-settled stripes cost one list walk.
+ *
+ * The drain is bounded: run inflight is counted from allocation, which
+ * happens during writeback with submission following in the same pass, so
+ * 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.
+ */
+void btrfs_log_settle_stripes(struct btrfs_fs_info *fs_info, u64 bytenr,
+                             u64 num_bytes)
+{
+       struct btrfs_block_group *bg;
+       struct btrfs_open_stripe_run *run;
+       unsigned long flags;
+       u64 tail_start = 0;
+       u64 tail_len = 0;
+       u64 flush_start = 0;
+       u64 flush_len = 0;
+
+       if (list_empty_careful(&fs_info->open_stripe_bgs))
+               return;
+       bg = btrfs_lookup_block_group(fs_info, bytenr);
+       if (!bg)
+               return;
+
+       spin_lock_irqsave(&bg->stripe_run_lock, flags);
+       list_for_each_entry(run, &bg->open_stripe_runs, list) {
+               if (bytenr < run->start || bytenr >= run->offset)
+                       continue;
+               flush_start = run->start;
+               flush_len = run->end - run->start;
+               if (run->open) {
+                       int class;
+                       int band;
+
+                       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)
+                                               bg->open_stripe[class][band] = NULL;
+                       tail_len = close_open_stripe_run(bg, run, &tail_start);
+               }
+               break;
+       }
+       spin_unlock_irqrestore(&bg->stripe_run_lock, flags);
+
+       if (tail_len)
+               btrfs_add_free_space(bg, tail_start, tail_len);
+       if (flush_len) {
+               btrfs_flush_parked_rbios(fs_info, flush_start, flush_len);
+               wait_var_event(&bg->open_stripe_runs,
+                              stripe_log_range_settled(bg, bytenr));
+       }
+       btrfs_put_block_group(bg);
+}
+
 /*
  * Can stripe-exclusive allocation be enabled on this filesystem?  Shared by
  * the mount option validation and the "stripe_alloc" filesystem property.
index 6740c658702229cc67130145646d7d943f1c5b94..74528f0175e98b3ff2c61e52c661c1537e0d0dcf 100644 (file)
@@ -405,6 +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);
 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 1d9dfb63fc5a4f2bb372ee0e8d52dad4a54f409b..51aeb3ed680e976770a581bf5a716afbeb9b24c6 100644 (file)
@@ -4847,6 +4847,23 @@ static noinline int copy_items(struct btrfs_trans_handle *trans,
                    inode->last_reflink_trans < trans->transid)
                        continue;
 
+               /*
+                * New data extent made durable by this log commit: settle its
+                * stripes.  This must not hide behind the csum skips below --
+                * a nodatasum inode's extents need their stripes settled all
+                * the same.  Inline extents and holes have no stripes.
+                */
+               if (!is_old_extent &&
+                   btrfs_file_extent_type(src, extent) == BTRFS_FILE_EXTENT_REG) {
+                       disk_bytenr = btrfs_file_extent_disk_bytenr(src, extent);
+                       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);
+               }
+
                if (skip_csum)
                        goto add_to_batch;
 
@@ -5132,6 +5149,18 @@ static int log_one_extent(struct btrfs_trans_handle *trans,
 
        block_len = em->disk_num_bytes;
        compress_type = btrfs_extent_map_compression(em);
+
+       /*
+        * This log commit makes the extent durable: settle its stripes so no
+        * later write in this transaction can tear them (see
+        * btrfs_log_settle_stripes()).  Preallocated extents carry no data
+        * and holes have no stripes; both are skipped.
+        */
+       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);
+
        if (compress_type != BTRFS_COMPRESS_NONE) {
                btrfs_set_stack_file_extent_disk_bytenr(&fi, block_start);
                btrfs_set_stack_file_extent_disk_num_bytes(&fi, block_len);