[PATCH RFC v2] btrfs: keep mixed block group writable for relocation setup commit
From: Bartosz Chronowski
Date: Thu Aug 13 2026 - 07:20:17 EST
Relocating a nearly full mixed block group can abort the filesystem
transaction with -ENOSPC and trigger a warning in cleanup_transaction().
Making the mixed target read-only can lead to a condition where
metadata COW cannot use its free space. In particular,
btrfs_relocate_block_group() marks the mixed target read-only before
prepare_to_relocate() commits the setup transaction. find_free_extent()
then skips all free extents in the target. Commit-time COW still needs
new tree blocks, so the transaction can fail with -ENOSPC when no
suitable extent remains in another block group.
Committing before the read-only transition does not fix the bug.
Another workload can reserve space or start transaction N+1 between the
commit and btrfs_inc_block_group_ro().
Keep a non-zoned, non-remap mixed target writable until its relocation
setup transaction finishes. Fence data, tree-log and NOCOW admission
while ordinary metadata COW remains allowed. Drain operations that
crossed the fence before committing the setup transaction with
reloc_ctl unpublished.
Implement the boundary at the source files that own each state:
- block-group.c owns the setup fence and final read-only transition,
treats the fence as read-only for NOCOW and swap-extents admission,
and makes other read-only holders wait for setup completion;
- extent-tree.c rejects data and tree-log allocation into the fenced
target, allows ordinary metadata COW, and keeps block group
reservations only for data allocations until ordered extent
registration;
- inode.c treats the fenced target as read-only during NOCOW checks;
- relocation.c drains each pass, binds setup to the running transaction
and owns the read-only and reloc_ctl lifecycle;
- transaction.c completes setup after switching commit roots and before
transaction N+1 can start;
- disk-io.c cancels a pending setup when its transaction is cleaned up.
At the transaction tail, either mark the target read-only and publish
reloc_ctl, or return the read-only transition error to relocation while
the transaction completes and the target stays writable. Apply this
boundary to every non-remap mixed relocation pass. Keep the existing
paths unchanged for zoned, remap-tree and non-mixed block groups.
Fixes: 3fd0a5585eb9 ("Btrfs: Metadata ENOSPC handling for balance")
Reported-by: syzbot+021d10c4d4edc87daa03@xxxxxxxxxxxxxxxxxxxxxxxxx
Closes: https://syzkaller.appspot.com/bug?extid=021d10c4d4edc87daa03
Link: https://lore.kernel.org/r/9d9d207e-ad2a-4af6-9d0b-9a2bfc61d442@xxxxxxxx
Assisted-by: Codex:gpt-5.6-sol syzkaller
Signed-off-by: Bartosz Chronowski <immersa.bartosz.chronowski@xxxxxxxxx>
---
Changes in v2:
- Drop the pre-commit-only approach because it leaves an admission window
before the block group becomes read-only.
- Keep the mixed target writable for setup metadata COW while fencing data,
tree-log and NOCOW admission.
- Bind setup to the exact transaction and publish the read-only state and
reloc_ctl before transaction N+1 can start.
- Apply the same boundary to every non-remap relocation pass and handle
abort cleanup explicitly.
Tested:
- Focused and full x86_64 builds.
- The syzbot C reproducer completed 16 independent runs without a crash.
v1: https://lore.kernel.org/r/a06b5077-baa5-473f-9c65-bf72ac651b14@xxxxxxxxxxxxxxx
fs/btrfs/block-group.c | 128 ++++++++++++++++---
fs/btrfs/block-group.h | 8 +-
fs/btrfs/disk-io.c | 1 +
fs/btrfs/extent-tree.c | 19 ++-
fs/btrfs/extent-tree.h | 1 +
fs/btrfs/inode.c | 4 +-
fs/btrfs/relocation.c | 280 ++++++++++++++++++++++++++++++++++++-----
fs/btrfs/relocation.h | 4 +
fs/btrfs/transaction.c | 4 +
fs/btrfs/transaction.h | 3 +
10 files changed, 395 insertions(+), 57 deletions(-)
diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c
index 8def7abb728f..332fc2721e01 100644
--- a/fs/btrfs/block-group.c
+++ b/fs/btrfs/block-group.c
@@ -21,6 +21,7 @@
#include "fs.h"
#include "accessors.h"
#include "extent-tree.h"
+#include "relocation.h"
static struct kmem_cache *block_group_cache;
static struct kmem_cache *free_space_ctl_cache;
@@ -363,7 +364,8 @@ struct btrfs_block_group *btrfs_inc_nocow_writers(struct btrfs_fs_info *fs_info,
return NULL;
spin_lock(&bg->lock);
- if (bg->ro)
+ if (bg->ro || test_bit(BLOCK_GROUP_FLAG_RELOC_SETUP,
+ &bg->runtime_flags))
can_nocow = false;
else
atomic_inc(&bg->nocow_writers);
@@ -419,7 +421,8 @@ void btrfs_wait_block_group_reservations(struct btrfs_block_group *bg)
{
struct btrfs_space_info *space_info = bg->space_info;
- ASSERT(bg->ro);
+ ASSERT(bg->ro || test_bit(BLOCK_GROUP_FLAG_RELOC_SETUP,
+ &bg->runtime_flags));
if (!(bg->flags & BTRFS_BLOCK_GROUP_DATA))
return;
@@ -1434,7 +1437,8 @@ struct btrfs_trans_handle *btrfs_start_trans_remove_block_group(
* data in this block group. That check should be done by relocation routine,
* not this function.
*/
-static int inc_block_group_ro(struct btrfs_block_group *cache, bool force)
+static int __inc_block_group_ro(struct btrfs_block_group *cache, bool force,
+ bool reloc_setup)
{
struct btrfs_space_info *sinfo = cache->space_info;
u64 num_bytes;
@@ -1442,6 +1446,11 @@ static int inc_block_group_ro(struct btrfs_block_group *cache, bool force)
spin_lock(&sinfo->lock);
spin_lock(&cache->lock);
+ if (!reloc_setup && test_bit(BLOCK_GROUP_FLAG_RELOC_SETUP,
+ &cache->runtime_flags)) {
+ ret = -EAGAIN;
+ goto out;
+ }
if (cache->swap_extents) {
ret = -ETXTBSY;
@@ -1504,6 +1513,54 @@ static int inc_block_group_ro(struct btrfs_block_group *cache, bool force)
return ret;
}
+static int inc_block_group_ro(struct btrfs_block_group *cache, bool force)
+{
+ return __inc_block_group_ro(cache, force, false);
+}
+
+int btrfs_bg_reloc_setup_start(struct btrfs_block_group *cache, bool drop_ro)
+{
+ struct btrfs_fs_info *fs_info = cache->fs_info;
+ struct btrfs_space_info *sinfo = cache->space_info;
+ int ret = 0;
+
+ ASSERT(!btrfs_is_zoned(fs_info));
+
+ mutex_lock(&fs_info->ro_block_group_mutex);
+ spin_lock(&sinfo->lock);
+ spin_lock(&cache->lock);
+ if (test_bit(BLOCK_GROUP_FLAG_RELOC_SETUP, &cache->runtime_flags) ||
+ cache->ro != (drop_ro ? 1 : 0)) {
+ ret = -EAGAIN;
+ goto out;
+ }
+
+ set_bit(BLOCK_GROUP_FLAG_RELOC_SETUP, &cache->runtime_flags);
+ if (drop_ro) {
+ cache->ro = 0;
+ sinfo->bytes_readonly -= btrfs_block_group_available_space(cache);
+ list_del_init(&cache->ro_list);
+ }
+out:
+ spin_unlock(&cache->lock);
+ spin_unlock(&sinfo->lock);
+ mutex_unlock(&fs_info->ro_block_group_mutex);
+ return ret;
+}
+
+int btrfs_bg_reloc_setup_finish(struct btrfs_block_group *cache)
+{
+ ASSERT(test_bit(BLOCK_GROUP_FLAG_RELOC_SETUP, &cache->runtime_flags));
+ return __inc_block_group_ro(cache, false, true);
+}
+
+void btrfs_bg_reloc_setup_abort(struct btrfs_block_group *cache)
+{
+ ASSERT(test_bit(BLOCK_GROUP_FLAG_RELOC_SETUP, &cache->runtime_flags));
+ clear_and_wake_up_bit(BLOCK_GROUP_FLAG_RELOC_SETUP,
+ &cache->runtime_flags);
+}
+
static bool clean_pinned_extents(struct btrfs_trans_handle *trans,
const struct btrfs_block_group *bg)
{
@@ -1945,6 +2002,7 @@ static int btrfs_reclaim_block_group(struct btrfs_block_group *bg, int *reclaime
u64 reserved;
u64 old_total;
int ret = 0;
+ bool marked_ro = false;
/* Don't race with allocators so take the groups_sem */
down_write(&space_info->groups_sem);
@@ -2018,15 +2076,19 @@ static int btrfs_reclaim_block_group(struct btrfs_block_group *bg, int *reclaime
return 0;
}
- ret = inc_block_group_ro(bg, false);
+ if (!btrfs_relocation_uses_fenced_setup(bg)) {
+ ret = inc_block_group_ro(bg, false);
+ if (!ret)
+ marked_ro = true;
+ }
up_write(&space_info->groups_sem);
if (ret < 0)
return ret;
/*
* The amount of bytes reclaimed corresponds to the sum of the
- * "used" and "reserved" counters. We have set the block group
- * to RO above, which prevents reservations from happening but
+ * "used" and "reserved" counters. Relocation prevents new data
+ * reservations before it drains existing reservations, but
* we may have existing reservations for which allocation has
* not yet been done - btrfs_update_block_group() was not yet
* called, which is where we will transfer a reserved extent's
@@ -2048,7 +2110,8 @@ static int btrfs_reclaim_block_group(struct btrfs_block_group *bg, int *reclaime
trace_btrfs_reclaim_block_group(bg);
ret = btrfs_relocate_chunk(fs_info, bg->start, false);
if (ret) {
- btrfs_dec_block_group_ro(bg);
+ if (marked_ro)
+ btrfs_dec_block_group_ro(bg);
btrfs_err(fs_info, "error relocating chunk %llu",
bg->start);
used = 0;
@@ -3131,7 +3194,7 @@ int btrfs_inc_block_group_ro(struct btrfs_block_group *cache,
struct btrfs_root *root = btrfs_block_group_root(fs_info);
u64 alloc_flags;
int ret;
- bool dirty_bg_running;
+ bool retry;
if (unlikely(!root)) {
btrfs_err(fs_info, "missing block group root");
@@ -3145,9 +3208,18 @@ int btrfs_inc_block_group_ro(struct btrfs_block_group *cache,
* Thus here we skip all chunk allocations.
*/
if (sb_rdonly(fs_info->sb)) {
- mutex_lock(&fs_info->ro_block_group_mutex);
- ret = inc_block_group_ro(cache, false);
- mutex_unlock(&fs_info->ro_block_group_mutex);
+ do {
+ mutex_lock(&fs_info->ro_block_group_mutex);
+ retry = test_bit(BLOCK_GROUP_FLAG_RELOC_SETUP,
+ &cache->runtime_flags);
+ if (!retry)
+ ret = inc_block_group_ro(cache, false);
+ mutex_unlock(&fs_info->ro_block_group_mutex);
+ if (retry)
+ ret = wait_on_bit(&cache->runtime_flags,
+ BLOCK_GROUP_FLAG_RELOC_SETUP,
+ TASK_INTERRUPTIBLE);
+ } while (retry && !ret);
return ret;
}
@@ -3156,7 +3228,7 @@ int btrfs_inc_block_group_ro(struct btrfs_block_group *cache,
if (IS_ERR(trans))
return PTR_ERR(trans);
- dirty_bg_running = false;
+ retry = false;
/*
* We're not allowed to set block groups readonly after the dirty
@@ -3164,7 +3236,19 @@ int btrfs_inc_block_group_ro(struct btrfs_block_group *cache,
* back off and let this transaction commit.
*/
mutex_lock(&fs_info->ro_block_group_mutex);
- if (test_bit(BTRFS_TRANS_DIRTY_BG_RUN, &trans->transaction->flags)) {
+ if (test_bit(BLOCK_GROUP_FLAG_RELOC_SETUP,
+ &cache->runtime_flags)) {
+ mutex_unlock(&fs_info->ro_block_group_mutex);
+ btrfs_end_transaction(trans);
+
+ ret = wait_on_bit(&cache->runtime_flags,
+ BLOCK_GROUP_FLAG_RELOC_SETUP,
+ TASK_INTERRUPTIBLE);
+ if (ret)
+ return ret;
+ retry = true;
+ } else if (test_bit(BTRFS_TRANS_DIRTY_BG_RUN,
+ &trans->transaction->flags)) {
u64 transid = trans->transid;
mutex_unlock(&fs_info->ro_block_group_mutex);
@@ -3173,9 +3257,9 @@ int btrfs_inc_block_group_ro(struct btrfs_block_group *cache,
ret = btrfs_wait_for_commit(fs_info, transid);
if (ret)
return ret;
- dirty_bg_running = true;
+ retry = true;
}
- } while (dirty_bg_running);
+ } while (retry);
if (do_chunk_alloc) {
/*
@@ -3411,7 +3495,9 @@ static void cache_save_setup(struct btrfs_block_group *block_group,
}
retries++;
- if (block_group->ro)
+ if (block_group->ro ||
+ test_bit(BLOCK_GROUP_FLAG_RELOC_SETUP,
+ &block_group->runtime_flags))
goto out_free;
ret = create_free_space_inode(trans, block_group, path);
@@ -3981,6 +4067,7 @@ int btrfs_update_block_group(struct btrfs_trans_handle *trans,
* @num_bytes except for the compress path.
* @num_bytes: The number of bytes in question
* @delalloc: The blocks are allocated for the delalloc write
+ * @allow_reloc_setup: Allow ordinary metadata into a relocation setup target.
*
* This is called by the allocator when it reserves space. If this is a
* reservation and the block group has become read only we cannot make the
@@ -3988,7 +4075,8 @@ int btrfs_update_block_group(struct btrfs_trans_handle *trans,
*/
int btrfs_add_reserved_bytes(struct btrfs_block_group *cache,
u64 ram_bytes, u64 num_bytes, bool delalloc,
- bool force_wrong_size_class)
+ bool force_wrong_size_class,
+ bool allow_reloc_setup)
{
struct btrfs_space_info *space_info = cache->space_info;
enum btrfs_block_group_size_class size_class;
@@ -3996,7 +4084,9 @@ int btrfs_add_reserved_bytes(struct btrfs_block_group *cache,
spin_lock(&space_info->lock);
spin_lock(&cache->lock);
- if (cache->ro) {
+ if (cache->ro ||
+ (!allow_reloc_setup &&
+ test_bit(BLOCK_GROUP_FLAG_RELOC_SETUP, &cache->runtime_flags))) {
ret = -EAGAIN;
goto out_error;
}
@@ -4832,7 +4922,7 @@ bool btrfs_inc_block_group_swap_extents(struct btrfs_block_group *bg)
bool ret = true;
spin_lock(&bg->lock);
- if (bg->ro)
+ if (bg->ro || test_bit(BLOCK_GROUP_FLAG_RELOC_SETUP, &bg->runtime_flags))
ret = false;
else
bg->swap_extents++;
diff --git a/fs/btrfs/block-group.h b/fs/btrfs/block-group.h
index 790c2d467af5..d2b1dd01b45e 100644
--- a/fs/btrfs/block-group.h
+++ b/fs/btrfs/block-group.h
@@ -95,6 +95,8 @@ enum btrfs_block_group_flags {
BLOCK_GROUP_FLAG_NEW,
BLOCK_GROUP_FLAG_FULLY_REMAPPED,
BLOCK_GROUP_FLAG_STRIPE_REMOVAL_PENDING,
+ /* Block data, tree-log and NOCOW admission during relocation setup. */
+ BLOCK_GROUP_FLAG_RELOC_SETUP,
};
enum btrfs_caching_type {
@@ -364,6 +366,9 @@ void btrfs_create_pending_block_groups(struct btrfs_trans_handle *trans);
int btrfs_inc_block_group_ro(struct btrfs_block_group *cache,
bool do_chunk_alloc);
void btrfs_dec_block_group_ro(struct btrfs_block_group *cache);
+int btrfs_bg_reloc_setup_start(struct btrfs_block_group *cache, bool drop_ro);
+int btrfs_bg_reloc_setup_finish(struct btrfs_block_group *cache);
+void btrfs_bg_reloc_setup_abort(struct btrfs_block_group *cache);
int btrfs_start_dirty_block_groups(struct btrfs_trans_handle *trans);
int btrfs_write_dirty_block_groups(struct btrfs_trans_handle *trans);
int btrfs_setup_space_cache(struct btrfs_trans_handle *trans);
@@ -371,7 +376,8 @@ int btrfs_update_block_group(struct btrfs_trans_handle *trans,
u64 bytenr, u64 num_bytes, bool alloc);
int btrfs_add_reserved_bytes(struct btrfs_block_group *cache,
u64 ram_bytes, u64 num_bytes, bool delalloc,
- bool force_wrong_size_class);
+ bool force_wrong_size_class,
+ bool allow_reloc_setup);
void btrfs_free_reserved_bytes(struct btrfs_block_group *cache, u64 num_bytes,
bool is_delalloc);
int btrfs_chunk_alloc(struct btrfs_trans_handle *trans,
diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
index 2f1666d9544e..eab2fc5bf8b9 100644
--- a/fs/btrfs/disk-io.c
+++ b/fs/btrfs/disk-io.c
@@ -4936,6 +4936,7 @@ void btrfs_cleanup_one_transaction(struct btrfs_transaction *cur_trans)
}
btrfs_destroy_delayed_refs(cur_trans);
+ btrfs_abort_relocation_setup(cur_trans, cur_trans->aborted);
cur_trans->state = TRANS_STATE_COMMIT_START;
wake_up(&fs_info->transaction_blocked_wait);
diff --git a/fs/btrfs/extent-tree.c b/fs/btrfs/extent-tree.c
index 624d76e0ca01..962af1840781 100644
--- a/fs/btrfs/extent-tree.c
+++ b/fs/btrfs/extent-tree.c
@@ -4639,6 +4639,9 @@ static noinline int find_free_extent(struct btrfs_root *root,
down_read(&space_info->groups_sem);
if (list_empty(&block_group->list) ||
block_group->ro ||
+ (test_bit(BLOCK_GROUP_FLAG_RELOC_SETUP,
+ &block_group->runtime_flags) &&
+ (ffe_ctl->is_data || ffe_ctl->for_treelog)) ||
(block_group->flags & BTRFS_BLOCK_GROUP_REMAPPED)) {
/*
* someone is removing this block group,
@@ -4674,7 +4677,10 @@ static noinline int find_free_extent(struct btrfs_root *root,
ffe_ctl->hinted = false;
/* If the block group is read-only, we can skip it entirely. */
if (unlikely(block_group->ro ||
- (block_group->flags & BTRFS_BLOCK_GROUP_REMAPPED))) {
+ (test_bit(BLOCK_GROUP_FLAG_RELOC_SETUP,
+ &block_group->runtime_flags) &&
+ (ffe_ctl->is_data || ffe_ctl->for_treelog)) ||
+ (block_group->flags & BTRFS_BLOCK_GROUP_REMAPPED))) {
if (ffe_ctl->for_treelog)
btrfs_clear_treelog_bg(block_group);
if (ffe_ctl->for_data_reloc)
@@ -4776,14 +4782,16 @@ static noinline int find_free_extent(struct btrfs_root *root,
ret = btrfs_add_reserved_bytes(block_group, ffe_ctl->ram_bytes,
ffe_ctl->num_bytes,
ffe_ctl->delalloc,
- ffe_ctl->loop >= LOOP_WRONG_SIZE_CLASS);
+ ffe_ctl->loop >= LOOP_WRONG_SIZE_CLASS,
+ !ffe_ctl->is_data && !ffe_ctl->for_treelog);
if (ret == -EAGAIN) {
btrfs_add_free_space_unused(block_group,
ffe_ctl->found_offset,
ffe_ctl->num_bytes);
goto loop;
}
- btrfs_inc_block_group_reservations(block_group);
+ if (ffe_ctl->is_data)
+ btrfs_inc_block_group_reservations(block_group);
/* we are all good, lets return */
ins->objectid = ffe_ctl->search_start;
@@ -4897,14 +4905,13 @@ int btrfs_reserve_extent(struct btrfs_root *root, u64 ram_bytes,
ffe_ctl.empty_size = empty_size;
ffe_ctl.flags = flags;
ffe_ctl.delalloc = delalloc;
+ ffe_ctl.is_data = is_data;
ffe_ctl.hint_byte = hint_byte;
ffe_ctl.for_treelog = for_treelog;
ffe_ctl.for_data_reloc = for_data_reloc;
ret = find_free_extent(root, ins, &ffe_ctl);
- if (!ret && !is_data) {
- btrfs_dec_block_group_reservations(fs_info, ins->objectid);
- } else if (ret == -ENOSPC) {
+ if (ret == -ENOSPC) {
if (!final_tried && ins->offset) {
num_bytes = min(num_bytes >> 1, ins->offset);
num_bytes = round_down(num_bytes,
diff --git a/fs/btrfs/extent-tree.h b/fs/btrfs/extent-tree.h
index ff330d4896d6..74ba10a46951 100644
--- a/fs/btrfs/extent-tree.h
+++ b/fs/btrfs/extent-tree.h
@@ -40,6 +40,7 @@ struct find_free_extent_ctl {
bool use_cluster;
bool delalloc;
+ bool is_data;
bool have_caching_bg;
bool orig_have_caching_bg;
diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c
index 2534cd9284d5..28c5902636d3 100644
--- a/fs/btrfs/inode.c
+++ b/fs/btrfs/inode.c
@@ -7398,7 +7398,9 @@ static bool btrfs_extent_readonly(struct btrfs_fs_info *fs_info, u64 bytenr)
bool readonly = false;
block_group = btrfs_lookup_block_group(fs_info, bytenr);
- if (!block_group || block_group->ro)
+ if (!block_group || block_group->ro ||
+ test_bit(BLOCK_GROUP_FLAG_RELOC_SETUP,
+ &block_group->runtime_flags))
readonly = true;
if (block_group)
btrfs_put_block_group(block_group);
diff --git a/fs/btrfs/relocation.c b/fs/btrfs/relocation.c
index fc5c14b5adad..92059ebc8345 100644
--- a/fs/btrfs/relocation.c
+++ b/fs/btrfs/relocation.c
@@ -173,11 +173,16 @@ struct reloc_control {
u64 search_start;
u64 extents_found;
+ int setup_result;
enum reloc_stage stage;
bool create_reloc_tree;
bool merge_reloc_tree;
bool found_file_extent;
+ bool fenced_setup;
+ bool setup_pending;
+ bool block_group_ro;
+ bool reloc_ctl_set;
refcount_t refs;
};
@@ -3507,14 +3512,24 @@ int find_next_extent(struct reloc_control *rc, struct btrfs_path *path,
return ret;
}
-static void set_reloc_control(struct reloc_control *rc)
+static void __set_reloc_control(struct reloc_control *rc)
{
struct btrfs_fs_info *fs_info = rc->extent_root->fs_info;
- mutex_lock(&fs_info->reloc_mutex);
+ lockdep_assert_held(&fs_info->reloc_mutex);
spin_lock(&fs_info->reloc_ctl_lock);
+ ASSERT(!fs_info->reloc_ctl || fs_info->reloc_ctl == rc);
fs_info->reloc_ctl = rc;
+ rc->reloc_ctl_set = true;
spin_unlock(&fs_info->reloc_ctl_lock);
+}
+
+static void set_reloc_control(struct reloc_control *rc)
+{
+ struct btrfs_fs_info *fs_info = rc->extent_root->fs_info;
+
+ mutex_lock(&fs_info->reloc_mutex);
+ __set_reloc_control(rc);
mutex_unlock(&fs_info->reloc_mutex);
}
@@ -3524,18 +3539,137 @@ static void unset_reloc_control(struct reloc_control *rc)
mutex_lock(&fs_info->reloc_mutex);
spin_lock(&fs_info->reloc_ctl_lock);
- fs_info->reloc_ctl = NULL;
+ if (rc->reloc_ctl_set) {
+ ASSERT(fs_info->reloc_ctl == rc);
+ fs_info->reloc_ctl = NULL;
+ rc->reloc_ctl_set = false;
+ } else {
+ ASSERT(fs_info->reloc_ctl != rc);
+ }
spin_unlock(&fs_info->reloc_ctl_lock);
mutex_unlock(&fs_info->reloc_mutex);
}
+static void complete_relocation_setup(struct reloc_control *rc, int result)
+{
+ ASSERT(rc->setup_pending);
+ WRITE_ONCE(rc->setup_result, result);
+ WRITE_ONCE(rc->setup_pending, false);
+ btrfs_bg_reloc_setup_abort(rc->block_group);
+ put_reloc_control(rc);
+}
+
+void btrfs_finish_relocation_setup(struct btrfs_transaction *trans)
+{
+ struct btrfs_fs_info *fs_info = trans->fs_info;
+ struct reloc_control *rc;
+ int ret;
+
+ lockdep_assert_held(&fs_info->reloc_mutex);
+
+ spin_lock(&fs_info->trans_lock);
+ rc = trans->reloc_setup;
+ trans->reloc_setup = NULL;
+ spin_unlock(&fs_info->trans_lock);
+ if (!rc)
+ return;
+
+ ret = btrfs_bg_reloc_setup_finish(rc->block_group);
+ if (!ret) {
+ WRITE_ONCE(rc->block_group_ro, true);
+ __set_reloc_control(rc);
+ }
+ complete_relocation_setup(rc, ret);
+}
+
+void btrfs_abort_relocation_setup(struct btrfs_transaction *trans, int error)
+{
+ struct btrfs_fs_info *fs_info = trans->fs_info;
+ struct reloc_control *rc;
+
+ spin_lock(&fs_info->trans_lock);
+ rc = trans->reloc_setup;
+ trans->reloc_setup = NULL;
+ spin_unlock(&fs_info->trans_lock);
+ if (!rc)
+ return;
+
+ complete_relocation_setup(rc, error ?: -EIO);
+}
+
+static int bind_relocation_setup(struct btrfs_trans_handle *trans,
+ struct reloc_control *rc,
+ struct btrfs_transaction **transaction)
+{
+ struct btrfs_fs_info *fs_info = trans->fs_info;
+ struct btrfs_transaction *cur_trans = trans->transaction;
+ int ret = 0;
+
+ mutex_lock(&fs_info->ro_block_group_mutex);
+ spin_lock(&fs_info->trans_lock);
+ if (TRANS_ABORTED(cur_trans)) {
+ ret = cur_trans->aborted;
+ } else if (cur_trans != fs_info->running_transaction ||
+ cur_trans->state != TRANS_STATE_RUNNING ||
+ test_bit(BTRFS_TRANS_DIRTY_BG_RUN, &cur_trans->flags)) {
+ ret = -EAGAIN;
+ } else if (cur_trans->reloc_setup) {
+ ret = -EBUSY;
+ } else {
+ ASSERT(rc->setup_pending);
+ WRITE_ONCE(rc->setup_result, -EINPROGRESS);
+ refcount_inc(&rc->refs);
+ cur_trans->reloc_setup = rc;
+ refcount_inc(&cur_trans->use_count);
+ *transaction = cur_trans;
+ }
+ spin_unlock(&fs_info->trans_lock);
+ mutex_unlock(&fs_info->ro_block_group_mutex);
+
+ return ret;
+}
+
+static int reconcile_relocation_setup(struct btrfs_transaction *trans,
+ struct reloc_control *rc,
+ int commit_ret)
+{
+ struct btrfs_fs_info *fs_info = trans->fs_info;
+ bool cancel = false;
+ bool wait = false;
+ int setup_ret;
+
+ spin_lock(&fs_info->trans_lock);
+ if (trans->reloc_setup == rc &&
+ trans->state < TRANS_STATE_COMMIT_PREP) {
+ trans->reloc_setup = NULL;
+ cancel = true;
+ } else if (READ_ONCE(rc->setup_result) == -EINPROGRESS) {
+ wait = true;
+ }
+ spin_unlock(&fs_info->trans_lock);
+
+ if (cancel)
+ complete_relocation_setup(rc, commit_ret ?: -EIO);
+ else if (wait)
+ wait_event(trans->commit_wait,
+ READ_ONCE(trans->state) >= TRANS_STATE_COMPLETED);
+
+ setup_ret = READ_ONCE(rc->setup_result);
+ ASSERT(setup_ret != -EINPROGRESS);
+ btrfs_put_transaction(trans);
+
+ return commit_ret ?: setup_ret;
+}
+
static noinline_for_stack
int prepare_to_relocate(struct reloc_control *rc)
{
+ struct btrfs_fs_info *fs_info = rc->extent_root->fs_info;
struct btrfs_trans_handle *trans;
+ struct btrfs_transaction *transaction = NULL;
int ret;
- rc->block_rsv = btrfs_alloc_block_rsv(rc->extent_root->fs_info,
+ rc->block_rsv = btrfs_alloc_block_rsv(fs_info,
BTRFS_BLOCK_RSV_TEMP);
if (!rc->block_rsv)
return -ENOMEM;
@@ -3546,32 +3680,93 @@ int prepare_to_relocate(struct reloc_control *rc)
rc->nodes_relocated = 0;
rc->merging_rsv_size = 0;
rc->reserved_bytes = 0;
- rc->block_rsv->size = rc->extent_root->fs_info->nodesize *
- RELOCATION_RESERVED_NODES;
- ret = btrfs_block_rsv_refill(rc->extent_root->fs_info,
+ rc->block_rsv->size = fs_info->nodesize * RELOCATION_RESERVED_NODES;
+
+ if (!rc->fenced_setup) {
+ ret = btrfs_block_rsv_refill(fs_info,
+ rc->block_rsv, rc->block_rsv->size,
+ BTRFS_RESERVE_FLUSH_ALL);
+ if (ret)
+ return ret;
+
+ rc->create_reloc_tree = true;
+ set_reloc_control(rc);
+
+ trans = btrfs_join_transaction(rc->extent_root);
+ if (IS_ERR(trans)) {
+ unset_reloc_control(rc);
+ /*
+ * The extent tree is not a ref-cow tree and has no reloc
+ * root to clean up. Callers free the block reserve.
+ */
+ return PTR_ERR(trans);
+ }
+
+ ret = btrfs_commit_transaction(trans);
+ if (ret)
+ unset_reloc_control(rc);
+ return ret;
+ }
+
+ if (!rc->setup_pending) {
+ ret = btrfs_bg_reloc_setup_start(rc->block_group,
+ rc->block_group_ro);
+ if (ret)
+ return ret;
+ WRITE_ONCE(rc->setup_pending, true);
+ WRITE_ONCE(rc->block_group_ro, false);
+ } else {
+ ASSERT(!rc->block_group_ro);
+ }
+
+ btrfs_wait_block_group_reservations(rc->block_group);
+ btrfs_wait_nocow_writers(rc->block_group);
+ btrfs_wait_ordered_roots(fs_info, U64_MAX, rc->block_group);
+
+ ret = btrfs_block_rsv_refill(fs_info,
rc->block_rsv, rc->block_rsv->size,
BTRFS_RESERVE_FLUSH_ALL);
if (ret)
- return ret;
+ goto abort_setup;
+ /* The transaction tail publishes reloc_ctl with the new commit roots. */
rc->create_reloc_tree = true;
- set_reloc_control(rc);
+ for (;;) {
+ u64 transid;
- trans = btrfs_join_transaction(rc->extent_root);
- if (IS_ERR(trans)) {
- unset_reloc_control(rc);
- /*
- * extent tree is not a ref_cow tree and has no reloc_root to
- * cleanup. And callers are responsible to free the above
- * block rsv.
- */
- return PTR_ERR(trans);
+ trans = btrfs_join_transaction(rc->extent_root);
+ if (IS_ERR(trans)) {
+ ret = PTR_ERR(trans);
+ goto abort_setup;
+ }
+ transid = trans->transid;
+
+ ret = bind_relocation_setup(trans, rc, &transaction);
+ if (ret == -EAGAIN) {
+ btrfs_end_transaction(trans);
+ ret = btrfs_wait_for_commit(fs_info, transid);
+ if (ret)
+ goto abort_setup;
+ continue;
+ }
+ if (ret) {
+ btrfs_end_transaction(trans);
+ goto abort_setup;
+ }
+ break;
}
ret = btrfs_commit_transaction(trans);
- if (ret)
+ ret = reconcile_relocation_setup(transaction, rc, ret);
+ if (ret && rc->reloc_ctl_set)
unset_reloc_control(rc);
+ return ret;
+abort_setup:
+ ASSERT(rc->setup_pending);
+ WRITE_ONCE(rc->setup_result, ret);
+ WRITE_ONCE(rc->setup_pending, false);
+ btrfs_bg_reloc_setup_abort(rc->block_group);
return ret;
}
@@ -3937,6 +4132,14 @@ static const char *stage_to_string(enum reloc_stage stage)
return "unknown";
}
+bool btrfs_relocation_uses_fenced_setup(const struct btrfs_block_group *bg)
+{
+ const u64 mixed = BTRFS_BLOCK_GROUP_DATA | BTRFS_BLOCK_GROUP_METADATA;
+
+ return (bg->flags & mixed) == mixed && !btrfs_is_zoned(bg->fs_info) &&
+ !should_relocate_using_remap_tree(bg);
+}
+
static int add_remap_tree_entries(struct btrfs_trans_handle *trans, struct btrfs_path *path,
struct btrfs_key *entries, unsigned int num_entries)
{
@@ -5404,7 +5607,6 @@ int btrfs_relocate_block_group(struct btrfs_fs_info *fs_info, u64 group_start,
struct inode *inode;
struct btrfs_path *path = NULL;
int ret;
- bool bg_is_ro = false;
if (unlikely(!extent_root)) {
btrfs_err(fs_info,
@@ -5455,15 +5657,24 @@ int btrfs_relocate_block_group(struct btrfs_fs_info *fs_info, u64 group_start,
rc->extent_root = extent_root;
/* Block group ref now owned by rc, put_reloc_control() will drop it. */
rc->block_group = bg;
+ rc->fenced_setup = btrfs_relocation_uses_fenced_setup(bg);
ret = reloc_chunk_start(fs_info);
if (ret < 0)
goto out_put_rc;
- ret = btrfs_inc_block_group_ro(rc->block_group, true);
- if (ret)
- goto out;
- bg_is_ro = true;
+ if (rc->fenced_setup) {
+ /* Keep non-metadata writers out until the setup tail marks RO. */
+ ret = btrfs_bg_reloc_setup_start(rc->block_group, false);
+ if (ret)
+ goto out;
+ WRITE_ONCE(rc->setup_pending, true);
+ } else {
+ ret = btrfs_inc_block_group_ro(rc->block_group, true);
+ if (ret)
+ goto out;
+ rc->block_group_ro = true;
+ }
path = btrfs_alloc_path();
if (!path) {
@@ -5494,12 +5705,14 @@ int btrfs_relocate_block_group(struct btrfs_fs_info *fs_info, u64 group_start,
if (verbose)
describe_relocation(rc->block_group);
- btrfs_wait_block_group_reservations(rc->block_group);
- btrfs_wait_nocow_writers(rc->block_group);
- btrfs_wait_ordered_roots(fs_info, U64_MAX, rc->block_group);
+ if (!rc->fenced_setup) {
+ btrfs_wait_block_group_reservations(rc->block_group);
+ btrfs_wait_nocow_writers(rc->block_group);
+ btrfs_wait_ordered_roots(fs_info, U64_MAX, rc->block_group);
- ret = btrfs_zone_finish(rc->block_group);
- WARN_ON(ret && ret != -EAGAIN);
+ ret = btrfs_zone_finish(rc->block_group);
+ WARN_ON(ret && ret != -EAGAIN);
+ }
if (should_relocate_using_remap_tree(bg)) {
if (bg->remap_bytes != 0) {
@@ -5521,8 +5734,15 @@ int btrfs_relocate_block_group(struct btrfs_fs_info *fs_info, u64 group_start,
}
out:
- if (ret && bg_is_ro)
+ if (rc->setup_pending) {
+ ASSERT(ret);
+ WRITE_ONCE(rc->setup_pending, false);
+ btrfs_bg_reloc_setup_abort(rc->block_group);
+ }
+ if (ret && rc->block_group_ro) {
btrfs_dec_block_group_ro(rc->block_group);
+ rc->block_group_ro = false;
+ }
if (!btrfs_fs_incompat(fs_info, REMAP_TREE))
iput(rc->data_inode);
btrfs_free_path(path);
diff --git a/fs/btrfs/relocation.h b/fs/btrfs/relocation.h
index bb7a86e7dbe3..210d0bbd7d48 100644
--- a/fs/btrfs/relocation.h
+++ b/fs/btrfs/relocation.h
@@ -11,6 +11,7 @@ struct btrfs_root;
struct btrfs_trans_handle;
struct btrfs_ordered_extent;
struct btrfs_pending_snapshot;
+struct btrfs_transaction;
static inline bool should_relocate_using_remap_tree(const struct btrfs_block_group *bg)
{
@@ -25,6 +26,9 @@ static inline bool should_relocate_using_remap_tree(const struct btrfs_block_gro
int btrfs_relocate_block_group(struct btrfs_fs_info *fs_info, u64 group_start,
bool verbose);
+bool btrfs_relocation_uses_fenced_setup(const struct btrfs_block_group *bg);
+void btrfs_finish_relocation_setup(struct btrfs_transaction *trans);
+void btrfs_abort_relocation_setup(struct btrfs_transaction *trans, int error);
int btrfs_init_reloc_root(struct btrfs_trans_handle *trans, struct btrfs_root *root);
int btrfs_update_reloc_root(struct btrfs_trans_handle *trans,
struct btrfs_root *root);
diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c
index 8f9419728100..97556bdfdead 100644
--- a/fs/btrfs/transaction.c
+++ b/fs/btrfs/transaction.c
@@ -173,6 +173,7 @@ void btrfs_put_transaction(struct btrfs_transaction *transaction)
btrfs_put_block_group(cache);
}
WARN_ON(!list_empty(&transaction->dev_update_list));
+ WARN_ON(transaction->reloc_setup);
kfree(transaction);
}
}
@@ -379,6 +380,7 @@ static noinline int join_transaction(struct btrfs_fs_info *fs_info,
INIT_LIST_HEAD(&cur_trans->dev_update_list);
INIT_LIST_HEAD(&cur_trans->switch_commits);
INIT_LIST_HEAD(&cur_trans->dirty_bgs);
+ cur_trans->reloc_setup = NULL;
INIT_LIST_HEAD(&cur_trans->io_bgs);
INIT_LIST_HEAD(&cur_trans->dropped_roots);
mutex_init(&cur_trans->cache_write_mutex);
@@ -2552,6 +2554,8 @@ int btrfs_commit_transaction(struct btrfs_trans_handle *trans)
clear_bit(BTRFS_FS_LOG2_ERR, &fs_info->flags);
btrfs_trans_release_chunk_metadata(trans);
+ /* Resolve the relocation setup before transaction N+1 can start. */
+ btrfs_finish_relocation_setup(cur_trans);
/*
* Before changing the transaction state to TRANS_STATE_UNBLOCKED and
diff --git a/fs/btrfs/transaction.h b/fs/btrfs/transaction.h
index 5e4b1106fd90..bbf3c2b78ce1 100644
--- a/fs/btrfs/transaction.h
+++ b/fs/btrfs/transaction.h
@@ -23,6 +23,7 @@ struct btrfs_fs_info;
struct btrfs_root_item;
struct btrfs_root;
struct btrfs_path;
+struct reloc_control;
/*
* Signal that a direct IO write is in progress, to avoid deadlock for sync
@@ -77,6 +78,8 @@ struct btrfs_transaction {
struct list_head dev_update_list;
struct list_head switch_commits;
struct list_head dirty_bgs;
+ /* Protected by fs_info->trans_lock. */
+ struct reloc_control *reloc_setup;
/*
* There is no explicit lock which protects io_bgs, rather its
base-commit: f5bbbfec59b4e2fb7520a91de3df8a6174325d6a
--
2.43.0