From 58948c83bcda8c0f9df292bf1a4728d8d0248cd9 Mon Sep 17 00:00:00 2001 From: Zygo Blaxell Date: Sat, 12 Sep 2026 01:41:46 -0400 Subject: [PATCH] btrfs: stripe_alloc: hold the probe margin until the metadata reservation charges it btrfs_check_data_free_space() admits a write for its bytes plus a whole-stripe margin per extent, then releases the margin at once, on the understanding that btrfs_delalloc_reserve_metadata() re-charges it into bytes_stripe_margin moments later. Between the two nothing holds it: at the fill edge, eight fsstress writers were admitted into each other's released margins, and the stripes their claims then needed had already gone to their neighbours. The failure-time dump showed exactly that shape -- claimable 0, bytes_may_use holding only the failing write, one stripe of margin per outstanding extent, nothing stranded -- and it survived charging preallocation by whole stripes and holding a stripe across it, because it was never preallocation's stripe. Let the callers that go on to reserve metadata keep the probe's margin in bytes_may_use and release it only after that reservation has charged bytes_stripe_margin: the buffered write, page_mkwrite, the block truncation, the direct IO path (carried in btrfs_dio_data across the two functions), the log-tail carry and btrfs_delalloc_reserve_space(). The one caller that reserves no metadata, the v1 space cache write-out, keeps the immediate release. Assisted-by: Claude:claude-fable-5 Assisted-by: Claude:claude-fable-5-1 --- fs/btrfs/defrag.c | 4 +++- fs/btrfs/delalloc-space.c | 33 +++++++++++++++++++++++++++------ fs/btrfs/delalloc-space.h | 3 ++- fs/btrfs/direct-io.c | 9 ++++++++- fs/btrfs/file.c | 9 +++++++-- fs/btrfs/inode.c | 4 +++- 6 files changed, 50 insertions(+), 12 deletions(-) diff --git a/fs/btrfs/defrag.c b/fs/btrfs/defrag.c index d1345a60e9552..7d3bc7da6bbc7 100644 --- a/fs/btrfs/defrag.c +++ b/fs/btrfs/defrag.c @@ -1280,6 +1280,7 @@ static int carry_one_range(struct btrfs_inode *inode, u64 start, u64 len, const unsigned int nr_pages = ((start + len - 1) >> PAGE_SHIFT) - (start >> PAGE_SHIFT) + 1; u64 cur; + u64 held = 0; int ret = 0; ASSERT(IS_ALIGNED(start, fs_info->sectorsize) && @@ -1333,10 +1334,11 @@ static int carry_one_range(struct btrfs_inode *inode, u64 start, u64 len, * stays in its old stripe (see btrfs_carry_log_tail). */ ret = btrfs_check_data_free_space(inode, &data_reserved, start, len, - true); + true, &held); if (ret < 0) goto unlock_extent; ret = btrfs_delalloc_reserve_metadata(inode, len, len, true); + btrfs_release_data_margin_probe(inode, held); if (ret < 0) { btrfs_free_reserved_data_space(inode, data_reserved, start, len); diff --git a/fs/btrfs/delalloc-space.c b/fs/btrfs/delalloc-space.c index feaa983a4e6f2..27b492cb0c1d8 100644 --- a/fs/btrfs/delalloc-space.c +++ b/fs/btrfs/delalloc-space.c @@ -144,7 +144,7 @@ int btrfs_alloc_data_chunk_ondemand(const struct btrfs_inode *inode, u64 bytes) int btrfs_check_data_free_space(struct btrfs_inode *inode, struct extent_changeset **reserved, u64 start, - u64 len, bool noflush) + u64 len, bool noflush, u64 *stripe_held) { struct btrfs_fs_info *fs_info = inode->root->fs_info; enum btrfs_reserve_flush_enum flush = BTRFS_RESERVE_FLUSH_DATA; @@ -165,9 +165,15 @@ int btrfs_check_data_free_space(struct btrfs_inode *inode, * 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. + * then re-charged into bytes_stripe_margin by the metadata reservation + * (btrfs_mod_outstanding_extents()), which carries it until writeback. + * A caller that passes @stripe_held keeps the probe's margin in + * bytes_may_use until then and releases it with + * btrfs_release_data_margin_probe() after its metadata reservation: + * released here, the gap between the two let concurrent writers be + * admitted into each other's margin at the fill edge, and their + * writebacks then found no stripe. Without @stripe_held (callers + * that take no metadata reservation) it is released at once. */ if (flush == BTRFS_RESERVE_FLUSH_DATA && btrfs_test_opt(fs_info, STRIPE_ALLOC)) { @@ -181,13 +187,19 @@ int btrfs_check_data_free_space(struct btrfs_inode *inode, len + stripe_margin, flush); if (ret < 0) return ret; - if (stripe_margin) + if (stripe_margin && !stripe_held) btrfs_space_info_free_bytes_may_use(data_sinfo_for_inode(inode), stripe_margin); + if (stripe_held) + *stripe_held = stripe_margin; /* Use new btrfs_qgroup_reserve_data to reserve precious data space. */ ret = btrfs_qgroup_reserve_data(inode, reserved, start, len); if (ret < 0) { + if (stripe_held) { + btrfs_release_data_margin_probe(inode, stripe_margin); + *stripe_held = 0; + } btrfs_free_reserved_data_space_noquota(inode, len); extent_changeset_free(*reserved); *reserved = NULL; @@ -197,6 +209,13 @@ int btrfs_check_data_free_space(struct btrfs_inode *inode, return ret; } +/* Release the probe margin btrfs_check_data_free_space() handed back in @stripe_held. */ +void btrfs_release_data_margin_probe(struct btrfs_inode *inode, u64 held) +{ + if (held) + btrfs_space_info_free_bytes_may_use(data_sinfo_for_inode(inode), held); +} + /* * Called if we need to clear a data reservation for this inode * Normally in a error case. @@ -520,12 +539,14 @@ void btrfs_delalloc_shrink_extents(struct btrfs_inode *inode, u64 reserved_len, int btrfs_delalloc_reserve_space(struct btrfs_inode *inode, struct extent_changeset **reserved, u64 start, u64 len) { + u64 held = 0; int ret; - ret = btrfs_check_data_free_space(inode, reserved, start, len, false); + ret = btrfs_check_data_free_space(inode, reserved, start, len, false, &held); if (ret < 0) return ret; ret = btrfs_delalloc_reserve_metadata(inode, len, len, false); + btrfs_release_data_margin_probe(inode, held); if (ret < 0) { btrfs_free_reserved_data_space(inode, *reserved, start, len); extent_changeset_free(*reserved); diff --git a/fs/btrfs/delalloc-space.h b/fs/btrfs/delalloc-space.h index 6119c0d3f883c..c464cf0a88d9b 100644 --- a/fs/btrfs/delalloc-space.h +++ b/fs/btrfs/delalloc-space.h @@ -12,7 +12,8 @@ struct btrfs_fs_info; int btrfs_alloc_data_chunk_ondemand(const struct btrfs_inode *inode, u64 bytes); int btrfs_check_data_free_space(struct btrfs_inode *inode, struct extent_changeset **reserved, u64 start, u64 len, - bool noflush); + bool noflush, u64 *stripe_held); +void btrfs_release_data_margin_probe(struct btrfs_inode *inode, u64 held); void btrfs_free_reserved_data_space(struct btrfs_inode *inode, struct extent_changeset *reserved, u64 start, u64 len); void btrfs_delalloc_release_space(struct btrfs_inode *inode, diff --git a/fs/btrfs/direct-io.c b/fs/btrfs/direct-io.c index 673c2ed705551..be5caf29f77b9 100644 --- a/fs/btrfs/direct-io.c +++ b/fs/btrfs/direct-io.c @@ -18,6 +18,8 @@ struct btrfs_dio_data { struct extent_changeset *data_reserved; struct btrfs_ordered_extent *ordered; bool data_space_reserved; + /* stripe_alloc probe margin held in bytes_may_use until the metadata reservation */ + u64 stripe_held; bool nocow_done; bool updated_isize; }; @@ -452,7 +454,8 @@ static int btrfs_dio_iomap_begin(struct inode *inode, loff_t start, if (write && !(flags & IOMAP_NOWAIT)) { ret = btrfs_check_data_free_space(BTRFS_I(inode), &dio_data->data_reserved, - start, data_alloc_len, false); + start, data_alloc_len, false, + &dio_data->stripe_held); if (!ret) dio_data->data_space_reserved = true; else if (!(BTRFS_I(inode)->flags & @@ -539,6 +542,8 @@ static int btrfs_dio_iomap_begin(struct inode *inode, loff_t start, if (write) { ret = btrfs_get_blocks_direct_write(&em, inode, dio_data, start, &len, flags); + btrfs_release_data_margin_probe(BTRFS_I(inode), dio_data->stripe_held); + dio_data->stripe_held = 0; if (ret < 0) goto unlock_err; /* Recalc len in case the new em is smaller than requested */ @@ -609,6 +614,8 @@ unlock_err: btrfs_clear_extent_bit(&BTRFS_I(inode)->io_tree, lockstart, lockend, EXTENT_LOCKED | EXTENT_DIO_LOCKED, &cached_state); err: + btrfs_release_data_margin_probe(BTRFS_I(inode), dio_data->stripe_held); + dio_data->stripe_held = 0; if (dio_data->data_space_reserved) { btrfs_free_reserved_data_space(BTRFS_I(inode), dio_data->data_reserved, diff --git a/fs/btrfs/file.c b/fs/btrfs/file.c index 6db77bed82be7..e67b2aa39dba9 100644 --- a/fs/btrfs/file.c +++ b/fs/btrfs/file.c @@ -1106,9 +1106,11 @@ static ssize_t reserve_space(struct btrfs_inode *inode, const struct btrfs_fs_info *fs_info = inode->root->fs_info; const unsigned int block_offset = (start & (fs_info->sectorsize - 1)); size_t reserve_bytes; + u64 held = 0; int ret; - ret = btrfs_check_data_free_space(inode, data_reserved, start, *len, nowait); + ret = btrfs_check_data_free_space(inode, data_reserved, start, *len, nowait, + &held); if (ret < 0) { int can_nocow; @@ -1133,6 +1135,7 @@ static ssize_t reserve_space(struct btrfs_inode *inode, WARN_ON(reserve_bytes == 0); ret = btrfs_delalloc_reserve_metadata(inode, reserve_bytes, reserve_bytes, nowait); + btrfs_release_data_margin_probe(inode, held); if (ret) { if (!*only_release_metadata) btrfs_free_reserved_data_space(inode, *data_reserved, @@ -1868,6 +1871,7 @@ static vm_fault_t btrfs_page_mkwrite(struct vm_fault *vmf) struct btrfs_fs_info *fs_info = inode->root->fs_info; struct extent_io_tree *io_tree = &inode->io_tree; struct btrfs_ordered_extent *ordered; + u64 held = 0; struct extent_state *cached_state = NULL; struct extent_changeset *data_reserved = NULL; unsigned long zero_start; @@ -1896,7 +1900,7 @@ static vm_fault_t btrfs_page_mkwrite(struct vm_fault *vmf) * being processed by btrfs_page_mkwrite() function. */ ret = btrfs_check_data_free_space(inode, &data_reserved, page_start, - reserved_space, false); + reserved_space, false, &held); if (ret < 0) { size_t write_bytes = reserved_space; @@ -1916,6 +1920,7 @@ static vm_fault_t btrfs_page_mkwrite(struct vm_fault *vmf) } ret = btrfs_delalloc_reserve_metadata(inode, reserved_space, reserved_space, false); + btrfs_release_data_margin_probe(inode, held); if (ret < 0) { if (!only_release_metadata) btrfs_free_reserved_data_space(inode, data_reserved, diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index c2ce5c7905695..24ffc6c820042 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -5087,6 +5087,7 @@ int btrfs_truncate_block(struct btrfs_inode *inode, u64 offset, u64 start, u64 e struct extent_changeset *data_reserved = NULL; bool only_release_metadata = false; u32 blocksize = fs_info->sectorsize; + u64 held = 0; pgoff_t index = (offset >> PAGE_SHIFT); struct folio *folio; gfp_t mask = btrfs_alloc_write_mask(mapping); @@ -5139,7 +5140,7 @@ int btrfs_truncate_block(struct btrfs_inode *inode, u64 offset, u64 start, u64 e block_end = block_start + blocksize - 1; ret = btrfs_check_data_free_space(inode, &data_reserved, block_start, - blocksize, false); + blocksize, false, &held); if (ret < 0) { size_t write_bytes = blocksize; @@ -5153,6 +5154,7 @@ int btrfs_truncate_block(struct btrfs_inode *inode, u64 offset, u64 start, u64 e } } ret = btrfs_delalloc_reserve_metadata(inode, blocksize, blocksize, false); + btrfs_release_data_margin_probe(inode, held); if (ret < 0) { if (!only_release_metadata) btrfs_free_reserved_data_space(inode, data_reserved, -- 2.53.0