From b07fe9a3ff5dc2d49358c2e18b268eb2307e1152 Mon Sep 17 00:00:00 2001 From: Zygo Blaxell Date: Sat, 25 Jul 2026 01:32:11 -0400 Subject: [PATCH] btrfs: report stripe run data IO through the ordered extent lifecycle Pair every byte reserved from an open stripe run with exactly one "write done" report, so commit-time retirement can wait for all data IO into a window's stripes: - Ordered extents get a stripe_run pointer, attached at creation by a range lookup (cheap: gated on the fs having any stripe runs at all, and a run's block group membership is established before its allocation returns, so the gate cannot miss). NOCOW and PREALLOC ordered extents write into previously allocated extents, which can never lie inside a run claimed from fully-free stripes, and are skipped. The report fires once at IO completion (the IO_DONE moment in can_finish_ordered_extent(), before any transaction join, so the commit-time drain can never deadlock on a blocked join), with a catch-all when an ordered extent is freed without completing IO. - Reservations freed without an ordered extent ever owning them (error paths) report through btrfs_free_reserved_extent(); the one caller that frees a range an ordered extent did own (the finish-error path) uses btrfs_free_reserved_extent_ordered() to avoid double reporting. - Preallocated extents never issue data IO and report at insertion; this also covers relocation's data inode preallocations. Assisted-by: Claude:claude-fable-5 --- fs/btrfs/block-group.c | 51 ++++++++++++++++++++++++++++++++++++++ fs/btrfs/block-group.h | 4 +++ fs/btrfs/extent-tree.c | 55 +++++++++++++++++++++++++++++++++++++++-- fs/btrfs/extent-tree.h | 2 ++ fs/btrfs/inode.c | 17 ++++++++++--- fs/btrfs/ordered-data.c | 35 ++++++++++++++++++++++++++ fs/btrfs/ordered-data.h | 9 +++++++ 7 files changed, 168 insertions(+), 5 deletions(-) diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index 07434bae806f6..cce6c50cb60ce 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -654,6 +654,57 @@ void btrfs_open_stripe_write_done(struct btrfs_block_group *bg, u64 start, spin_unlock_irqrestore(&bg->stripe_run_lock, flags); } +/* + * Range variant of btrfs_open_stripe_write_done() for callers without the + * block group at hand (e.g. the prealloc path, which never issues data IO + * for its extent and reports it done at insertion). + */ +void btrfs_open_stripe_write_done_bytenr(struct btrfs_fs_info *fs_info, + u64 start, u64 num_bytes) +{ + struct btrfs_block_group *bg; + + if (list_empty_careful(&fs_info->open_stripe_bgs)) + return; + bg = btrfs_lookup_block_group(fs_info, start); + if (!bg) + return; + btrfs_open_stripe_write_done(bg, start, num_bytes); + btrfs_put_block_group(bg); +} + +/* + * Find the open stripe run whose allocated region contains + * [bytenr, bytenr + num_bytes). The caller must own unreported inflight + * bytes in that region (i.e. it was allocated from the run and not yet + * reported done), which is what keeps the returned pointer valid: a run + * with inflight bytes is never freed. + */ +struct btrfs_open_stripe_run *btrfs_get_open_stripe_run( + struct btrfs_fs_info *fs_info, u64 bytenr, u64 num_bytes) +{ + struct btrfs_open_stripe_run *run = NULL; + struct btrfs_open_stripe_run *iter; + struct btrfs_block_group *bg; + unsigned long flags; + + bg = btrfs_lookup_block_group(fs_info, bytenr); + if (!bg) + return NULL; + spin_lock_irqsave(&bg->stripe_run_lock, flags); + list_for_each_entry(iter, &bg->open_stripe_runs, list) { + if (bytenr >= iter->start && bytenr < iter->offset) { + ASSERT(bytenr + num_bytes <= iter->offset); + ASSERT(iter->inflight_bytes >= num_bytes); + run = iter; + break; + } + } + spin_unlock_irqrestore(&bg->stripe_run_lock, flags); + btrfs_put_block_group(bg); + return run; +} + /* * Pointer-based variant of btrfs_open_stripe_write_done() for callers that * recorded the run at allocation or ordered extent creation time; safe from diff --git a/fs/btrfs/block-group.h b/fs/btrfs/block-group.h index 16d07db9095f8..f3318d6ed4187 100644 --- a/fs/btrfs/block-group.h +++ b/fs/btrfs/block-group.h @@ -348,6 +348,10 @@ 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, u64 num_bytes); +void btrfs_open_stripe_write_done_bytenr(struct btrfs_fs_info *fs_info, + u64 start, u64 num_bytes); +struct btrfs_open_stripe_run *btrfs_get_open_stripe_run( + struct btrfs_fs_info *fs_info, u64 bytenr, u64 num_bytes); void btrfs_close_bg_open_stripes(struct btrfs_block_group *bg); void btrfs_retire_block_group_stripes(struct btrfs_block_group *bg); void btrfs_retire_open_stripes(struct btrfs_fs_info *fs_info); diff --git a/fs/btrfs/extent-tree.c b/fs/btrfs/extent-tree.c index c5035a5a06364..58602b21fd73b 100644 --- a/fs/btrfs/extent-tree.c +++ b/fs/btrfs/extent-tree.c @@ -4067,6 +4067,25 @@ static int do_allocation_stripe(struct btrfs_block_group *block_group, return 0; } +/* + * Undo a stripe-policy allocation that find_free_extent() is discarding + * before it is handed to the caller (allocation would cross the block group + * end, or btrfs_add_reserved_bytes() raced and returned -EAGAIN). The bytes + * were counted as inflight in the block group's open stripe run at allocation + * time; report them done so the run's inflight accounting stays balanced, + * otherwise a commit-time stripe drain waits on them forever. The bytes are + * returned to the free space cache by the caller's btrfs_add_free_space_unused() + * and, being inside a partially filled stripe, stay unclaimable -- exactly + * like a hole left by a deleted extent. + */ +static void undo_stripe_allocation(struct btrfs_block_group *block_group, + struct find_free_extent_ctl *ffe_ctl) +{ + if (btrfs_is_stripe_alloc_bg(block_group)) + btrfs_open_stripe_write_done(block_group, ffe_ctl->found_offset, + ffe_ctl->num_bytes); +} + static int do_allocation(struct btrfs_block_group *block_group, struct find_free_extent_ctl *ffe_ctl, struct btrfs_block_group **bg_ret) @@ -4625,6 +4644,7 @@ have_block_group: /* move on to the next group */ if (ffe_ctl->search_start + ffe_ctl->num_bytes > block_group->start + block_group->length) { + undo_stripe_allocation(block_group, ffe_ctl); btrfs_add_free_space_unused(block_group, ffe_ctl->found_offset, ffe_ctl->num_bytes); @@ -4641,6 +4661,7 @@ have_block_group: ffe_ctl->delalloc, ffe_ctl->loop >= LOOP_WRONG_SIZE_CLASS); if (ret == -EAGAIN) { + undo_stripe_allocation(block_group, ffe_ctl); btrfs_add_free_space_unused(block_group, ffe_ctl->found_offset, ffe_ctl->num_bytes); @@ -4793,8 +4814,9 @@ again: return ret; } -int btrfs_free_reserved_extent(struct btrfs_fs_info *fs_info, u64 start, u64 len, - bool is_delalloc) +static int __btrfs_free_reserved_extent(struct btrfs_fs_info *fs_info, + u64 start, u64 len, bool is_delalloc, + bool stripe_reported) { struct btrfs_block_group *cache; @@ -4805,6 +4827,15 @@ int btrfs_free_reserved_extent(struct btrfs_fs_info *fs_info, u64 start, u64 len return -ENOSPC; } + /* + * A freed reservation from a stripe run will never be written; report + * it so the run's inflight accounting stays paired with the reserve. + * Skipped when an ordered extent owned the range: its IO completion + * (or teardown) already reported these bytes. + */ + if (!stripe_reported) + btrfs_open_stripe_write_done(cache, start, len); + btrfs_add_free_space(cache, start, len); btrfs_free_reserved_bytes(cache, len, is_delalloc); trace_btrfs_reserved_extent_free(fs_info, start, len); @@ -4813,6 +4844,26 @@ int btrfs_free_reserved_extent(struct btrfs_fs_info *fs_info, u64 start, u64 len return 0; } +int btrfs_free_reserved_extent(struct btrfs_fs_info *fs_info, u64 start, u64 len, + bool is_delalloc) +{ + return __btrfs_free_reserved_extent(fs_info, start, len, is_delalloc, + false); +} + +/* + * As btrfs_free_reserved_extent(), for ranges whose bytes were already + * reported to the stripe allocator: either an ordered extent owned the + * range (its lifecycle reports the data IO), or it was a preallocation + * (reported immediately after the reserve). + */ +int btrfs_free_reserved_extent_reported(struct btrfs_fs_info *fs_info, + u64 start, u64 len, bool is_delalloc) +{ + return __btrfs_free_reserved_extent(fs_info, start, len, is_delalloc, + true); +} + int btrfs_pin_reserved_extent(struct btrfs_trans_handle *trans, const struct extent_buffer *eb) { diff --git a/fs/btrfs/extent-tree.h b/fs/btrfs/extent-tree.h index e970ac42a871a..f37a307714b09 100644 --- a/fs/btrfs/extent-tree.h +++ b/fs/btrfs/extent-tree.h @@ -151,6 +151,8 @@ u64 btrfs_get_extent_owner_root(struct btrfs_fs_info *fs_info, struct extent_buffer *leaf, int slot); int btrfs_free_reserved_extent(struct btrfs_fs_info *fs_info, u64 start, u64 len, bool is_delalloc); +int btrfs_free_reserved_extent_reported(struct btrfs_fs_info *fs_info, + u64 start, u64 len, bool is_delalloc); int btrfs_pin_reserved_extent(struct btrfs_trans_handle *trans, const struct extent_buffer *eb); int btrfs_finish_extent_commit(struct btrfs_trans_handle *trans); diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index cca9840bee896..79b43277a4a6f 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -3361,7 +3361,7 @@ out: ordered_extent->disk_bytenr, ordered_extent->disk_num_bytes, NULL); - btrfs_free_reserved_extent(fs_info, + btrfs_free_reserved_extent_reported(fs_info, ordered_extent->disk_bytenr, ordered_extent->disk_num_bytes, true); /* @@ -9161,6 +9161,17 @@ static int __btrfs_prealloc_file_range(struct inode *inode, int mode, min_size, 0, *alloc_hint, &ins, 1, 0); if (ret) break; + /* + * A preallocated extent never issues data IO, so report it to + * the stripe allocator right away. This must happen before + * anything that can wait for a transaction (the file extent + * insertion below starts one): a commit's stripe retirement + * waits for unreported bytes, so holding them across a + * transaction join would deadlock. The error paths below + * must use the _reported variant of the reservation free. + */ + btrfs_open_stripe_write_done_bytenr(fs_info, ins.objectid, + ins.offset); /* * We've reserved this space, and thus converted it from @@ -9183,8 +9194,8 @@ static int __btrfs_prealloc_file_range(struct inode *inode, int mode, btrfs_dec_block_group_reservations(fs_info, ins.objectid); if (IS_ERR(trans)) { ret = PTR_ERR(trans); - btrfs_free_reserved_extent(fs_info, ins.objectid, - ins.offset, false); + btrfs_free_reserved_extent_reported(fs_info, + ins.objectid, ins.offset, false); break; } diff --git a/fs/btrfs/ordered-data.c b/fs/btrfs/ordered-data.c index b3ed4212bfecd..5356c8329273c 100644 --- a/fs/btrfs/ordered-data.c +++ b/fs/btrfs/ordered-data.c @@ -199,6 +199,22 @@ static struct btrfs_ordered_extent *alloc_ordered_extent( init_waitqueue_head(&entry->wait); INIT_LIST_HEAD(&entry->list); INIT_LIST_HEAD(&entry->log_list); + + /* + * Attach the open stripe run backing this extent's disk range, if + * any, so its data IO can be reported to the stripe allocator. The + * list check makes this free when no stripe runs exist; a run's + * block group membership is established before the allocation + * returns, so an ordered extent for a stripe-run allocation cannot + * observe an empty list. NOCOW and PREALLOC ordered extents write + * to previously allocated extents, which can never sit inside a + * stripe run (runs are claimed from fully-free stripes). + */ + if (!is_nocow && + !list_empty_careful(&inode->root->fs_info->open_stripe_bgs)) + entry->stripe_run = btrfs_get_open_stripe_run( + inode->root->fs_info, disk_bytenr, + disk_num_bytes); INIT_LIST_HEAD(&entry->root_extent_list); INIT_LIST_HEAD(&entry->work_list); INIT_LIST_HEAD(&entry->bioc_list); @@ -359,6 +375,22 @@ static void finish_ordered_fn(struct btrfs_work *work) btrfs_finish_ordered_io(ordered_extent); } +/* + * Report this ordered extent's data IO to its open stripe run, exactly + * once. Called at IO completion, and as a catch-all when an ordered + * extent is freed without ever completing its IO (error teardown); either + * way the stripe allocator must stop counting these bytes as in flight. + */ +static void btrfs_ordered_stripe_write_done(struct btrfs_ordered_extent *ordered) +{ + struct btrfs_open_stripe_run *run = ordered->stripe_run; + + if (!run) + return; + ordered->stripe_run = NULL; + btrfs_open_stripe_write_done_run(run, ordered->disk_num_bytes); +} + static bool can_finish_ordered_extent(struct btrfs_ordered_extent *ordered, u64 file_offset, u64 len, bool uptodate) { @@ -390,6 +422,7 @@ static bool can_finish_ordered_extent(struct btrfs_ordered_extent *ordered, * the finish_func to be executed. */ set_bit(BTRFS_ORDERED_IO_DONE, &ordered->flags); + btrfs_ordered_stripe_write_done(ordered); cond_wake_up(&ordered->wait); refcount_inc(&ordered->refs); trace_btrfs_ordered_extent_mark_finished(inode, ordered); @@ -625,6 +658,8 @@ void btrfs_put_ordered_extent(struct btrfs_ordered_extent *entry) ASSERT(list_empty(&entry->root_extent_list)); ASSERT(list_empty(&entry->log_list)); ASSERT(RB_EMPTY_NODE(&entry->rb_node)); + /* Catch-all for extents torn down without completing IO. */ + btrfs_ordered_stripe_write_done(entry); btrfs_add_delayed_iput(entry->inode); list_for_each_entry_safe(sum, tmp, &entry->list, list) kvfree(sum); diff --git a/fs/btrfs/ordered-data.h b/fs/btrfs/ordered-data.h index 2e7879c6cbc77..e416aa4e8fa23 100644 --- a/fs/btrfs/ordered-data.h +++ b/fs/btrfs/ordered-data.h @@ -19,6 +19,7 @@ struct page; struct extent_state; struct btrfs_block_group; struct btrfs_inode; +struct btrfs_open_stripe_run; struct btrfs_root; struct btrfs_fs_info; @@ -121,6 +122,14 @@ struct btrfs_ordered_extent { /* flags (described above) */ unsigned long flags; + /* + * The open stripe run backing this extent's disk range, if it was + * allocated by the raid56 stripe allocation policy; this extent's + * data IO is reported to it exactly once (at IO completion, or when + * the ordered extent is freed without completing IO). + */ + struct btrfs_open_stripe_run *stripe_run; + /* compression algorithm */ int compress_type; -- 2.53.0