* [PATCH 0/4] btrfs: zoned: fix active-zone accounting and transitions
@ 2026-08-24 6:49 Dongjiang Zhu
2026-08-24 6:49 ` [PATCH 1/4] btrfs: zoned: track only sequential zones as active Dongjiang Zhu
` (3 more replies)
0 siblings, 4 replies; 15+ messages in thread
From: Dongjiang Zhu @ 2026-08-24 6:49 UTC (permalink / raw)
To: linux-btrfs; +Cc: johannes.thumshirn, naohiro.aota
After systems using HC620 host-managed zoned devices were upgraded to
Linux 6.18, multiple users reported transaction writeback failures with
-EAGAIN that forced filesystems read-only, as well as hung tasks.
Enforcing the device's max_active_zones=128 limit exposed existing
active-zone handling problems. We reproduced representative cases
locally, including an -EAGAIN transaction abort that forced the filesystem
read-only and a balance self-deadlock that stalled transaction commit and
data writers.
Further investigation traced these failures to several related gaps in
Btrfs active-zone handling around mount recovery, non-data block group
activation and zone finishing:
- Conventional zones participate in runtime active-zone accounting, but
that state cannot be reconstructed from device zone conditions at mount.
- Active metadata and system block groups are reconstructed at mount, but
their runtime write roles are not.
- An obsolete allocation-time activation helper bypasses the write-time
role pivot and can recursively enter zone finishing.
- do_zone_finish() clears the active state before the device operation has
released the active-zone slot.
Together, these bugs can leave no active-zone slot available to metadata
or system writeback, or recursively enter zone finishing while holding
ro_block_group_mutex, resulting in the two failure modes described above.
Fix the active-zone model and its state transitions as follows:
1. Track only sequential zones as active.
2. Recover the normal metadata and system write roles on mount, and finish
extra active block groups left by the old behavior.
3. Remove the obsolete allocation-time non-data activation helper so
metadata and system activation goes through the write-time role pivot.
4. Serialize concurrent finish operations on the same block group and keep
it active until the device operation and software cleanup complete.
The individual commits contain the corresponding reproduction conditions,
kernel stacks and state-transition traces.
Dongjiang Zhu (4):
btrfs: zoned: track only sequential zones as active
btrfs: zoned: recover active non-data block group roles on mount
btrfs: zoned: remove obsolete non-data block group activation helper
btrfs: zoned: serialize zone finishing per block group
fs/btrfs/block-group.c | 38 +++-----
fs/btrfs/block-group.h | 7 +-
fs/btrfs/disk-io.c | 7 +-
fs/btrfs/fs.h | 4 +
fs/btrfs/zoned.c | 193 ++++++++++++++++++++++++++++-------------
fs/btrfs/zoned.h | 9 +-
6 files changed, 160 insertions(+), 98 deletions(-)
--
2.39.5
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH 1/4] btrfs: zoned: track only sequential zones as active
2026-08-24 6:49 [PATCH 0/4] btrfs: zoned: fix active-zone accounting and transitions Dongjiang Zhu
@ 2026-08-24 6:49 ` Dongjiang Zhu
2026-08-24 13:55 ` Johannes Thumshirn
2026-08-24 6:49 ` [PATCH 2/4] btrfs: zoned: recover active non-data block group roles on mount Dongjiang Zhu
` (2 subsequent siblings)
3 siblings, 1 reply; 15+ messages in thread
From: Dongjiang Zhu @ 2026-08-24 6:49 UTC (permalink / raw)
To: linux-btrfs; +Cc: johannes.thumshirn, naohiro.aota
[CAUSE]
The runtime activation and finish paths apply active-zone accounting to
both conventional and sequential stripes.
At mount, however, active_zones_left is rebuilt from device zone
conditions. Conventional zones report NOT_WP and are therefore not
included. Meanwhile, all-conventional block groups are marked active
and added to zone_active_bgs, so mount recovery still decreases
reserved_active_zones for their metadata and system stripes.
The two counters can therefore diverge across a remount.
[BUG]
Each unmatched reservation decrement lets data consume one more
sequential active-zone slot. Once the reservation becomes negative,
data can exhaust all such slots.
A later transition from a conventional metadata or system target to a
sequential one can then fail, because finishing the conventional target
does not release a sequential active-zone slot.
If this happens during synchronous transaction writeback,
btrfs_check_meta_write_pointer() returns -EAGAIN. The transaction is
then aborted and the filesystem is forced read-only.
The reservation mismatch was reproduced by creating small files with
large xattrs in batches, syncing them and remounting an HC620 filesystem
using SINGLE profiles:
active_zones_left: 126
reserved_active_zones: 3 -> -8
The active list contained one partially used conventional system block
group, nine full conventional metadata block groups and one partially
used conventional metadata block group.
[FIX]
Use sequential stripes as the unit of active-zone tracking.
Do not mark all-conventional block groups active. Only sequential
stripes update the active-zone bitmap and non-data reservation during
activation, finish and mount recovery.
Assisted-by: LLM
Signed-off-by: Dongjiang Zhu <zhudongjiang@fygo.io>
---
fs/btrfs/block-group.h | 6 +++++-
fs/btrfs/zoned.c | 48 +++++++++++++++++++++++++++++-------------
2 files changed, 38 insertions(+), 16 deletions(-)
diff --git a/fs/btrfs/block-group.h b/fs/btrfs/block-group.h
index 69d56864d4ba..37ab8b7eee9c 100644
--- a/fs/btrfs/block-group.h
+++ b/fs/btrfs/block-group.h
@@ -80,13 +80,17 @@ enum btrfs_block_group_flags {
BLOCK_GROUP_FLAG_TO_COPY,
BLOCK_GROUP_FLAG_RELOCATING_REPAIR,
BLOCK_GROUP_FLAG_CHUNK_ITEM_INSERTED,
+ /*
+ * Only block groups containing sequential zones can have this bit set;
+ * conventional-only block groups never do.
+ */
BLOCK_GROUP_FLAG_ZONE_IS_ACTIVE,
BLOCK_GROUP_FLAG_ZONED_DATA_RELOC,
/* Does the block group need to be added to the free space tree? */
BLOCK_GROUP_FLAG_NEEDS_FREE_SPACE,
/* Set after we add a new block group to the free space tree. */
BLOCK_GROUP_FLAG_FREE_SPACE_ADDED,
- /* Indicate that the block group is placed on a sequential zone */
+ /* Indicate that the block group contains at least one sequential zone. */
BLOCK_GROUP_FLAG_SEQUENTIAL_ZONE,
/*
* Indicate that block group is in the list of new block groups of a
diff --git a/fs/btrfs/zoned.c b/fs/btrfs/zoned.c
index 9d448cdd60c4..bd079d3b31ba 100644
--- a/fs/btrfs/zoned.c
+++ b/fs/btrfs/zoned.c
@@ -1969,7 +1969,6 @@ int btrfs_load_block_group_zone_info(struct btrfs_block_group *cache, bool new)
} else if (map->num_stripes == num_conventional) {
cache->alloc_offset = last_alloc;
cache->zone_capacity = cache->length;
- set_bit(BLOCK_GROUP_FLAG_ZONE_IS_ACTIVE, &cache->runtime_flags);
goto out;
}
}
@@ -2005,6 +2004,8 @@ int btrfs_load_block_group_zone_info(struct btrfs_block_group *cache, bool new)
if (!ret) {
cache->meta_write_pointer = cache->alloc_offset + cache->start;
if (test_bit(BLOCK_GROUP_FLAG_ZONE_IS_ACTIVE, &cache->runtime_flags)) {
+ ASSERT(test_bit(BLOCK_GROUP_FLAG_SEQUENTIAL_ZONE,
+ &cache->runtime_flags));
btrfs_get_block_group(cache);
spin_lock(&fs_info->zone_active_bgs_lock);
list_add_tail(&cache->active_bg_list,
@@ -2198,6 +2199,9 @@ static bool check_bg_is_active(struct btrfs_eb_write_context *ctx,
if (test_bit(BLOCK_GROUP_FLAG_ZONE_IS_ACTIVE, &block_group->runtime_flags))
return true;
+ if (!test_bit(BLOCK_GROUP_FLAG_SEQUENTIAL_ZONE, &block_group->runtime_flags))
+ return true;
+
if (fs_info->treelog_bg == block_group->start) {
if (!btrfs_zone_activate(block_group)) {
int ret_fin;
@@ -2417,6 +2421,9 @@ bool btrfs_zone_activate(struct btrfs_block_group *block_group)
if (unlikely(btrfs_is_testing(fs_info)))
return true;
+ if (!test_bit(BLOCK_GROUP_FLAG_SEQUENTIAL_ZONE, &block_group->runtime_flags))
+ return true;
+
map = block_group->physical_map;
spin_lock(&fs_info->zone_active_bgs_lock);
@@ -2448,6 +2455,9 @@ bool btrfs_zone_activate(struct btrfs_block_group *block_group)
if (!device->bdev)
continue;
+ if (!btrfs_dev_is_sequential(device, physical))
+ continue;
+
if (zinfo->max_active_zones == 0)
continue;
@@ -2514,26 +2524,25 @@ static int call_zone_finish(struct btrfs_block_group *block_group,
struct btrfs_device *device = stripe->dev;
const u64 physical = stripe->physical;
struct btrfs_zoned_device_info *zinfo = device->zone_info;
+ unsigned int nofs_flags;
int ret;
if (!device->bdev)
return 0;
- if (zinfo->max_active_zones == 0)
+ if (!btrfs_dev_is_sequential(device, physical))
return 0;
- if (btrfs_dev_is_sequential(device, physical)) {
- unsigned int nofs_flags;
-
- nofs_flags = memalloc_nofs_save();
- ret = blkdev_zone_mgmt(device->bdev, REQ_OP_ZONE_FINISH,
- physical >> SECTOR_SHIFT,
- zinfo->zone_size >> SECTOR_SHIFT);
- memalloc_nofs_restore(nofs_flags);
+ if (zinfo->max_active_zones == 0)
+ return 0;
- if (ret)
- return ret;
- }
+ nofs_flags = memalloc_nofs_save();
+ ret = blkdev_zone_mgmt(device->bdev, REQ_OP_ZONE_FINISH,
+ physical >> SECTOR_SHIFT,
+ zinfo->zone_size >> SECTOR_SHIFT);
+ memalloc_nofs_restore(nofs_flags);
+ if (ret)
+ return ret;
if (!(block_group->flags & BTRFS_BLOCK_GROUP_DATA))
zinfo->reserved_active_zones++;
@@ -3087,8 +3096,17 @@ void btrfs_check_active_zone_reservation(struct btrfs_fs_info *fs_info)
(BTRFS_BLOCK_GROUP_METADATA | BTRFS_BLOCK_GROUP_SYSTEM)))
continue;
- for (int i = 0; i < map->num_stripes; i++)
- map->stripes[i].dev->zone_info->reserved_active_zones--;
+ for (int i = 0; i < map->num_stripes; i++) {
+ struct btrfs_device *device = map->stripes[i].dev;
+ u64 physical = map->stripes[i].physical;
+
+ if (!device->bdev ||
+ !btrfs_dev_is_sequential(device, physical) ||
+ !device->zone_info->max_active_zones)
+ continue;
+
+ device->zone_info->reserved_active_zones--;
+ }
}
spin_unlock(&fs_info->zone_active_bgs_lock);
}
--
2.39.5
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH 2/4] btrfs: zoned: recover active non-data block group roles on mount
2026-08-24 6:49 [PATCH 0/4] btrfs: zoned: fix active-zone accounting and transitions Dongjiang Zhu
2026-08-24 6:49 ` [PATCH 1/4] btrfs: zoned: track only sequential zones as active Dongjiang Zhu
@ 2026-08-24 6:49 ` Dongjiang Zhu
2026-08-24 14:42 ` Johannes Thumshirn
2026-08-24 6:49 ` [PATCH 3/4] btrfs: zoned: remove obsolete non-data block group activation helper Dongjiang Zhu
2026-08-24 6:49 ` [PATCH 4/4] btrfs: zoned: serialize zone finishing per block group Dongjiang Zhu
3 siblings, 1 reply; 15+ messages in thread
From: Dongjiang Zhu @ 2026-08-24 6:49 UTC (permalink / raw)
To: linux-btrfs; +Cc: johannes.thumshirn, naohiro.aota
[BUG]
After a remount, a later write can activate a new metadata or system
block group without finishing the recovered one. Repeating this across
remounts consumes additional active-zone slots and reservations, and can
eventually exhaust all active-zone slots.
Once no slot is available, activation of a metadata or system block group
fails. During synchronous transaction writeback this propagates as
-EAGAIN, aborting the transaction and forcing the filesystem read-only.
A remount/fsync/remount reproducer on an HC620 left two partially used
sequential metadata block groups active while active_meta_bg remained
NULL:
bg=343865819136 alloc=176095232 cap=268435456
bg=344402690048 alloc=3260416 cap=268435456
[CAUSE]
active_meta_bg and active_system_bg assign active block groups to the
normal metadata and system write roles. These runtime-only pointers are
reset to NULL on every mount.
Mount reconstructs partially used sequential block groups in
zone_active_bgs and adjusts their reservations, but does not recover the
role assignments.
The write-time pivot can finish the previous block group only when the
corresponding role pointer is set. If it is NULL, activation proceeds
without releasing an already active block group of the same role.
[FIX]
Recover one active block group for the normal metadata role and one for
the system role from zone_active_bgs. For each role, keep the block group
with the largest writable tail and finish the others.
The tree-log block group role is runtime-only and cannot be recovered
after a remount. Use the selected metadata block group for the normal
metadata role and leave one metadata reservation for a new tree-log
block group.
Run the recovery on every mount. ZONE_FINISH preserves existing extents
but abandons the unused tail, so account that tail as zone unusable
immediately.
Assisted-by: LLM
Signed-off-by: Dongjiang Zhu <zhudongjiang@fygo.io>
---
fs/btrfs/disk-io.c | 7 +++-
fs/btrfs/fs.h | 4 +++
fs/btrfs/zoned.c | 90 ++++++++++++++++++++++++++++++++++++++++++----
fs/btrfs/zoned.h | 7 ++--
4 files changed, 98 insertions(+), 10 deletions(-)
diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
index 819727460bcf..9092487171ad 100644
--- a/fs/btrfs/disk-io.c
+++ b/fs/btrfs/disk-io.c
@@ -3702,7 +3702,12 @@ int __cold open_ctree(struct super_block *sb, struct btrfs_fs_devices *fs_device
btrfs_free_zone_cache(fs_info);
- btrfs_check_active_zone_reservation(fs_info);
+ ret = btrfs_restore_active_nondata_bgs(fs_info);
+ if (ret) {
+ btrfs_err(fs_info, "failed to restore active non-data block groups: %pe",
+ ERR_PTR(ret));
+ goto fail_sysfs;
+ }
if (!sb_rdonly(sb) && fs_info->fs_devices->missing_devices &&
!btrfs_check_rw_degradable(fs_info, NULL)) {
diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h
index 10e15a319b93..aa3d6b66495e 100644
--- a/fs/btrfs/fs.h
+++ b/fs/btrfs/fs.h
@@ -931,6 +931,10 @@ struct btrfs_fs_info {
u64 data_reloc_bg;
struct mutex zoned_data_reloc_io_lock;
+ /*
+ * Active block groups for normal metadata and system writes. Mount
+ * recovery restores one block group for each role and finishes any extras.
+ */
struct btrfs_block_group *active_meta_bg;
struct btrfs_block_group *active_system_bg;
diff --git a/fs/btrfs/zoned.c b/fs/btrfs/zoned.c
index bd079d3b31ba..7d91c074a24a 100644
--- a/fs/btrfs/zoned.c
+++ b/fs/btrfs/zoned.c
@@ -3050,22 +3050,73 @@ int btrfs_zoned_activate_one_bg(struct btrfs_space_info *space_info, bool do_fin
return 0;
}
+static int finish_extra_active_nondata_bgs(struct btrfs_fs_info *fs_info)
+{
+ struct btrfs_block_group *block_group;
+ struct btrfs_block_group *next;
+ u64 tail_unusable;
+ int ret;
+
+ list_for_each_entry_safe(block_group, next, &fs_info->zone_active_bgs,
+ active_bg_list) {
+ if (!(block_group->flags &
+ (BTRFS_BLOCK_GROUP_METADATA | BTRFS_BLOCK_GROUP_SYSTEM)))
+ continue;
+
+ if (block_group == fs_info->active_meta_bg ||
+ block_group == fs_info->active_system_bg)
+ continue;
+
+ btrfs_get_block_group(block_group);
+ tail_unusable = block_group->zone_capacity - block_group->alloc_offset;
+
+ ret = do_zone_finish(block_group, true);
+ if (!ret) {
+ struct btrfs_space_info *space_info = block_group->space_info;
+
+ /* Account for the unused tail abandoned by ZONE_FINISH. */
+ spin_lock(&space_info->lock);
+ block_group->zone_unusable += tail_unusable;
+ btrfs_space_info_update_bytes_zone_unusable(space_info,
+ tail_unusable);
+ spin_unlock(&space_info->lock);
+ }
+ btrfs_put_block_group(block_group);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
/*
- * Reserve zones for one metadata block group, one tree-log block group, and one
- * system block group.
+ * Restore one active block group for normal metadata and system writes.
+ *
+ * The tree-log block group identity is not persisted, so recovered active
+ * metadata block groups cannot be distinguished by their previous role. Keep
+ * one for the normal metadata role and leave one reservation for a new
+ * tree-log block group.
+ *
+ * Older kernels rebuilt zone_active_bgs at mount without recovering the role
+ * assignments, which could result in multiple active metadata or system block
+ * groups. Finish all but the selected block group for each role.
*/
-void btrfs_check_active_zone_reservation(struct btrfs_fs_info *fs_info)
+int btrfs_restore_active_nondata_bgs(struct btrfs_fs_info *fs_info)
{
struct btrfs_fs_devices *fs_devices = fs_info->fs_devices;
struct btrfs_block_group *block_group;
+ struct btrfs_block_group *active_meta_bg = NULL;
+ struct btrfs_block_group *active_system_bg = NULL;
struct btrfs_device *device;
+ u64 active_meta_avail = 0;
+ u64 active_system_avail = 0;
/* Reserve zones for normal SINGLE metadata and tree-log block group. */
unsigned int metadata_reserve = 2;
/* Reserve a zone for SINGLE system block group. */
unsigned int system_reserve = 1;
if (!test_bit(BTRFS_FS_ACTIVE_ZONE_TRACKING, &fs_info->flags))
- return;
+ return 0;
/*
* This function is called from the mount context. So, there is no
@@ -3087,14 +3138,35 @@ void btrfs_check_active_zone_reservation(struct btrfs_fs_info *fs_info)
}
mutex_unlock(&fs_devices->device_list_mutex);
- /* Release reservation for currently active block groups. */
+ /*
+ * Account all active non-data block groups before selecting one for each role.
+ * Finishing the extra block groups releases their reservations again.
+ */
spin_lock(&fs_info->zone_active_bgs_lock);
list_for_each_entry(block_group, &fs_info->zone_active_bgs, active_bg_list) {
struct btrfs_chunk_map *map = block_group->physical_map;
+ struct btrfs_block_group **active_bg;
+ u64 *active_avail;
+ u64 avail;
- if (!(block_group->flags &
- (BTRFS_BLOCK_GROUP_METADATA | BTRFS_BLOCK_GROUP_SYSTEM)))
+ if (block_group->flags & BTRFS_BLOCK_GROUP_METADATA) {
+ active_bg = &active_meta_bg;
+ active_avail = &active_meta_avail;
+ } else if (block_group->flags & BTRFS_BLOCK_GROUP_SYSTEM) {
+ active_bg = &active_system_bg;
+ active_avail = &active_system_avail;
+ } else {
continue;
+ }
+
+ avail = block_group->zone_capacity - block_group->alloc_offset;
+ if (!*active_bg || avail > *active_avail) {
+ if (*active_bg)
+ btrfs_put_block_group(*active_bg);
+ *active_bg = block_group;
+ *active_avail = avail;
+ btrfs_get_block_group(*active_bg);
+ }
for (int i = 0; i < map->num_stripes; i++) {
struct btrfs_device *device = map->stripes[i].dev;
@@ -3108,7 +3180,11 @@ void btrfs_check_active_zone_reservation(struct btrfs_fs_info *fs_info)
device->zone_info->reserved_active_zones--;
}
}
+ fs_info->active_meta_bg = active_meta_bg;
+ fs_info->active_system_bg = active_system_bg;
spin_unlock(&fs_info->zone_active_bgs_lock);
+
+ return finish_extra_active_nondata_bgs(fs_info);
}
/*
diff --git a/fs/btrfs/zoned.h b/fs/btrfs/zoned.h
index 8e21a836f858..934a9368ed15 100644
--- a/fs/btrfs/zoned.h
+++ b/fs/btrfs/zoned.h
@@ -95,7 +95,7 @@ void btrfs_zoned_release_data_reloc_bg(struct btrfs_fs_info *fs_info, u64 logica
u64 length);
int btrfs_zone_finish_one_bg(struct btrfs_fs_info *fs_info);
int btrfs_zoned_activate_one_bg(struct btrfs_space_info *space_info, bool do_finish);
-void btrfs_check_active_zone_reservation(struct btrfs_fs_info *fs_info);
+int btrfs_restore_active_nondata_bgs(struct btrfs_fs_info *fs_info);
int btrfs_reset_unused_block_groups(struct btrfs_space_info *space_info, u64 num_bytes);
void btrfs_show_zoned_stats(struct btrfs_fs_info *fs_info, struct seq_file *seq);
@@ -279,7 +279,10 @@ static inline int btrfs_zoned_activate_one_bg(struct btrfs_space_info *space_inf
return 0;
}
-static inline void btrfs_check_active_zone_reservation(struct btrfs_fs_info *fs_info) { }
+static inline int btrfs_restore_active_nondata_bgs(struct btrfs_fs_info *fs_info)
+{
+ return 0;
+}
static inline int btrfs_reset_unused_block_groups(struct btrfs_space_info *space_info,
u64 num_bytes)
--
2.39.5
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH 3/4] btrfs: zoned: remove obsolete non-data block group activation helper
2026-08-24 6:49 [PATCH 0/4] btrfs: zoned: fix active-zone accounting and transitions Dongjiang Zhu
2026-08-24 6:49 ` [PATCH 1/4] btrfs: zoned: track only sequential zones as active Dongjiang Zhu
2026-08-24 6:49 ` [PATCH 2/4] btrfs: zoned: recover active non-data block group roles on mount Dongjiang Zhu
@ 2026-08-24 6:49 ` Dongjiang Zhu
2026-08-24 15:51 ` Johannes Thumshirn
2026-08-24 6:49 ` [PATCH 4/4] btrfs: zoned: serialize zone finishing per block group Dongjiang Zhu
3 siblings, 1 reply; 15+ messages in thread
From: Dongjiang Zhu @ 2026-08-24 6:49 UTC (permalink / raw)
To: linux-btrfs; +Cc: johannes.thumshirn, naohiro.aota
[BUG]
On an HC620 with max_active_zones=128, file writes occupied all 128
active-zone slots. With four direct-I/O writers still running, starting
a balance with
btrfs balance start -f -dusage=0 -musage=0 -susage=100 <mountpoint>
left the balance task blocked in D state. SysRq-w reported:
task:btrfs state:D stack:0 pid:8145
Call Trace:
<TASK>
__schedule+0x50e/0xfe0
schedule+0x22/0xa0
schedule_preempt_disabled+0x10/0x20
__mutex_lock.constprop.0+0x380/0x920
btrfs_inc_block_group_ro+0x6a/0x270 [btrfs]
do_zone_finish+0x2b6/0x4c0 [btrfs]
btrfs_zone_finish_one_bg+0x184/0x1f0 [btrfs]
btrfs_zoned_activate_one_bg+0x189/0x1d0 [btrfs]
reserve_chunk_space+0xdb/0x180 [btrfs]
btrfs_inc_block_group_ro+0x252/0x270 [btrfs]
btrfs_relocate_block_group+0xc2/0x2260 [btrfs]
btrfs_relocate_chunk+0x39/0x1c0 [btrfs]
btrfs_balance+0x911/0x1810 [btrfs]
</TASK>
The blocked balance task also stalls transaction commit and data writers,
so the filesystem can no longer make write progress or unmount cleanly.
[CAUSE]
The outer btrfs_inc_block_group_ro() holds ro_block_group_mutex while
reserve_chunk_space() tries to activate a system block group. When no
active-zone slot is available, btrfs_zoned_activate_one_bg() starts
finishing a partially written data block group.
do_zone_finish() then enters btrfs_inc_block_group_ro() again. The same
task waits for ro_block_group_mutex that it already holds and
self-deadlocks.
[FIX]
Remove the two allocation-time activation calls and their helper for two
reasons:
1. They were added for active_total_bytes accounting, which no longer
exists. Metadata and system block groups are now activated at write
time, and both callers already handle no block group being activated.
2. Allocation-time activation bypasses the role pivot. If A is the
current system block group, activating B consumes another active-zone
slot instead of finishing A. If no slot is available, the helper
tries to finish an unrelated data block group, creating the recursive
path above.
With these activation paths removed, the first write to a sequential
metadata or system block group enters check_bg_is_active(), which finishes
the previous block group for that role before activating the new one.
Assisted-by: LLM
Signed-off-by: Dongjiang Zhu <zhudongjiang@fygo.io>
---
fs/btrfs/block-group.c | 38 +++++++++-----------------------
fs/btrfs/zoned.c | 50 ------------------------------------------
fs/btrfs/zoned.h | 8 -------
3 files changed, 10 insertions(+), 86 deletions(-)
diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c
index 830460a40e86..c58bac5cb134 100644
--- a/fs/btrfs/block-group.c
+++ b/fs/btrfs/block-group.c
@@ -3219,13 +3219,6 @@ int btrfs_inc_block_group_ro(struct btrfs_block_group *cache,
alloc_flags = btrfs_get_alloc_profile(fs_info, space_info->flags);
ret = btrfs_chunk_alloc(trans, space_info, alloc_flags, CHUNK_ALLOC_FORCE);
- if (ret < 0)
- goto out;
- /*
- * We have allocated a new chunk. We also need to activate that chunk to
- * grant metadata tickets for zoned filesystem.
- */
- ret = btrfs_zoned_activate_one_bg(space_info, true);
if (ret < 0)
goto out;
@@ -4537,29 +4530,18 @@ static void reserve_chunk_space(struct btrfs_trans_handle *trans,
if (IS_ERR(bg)) {
ret = PTR_ERR(bg);
} else {
- int activate_ret;
-
/*
- * We have a new chunk. We also need to activate it for
- * zoned filesystem.
+ * If we fail to add the chunk item here, we end
+ * up trying again at phase 2 of chunk allocation,
+ * at btrfs_create_pending_block_groups(). So
+ * ignore any error here. An ENOSPC here could
+ * happen, due to the cases described at
+ * do_chunk_alloc() - the system block group we
+ * just created was just turned into RO mode by a
+ * scrub for example, or a running discard
+ * temporarily removed its free space entries, etc.
*/
- activate_ret = btrfs_zoned_activate_one_bg(info, true);
- if (activate_ret < 0) {
- ret = activate_ret;
- } else {
- /*
- * If we fail to add the chunk item here, we end
- * up trying again at phase 2 of chunk allocation,
- * at btrfs_create_pending_block_groups(). So
- * ignore any error here. An ENOSPC here could
- * happen, due to the cases described at
- * do_chunk_alloc() - the system block group we
- * just created was just turned into RO mode by a
- * scrub for example, or a running discard
- * temporarily removed its free space entries, etc.
- */
- btrfs_chunk_alloc_add_chunk_item(trans, bg);
- }
+ btrfs_chunk_alloc_add_chunk_item(trans, bg);
}
}
diff --git a/fs/btrfs/zoned.c b/fs/btrfs/zoned.c
index 7d91c074a24a..9896a6b64d07 100644
--- a/fs/btrfs/zoned.c
+++ b/fs/btrfs/zoned.c
@@ -3000,56 +3000,6 @@ int btrfs_zone_finish_one_bg(struct btrfs_fs_info *fs_info)
return ret < 0 ? ret : 1;
}
-int btrfs_zoned_activate_one_bg(struct btrfs_space_info *space_info, bool do_finish)
-{
- struct btrfs_fs_info *fs_info = space_info->fs_info;
- struct btrfs_block_group *bg;
- int index;
-
- if (!btrfs_is_zoned(fs_info) || (space_info->flags & BTRFS_BLOCK_GROUP_DATA))
- return 0;
-
- for (;;) {
- int ret;
- bool need_finish = false;
-
- down_read(&space_info->groups_sem);
- for (index = 0; index < BTRFS_NR_RAID_TYPES; index++) {
- list_for_each_entry(bg, &space_info->block_groups[index],
- list) {
- if (!spin_trylock(&bg->lock))
- continue;
- if (btrfs_zoned_bg_is_full(bg) ||
- test_bit(BLOCK_GROUP_FLAG_ZONE_IS_ACTIVE,
- &bg->runtime_flags)) {
- spin_unlock(&bg->lock);
- continue;
- }
- spin_unlock(&bg->lock);
-
- if (btrfs_zone_activate(bg)) {
- up_read(&space_info->groups_sem);
- return 1;
- }
-
- need_finish = true;
- }
- }
- up_read(&space_info->groups_sem);
-
- if (!do_finish || !need_finish)
- break;
-
- ret = btrfs_zone_finish_one_bg(fs_info);
- if (ret == 0)
- break;
- if (ret < 0)
- return ret;
- }
-
- return 0;
-}
-
static int finish_extra_active_nondata_bgs(struct btrfs_fs_info *fs_info)
{
struct btrfs_block_group *block_group;
diff --git a/fs/btrfs/zoned.h b/fs/btrfs/zoned.h
index 934a9368ed15..dc7c453fc979 100644
--- a/fs/btrfs/zoned.h
+++ b/fs/btrfs/zoned.h
@@ -94,7 +94,6 @@ bool btrfs_zoned_should_reclaim(const struct btrfs_fs_info *fs_info);
void btrfs_zoned_release_data_reloc_bg(struct btrfs_fs_info *fs_info, u64 logical,
u64 length);
int btrfs_zone_finish_one_bg(struct btrfs_fs_info *fs_info);
-int btrfs_zoned_activate_one_bg(struct btrfs_space_info *space_info, bool do_finish);
int btrfs_restore_active_nondata_bgs(struct btrfs_fs_info *fs_info);
int btrfs_reset_unused_block_groups(struct btrfs_space_info *space_info, u64 num_bytes);
void btrfs_show_zoned_stats(struct btrfs_fs_info *fs_info, struct seq_file *seq);
@@ -272,13 +271,6 @@ static inline int btrfs_zone_finish_one_bg(struct btrfs_fs_info *fs_info)
return 1;
}
-static inline int btrfs_zoned_activate_one_bg(struct btrfs_space_info *space_info,
- bool do_finish)
-{
- /* Consider all the block groups are active */
- return 0;
-}
-
static inline int btrfs_restore_active_nondata_bgs(struct btrfs_fs_info *fs_info)
{
return 0;
--
2.39.5
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH 4/4] btrfs: zoned: serialize zone finishing per block group
2026-08-24 6:49 [PATCH 0/4] btrfs: zoned: fix active-zone accounting and transitions Dongjiang Zhu
` (2 preceding siblings ...)
2026-08-24 6:49 ` [PATCH 3/4] btrfs: zoned: remove obsolete non-data block group activation helper Dongjiang Zhu
@ 2026-08-24 6:49 ` Dongjiang Zhu
2026-08-24 16:00 ` Johannes Thumshirn
3 siblings, 1 reply; 15+ messages in thread
From: Dongjiang Zhu @ 2026-08-24 6:49 UTC (permalink / raw)
To: linux-btrfs; +Cc: johannes.thumshirn, naohiro.aota
[BUG]
On an HC620 with max_active_zones=128, buffered writes occupied all 128
active-zone slots. A metadata-heavy create/delete workload followed by
sync then forced a metadata block group switch.
A kernel with additional zone finish tracing reported:
zoned-debug: finish clear-runtime bg=140928614400 caller=btrfs_zone_finish_endio_workfn comm=kworker/u8:7
zoned-debug: finish early-inactive bg=140928614400 caller=btrfs_check_meta_write_pointer comm=sync
zoned-debug: pivot finish ret=0 old_bg=140928614400 new_bg=339839287296
zoned-debug: activate fail slot bg=339839287296 type=metadata left=0 max_active=128 comm=sync
zoned-debug: finish zone-mgmt bg=140928614400 ret=0 left=0 caller=btrfs_zone_finish_endio_workfn comm=kworker/u8:7
BTRFS error (device sdb): error while writing out transaction: -EAGAIN
BTRFS error (device sdb state A): Transaction 67 aborted (-EAGAIN)
BTRFS info (device sdb state EA): forced readonly
zoned-debug: clear-active dev=/dev/sdb pos=140928614400 left=1 max_active=128
The transaction was aborted before the old block group returned its
active-zone slot, forcing the filesystem read-only.
[CAUSE]
do_zone_finish() clears BLOCK_GROUP_FLAG_ZONE_IS_ACTIVE before issuing
the zone finish operation. This allows the following interleaving:
Thread A, endio worker Thread B, transaction writeback
clear ZONE_IS_ACTIVE
call_zone_finish(old)
observe old as inactive
return from do_zone_finish(old)
activate(new)
fail with left == 0
clear the device active bit
increment active_zones_left
Thread B treats the cleared active bit as a completed finish while thread
A is still performing the device operation.
[FIX]
Add BLOCK_GROUP_FLAG_ZONE_FINISHING and keep ZONE_IS_ACTIVE set until the
zone finish operation and active-list cleanup are complete.
Concurrent finishers wait for ZONE_FINISHING to be cleared instead of
returning early. Wake them after clearing ZONE_IS_ACTIVE and removing
the block group from the active list. If a waiter acquired a temporary
read-only reference, release it after the wait.
Assisted-by: LLM
Signed-off-by: Dongjiang Zhu <zhudongjiang@fygo.io>
---
fs/btrfs/block-group.h | 1 +
fs/btrfs/zoned.c | 29 ++++++++++++++++++++++++++++-
2 files changed, 29 insertions(+), 1 deletion(-)
diff --git a/fs/btrfs/block-group.h b/fs/btrfs/block-group.h
index 37ab8b7eee9c..d8ae56f3956d 100644
--- a/fs/btrfs/block-group.h
+++ b/fs/btrfs/block-group.h
@@ -99,6 +99,7 @@ enum btrfs_block_group_flags {
BLOCK_GROUP_FLAG_NEW,
BLOCK_GROUP_FLAG_FULLY_REMAPPED,
BLOCK_GROUP_FLAG_STRIPE_REMOVAL_PENDING,
+ BLOCK_GROUP_FLAG_ZONE_FINISHING,
};
enum btrfs_caching_type {
diff --git a/fs/btrfs/zoned.c b/fs/btrfs/zoned.c
index 9896a6b64d07..3bda40c0d4de 100644
--- a/fs/btrfs/zoned.c
+++ b/fs/btrfs/zoned.c
@@ -2558,6 +2558,7 @@ static int do_zone_finish(struct btrfs_block_group *block_group, bool fully_writ
const bool is_metadata = (block_group->flags &
(BTRFS_BLOCK_GROUP_METADATA | BTRFS_BLOCK_GROUP_SYSTEM));
struct btrfs_dev_replace *dev_replace = &fs_info->dev_replace;
+ bool bg_ro = false;
int ret = 0;
int i;
@@ -2567,6 +2568,11 @@ static int do_zone_finish(struct btrfs_block_group *block_group, bool fully_writ
return 0;
}
+ if (test_bit(BLOCK_GROUP_FLAG_ZONE_FINISHING, &block_group->runtime_flags)) {
+ spin_unlock(&block_group->lock);
+ goto wait_finish;
+ }
+
/* Check if we have unwritten allocated space */
if (is_metadata &&
block_group->start + block_group->alloc_offset > block_group->meta_write_pointer) {
@@ -2591,6 +2597,7 @@ static int do_zone_finish(struct btrfs_block_group *block_group, bool fully_writ
ret = btrfs_inc_block_group_ro(block_group, false);
if (ret)
return ret;
+ bg_ro = true;
/* Ensure all writes in this block group finish */
btrfs_wait_block_group_reservations(block_group);
@@ -2613,6 +2620,12 @@ static int do_zone_finish(struct btrfs_block_group *block_group, bool fully_writ
return 0;
}
+ if (test_bit(BLOCK_GROUP_FLAG_ZONE_FINISHING,
+ &block_group->runtime_flags)) {
+ spin_unlock(&block_group->lock);
+ goto wait_finish;
+ }
+
if (block_group->reserved ||
test_bit(BLOCK_GROUP_FLAG_ZONED_DATA_RELOC,
&block_group->runtime_flags)) {
@@ -2622,7 +2635,7 @@ static int do_zone_finish(struct btrfs_block_group *block_group, bool fully_writ
}
}
- clear_bit(BLOCK_GROUP_FLAG_ZONE_IS_ACTIVE, &block_group->runtime_flags);
+ set_bit(BLOCK_GROUP_FLAG_ZONE_FINISHING, &block_group->runtime_flags);
block_group->alloc_offset = block_group->zone_capacity;
if (block_group->flags & (BTRFS_BLOCK_GROUP_METADATA | BTRFS_BLOCK_GROUP_SYSTEM))
block_group->meta_write_pointer = block_group->start +
@@ -2646,8 +2659,13 @@ static int do_zone_finish(struct btrfs_block_group *block_group, bool fully_writ
btrfs_dec_block_group_ro(block_group);
spin_lock(&fs_info->zone_active_bgs_lock);
+ spin_lock(&block_group->lock);
+ clear_bit(BLOCK_GROUP_FLAG_ZONE_IS_ACTIVE, &block_group->runtime_flags);
ASSERT(!list_empty(&block_group->active_bg_list));
list_del_init(&block_group->active_bg_list);
+ clear_and_wake_up_bit(BLOCK_GROUP_FLAG_ZONE_FINISHING,
+ &block_group->runtime_flags);
+ spin_unlock(&block_group->lock);
spin_unlock(&fs_info->zone_active_bgs_lock);
/* For active_bg_list */
@@ -2656,6 +2674,15 @@ static int do_zone_finish(struct btrfs_block_group *block_group, bool fully_writ
clear_and_wake_up_bit(BTRFS_FS_NEED_ZONE_FINISH, &fs_info->flags);
return ret;
+
+wait_finish:
+ wait_on_bit_io(&block_group->runtime_flags,
+ BLOCK_GROUP_FLAG_ZONE_FINISHING,
+ TASK_UNINTERRUPTIBLE);
+ if (bg_ro)
+ btrfs_dec_block_group_ro(block_group);
+
+ return 0;
}
int btrfs_zone_finish(struct btrfs_block_group *block_group)
--
2.39.5
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH 1/4] btrfs: zoned: track only sequential zones as active
2026-08-24 6:49 ` [PATCH 1/4] btrfs: zoned: track only sequential zones as active Dongjiang Zhu
@ 2026-08-24 13:55 ` Johannes Thumshirn
2026-08-24 22:40 ` Qu Wenruo
2026-08-25 3:01 ` Dongjiang Zhu
0 siblings, 2 replies; 15+ messages in thread
From: Johannes Thumshirn @ 2026-08-24 13:55 UTC (permalink / raw)
To: Dongjiang Zhu; +Cc: linux-btrfs, naohiro.aota
On Mon, Aug 24, 2026 at 02:49:53PM +0800, Dongjiang Zhu wrote:
> [CAUSE]
> The runtime activation and finish paths apply active-zone accounting to
> both conventional and sequential stripes.
>
> At mount, however, active_zones_left is rebuilt from device zone
> conditions. Conventional zones report NOT_WP and are therefore not
> included. Meanwhile, all-conventional block groups are marked active
> and added to zone_active_bgs, so mount recovery still decreases
> reserved_active_zones for their metadata and system stripes.
>
> The two counters can therefore diverge across a remount.
>
> [BUG]
> Each unmatched reservation decrement lets data consume one more
> sequential active-zone slot. Once the reservation becomes negative,
> data can exhaust all such slots.
>
> A later transition from a conventional metadata or system target to a
> sequential one can then fail, because finishing the conventional target
> does not release a sequential active-zone slot.
>
> If this happens during synchronous transaction writeback,
> btrfs_check_meta_write_pointer() returns -EAGAIN. The transaction is
> then aborted and the filesystem is forced read-only.
>
> The reservation mismatch was reproduced by creating small files with
> large xattrs in batches, syncing them and remounting an HC620 filesystem
> using SINGLE profiles:
>
> active_zones_left: 126
> reserved_active_zones: 3 -> -8
>
> The active list contained one partially used conventional system block
> group, nine full conventional metadata block groups and one partially
> used conventional metadata block group.
>
> [FIX]
> Use sequential stripes as the unit of active-zone tracking.
>
> Do not mark all-conventional block groups active. Only sequential
> stripes update the active-zone bitmap and non-data reservation during
> activation, finish and mount recovery.
>
> Assisted-by: LLM
> Signed-off-by: Dongjiang Zhu <zhudongjiang@fygo.io>
Looks good, two small nits though in case you need to resend the series:
1) Please don't do the [CAUSE], [BUG], [FIX] thingy, I know Qu does it but
noone else does that.
2) We now have a repeated pattern of:
if (!btrfs_dev_is_sequential())
return
if (max_active_zones)
return
Maybe have a helper for that? Something like
static inline bool btrfs_needs_active_zone_tracking(struct btrfs_device *dev,
u64 physical)
{
if (!btrfs_dev_is_sequential(dev, physical))
return false;
if (dev->zone_info->max_active_zones == 0)
return false;
return true;
}
Otherwise looks good,
Reviewed-by: Johannes Thumshirn <johannes.thumshirn@wdc.com>
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH 2/4] btrfs: zoned: recover active non-data block group roles on mount
2026-08-24 6:49 ` [PATCH 2/4] btrfs: zoned: recover active non-data block group roles on mount Dongjiang Zhu
@ 2026-08-24 14:42 ` Johannes Thumshirn
2026-08-25 3:08 ` Dongjiang Zhu
0 siblings, 1 reply; 15+ messages in thread
From: Johannes Thumshirn @ 2026-08-24 14:42 UTC (permalink / raw)
To: Dongjiang Zhu; +Cc: linux-btrfs, naohiro.aota
On Mon, Aug 24, 2026 at 02:49:54PM +0800, Dongjiang Zhu wrote:
> [BUG]
> After a remount, a later write can activate a new metadata or system
> block group without finishing the recovered one. Repeating this across
> remounts consumes additional active-zone slots and reservations, and can
> eventually exhaust all active-zone slots.
>
> Once no slot is available, activation of a metadata or system block group
> fails. During synchronous transaction writeback this propagates as
> -EAGAIN, aborting the transaction and forcing the filesystem read-only.
>
> A remount/fsync/remount reproducer on an HC620 left two partially used
> sequential metadata block groups active while active_meta_bg remained
> NULL:
>
> bg=343865819136 alloc=176095232 cap=268435456
> bg=344402690048 alloc=3260416 cap=268435456
>
Can you submit a fstest for that please? Maybe even in generic/ so XFS and
F2FS also get coverage?
> diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h
> index 10e15a319b93..aa3d6b66495e 100644
> --- a/fs/btrfs/fs.h
> +++ b/fs/btrfs/fs.h
> @@ -931,6 +931,10 @@ struct btrfs_fs_info {
> u64 data_reloc_bg;
> struct mutex zoned_data_reloc_io_lock;
>
> + /*
> + * Active block groups for normal metadata and system writes. Mount
> + * recovery restores one block group for each role and finishes any extras.
> + */
I don't think this comments adds any value.
> struct btrfs_block_group *active_meta_bg;
> struct btrfs_block_group *active_system_bg;
>
> diff --git a/fs/btrfs/zoned.c b/fs/btrfs/zoned.c
> index bd079d3b31ba..7d91c074a24a 100644
> --- a/fs/btrfs/zoned.c
> +++ b/fs/btrfs/zoned.c
> @@ -3050,22 +3050,73 @@ int btrfs_zoned_activate_one_bg(struct btrfs_space_info *space_info, bool do_fin
> return 0;
> }
>
> +static int finish_extra_active_nondata_bgs(struct btrfs_fs_info *fs_info)
> +{
> + struct btrfs_block_group *block_group;
> + struct btrfs_block_group *next;
> + u64 tail_unusable;
> + int ret;
> +
> + list_for_each_entry_safe(block_group, next, &fs_info->zone_active_bgs,
> + active_bg_list) {
> + if (!(block_group->flags &
> + (BTRFS_BLOCK_GROUP_METADATA | BTRFS_BLOCK_GROUP_SYSTEM)))
> + continue;
> +
> + if (block_group == fs_info->active_meta_bg ||
> + block_group == fs_info->active_system_bg)
> + continue;
> +
> + btrfs_get_block_group(block_group);
> + tail_unusable = block_group->zone_capacity - block_group->alloc_offset;
Shouldn't these be accessed under the block_group->lock?
> - if (!(block_group->flags &
> - (BTRFS_BLOCK_GROUP_METADATA | BTRFS_BLOCK_GROUP_SYSTEM)))
> + if (block_group->flags & BTRFS_BLOCK_GROUP_METADATA) {
> + active_bg = &active_meta_bg;
> + active_avail = &active_meta_avail;
> + } else if (block_group->flags & BTRFS_BLOCK_GROUP_SYSTEM) {
> + active_bg = &active_system_bg;
> + active_avail = &active_system_avail;
> + } else {
> continue;
> + }
> +
> + avail = block_group->zone_capacity - block_group->alloc_offset;
Same here, don't you need the block_group->lock?
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH 3/4] btrfs: zoned: remove obsolete non-data block group activation helper
2026-08-24 6:49 ` [PATCH 3/4] btrfs: zoned: remove obsolete non-data block group activation helper Dongjiang Zhu
@ 2026-08-24 15:51 ` Johannes Thumshirn
0 siblings, 0 replies; 15+ messages in thread
From: Johannes Thumshirn @ 2026-08-24 15:51 UTC (permalink / raw)
To: Dongjiang Zhu; +Cc: linux-btrfs, naohiro.aota
Apart from the commit message style nit
Reviewed-by: Johannes Thumshirn <johannes.thumshirn@wdc.com>
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH 4/4] btrfs: zoned: serialize zone finishing per block group
2026-08-24 6:49 ` [PATCH 4/4] btrfs: zoned: serialize zone finishing per block group Dongjiang Zhu
@ 2026-08-24 16:00 ` Johannes Thumshirn
2026-08-25 3:16 ` Dongjiang Zhu
0 siblings, 1 reply; 15+ messages in thread
From: Johannes Thumshirn @ 2026-08-24 16:00 UTC (permalink / raw)
To: Dongjiang Zhu; +Cc: linux-btrfs, naohiro.aota
On Mon, Aug 24, 2026 at 02:49:56PM +0800, Dongjiang Zhu wrote:
> [BUG]
> On an HC620 with max_active_zones=128, buffered writes occupied all 128
> active-zone slots. A metadata-heavy create/delete workload followed by
> sync then forced a metadata block group switch.
Can you explain that to me. In my mental model, if all 128 active-zones are
used and you call sync, we need to add a treelog-bg. But there is no more
active zone resource available so we cannot create the treelog bg. This is why
we fail, so we need to finish a zone first to then create a treelog bg.
>
> A kernel with additional zone finish tracing reported:
>
> zoned-debug: finish clear-runtime bg=140928614400 caller=btrfs_zone_finish_endio_workfn comm=kworker/u8:7
> zoned-debug: finish early-inactive bg=140928614400 caller=btrfs_check_meta_write_pointer comm=sync
> zoned-debug: pivot finish ret=0 old_bg=140928614400 new_bg=339839287296
> zoned-debug: activate fail slot bg=339839287296 type=metadata left=0 max_active=128 comm=sync
> zoned-debug: finish zone-mgmt bg=140928614400 ret=0 left=0 caller=btrfs_zone_finish_endio_workfn comm=kworker/u8:7
> BTRFS error (device sdb): error while writing out transaction: -EAGAIN
> BTRFS error (device sdb state A): Transaction 67 aborted (-EAGAIN)
> BTRFS info (device sdb state EA): forced readonly
> zoned-debug: clear-active dev=/dev/sdb pos=140928614400 left=1 max_active=128
>
> The transaction was aborted before the old block group returned its
> active-zone slot, forcing the filesystem read-only.
Ah now I see we're on the same page.
>
> [CAUSE]
> do_zone_finish() clears BLOCK_GROUP_FLAG_ZONE_IS_ACTIVE before issuing
> the zone finish operation. This allows the following interleaving:
>
> Thread A, endio worker Thread B, transaction writeback
>
> clear ZONE_IS_ACTIVE
> call_zone_finish(old)
> observe old as inactive
> return from do_zone_finish(old)
> activate(new)
> fail with left == 0
> clear the device active bit
> increment active_zones_left
>
> Thread B treats the cleared active bit as a completed finish while thread
> A is still performing the device operation.
>
> [FIX]
> Add BLOCK_GROUP_FLAG_ZONE_FINISHING and keep ZONE_IS_ACTIVE set until the
> zone finish operation and active-list cleanup are complete.
>
> Concurrent finishers wait for ZONE_FINISHING to be cleared instead of
> returning early. Wake them after clearing ZONE_IS_ACTIVE and removing
> the block group from the active list. If a waiter acquired a temporary
> read-only reference, release it after the wait.
>
Stupid question, wouldn't it be enough to re-order the call to
do_zone_finish() vs the clearing of BLOCK_GROUP_FLAG_ZONE_IS_ACTIVE?
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH 1/4] btrfs: zoned: track only sequential zones as active
2026-08-24 13:55 ` Johannes Thumshirn
@ 2026-08-24 22:40 ` Qu Wenruo
2026-08-25 9:10 ` Johannes Thumshirn
2026-08-25 3:01 ` Dongjiang Zhu
1 sibling, 1 reply; 15+ messages in thread
From: Qu Wenruo @ 2026-08-24 22:40 UTC (permalink / raw)
To: Johannes Thumshirn, Dongjiang Zhu; +Cc: linux-btrfs, naohiro.aota
在 2026/8/24 23:25, Johannes Thumshirn 写道:
> On Mon, Aug 24, 2026 at 02:49:53PM +0800, Dongjiang Zhu wrote:
>> [CAUSE]
>> The runtime activation and finish paths apply active-zone accounting to
>> both conventional and sequential stripes.
>>
>> At mount, however, active_zones_left is rebuilt from device zone
>> conditions. Conventional zones report NOT_WP and are therefore not
>> included. Meanwhile, all-conventional block groups are marked active
>> and added to zone_active_bgs, so mount recovery still decreases
>> reserved_active_zones for their metadata and system stripes.
>>
>> The two counters can therefore diverge across a remount.
>>
>> [BUG]
>> Each unmatched reservation decrement lets data consume one more
>> sequential active-zone slot. Once the reservation becomes negative,
>> data can exhaust all such slots.
>>
>> A later transition from a conventional metadata or system target to a
>> sequential one can then fail, because finishing the conventional target
>> does not release a sequential active-zone slot.
>>
>> If this happens during synchronous transaction writeback,
>> btrfs_check_meta_write_pointer() returns -EAGAIN. The transaction is
>> then aborted and the filesystem is forced read-only.
>>
>> The reservation mismatch was reproduced by creating small files with
>> large xattrs in batches, syncing them and remounting an HC620 filesystem
>> using SINGLE profiles:
>>
>> active_zones_left: 126
>> reserved_active_zones: 3 -> -8
>>
>> The active list contained one partially used conventional system block
>> group, nine full conventional metadata block groups and one partially
>> used conventional metadata block group.
>>
>> [FIX]
>> Use sequential stripes as the unit of active-zone tracking.
>>
>> Do not mark all-conventional block groups active. Only sequential
>> stripes update the active-zone bitmap and non-data reservation during
>> activation, finish and mount recovery.
>>
>> Assisted-by: LLM
>> Signed-off-by: Dongjiang Zhu <zhudongjiang@fygo.io>
>
> Looks good, two small nits though in case you need to resend the series:
> 1) Please don't do the [CAUSE], [BUG], [FIX] thingy, I know Qu does it but
> noone else does that.
Sorry, I thought that was a good way to explain a bug, but this doesn't
look like so for everyone else.
I'll also change the future style.
Thanks,
Qu
> 2) We now have a repeated pattern of:
>
> if (!btrfs_dev_is_sequential())
> return
> if (max_active_zones)
> return
>
> Maybe have a helper for that? Something like
>
> static inline bool btrfs_needs_active_zone_tracking(struct btrfs_device *dev,
> u64 physical)
> {
> if (!btrfs_dev_is_sequential(dev, physical))
> return false;
>
> if (dev->zone_info->max_active_zones == 0)
> return false;
>
> return true;
> }
>
> Otherwise looks good,
> Reviewed-by: Johannes Thumshirn <johannes.thumshirn@wdc.com>
>
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH 1/4] btrfs: zoned: track only sequential zones as active
2026-08-24 13:55 ` Johannes Thumshirn
2026-08-24 22:40 ` Qu Wenruo
@ 2026-08-25 3:01 ` Dongjiang Zhu
1 sibling, 0 replies; 15+ messages in thread
From: Dongjiang Zhu @ 2026-08-25 3:01 UTC (permalink / raw)
To: Johannes Thumshirn; +Cc: linux-btrfs, naohiro.aota
在 2026/8/24 21:55, Johannes Thumshirn 写道:
> On Mon, Aug 24, 2026 at 02:49:53PM +0800, Dongjiang Zhu wrote:
>> [CAUSE]
>> The runtime activation and finish paths apply active-zone accounting to
>> both conventional and sequential stripes.
>>
>> At mount, however, active_zones_left is rebuilt from device zone
>> conditions. Conventional zones report NOT_WP and are therefore not
>> included. Meanwhile, all-conventional block groups are marked active
>> and added to zone_active_bgs, so mount recovery still decreases
>> reserved_active_zones for their metadata and system stripes.
>>
>> The two counters can therefore diverge across a remount.
>>
>> [BUG]
>> Each unmatched reservation decrement lets data consume one more
>> sequential active-zone slot. Once the reservation becomes negative,
>> data can exhaust all such slots.
>>
>> A later transition from a conventional metadata or system target to a
>> sequential one can then fail, because finishing the conventional target
>> does not release a sequential active-zone slot.
>>
>> If this happens during synchronous transaction writeback,
>> btrfs_check_meta_write_pointer() returns -EAGAIN. The transaction is
>> then aborted and the filesystem is forced read-only.
>>
>> The reservation mismatch was reproduced by creating small files with
>> large xattrs in batches, syncing them and remounting an HC620 filesystem
>> using SINGLE profiles:
>>
>> active_zones_left: 126
>> reserved_active_zones: 3 -> -8
>>
>> The active list contained one partially used conventional system block
>> group, nine full conventional metadata block groups and one partially
>> used conventional metadata block group.
>>
>> [FIX]
>> Use sequential stripes as the unit of active-zone tracking.
>>
>> Do not mark all-conventional block groups active. Only sequential
>> stripes update the active-zone bitmap and non-data reservation during
>> activation, finish and mount recovery.
>>
>> Assisted-by: LLM
>> Signed-off-by: Dongjiang Zhu<zhudongjiang@fygo.io>
> Looks good, two small nits though in case you need to resend the series:
> 1) Please don't do the [CAUSE], [BUG], [FIX] thingy, I know Qu does it but
> noone else does that.
Sure, I will remove these headings from the commit messages in v2.
> 2) We now have a repeated pattern of:
>
> if (!btrfs_dev_is_sequential())
> return
> if (max_active_zones)
> return
>
> Maybe have a helper for that? Something like
>
> static inline bool btrfs_needs_active_zone_tracking(struct btrfs_device *dev,
> u64 physical)
> {
> if (!btrfs_dev_is_sequential(dev, physical))
> return false;
>
> if (dev->zone_info->max_active_zones == 0)
> return false;
>
> return true;
> }
>
Good idea. I will add the helper and use it for the repeated checks in
v2.
Thanks for the review.
Dongjiang Zhu
> Otherwise looks good,
> Reviewed-by: Johannes Thumshirn<johannes.thumshirn@wdc.com>
>
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH 2/4] btrfs: zoned: recover active non-data block group roles on mount
2026-08-24 14:42 ` Johannes Thumshirn
@ 2026-08-25 3:08 ` Dongjiang Zhu
2026-08-25 10:02 ` Johannes Thumshirn
0 siblings, 1 reply; 15+ messages in thread
From: Dongjiang Zhu @ 2026-08-25 3:08 UTC (permalink / raw)
To: Johannes Thumshirn; +Cc: linux-btrfs, naohiro.aota
在 2026/8/24 22:42, Johannes Thumshirn 写道:
> On Mon, Aug 24, 2026 at 02:49:54PM +0800, Dongjiang Zhu wrote:
>> [BUG]
>> After a remount, a later write can activate a new metadata or system
>> block group without finishing the recovered one. Repeating this across
>> remounts consumes additional active-zone slots and reservations, and can
>> eventually exhaust all active-zone slots.
>>
>> Once no slot is available, activation of a metadata or system block group
>> fails. During synchronous transaction writeback this propagates as
>> -EAGAIN, aborting the transaction and forcing the filesystem read-only.
>>
>> A remount/fsync/remount reproducer on an HC620 left two partially used
>> sequential metadata block groups active while active_meta_bg remained
>> NULL:
>>
>> bg=343865819136 alloc=176095232 cap=268435456
>> bg=344402690048 alloc=3260416 cap=268435456
>>
>
> Can you submit a fstest for that please? Maybe even in generic/ so XFS and
> F2FS also get coverage?
>
Yes, I can work on a separate fstest. To make sure I understand the
suggestion correctly, do you have in mind a zoned-device test that
creates a metadata-heavy workload, syncs, fully unmounts and mounts the
filesystem repeatedly, and then verifies that further writes and syncs
still succeed?
>> diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h
>> index 10e15a319b93..aa3d6b66495e 100644
>> --- a/fs/btrfs/fs.h
>> +++ b/fs/btrfs/fs.h
>> @@ -931,6 +931,10 @@ struct btrfs_fs_info {
>> u64 data_reloc_bg;
>> struct mutex zoned_data_reloc_io_lock;
>>
>> + /*
>> + * Active block groups for normal metadata and system writes. Mount
>> + * recovery restores one block group for each role and finishes any extras.
>> + */
>
> I don't think this comments adds any value.
>
Agreed, I will remove it.
>> struct btrfs_block_group *active_meta_bg;
>> struct btrfs_block_group *active_system_bg;
>>
>> diff --git a/fs/btrfs/zoned.c b/fs/btrfs/zoned.c
>> index bd079d3b31ba..7d91c074a24a 100644
>> --- a/fs/btrfs/zoned.c
>> +++ b/fs/btrfs/zoned.c
>> @@ -3050,22 +3050,73 @@ int btrfs_zoned_activate_one_bg(struct btrfs_space_info *space_info, bool do_fin
>> return 0;
>> }
>>
>> +static int finish_extra_active_nondata_bgs(struct btrfs_fs_info *fs_info)
>> +{
>> + struct btrfs_block_group *block_group;
>> + struct btrfs_block_group *next;
>> + u64 tail_unusable;
>> + int ret;
>> +
>> + list_for_each_entry_safe(block_group, next, &fs_info->zone_active_bgs,
>> + active_bg_list) {
>> + if (!(block_group->flags &
>> + (BTRFS_BLOCK_GROUP_METADATA | BTRFS_BLOCK_GROUP_SYSTEM)))
>> + continue;
>> +
>> + if (block_group == fs_info->active_meta_bg ||
>> + block_group == fs_info->active_system_bg)
>> + continue;
>> +
>> + btrfs_get_block_group(block_group);
>> + tail_unusable = block_group->zone_capacity - block_group->alloc_offset;
>
> Shouldn't these be accessed under the block_group->lock?
>
> [...]
>
> Same here, don't you need the block_group->lock?
Both accesses only happen during open_ctree(), after all block groups
have been loaded and before the background threads are started. Following
other mount-time initialization code, I assumed that block_group->lock
was not necessary because the allocation state cannot change concurrently.
On the other hand, taking the lock would make the usual protection of
alloc_offset explicit and avoid relying on the mount-time context for
future maintenance. Would you prefer that I add the locking around both
reads in v2?
Thanks,
Dongjiang Zhu
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH 4/4] btrfs: zoned: serialize zone finishing per block group
2026-08-24 16:00 ` Johannes Thumshirn
@ 2026-08-25 3:16 ` Dongjiang Zhu
0 siblings, 0 replies; 15+ messages in thread
From: Dongjiang Zhu @ 2026-08-25 3:16 UTC (permalink / raw)
To: Johannes Thumshirn; +Cc: linux-btrfs, naohiro.aota
在 2026/8/25 0:00, Johannes Thumshirn 写道:
> On Mon, Aug 24, 2026 at 02:49:56PM +0800, Dongjiang Zhu wrote:
>> [BUG]
>> On an HC620 with max_active_zones=128, buffered writes occupied all 128
>> active-zone slots. A metadata-heavy create/delete workload followed by
>> sync then forced a metadata block group switch.
>
> Can you explain that to me. In my mental model, if all 128 active-zones are
> used and you call sync, we need to add a treelog-bg. But there is no more
> active zone resource available so we cannot create the treelog bg. This is why
> we fail, so we need to finish a zone first to then create a treelog bg.
>
>>
>> A kernel with additional zone finish tracing reported:
>>
>> zoned-debug: finish clear-runtime bg=140928614400 caller=btrfs_zone_finish_endio_workfn comm=kworker/u8:7
>> zoned-debug: finish early-inactive bg=140928614400 caller=btrfs_check_meta_write_pointer comm=sync
>> zoned-debug: pivot finish ret=0 old_bg=140928614400 new_bg=339839287296
>> zoned-debug: activate fail slot bg=339839287296 type=metadata left=0 max_active=128 comm=sync
>> zoned-debug: finish zone-mgmt bg=140928614400 ret=0 left=0 caller=btrfs_zone_finish_endio_workfn comm=kworker/u8:7
>> BTRFS error (device sdb): error while writing out transaction: -EAGAIN
>> BTRFS error (device sdb state A): Transaction 67 aborted (-EAGAIN)
>> BTRFS info (device sdb state EA): forced readonly
>> zoned-debug: clear-active dev=/dev/sdb pos=140928614400 left=1 max_active=128
>>
>> The transaction was aborted before the old block group returned its
>> active-zone slot, forcing the filesystem read-only.
>
> Ah now I see we're on the same page.
>>
>> [CAUSE]
>> do_zone_finish() clears BLOCK_GROUP_FLAG_ZONE_IS_ACTIVE before issuing
>> the zone finish operation. This allows the following interleaving:
>>
>> Thread A, endio worker Thread B, transaction writeback
>>
>> clear ZONE_IS_ACTIVE
>> call_zone_finish(old)
>> observe old as inactive
>> return from do_zone_finish(old)
>> activate(new)
>> fail with left == 0
>> clear the device active bit
>> increment active_zones_left
>>
>> Thread B treats the cleared active bit as a completed finish while thread
>> A is still performing the device operation.
>>
>> [FIX]
>> Add BLOCK_GROUP_FLAG_ZONE_FINISHING and keep ZONE_IS_ACTIVE set until the
>> zone finish operation and active-list cleanup are complete.
>>
>> Concurrent finishers wait for ZONE_FINISHING to be cleared instead of
>> returning early. Wake them after clearing ZONE_IS_ACTIVE and removing
>> the block group from the active list. If a waiter acquired a temporary
>> read-only reference, release it after the wait.
>>
>
> Stupid question, wouldn't it be enough to re-order the call to
> do_zone_finish() vs the clearing of BLOCK_GROUP_FLAG_ZONE_IS_ACTIVE?
Do you mean keeping BLOCK_GROUP_FLAG_ZONE_IS_ACTIVE set while
call_zone_finish() is running, and clearing it together with
list_del_init() after the device operation completes?
If so, I think that closes the early-inactive window, but it still allows
two callers to finish the same block group concurrently:
Thread A Thread B
call_zone_finish()
observe ZONE_IS_ACTIVE
call_zone_finish()
While thread A is blocked in blkdev_zone_mgmt(), thread B still sees the
block group as active and can enter call_zone_finish() as well.
Even if issuing ZONE_FINISH twice is harmless at the device level, the
software cleanup is not idempotent. If both calls succeed for a non-data
block group, both increment reserved_active_zones, while
active_zones_left is restored only once because the device active bitmap
uses test_and_clear_bit(). Both callers would also remove the same
active-list entry and drop its reference.
Am I missing some existing serialization that prevents two
do_zone_finish() callers for the same block group from reaching
call_zone_finish() concurrently?
Thanks,
Dongjiang Zhu
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH 1/4] btrfs: zoned: track only sequential zones as active
2026-08-24 22:40 ` Qu Wenruo
@ 2026-08-25 9:10 ` Johannes Thumshirn
0 siblings, 0 replies; 15+ messages in thread
From: Johannes Thumshirn @ 2026-08-25 9:10 UTC (permalink / raw)
To: Qu Wenruo; +Cc: linux-btrfs, naohiro.aota, Dongjiang Zhu
On 8/25/26 12:40 AM, Qu Wenruo wrote:
>> Looks good, two small nits though in case you need to resend the series:
>> 1) Please don't do the [CAUSE], [BUG], [FIX] thingy, I know Qu does
>> it but
>> noone else does that.
>
> Sorry, I thought that was a good way to explain a bug, but this
> doesn't look like so for everyone else.
>
> I'll also change the future style.
Nah it's kind of your signature, like me writing Byte instead of Bye.
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH 2/4] btrfs: zoned: recover active non-data block group roles on mount
2026-08-25 3:08 ` Dongjiang Zhu
@ 2026-08-25 10:02 ` Johannes Thumshirn
0 siblings, 0 replies; 15+ messages in thread
From: Johannes Thumshirn @ 2026-08-25 10:02 UTC (permalink / raw)
To: Dongjiang Zhu; +Cc: linux-btrfs, naohiro.aota
On 8/25/26 5:08 AM, Dongjiang Zhu wrote:
> 在 2026/8/24 22:42, Johannes Thumshirn 写道:
>> Can you submit a fstest for that please? Maybe even in generic/ so XFS and
>> F2FS also get coverage?
>>
> Yes, I can work on a separate fstest. To make sure I understand the
> suggestion correctly, do you have in mind a zoned-device test that
> creates a metadata-heavy workload, syncs, fully unmounts and mounts the
> filesystem repeatedly, and then verifies that further writes and syncs
> still succeed?
Yep.
>>>
>>> diff --git a/fs/btrfs/zoned.c b/fs/btrfs/zoned.c
>>> index bd079d3b31ba..7d91c074a24a 100644
>>> --- a/fs/btrfs/zoned.c
>>> +++ b/fs/btrfs/zoned.c
>>> @@ -3050,22 +3050,73 @@ int btrfs_zoned_activate_one_bg(struct btrfs_space_info *space_info, bool do_fin
>>> return 0;
>>> }
>>>
>>> +static int finish_extra_active_nondata_bgs(struct btrfs_fs_info *fs_info)
>>> +{
>>> + struct btrfs_block_group *block_group;
>>> + struct btrfs_block_group *next;
>>> + u64 tail_unusable;
>>> + int ret;
>>> +
>>> + list_for_each_entry_safe(block_group, next, &fs_info->zone_active_bgs,
>>> + active_bg_list) {
>>> + if (!(block_group->flags &
>>> + (BTRFS_BLOCK_GROUP_METADATA | BTRFS_BLOCK_GROUP_SYSTEM)))
>>> + continue;
>>> +
>>> + if (block_group == fs_info->active_meta_bg ||
>>> + block_group == fs_info->active_system_bg)
>>> + continue;
>>> +
>>> + btrfs_get_block_group(block_group);
>>> + tail_unusable = block_group->zone_capacity - block_group->alloc_offset;
>> Shouldn't these be accessed under the block_group->lock?
>>
>> [...]
>>
>> Same here, don't you need the block_group->lock?
> Both accesses only happen during open_ctree(), after all block groups
> have been loaded and before the background threads are started. Following
> other mount-time initialization code, I assumed that block_group->lock
> was not necessary because the allocation state cannot change concurrently.
>
> On the other hand, taking the lock would make the usual protection of
> alloc_offset explicit and avoid relying on the mount-time context for
> future maintenance. Would you prefer that I add the locking around both
> reads in v2?
Yes please.
^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2026-08-25 10:02 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-24 6:49 [PATCH 0/4] btrfs: zoned: fix active-zone accounting and transitions Dongjiang Zhu
2026-08-24 6:49 ` [PATCH 1/4] btrfs: zoned: track only sequential zones as active Dongjiang Zhu
2026-08-24 13:55 ` Johannes Thumshirn
2026-08-24 22:40 ` Qu Wenruo
2026-08-25 9:10 ` Johannes Thumshirn
2026-08-25 3:01 ` Dongjiang Zhu
2026-08-24 6:49 ` [PATCH 2/4] btrfs: zoned: recover active non-data block group roles on mount Dongjiang Zhu
2026-08-24 14:42 ` Johannes Thumshirn
2026-08-25 3:08 ` Dongjiang Zhu
2026-08-25 10:02 ` Johannes Thumshirn
2026-08-24 6:49 ` [PATCH 3/4] btrfs: zoned: remove obsolete non-data block group activation helper Dongjiang Zhu
2026-08-24 15:51 ` Johannes Thumshirn
2026-08-24 6:49 ` [PATCH 4/4] btrfs: zoned: serialize zone finishing per block group Dongjiang Zhu
2026-08-24 16:00 ` Johannes Thumshirn
2026-08-25 3:16 ` Dongjiang Zhu
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox