From f47e0bd705834134a83493843c961989798da78e Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Tue, 17 Feb 2026 14:20:39 +0100 Subject: [PATCH 01/75] bcachefs: validate_member(): fix device not enough buckets message --- fs/bcachefs/sb/members.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/bcachefs/sb/members.c b/fs/bcachefs/sb/members.c index b3043cdcac5c4..99ed11d50af6c 100644 --- a/fs/bcachefs/sb/members.c +++ b/fs/bcachefs/sb/members.c @@ -170,7 +170,7 @@ static int validate_member(struct printbuf *err, if (le64_to_cpu(m.nbuckets) - le16_to_cpu(m.first_bucket) < BCH_MIN_NR_NBUCKETS) { - prt_printf(err, "device %u: not enough buckets (got %llu, max %u)", + prt_printf(err, "device %u: not enough buckets (got %llu, min %u)", i, le64_to_cpu(m.nbuckets), BCH_MIN_NR_NBUCKETS); return -BCH_ERR_invalid_sb_members; } From b5873eb4bcfd78bcf230b55313541f51e1bee056 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Tue, 17 Feb 2026 14:36:58 +0100 Subject: [PATCH 02/75] bcachefs: validate_member(): extract variables --- fs/bcachefs/sb/members.c | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/fs/bcachefs/sb/members.c b/fs/bcachefs/sb/members.c index 99ed11d50af6c..264424982adf5 100644 --- a/fs/bcachefs/sb/members.c +++ b/fs/bcachefs/sb/members.c @@ -162,30 +162,34 @@ static int validate_member(struct printbuf *err, struct bch_sb *sb, int i) { - if (le64_to_cpu(m.nbuckets) > BCH_MEMBER_NBUCKETS_MAX) { + u64 nbuckets = le64_to_cpu(m.nbuckets); + + if (nbuckets > BCH_MEMBER_NBUCKETS_MAX) { prt_printf(err, "device %u: too many buckets (got %llu, max %u)", - i, le64_to_cpu(m.nbuckets), BCH_MEMBER_NBUCKETS_MAX); + i, nbuckets, BCH_MEMBER_NBUCKETS_MAX); return -BCH_ERR_invalid_sb_members; } - if (le64_to_cpu(m.nbuckets) - - le16_to_cpu(m.first_bucket) < BCH_MIN_NR_NBUCKETS) { + u16 first_bucket = first_bucket; + + if (nbuckets - first_bucket < BCH_MIN_NR_NBUCKETS) { prt_printf(err, "device %u: not enough buckets (got %llu, min %u)", i, le64_to_cpu(m.nbuckets), BCH_MIN_NR_NBUCKETS); return -BCH_ERR_invalid_sb_members; } - if (le16_to_cpu(m.bucket_size) < - le16_to_cpu(sb->block_size)) { + u16 bucket_size = le16_to_cpu(m.bucket_size); + u16 block_size = le16_to_cpu(sb->block_size); + + if (bucket_size < block_size) { prt_printf(err, "device %u: bucket size %u smaller than block size %u", - i, le16_to_cpu(m.bucket_size), le16_to_cpu(sb->block_size)); + i, bucket_size, block_size); return -BCH_ERR_invalid_sb_members; } - if (le16_to_cpu(m.bucket_size) < - BCH_SB_BTREE_NODE_SIZE(sb)) { + if (bucket_size < BCH_SB_BTREE_NODE_SIZE(sb)) { prt_printf(err, "device %u: bucket size %u smaller than btree node size %llu", - i, le16_to_cpu(m.bucket_size), BCH_SB_BTREE_NODE_SIZE(sb)); + i, bucket_size, BCH_SB_BTREE_NODE_SIZE(sb)); return -BCH_ERR_invalid_sb_members; } From 609a1af26079e547823d240611674fcbf136b1b9 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Tue, 17 Feb 2026 14:36:58 +0100 Subject: [PATCH 03/75] bcachefs: validate_member(): not enough buckets: print actual amount of buckets --- fs/bcachefs/sb/members.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/bcachefs/sb/members.c b/fs/bcachefs/sb/members.c index 264424982adf5..a9a5f119746d0 100644 --- a/fs/bcachefs/sb/members.c +++ b/fs/bcachefs/sb/members.c @@ -174,7 +174,7 @@ static int validate_member(struct printbuf *err, if (nbuckets - first_bucket < BCH_MIN_NR_NBUCKETS) { prt_printf(err, "device %u: not enough buckets (got %llu, min %u)", - i, le64_to_cpu(m.nbuckets), BCH_MIN_NR_NBUCKETS); + i, nbuckets - first_bucket, BCH_MIN_NR_NBUCKETS); return -BCH_ERR_invalid_sb_members; } From 8157d95e478e084489c8e58d858fbd50bb748cdd Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Wed, 18 Feb 2026 11:08:26 +0100 Subject: [PATCH 04/75] bcachefs: validate_members(): fix bad search-and-replace | Reported-by: kernel test robot | Closes: https://lore.kernel.org/oe-kbuild-all/202602181006.rLTgu86r-lkp@intel.com/ --- fs/bcachefs/sb/members.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/bcachefs/sb/members.c b/fs/bcachefs/sb/members.c index a9a5f119746d0..5bd7678d65f22 100644 --- a/fs/bcachefs/sb/members.c +++ b/fs/bcachefs/sb/members.c @@ -170,7 +170,7 @@ static int validate_member(struct printbuf *err, return -BCH_ERR_invalid_sb_members; } - u16 first_bucket = first_bucket; + u16 first_bucket = le16_to_cpu(m.first_bucket); if (nbuckets - first_bucket < BCH_MIN_NR_NBUCKETS) { prt_printf(err, "device %u: not enough buckets (got %llu, min %u)", From 289e32bbbd4e5a5d64f51cc82821d68c98a5395c Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 16 Feb 2026 18:37:49 +0100 Subject: [PATCH 05/75] bcachefs: add target_nbuckets to bch_member(_cpu) this addition is backwards compatible because new fields are initialized to zero, which means no pending resize, and are not read by older kernels --- fs/bcachefs/sb/members.c | 19 +++++++++++++++++++ fs/bcachefs/sb/members.h | 3 ++- fs/bcachefs/sb/members_format.h | 1 + fs/bcachefs/sb/members_types.h | 1 + 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/fs/bcachefs/sb/members.c b/fs/bcachefs/sb/members.c index 5bd7678d65f22..d1a39a425a768 100644 --- a/fs/bcachefs/sb/members.c +++ b/fs/bcachefs/sb/members.c @@ -204,6 +204,19 @@ static int validate_member(struct printbuf *err, return -BCH_ERR_invalid_sb_members; } + if (le64_to_cpu(m.target_nbuckets) && le64_to_cpu(m.target_nbuckets) > BCH_MEMBER_NBUCKETS_MAX) { + prt_printf(err, "device %u: too many target buckets (got %llu, max %u)", + i, le64_to_cpu(m.target_nbuckets), BCH_MEMBER_NBUCKETS_MAX); + return -BCH_ERR_invalid_sb_members; + } + + if (le64_to_cpu(m.target_nbuckets) && le64_to_cpu(m.target_nbuckets) - + le16_to_cpu(m.first_bucket) < BCH_MIN_NR_NBUCKETS) { + prt_printf(err, "device %u: not enough target buckets (got %llu, max %u)", + i, le64_to_cpu(m.target_nbuckets), BCH_MIN_NR_NBUCKETS); + return -BCH_ERR_invalid_sb_members; + } + return 0; } @@ -215,6 +228,7 @@ void bch2_member_to_text(struct printbuf *out, { u64 bucket_size = le16_to_cpu(m->bucket_size); u64 device_size = le64_to_cpu(m->nbuckets) * bucket_size; + u64 target_device_size = le64_to_cpu(m->target_nbuckets) * bucket_size; prt_printf(out, "Label:\t"); if (BCH_MEMBER_GROUP(m)) @@ -232,6 +246,10 @@ void bch2_member_to_text(struct printbuf *out, prt_units_u64(out, device_size << 9); prt_newline(out); + prt_printf(out, "Target size:\t"); + prt_units_u64(out, target_device_size << 9); + prt_newline(out); + for (unsigned i = 0; i < BCH_MEMBER_ERROR_NR; i++) prt_printf(out, "%s errors:\t%llu\n", bch2_member_error_strs[i], le64_to_cpu(m->errors[i])); @@ -244,6 +262,7 @@ void bch2_member_to_text(struct printbuf *out, prt_printf(out, "First bucket:\t%u\n", le16_to_cpu(m->first_bucket)); prt_printf(out, "Buckets:\t%llu\n", le64_to_cpu(m->nbuckets)); + prt_printf(out, "Target buckets:\t%llu\n", le64_to_cpu(m->target_nbuckets)); prt_printf(out, "Last mount:\t"); if (m->last_mount) diff --git a/fs/bcachefs/sb/members.h b/fs/bcachefs/sb/members.h index 27a7df934f5e9..d9edaf65c9d19 100644 --- a/fs/bcachefs/sb/members.h +++ b/fs/bcachefs/sb/members.h @@ -405,12 +405,13 @@ static inline struct bch_member_cpu bch2_mi_to_cpu(struct bch_member *mi) .durability = BCH_MEMBER_DURABILITY(mi) ? BCH_MEMBER_DURABILITY(mi) - 1 : 1, - .freespace_initialized = BCH_MEMBER_FREESPACE_INITIALIZED(mi), + .freespace_initialized = BCH_MEMBER_FREESPACE_INITIALIZED(mi), .resize_on_mount = BCH_MEMBER_RESIZE_ON_MOUNT(mi), .rotational = BCH_MEMBER_ROTATIONAL(mi), .valid = bch2_member_alive(mi), .btree_bitmap_shift = mi->btree_bitmap_shift, .btree_allocated_bitmap = le64_to_cpu(mi->btree_allocated_bitmap), + .target_nbuckets = le64_to_cpu(mi->target_nbuckets), }; } diff --git a/fs/bcachefs/sb/members_format.h b/fs/bcachefs/sb/members_format.h index aa7544e647199..c66dec327b92f 100644 --- a/fs/bcachefs/sb/members_format.h +++ b/fs/bcachefs/sb/members_format.h @@ -76,6 +76,7 @@ struct bch_member { __u8 device_model[64] __nonstring; __le64 flush_errors; __u8 device_serial[64] __nonstring; + __le64 target_nbuckets; /* 0 => no pending resize */ }; /* diff --git a/fs/bcachefs/sb/members_types.h b/fs/bcachefs/sb/members_types.h index a1d69cddbff29..f2c9a53b3e0c8 100644 --- a/fs/bcachefs/sb/members_types.h +++ b/fs/bcachefs/sb/members_types.h @@ -18,6 +18,7 @@ struct bch_member_cpu { u8 valid; u8 btree_bitmap_shift; u64 btree_allocated_bitmap; + u64 target_nbuckets; /* (!= 0) => pending resize */ }; #endif /* _BCACHEFS_SB_MEMBERS_H */ From 5456cdc3a3f59dfd077d40cff695f48938aa8658 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 16 Feb 2026 18:37:49 +0100 Subject: [PATCH 06/75] bcachefs: print target_nbuckets = 0 as inactive instead of 0 --- fs/bcachefs/sb/members.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/fs/bcachefs/sb/members.c b/fs/bcachefs/sb/members.c index d1a39a425a768..0f16b8485e338 100644 --- a/fs/bcachefs/sb/members.c +++ b/fs/bcachefs/sb/members.c @@ -247,7 +247,11 @@ void bch2_member_to_text(struct printbuf *out, prt_newline(out); prt_printf(out, "Target size:\t"); - prt_units_u64(out, target_device_size << 9); + if (target_device_size) { + prt_units_u64(out, target_device_size << 9); + } else { + prt_printf(out, "Inactive"); + } prt_newline(out); for (unsigned i = 0; i < BCH_MEMBER_ERROR_NR; i++) @@ -262,7 +266,14 @@ void bch2_member_to_text(struct printbuf *out, prt_printf(out, "First bucket:\t%u\n", le16_to_cpu(m->first_bucket)); prt_printf(out, "Buckets:\t%llu\n", le64_to_cpu(m->nbuckets)); - prt_printf(out, "Target buckets:\t%llu\n", le64_to_cpu(m->target_nbuckets)); + + prt_printf(out, "Target buckets:\t"); + if (target_device_size) { + prt_printf(out, "%llu", le64_to_cpu(m->target_nbuckets)); + } else { + prt_printf(out, "Inactive"); + } + prt_newline(out); prt_printf(out, "Last mount:\t"); if (m->last_mount) From 151f43dc53caa9c8c6cf3a233be7a9cb1792ea1e Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 16 Feb 2026 18:37:49 +0100 Subject: [PATCH 07/75] bcachefs: consider pointers after target_nbuckets as evacuating --- fs/bcachefs/data/ec/create.c | 6 +++--- fs/bcachefs/data/extents.c | 4 ++-- fs/bcachefs/data/extents.h | 2 +- fs/bcachefs/data/reconcile/trigger.c | 2 +- fs/bcachefs/data/reconcile/work.c | 6 +++--- fs/bcachefs/data/update.c | 2 +- fs/bcachefs/sb/members.h | 12 ++++++++++++ 7 files changed, 23 insertions(+), 11 deletions(-) diff --git a/fs/bcachefs/data/ec/create.c b/fs/bcachefs/data/ec/create.c index 6e7c44f1e1ff6..09c0ac18648d5 100644 --- a/fs/bcachefs/data/ec/create.c +++ b/fs/bcachefs/data/ec/create.c @@ -1048,7 +1048,7 @@ static bool may_reuse_stripe(struct bch_fs *c, for (unsigned i = 0; i < nr_data; i++) if (stripe_blockcount_get(old, i)) { - if (!bch2_dev_bad_or_evacuating(c, old->ptrs[i].dev)) + if (!bch2_ptr_bad_or_evacuating(c, &old->ptrs[i])) __clear_bit(old->ptrs[i].dev, devs_may_alloc.d); live_data++; } @@ -1126,7 +1126,7 @@ static void init_new_stripe_from_old(struct bch_fs *c, struct ec_stripe_new *s, for (unsigned i = 0; i < old_nr_data; i++) { if (stripe_blockcount_get(old_v, i)) { - if (!bch2_dev_bad_or_evacuating(c, old_v->ptrs[i].dev)) + if (!bch2_ptr_bad_or_evacuating(c, &old_v->ptrs[i])) __set_bit(s->old_blocks_nr, s->blocks_gotten); else __set_bit(s->old_blocks_nr, s->blocks_moving); @@ -1576,7 +1576,7 @@ struct ec_stripe_head *bch2_ec_stripe_head_get(struct btree_trans *trans, static bool stripe_degraded(struct bch_fs *c, const struct bch_stripe *s) { for (unsigned i = 0; i < s->nr_blocks; i++) - if (bch2_dev_bad_or_evacuating(c, s->ptrs[i].dev)) + if (bch2_ptr_bad_or_evacuating(c, &s->ptrs[i])) return true; return false; } diff --git a/fs/bcachefs/data/extents.c b/fs/bcachefs/data/extents.c index f0f6ba5312f5a..1cae699d7e970 100644 --- a/fs/bcachefs/data/extents.c +++ b/fs/bcachefs/data/extents.c @@ -1199,12 +1199,12 @@ bool bch2_bkey_in_target(struct bch_fs *c, struct bkey_s_c k, unsigned target) return true; } -bool bch2_bkey_has_dev_bad_or_evacuating(struct bch_fs *c, struct bkey_s_c k) +bool bch2_bkey_has_ptr_bad_or_evacuating(struct bch_fs *c, struct bkey_s_c k) { guard(rcu)(); struct bkey_ptrs_c ptrs = bch2_bkey_ptrs_c(k); bkey_for_each_ptr(ptrs, ptr) - if (bch2_dev_bad_or_evacuating_rcu(c, ptr->dev)) + if (bch2_ptr_bad_or_evacuating_rcu(c, ptr)) return true; return false; } diff --git a/fs/bcachefs/data/extents.h b/fs/bcachefs/data/extents.h index fb9cabbdb5148..7af1a6d69f2ca 100644 --- a/fs/bcachefs/data/extents.h +++ b/fs/bcachefs/data/extents.h @@ -613,7 +613,7 @@ bool bch2_bkey_devs_rw(struct bch_fs *, struct bkey_s_c); bool bch2_bkey_has_target(struct bch_fs *, struct bkey_s_c, unsigned); bool bch2_bkey_in_target(struct bch_fs *, struct bkey_s_c, unsigned); -bool bch2_bkey_has_dev_bad_or_evacuating(struct bch_fs *, struct bkey_s_c); +bool bch2_bkey_has_ptr_bad_or_evacuating(struct bch_fs *, struct bkey_s_c); void bch2_bkey_extent_entry_drop_s(const struct bch_fs *, struct bkey_s, union bch_extent_entry *); void bch2_bkey_extent_entry_drop(const struct bch_fs *, struct bkey_i *, union bch_extent_entry *); diff --git a/fs/bcachefs/data/reconcile/trigger.c b/fs/bcachefs/data/reconcile/trigger.c index 6813cddeca176..01b748d6a9d09 100644 --- a/fs/bcachefs/data/reconcile/trigger.c +++ b/fs/bcachefs/data/reconcile/trigger.c @@ -490,7 +490,7 @@ static int bch2_bkey_needs_reconcile(struct btree_trans *trans, struct bkey_s_c incompressible |= p.crc.compression_type == BCH_COMPRESSION_TYPE_incompressible; unwritten |= p.ptr.unwritten; - bool evacuating = bch2_dev_bad_or_evacuating(c, p.ptr.dev) && !p.has_ec; + bool evacuating = bch2_ptr_bad_or_evacuating(c, &p.ptr) && !p.has_ec; if (!poisoned && !btree && diff --git a/fs/bcachefs/data/reconcile/work.c b/fs/bcachefs/data/reconcile/work.c index 5f1d200d188e0..42aa3bcd091a6 100644 --- a/fs/bcachefs/data/reconcile/work.c +++ b/fs/bcachefs/data/reconcile/work.c @@ -321,7 +321,7 @@ static int reconcile_set_data_opts(struct btree_trans *trans, guard(rcu)(); bkey_for_each_ptr(ptrs, ptr) { - if (bch2_dev_bad_or_evacuating(c, ptr->dev)) + if (bch2_ptr_bad_or_evacuating_rcu(c, ptr)) data_opts->ptrs_kill |= ptr_bit; ptr_bit <<= 1; } @@ -335,7 +335,7 @@ static int reconcile_set_data_opts(struct btree_trans *trans, if (d < 0) return d; - if (bch2_dev_bad_or_evacuating(c, p.ptr.dev) || + if (bch2_ptr_bad_or_evacuating(c, &p.ptr) || (!p.ptr.cached && d && durability.total - d >= r->data_replicas)) { data_opts->ptrs_kill |= ptr_bit; @@ -370,7 +370,7 @@ static int reconcile_set_data_opts(struct btree_trans *trans, if (d < 0) return d; - if (bch2_dev_bad_or_evacuating(c, p.ptr.dev) || + if (bch2_ptr_bad_or_evacuating(c, &p.ptr) || (!p.ptr.cached && d && durability.online - d >= r->data_replicas)) { data_opts->ptrs_kill |= ptr_bit; diff --git a/fs/bcachefs/data/update.c b/fs/bcachefs/data/update.c index ad8e5f303d39a..15e540081d57f 100644 --- a/fs/bcachefs/data/update.c +++ b/fs/bcachefs/data/update.c @@ -1105,7 +1105,7 @@ static int __bch2_can_do_write(struct bch_fs *c, if (btree && data_opts->type == BCH_DATA_UPDATE_reconcile && - !bch2_bkey_has_dev_bad_or_evacuating(c, k)) + !bch2_bkey_has_ptr_bad_or_evacuating(c, k)) return bch2_can_do_write_btree(c, opts, data_opts, k, trace); if (trace) { diff --git a/fs/bcachefs/sb/members.h b/fs/bcachefs/sb/members.h index d9edaf65c9d19..224c6adfa4207 100644 --- a/fs/bcachefs/sb/members.h +++ b/fs/bcachefs/sb/members.h @@ -258,6 +258,18 @@ static inline bool bch2_dev_bad_or_evacuating(struct bch_fs *c, unsigned dev) return bch2_dev_bad_or_evacuating_rcu(c, dev); } +static inline bool bch2_ptr_bad_or_evacuating_rcu(struct bch_fs *c, const struct bch_extent_ptr *ptr) { + struct bch_dev *ca = bch2_dev_rcu_noerror(c, ptr->dev); + + return !ca || bch2_dev_bad_or_evacuating_rcu(c, ptr->dev) || (ca->mi.target_nbuckets && ca->mi.target_nbuckets <= div_u64(ptr->offset, ca->mi.bucket_size)); +} + +static inline bool bch2_ptr_bad_or_evacuating(struct bch_fs *c, const struct bch_extent_ptr *ptr) { + guard(rcu)(); + return bch2_ptr_bad_or_evacuating_rcu(c, ptr); +} + + int bch2_dev_missing_bkey_msg(struct bch_fs *, struct bkey_s_c, unsigned, struct printbuf *out); int bch2_dev_missing_bkey(struct bch_fs *, struct bkey_s_c, unsigned); From b96ea41bd0f211a0e8838bfb712b8c0ff7772e8b Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Tue, 17 Feb 2026 13:47:39 +0100 Subject: [PATCH 08/75] bcachefs: use dev-based evacuating check for may_reuse_stripe avoids stripe reshuffling From 8c67419cb02c208586ef1ca8da8d87c982ff6876 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Tue, 17 Feb 2026 14:02:25 +0100 Subject: [PATCH 09/75] bcachefs: sharpen target_nbuckets semantics to only include (trivially) legal shrinks --- fs/bcachefs/sb/members.c | 29 +++++++++++++++++++---------- fs/bcachefs/sb/members_format.h | 2 +- fs/bcachefs/sb/members_types.h | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/fs/bcachefs/sb/members.c b/fs/bcachefs/sb/members.c index 0f16b8485e338..f8b6de3f4b485 100644 --- a/fs/bcachefs/sb/members.c +++ b/fs/bcachefs/sb/members.c @@ -204,17 +204,26 @@ static int validate_member(struct printbuf *err, return -BCH_ERR_invalid_sb_members; } - if (le64_to_cpu(m.target_nbuckets) && le64_to_cpu(m.target_nbuckets) > BCH_MEMBER_NBUCKETS_MAX) { - prt_printf(err, "device %u: too many target buckets (got %llu, max %u)", - i, le64_to_cpu(m.target_nbuckets), BCH_MEMBER_NBUCKETS_MAX); - return -BCH_ERR_invalid_sb_members; - } + u64 target_nbuckets = le64_to_cpu(m.target_nbuckets); - if (le64_to_cpu(m.target_nbuckets) && le64_to_cpu(m.target_nbuckets) - - le16_to_cpu(m.first_bucket) < BCH_MIN_NR_NBUCKETS) { - prt_printf(err, "device %u: not enough target buckets (got %llu, max %u)", - i, le64_to_cpu(m.target_nbuckets), BCH_MIN_NR_NBUCKETS); - return -BCH_ERR_invalid_sb_members; + if (target_nbuckets) { + if (target_nbuckets >= nbuckets) { + prt_printf(err, "device %u: target buckets >= buckets (got %llu, nbuckets %llu)", + i, target_nbuckets, nbuckets); + return -BCH_ERR_invalid_sb_members; + } + + if (target_nbuckets < first_bucket) { + prt_printf(err, "device %u: target buckets starts before first bucket (got %llu, first %u)", + i, target_nbuckets, first_bucket); + return -BCH_ERR_invalid_sb_members; + } + + if (target_nbuckets - first_bucket < BCH_MIN_NR_NBUCKETS) { + prt_printf(err, "device %u: not enough target buckets (got %llu, min %u)", + i, target_nbuckets - first_bucket, BCH_MIN_NR_NBUCKETS); + return -BCH_ERR_invalid_sb_members; + } } return 0; diff --git a/fs/bcachefs/sb/members_format.h b/fs/bcachefs/sb/members_format.h index c66dec327b92f..d87366d239e69 100644 --- a/fs/bcachefs/sb/members_format.h +++ b/fs/bcachefs/sb/members_format.h @@ -76,7 +76,7 @@ struct bch_member { __u8 device_model[64] __nonstring; __le64 flush_errors; __u8 device_serial[64] __nonstring; - __le64 target_nbuckets; /* 0 => no pending resize */ + __le64 target_nbuckets; /* 0 => no pending resize, (first_bucket + BCH_MIN_NR_NBUCKETS)..nbuckets => shrink, other => illegal */ }; /* diff --git a/fs/bcachefs/sb/members_types.h b/fs/bcachefs/sb/members_types.h index f2c9a53b3e0c8..3711cbd92139e 100644 --- a/fs/bcachefs/sb/members_types.h +++ b/fs/bcachefs/sb/members_types.h @@ -18,7 +18,7 @@ struct bch_member_cpu { u8 valid; u8 btree_bitmap_shift; u64 btree_allocated_bitmap; - u64 target_nbuckets; /* (!= 0) => pending resize */ + u64 target_nbuckets; /* 0 => no pending resize, [first_bucket + BCH_MIN_NR_NBUCKETS, nbuckets) => shrink, other => illegal */ }; #endif /* _BCACHEFS_SB_MEMBERS_H */ From 24a0d1607fa9150cdffb50a4987df49c94ef0f2f Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Tue, 17 Feb 2026 18:03:52 +0100 Subject: [PATCH 10/75] bcachefs: split bch2_dev_resze into grow & shrink paths also comment in outline of shrink path --- fs/bcachefs/init/dev.c | 50 ++++++++++++++++++++++++++++++++---------- fs/bcachefs/init/dev.h | 3 ++- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 29d893b1989f5..bcb8f60f9db3c 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1242,40 +1242,55 @@ int bch2_dev_offline(struct bch_fs *c, struct bch_dev *ca, int flags, struct pri return 0; } -int bch2_dev_resize(struct bch_fs *c, struct bch_dev *ca, u64 nbuckets, struct printbuf *err) +int bch2_dev_resize(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err) { u64 old_nbuckets; - int ret = 0; guard(rwsem_write)(&c->state_lock); old_nbuckets = ca->mi.nbuckets; - if (nbuckets < ca->mi.nbuckets) { + if (new_nbuckets > old_nbuckets) { + return bch2_dev_grow(c, ca, new_nbuckets, err); + } else if (new_nbuckets < old_nbuckets) { + return bch2_dev_shrink(c, ca, new_nbuckets, err); + } else { + return 0; + } +} + +int bch2_dev_grow(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err) +{ + int ret = 0; + + u64 old_nbuckets = ca->mi.nbuckets; + + if (new_nbuckets < old_nbuckets) { prt_printf(err, "Cannot shrink yet\n"); return bch_err_throw(c, EINVAL_dev_resize_shrink); } - bool wakeup_reconcile_pending = nbuckets > ca->mi.nbuckets; + /* we have more space -> wake up pending */ + bool wakeup_reconcile_pending = new_nbuckets > old_nbuckets; struct reconcile_scan s = { .type = RECONCILE_SCAN_pending }; if (wakeup_reconcile_pending) try(bch2_set_reconcile_needs_scan(c, s, false)); - if (nbuckets > BCH_MEMBER_NBUCKETS_MAX) { + if (new_nbuckets > BCH_MEMBER_NBUCKETS_MAX) { prt_printf(err, "New device size too big (%llu greater than max %u)\n", - nbuckets, BCH_MEMBER_NBUCKETS_MAX); + new_nbuckets, BCH_MEMBER_NBUCKETS_MAX); return bch_err_throw(c, device_size_too_big); } if (bch2_dev_is_online(ca) && get_capacity(ca->disk_sb.bdev->bd_disk) < - ca->mi.bucket_size * nbuckets) { + ca->mi.bucket_size * new_nbuckets) { prt_printf(err, "New size %llu larger than device size %llu\n", - ca->mi.bucket_size * nbuckets, + ca->mi.bucket_size * new_nbuckets, get_capacity(ca->disk_sb.bdev->bd_disk)); return bch_err_throw(c, device_size_too_small); } - ret = bch2_dev_buckets_resize(c, ca, nbuckets); + ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); if (ret) { prt_printf(err, "bch2_dev_buckets_resize() error: %s\n", bch2_err_str(ret)); return ret; @@ -1290,13 +1305,13 @@ int bch2_dev_resize(struct bch_fs *c, struct bch_dev *ca, u64 nbuckets, struct p scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { guard(mutex)(&c->sb_lock); struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); - m->nbuckets = cpu_to_le64(nbuckets); + m->nbuckets = cpu_to_le64(new_nbuckets); bch2_write_super(c); } if (ca->mi.freespace_initialized) { - ret = __bch2_dev_resize_alloc(ca, old_nbuckets, nbuckets); + ret = __bch2_dev_resize_alloc(ca, old_nbuckets, new_nbuckets); if (ret) { prt_printf(err, "__bch2_dev_resize_alloc() error: %s\n", bch2_err_str(ret)); return ret; @@ -1310,6 +1325,19 @@ int bch2_dev_resize(struct bch_fs *c, struct bch_dev *ca, u64 nbuckets, struct p return 0; } +int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err) { + /* validate shrink size */ + /* write & commit target_nbuckets */ + /* block allocation (buckets_nouse) */ + /* trigger reconcile range scan -> should kick off evacuation from range */ + /* somehow wait for reconcile to finish */ + /* zero target_nbuckets */ + /* validate that no pointers are left into to-be-shrunk region */ + /* write & commit new_nbuckets */ + /* update free-space accounting */ + /* resize in-memory data structures */ +} + /* Resize on mount */ int __bch2_dev_resize_alloc(struct bch_dev *ca, u64 old_nbuckets, u64 new_nbuckets) diff --git a/fs/bcachefs/init/dev.h b/fs/bcachefs/init/dev.h index 9aa26ad03f6c5..c5717f1fe3ef2 100644 --- a/fs/bcachefs/init/dev.h +++ b/fs/bcachefs/init/dev.h @@ -32,6 +32,8 @@ int bch2_dev_add(struct bch_fs *, const char *, struct printbuf *); int bch2_dev_online(struct bch_fs *, const char *, struct printbuf *); int bch2_dev_offline(struct bch_fs *, struct bch_dev *, int, struct printbuf *); int bch2_dev_resize(struct bch_fs *, struct bch_dev *, u64, struct printbuf *); +int bch2_dev_grow(struct bch_fs *, struct bch_dev *, u64, struct printbuf *); +int bch2_dev_shrink(struct bch_fs *, struct bch_dev *, u64, struct printbuf *); int __bch2_dev_resize_alloc(struct bch_dev *, u64, u64); @@ -40,4 +42,3 @@ struct bch_dev *bch2_dev_lookup(struct bch_fs *, const char *); extern const struct blk_holder_ops bch2_sb_handle_bdev_ops; #endif /* _BCACHEFS_INIT_DEV_H */ - From 76bb03723df5354df02eb60433ba36d1e86b5801 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Tue, 17 Feb 2026 19:43:54 +0100 Subject: [PATCH 11/75] bcachefs: implement shrink superblock & alloc nouse interactions --- fs/bcachefs/alloc/buckets.c | 2 +- fs/bcachefs/init/dev.c | 48 ++++++++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/fs/bcachefs/alloc/buckets.c b/fs/bcachefs/alloc/buckets.c index 46020dd5d9b77..aa99046f1b1ee 100644 --- a/fs/bcachefs/alloc/buckets.c +++ b/fs/bcachefs/alloc/buckets.c @@ -1265,7 +1265,7 @@ int bch2_dev_buckets_resize(struct bch_fs *c, struct bch_dev *ca, u64 nbuckets) lockdep_assert_held(&c->state_lock); if (resize && ca->buckets_nouse) - return bch_err_throw(c, no_resize_with_buckets_nouse); + return bch_err_throw(c, no_resize_with_buckets_nouse); // TODO: make this work bucket_gens = bch2_kvmalloc(struct_size(bucket_gens, b, nbuckets), GFP_KERNEL|__GFP_ZERO); diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index bcb8f60f9db3c..1f1fe61dc6e3c 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -210,6 +210,7 @@ #include "alloc/check.h" #include "alloc/discard.h" #include "alloc/replicas.h" +#include "alloc/foreground.h" #include "btree/interior.h" @@ -225,7 +226,9 @@ #include "init/dev.h" #include "init/fs.h" +#include "linux/bitmap.h" #include "sb/members.h" +#include "sb/members_format.h" #define x(n) #n, const char * const bch2_dev_read_refs[] = { @@ -1258,15 +1261,17 @@ int bch2_dev_resize(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru } } +/* requires write state lock */ int bch2_dev_grow(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err) { + lockdep_assert_held(&c->state_lock); + int ret = 0; u64 old_nbuckets = ca->mi.nbuckets; - if (new_nbuckets < old_nbuckets) { - prt_printf(err, "Cannot shrink yet\n"); - return bch_err_throw(c, EINVAL_dev_resize_shrink); + if (new_nbuckets <= old_nbuckets) { + return 0; } /* we have more space -> wake up pending */ @@ -1325,15 +1330,52 @@ int bch2_dev_grow(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct return 0; } +// TODO: resume shrink on startup int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err) { /* validate shrink size */ + u64 old_nbuckets = ca->mi.nbuckets; + if (new_nbuckets >= old_nbuckets) { + return 0; + } + + u64 first_bucket = ca->mi.first_bucket; + if (new_nbuckets < first_bucket + BCH_MIN_NR_NBUCKETS) { + prt_printf(err, "New device size too small (%llu smaller than min %llu)\n", + new_nbuckets, first_bucket + BCH_MIN_NR_NBUCKETS); + return bch_err_throw(c, device_size_too_small); + } + /* write & commit target_nbuckets */ + scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { + guard(mutex)(&c->sb_lock); + struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); + m->target_nbuckets = cpu_to_le64(new_nbuckets); + + bch2_write_super(c); + } + /* block allocation (buckets_nouse) */ + bitmap_set(ca->buckets_nouse, new_nbuckets, old_nbuckets - new_nbuckets); + bch2_reset_alloc_cursors(c); /* trigger reconcile range scan -> should kick off evacuation from range */ /* somehow wait for reconcile to finish */ /* zero target_nbuckets */ + scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { + guard(mutex)(&c->sb_lock); + struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); + m->target_nbuckets = 0; + + bch2_write_super(c); + } /* validate that no pointers are left into to-be-shrunk region */ /* write & commit new_nbuckets */ + scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { + guard(mutex)(&c->sb_lock); + struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); + m->nbuckets = new_nbuckets; + + bch2_write_super(c); + } /* update free-space accounting */ /* resize in-memory data structures */ } From c9714f50ca128b1ba4f75c5dab37441268cda820 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Tue, 17 Feb 2026 22:29:38 +0100 Subject: [PATCH 12/75] bcachefs: refactor out per-device buckets_nouse alloc/free --- fs/bcachefs/alloc/buckets.c | 28 +++++++++++++++++++--------- fs/bcachefs/alloc/buckets.h | 2 ++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/fs/bcachefs/alloc/buckets.c b/fs/bcachefs/alloc/buckets.c index aa99046f1b1ee..5a95530f47ea9 100644 --- a/fs/bcachefs/alloc/buckets.c +++ b/fs/bcachefs/alloc/buckets.c @@ -1224,24 +1224,34 @@ int __bch2_disk_reservation_add(struct bch_fs *c, struct disk_reservation *res, /* Startup/shutdown: */ +void bch2_dev_buckets_nouse_free(struct bch_fs *c, struct bch_dev *ca) +{ + kvfree_rcu_mightsleep(ca->buckets_nouse); + ca->buckets_nouse = NULL; +} void bch2_buckets_nouse_free(struct bch_fs *c) { for_each_member_device(c, ca) { - kvfree_rcu_mightsleep(ca->buckets_nouse); - ca->buckets_nouse = NULL; + bch2_dev_buckets_nouse_free(c, ca); } } +int bch2_dev_buckets_nouse_alloc(struct bch_fs *c, struct bch_dev *ca) +{ + BUG_ON(ca->buckets_nouse); + + ca->buckets_nouse = bch2_kvmalloc(BITS_TO_LONGS(ca->mi.nbuckets) * + sizeof(unsigned long), + GFP_KERNEL|__GFP_ZERO); + if (!ca->buckets_nouse) + return bch_err_throw(c, ENOMEM_buckets_nouse); + + return 0; +} int bch2_buckets_nouse_alloc(struct bch_fs *c) { for_each_member_device(c, ca) { - BUG_ON(ca->buckets_nouse); - - ca->buckets_nouse = bch2_kvmalloc(BITS_TO_LONGS(ca->mi.nbuckets) * - sizeof(unsigned long), - GFP_KERNEL|__GFP_ZERO); - if (!ca->buckets_nouse) - return bch_err_throw(c, ENOMEM_buckets_nouse); + try(bch2_dev_buckets_nouse_alloc(c, ca)); } return 0; diff --git a/fs/bcachefs/alloc/buckets.h b/fs/bcachefs/alloc/buckets.h index 3b4c7311ecf5e..2bf1a0036c7aa 100644 --- a/fs/bcachefs/alloc/buckets.h +++ b/fs/bcachefs/alloc/buckets.h @@ -421,7 +421,9 @@ static inline u64 avail_factor(u64 r) return div_u64(r << RESERVE_FACTOR, (1 << RESERVE_FACTOR) + 1); } +void bch2_dev_buckets_nouse_free(struct bch_fs *, struct bch_dev *); void bch2_buckets_nouse_free(struct bch_fs *); +int bch2_dev_buckets_nouse_alloc(struct bch_fs *, struct bch_dev *); int bch2_buckets_nouse_alloc(struct bch_fs *); static inline bool bch2_bucket_nouse(struct bch_dev *ca, u64 bucket) From 1b2c742749062b19db6bce13458d57fe2efb051c Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Tue, 17 Feb 2026 22:29:38 +0100 Subject: [PATCH 13/75] bcachefs: implement filesystem shrinking mvp --- fs/bcachefs/errcode.h | 1 + fs/bcachefs/init/dev.c | 179 ++++++++++++++++++++++++++++++++--------- 2 files changed, 141 insertions(+), 39 deletions(-) diff --git a/fs/bcachefs/errcode.h b/fs/bcachefs/errcode.h index 104122e5bd33b..dac8283bfcb85 100644 --- a/fs/bcachefs/errcode.h +++ b/fs/bcachefs/errcode.h @@ -280,6 +280,7 @@ x(EINVAL, bucket_size_too_small) \ x(EINVAL, device_size_too_small) \ x(EINVAL, device_size_too_big) \ + x(EINVAL, device_already_resizing) \ x(EINVAL, device_not_a_member_of_filesystem) \ x(EINVAL, device_has_been_removed) \ x(EINVAL, device_splitbrain) \ diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 1f1fe61dc6e3c..f7edc3b918354 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -202,6 +202,7 @@ * remaining devices automatically. */ +#include "alloc/buckets.h" #include "bcachefs.h" #include "alloc/accounting.h" @@ -212,8 +213,13 @@ #include "alloc/replicas.h" #include "alloc/foreground.h" +#include "bcachefs_format.h" +#include "btree/bkey_types.h" #include "btree/interior.h" +#include "btree/iter.h" +#include "btree/types.h" +#include "btree/write_buffer.h" #include "data/ec/init.h" #include "data/migrate.h" #include "data/reconcile/work.h" @@ -227,8 +233,11 @@ #include "init/fs.h" #include "linux/bitmap.h" +#include "linux/sched.h" +#include "linux/sched/signal.h" #include "sb/members.h" #include "sb/members_format.h" +#include "util/util.h" #define x(n) #n, const char * const bch2_dev_read_refs[] = { @@ -1249,7 +1258,6 @@ int bch2_dev_resize(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru { u64 old_nbuckets; - guard(rwsem_write)(&c->state_lock); old_nbuckets = ca->mi.nbuckets; if (new_nbuckets > old_nbuckets) { @@ -1264,7 +1272,7 @@ int bch2_dev_resize(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru /* requires write state lock */ int bch2_dev_grow(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err) { - lockdep_assert_held(&c->state_lock); + guard(rwsem_write)(&c->state_lock); int ret = 0; @@ -1330,54 +1338,147 @@ int bch2_dev_grow(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct return 0; } + +static int tail_is_empty(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err, bool *empty) { + struct bpos bp_start = bucket_pos_to_bp_start(ca, POS(ca->dev_idx, new_nbuckets)); + struct bpos bp_end = bucket_pos_to_bp_start(ca, POS(ca->dev_idx, ca->mi.nbuckets)); + + CLASS(btree_trans, trans)(c); + CLASS(backpointer_scan_iter, iter)(BTREE_ID_backpointers, bp_start, NULL); + + struct wb_maybe_flush last_flushed __cleanup(wb_maybe_flush_exit); + wb_maybe_flush_init(&last_flushed); + + struct bkey_s_c_backpointer bp = bch2_bp_scan_iter_peek(trans, &iter, bp_end, &last_flushed); + + try(bkey_err(bp)); + + *empty = !bp.k; + return 0; +} + + // TODO: resume shrink on startup int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err) { - /* validate shrink size */ u64 old_nbuckets = ca->mi.nbuckets; - if (new_nbuckets >= old_nbuckets) { - return 0; - } - u64 first_bucket = ca->mi.first_bucket; - if (new_nbuckets < first_bucket + BCH_MIN_NR_NBUCKETS) { - prt_printf(err, "New device size too small (%llu smaller than min %llu)\n", - new_nbuckets, first_bucket + BCH_MIN_NR_NBUCKETS); - return bch_err_throw(c, device_size_too_small); - } + int ret = 0; - /* write & commit target_nbuckets */ - scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { - guard(mutex)(&c->sb_lock); - struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); - m->target_nbuckets = cpu_to_le64(new_nbuckets); + scoped_guard(rwsem_write, &c->state_lock) { + /* validate shrink size */ + if (new_nbuckets >= old_nbuckets) { + return 0; + } - bch2_write_super(c); - } + u64 first_bucket = ca->mi.first_bucket; + if (new_nbuckets < first_bucket + BCH_MIN_NR_NBUCKETS) { + prt_printf(err, "New device size too small (%llu smaller than min %llu)\n", + new_nbuckets, first_bucket + BCH_MIN_NR_NBUCKETS); + return bch_err_throw(c, device_size_too_small); + } - /* block allocation (buckets_nouse) */ - bitmap_set(ca->buckets_nouse, new_nbuckets, old_nbuckets - new_nbuckets); - bch2_reset_alloc_cursors(c); - /* trigger reconcile range scan -> should kick off evacuation from range */ - /* somehow wait for reconcile to finish */ - /* zero target_nbuckets */ - scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { - guard(mutex)(&c->sb_lock); - struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); - m->target_nbuckets = 0; + if (ca->mi.target_nbuckets) { + prt_printf(err, "Device already resizing (current target: %llu, new target: %llu\n", ca->mi.target_nbuckets, new_nbuckets); + return bch_err_throw(c, device_already_resizing); + } - bch2_write_super(c); + /* write & commit target_nbuckets */ + scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { + guard(mutex)(&c->sb_lock); + struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); + m->target_nbuckets = cpu_to_le64(new_nbuckets); + + bch2_write_super(c); + } + + /* block allocation */ + ret = bch2_dev_buckets_nouse_alloc(c, ca); + if (ret) { + prt_printf(err, "error allocating buckets_nouse for dev %u: %s\n", ca->dev_idx, bch2_err_str(ret)); + return ret; + } + + bitmap_set(ca->buckets_nouse, new_nbuckets, old_nbuckets - new_nbuckets); + bch2_reset_alloc_cursors(c); + + /* trigger reconcile range scan -> should kick off evacuation from range */ + struct reconcile_scan s = { + .type = RECONCILE_SCAN_device, // TODO(performance): make this range-based + .dev = ca->dev_idx, + }; + ret = bch2_set_reconcile_needs_scan(c, s, true); + }; + + /* wait for to-be-shrunk reason to be empty */ + while (true) { + bool empty = false; + try(tail_is_empty(c, ca, new_nbuckets, err, &empty)); + + if (empty) { + /* do a definitive check */ + CLASS(btree_trans, trans)(c); + try(bch2_btree_write_buffer_flush_sync(trans)); + + try(tail_is_empty(c, ca, new_nbuckets, err, &empty)); + if (empty) { + break; + } + } + + bch2_reconcile_wakeup(c); + + if (signal_pending(current)) + return -EINTR; + + schedule_timeout_killable(HZ/2); } - /* validate that no pointers are left into to-be-shrunk region */ - /* write & commit new_nbuckets */ - scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { - guard(mutex)(&c->sb_lock); - struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); - m->nbuckets = new_nbuckets; - bch2_write_super(c); + + scoped_guard(rwsem_write, &c->state_lock) { + /* zero target_nbuckets */ + scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { + guard(mutex)(&c->sb_lock); + struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); + m->target_nbuckets = 0; + + bch2_write_super(c); + } + + /* re-check that tail is really empty */ + bool empty = false; + try(tail_is_empty(c, ca, new_nbuckets, err, &empty)); + if (!empty) { + prt_printf(err, "Shrink failed: still has data\n"); + return -EBUSY; + } + + /* write & commit new_nbuckets */ + scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { + guard(mutex)(&c->sb_lock); + struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); + m->nbuckets = new_nbuckets; + + bch2_write_super(c); + } + /* resize in-memory data structures */ + bch2_dev_buckets_nouse_free(c, ca); + int ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); + if (ret) { + prt_printf(err, "bch2_dev_buckets_resize() error: %s\n", bch2_err_str(ret)); + return ret; + } + + if (ca->mi.freespace_initialized) { + ret = __bch2_dev_resize_alloc(ca, old_nbuckets, new_nbuckets); + if (ret) { + prt_printf(err, "__bch2_dev_resize_alloc() error: %s\n", bch2_err_str(ret)); + return ret; + } + } + + bch2_recalc_capacity(c); } - /* update free-space accounting */ - /* resize in-memory data structures */ + return 0; } /* Resize on mount */ From 75cc265e306bf9cb0142a8b13b433e0175ad0bba Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Tue, 17 Feb 2026 23:07:33 +0100 Subject: [PATCH 14/75] bcachefs: shrink: avoid holding two transactions at once --- fs/bcachefs/init/dev.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index f7edc3b918354..b2afeb4c26aef 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1414,10 +1414,12 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru bool empty = false; try(tail_is_empty(c, ca, new_nbuckets, err, &empty)); + /* do a definitive check */ if (empty) { - /* do a definitive check */ - CLASS(btree_trans, trans)(c); - try(bch2_btree_write_buffer_flush_sync(trans)); + { + CLASS(btree_trans, trans)(c); + try(bch2_btree_write_buffer_flush_sync(trans)); + } try(tail_is_empty(c, ca, new_nbuckets, err, &empty)); if (empty) { @@ -1468,6 +1470,7 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru return ret; } + // TODO: make this path shrink-compatible if (ca->mi.freespace_initialized) { ret = __bch2_dev_resize_alloc(ca, old_nbuckets, new_nbuckets); if (ret) { From ce82b76525cbabac24172a373e99d5698a8a7b3a Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Wed, 18 Feb 2026 13:58:40 +0100 Subject: [PATCH 15/75] bcachefs: alloc: directly check against target_nbuckets for skipping allocation --- fs/bcachefs/alloc/foreground.c | 2 +- fs/bcachefs/init/dev.c | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/fs/bcachefs/alloc/foreground.c b/fs/bcachefs/alloc/foreground.c index 89f883b24ab2c..fe541e7f3202c 100644 --- a/fs/bcachefs/alloc/foreground.c +++ b/fs/bcachefs/alloc/foreground.c @@ -233,7 +233,7 @@ static struct open_bucket *__try_alloc_bucket(struct bch_fs *c, if (unlikely(is_superblock_bucket(c, ca, bucket))) return NULL; - if (unlikely(bch2_bucket_nouse(ca, bucket))) { + if (unlikely(bch2_bucket_nouse(ca, bucket) || (ca->mi.target_nbuckets && bucket >= ca->mi.target_nbuckets))) { req->counters.skipped_nouse++; return NULL; } diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index b2afeb4c26aef..6dd98edc718bd 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1391,15 +1391,8 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru bch2_write_super(c); } - /* block allocation */ - ret = bch2_dev_buckets_nouse_alloc(c, ca); - if (ret) { - prt_printf(err, "error allocating buckets_nouse for dev %u: %s\n", ca->dev_idx, bch2_err_str(ret)); - return ret; - } - - bitmap_set(ca->buckets_nouse, new_nbuckets, old_nbuckets - new_nbuckets); - bch2_reset_alloc_cursors(c); + /* block allocation - done by setting target_nbuckets */ + bch2_reset_alloc_cursors(c); // avoid churn /* trigger reconcile range scan -> should kick off evacuation from range */ struct reconcile_scan s = { @@ -1463,7 +1456,6 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru bch2_write_super(c); } /* resize in-memory data structures */ - bch2_dev_buckets_nouse_free(c, ca); int ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); if (ret) { prt_printf(err, "bch2_dev_buckets_resize() error: %s\n", bch2_err_str(ret)); From 6dab5dd531e5b6f5f5f0f881404e7cea3470a752 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Wed, 18 Feb 2026 14:06:26 +0100 Subject: [PATCH 16/75] bcachefs: remove unused buckets_nouse code --- fs/bcachefs/alloc/background.c | 3 +-- fs/bcachefs/alloc/buckets.c | 37 ---------------------------------- fs/bcachefs/alloc/buckets.h | 10 --------- fs/bcachefs/alloc/foreground.c | 2 +- fs/bcachefs/bcachefs.h | 1 - fs/bcachefs/errcode.h | 2 -- 6 files changed, 2 insertions(+), 53 deletions(-) diff --git a/fs/bcachefs/alloc/background.c b/fs/bcachefs/alloc/background.c index bc8a97d850584..da78e0d23a4b7 100644 --- a/fs/bcachefs/alloc/background.c +++ b/fs/bcachefs/alloc/background.c @@ -1254,8 +1254,7 @@ int bch2_trigger_alloc(struct btree_trans *trans, if (is_empty_delta < 0 && (new_a->data_type != BCH_DATA_sb && new_a->data_type != BCH_DATA_journal) && - !bch2_bucket_is_open_safe(c, new.k->p.inode, new.k->p.offset) && - !bch2_bucket_nouse(ca, new.k->p.offset)) { + !bch2_bucket_is_open_safe(c, new.k->p.inode, new.k->p.offset)) { CLASS(printbuf, buf)(); log_fsck_err_on(true, trans, alloc_key_bucket_nonempty_to_empty_not_open, diff --git a/fs/bcachefs/alloc/buckets.c b/fs/bcachefs/alloc/buckets.c index 5a95530f47ea9..c4be7a361d613 100644 --- a/fs/bcachefs/alloc/buckets.c +++ b/fs/bcachefs/alloc/buckets.c @@ -1224,39 +1224,6 @@ int __bch2_disk_reservation_add(struct bch_fs *c, struct disk_reservation *res, /* Startup/shutdown: */ -void bch2_dev_buckets_nouse_free(struct bch_fs *c, struct bch_dev *ca) -{ - kvfree_rcu_mightsleep(ca->buckets_nouse); - ca->buckets_nouse = NULL; -} -void bch2_buckets_nouse_free(struct bch_fs *c) -{ - for_each_member_device(c, ca) { - bch2_dev_buckets_nouse_free(c, ca); - } -} - -int bch2_dev_buckets_nouse_alloc(struct bch_fs *c, struct bch_dev *ca) -{ - BUG_ON(ca->buckets_nouse); - - ca->buckets_nouse = bch2_kvmalloc(BITS_TO_LONGS(ca->mi.nbuckets) * - sizeof(unsigned long), - GFP_KERNEL|__GFP_ZERO); - if (!ca->buckets_nouse) - return bch_err_throw(c, ENOMEM_buckets_nouse); - - return 0; -} -int bch2_buckets_nouse_alloc(struct bch_fs *c) -{ - for_each_member_device(c, ca) { - try(bch2_dev_buckets_nouse_alloc(c, ca)); - } - - return 0; -} - static void bucket_gens_free_rcu(struct rcu_head *rcu) { struct bucket_gens *buckets = @@ -1274,9 +1241,6 @@ int bch2_dev_buckets_resize(struct bch_fs *c, struct bch_dev *ca, u64 nbuckets) if (resize) lockdep_assert_held(&c->state_lock); - if (resize && ca->buckets_nouse) - return bch_err_throw(c, no_resize_with_buckets_nouse); // TODO: make this work - bucket_gens = bch2_kvmalloc(struct_size(bucket_gens, b, nbuckets), GFP_KERNEL|__GFP_ZERO); if (!bucket_gens) @@ -1309,7 +1273,6 @@ int bch2_dev_buckets_resize(struct bch_fs *c, struct bch_dev *ca, u64 nbuckets) void bch2_dev_buckets_free(struct bch_dev *ca) { - kvfree(ca->buckets_nouse); kvfree(rcu_dereference_protected(ca->bucket_gens, 1)); free_percpu(ca->usage); } diff --git a/fs/bcachefs/alloc/buckets.h b/fs/bcachefs/alloc/buckets.h index 2bf1a0036c7aa..317354410241e 100644 --- a/fs/bcachefs/alloc/buckets.h +++ b/fs/bcachefs/alloc/buckets.h @@ -421,16 +421,6 @@ static inline u64 avail_factor(u64 r) return div_u64(r << RESERVE_FACTOR, (1 << RESERVE_FACTOR) + 1); } -void bch2_dev_buckets_nouse_free(struct bch_fs *, struct bch_dev *); -void bch2_buckets_nouse_free(struct bch_fs *); -int bch2_dev_buckets_nouse_alloc(struct bch_fs *, struct bch_dev *); -int bch2_buckets_nouse_alloc(struct bch_fs *); - -static inline bool bch2_bucket_nouse(struct bch_dev *ca, u64 bucket) -{ - return unlikely(ca->buckets_nouse && test_bit(bucket, ca->buckets_nouse)); -} - int bch2_dev_buckets_resize(struct bch_fs *, struct bch_dev *, u64); void bch2_dev_buckets_free(struct bch_dev *); int bch2_dev_buckets_alloc(struct bch_fs *, struct bch_dev *); diff --git a/fs/bcachefs/alloc/foreground.c b/fs/bcachefs/alloc/foreground.c index fe541e7f3202c..b38aee75b7ad5 100644 --- a/fs/bcachefs/alloc/foreground.c +++ b/fs/bcachefs/alloc/foreground.c @@ -233,7 +233,7 @@ static struct open_bucket *__try_alloc_bucket(struct bch_fs *c, if (unlikely(is_superblock_bucket(c, ca, bucket))) return NULL; - if (unlikely(bch2_bucket_nouse(ca, bucket) || (ca->mi.target_nbuckets && bucket >= ca->mi.target_nbuckets))) { + if (unlikely(ca->mi.target_nbuckets && bucket >= ca->mi.target_nbuckets)) { req->counters.skipped_nouse++; return NULL; } diff --git a/fs/bcachefs/bcachefs.h b/fs/bcachefs/bcachefs.h index 02c80c167c167..d7259cd08f8f9 100644 --- a/fs/bcachefs/bcachefs.h +++ b/fs/bcachefs/bcachefs.h @@ -506,7 +506,6 @@ struct bch_dev { GENRADIX(struct bucket) buckets_gc; struct bucket_gens __rcu *bucket_gens; u8 *oldest_gen; - unsigned long *buckets_nouse; struct bucket_bitmap bucket_backpointer_mismatch; struct bucket_bitmap bucket_backpointer_empty; diff --git a/fs/bcachefs/errcode.h b/fs/bcachefs/errcode.h index dac8283bfcb85..b9a8d6223a7b5 100644 --- a/fs/bcachefs/errcode.h +++ b/fs/bcachefs/errcode.h @@ -126,7 +126,6 @@ x(ENOMEM, ENOMEM_backpointer_mismatches_bitmap) \ x(EIO, compression_workspace_not_initialized) \ x(ENOMEM, ENOMEM_bucket_gens) \ - x(ENOMEM, ENOMEM_buckets_nouse) \ x(ENOMEM, ENOMEM_usage_init) \ x(ENOMEM, ENOMEM_btree_node_read_all_replicas) \ x(ENOMEM, ENOMEM_btree_node_reclaim) \ @@ -294,7 +293,6 @@ x(EINVAL, opt_parse_error) \ x(EINVAL, remove_with_metadata_missing_unimplemented)\ x(EINVAL, remove_would_lose_data) \ - x(EINVAL, no_resize_with_buckets_nouse) \ x(EINVAL, inode_unpack_error) \ x(EINVAL, inode_not_unlinked) \ x(EINVAL, inode_has_child_snapshot) \ From 7c803883aeee0325419ad969b9b5b20e031fe730 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Wed, 18 Feb 2026 14:06:26 +0100 Subject: [PATCH 17/75] bcachefs: shrink: print error message if reconcile scan fails --- fs/bcachefs/init/dev.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 6dd98edc718bd..0867f99111289 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1400,6 +1400,10 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru .dev = ca->dev_idx, }; ret = bch2_set_reconcile_needs_scan(c, s, true); + if (ret) { + prt_printf(err, "Failed to run device reconcile scan: %s\n", bch2_err_str(ret)); + return ret; + } }; /* wait for to-be-shrunk reason to be empty */ @@ -1456,7 +1460,7 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru bch2_write_super(c); } /* resize in-memory data structures */ - int ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); + ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); if (ret) { prt_printf(err, "bch2_dev_buckets_resize() error: %s\n", bch2_err_str(ret)); return ret; From 6e47a0682ef78d4002f8ef8e57631e2407a81d9b Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Wed, 18 Feb 2026 21:20:44 +0100 Subject: [PATCH 18/75] bcachefs: shrink: close open buckets before evacuating data --- fs/bcachefs/alloc/foreground.c | 12 +++++++++--- fs/bcachefs/alloc/foreground.h | 2 ++ fs/bcachefs/data/ec/init.c | 2 +- fs/bcachefs/init/dev.c | 10 ++++++++-- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/fs/bcachefs/alloc/foreground.c b/fs/bcachefs/alloc/foreground.c index b38aee75b7ad5..17c12ca66b794 100644 --- a/fs/bcachefs/alloc/foreground.c +++ b/fs/bcachefs/alloc/foreground.c @@ -906,6 +906,11 @@ static int bucket_alloc_set_partial(struct bch_fs *c, return 0; } +/* Returns whether the @open_buckets device matches @ca, and if we're currently shrinking, whether it falls into the to-be-shrunk region */ +static bool dev_and_region_matches(struct open_bucket *ob, struct bch_dev *ca) { + return ob->dev == ca->dev_idx && (!ca->mi.target_nbuckets || ob->bucket >= ca->mi.target_nbuckets); +} + /** * should_drop_bucket - check if this is open_bucket should go away * @ob: open_bucket to predicate on @@ -927,7 +932,7 @@ static bool should_drop_bucket(struct open_bucket *ob, struct bch_fs *c, if (ec) { return ob->ec != NULL; } else if (ca) { - bool drop = ob->dev == ca->dev_idx; + bool drop = dev_and_region_matches(ob, ca); if (!drop && ob->ec) { guard(mutex)(&ob->ec->lock); @@ -938,7 +943,7 @@ static bool should_drop_bucket(struct open_bucket *ob, struct bch_fs *c, continue; struct open_bucket *ob2 = a->open_buckets + ob->ec->blocks[i]; - drop |= ob2->dev == ca->dev_idx; + drop |= dev_and_region_matches(ob2, ca); } } @@ -964,13 +969,14 @@ static void bch2_writepoint_stop(struct bch_fs *c, struct bch_dev *ca, wp->ptrs = ptrs; } +/* stop open buckets on @ca. If we're shrinking, only stop buckets in to-be-shrunk region */ void bch2_open_buckets_stop(struct bch_fs *c, struct bch_dev *ca, bool ec) { struct bch_fs_allocator *a = &c->allocator; unsigned i; - /* Next, close write points that point to this device... */ + /* Next, close write points that point to this device or the to-be-shrunk region... */ for (i = 0; i < ARRAY_SIZE(a->write_points); i++) bch2_writepoint_stop(c, ca, ec, &a->write_points[i]); diff --git a/fs/bcachefs/alloc/foreground.h b/fs/bcachefs/alloc/foreground.h index c28094863c379..273ff2c4bf129 100644 --- a/fs/bcachefs/alloc/foreground.h +++ b/fs/bcachefs/alloc/foreground.h @@ -281,6 +281,8 @@ enum bch_write_flags; int bch2_bucket_alloc_set_trans(struct btree_trans *, struct alloc_request *, struct dev_stripe_state *); +static bool dev_and_region_matches(struct open_bucket *ob, struct bch_dev *ca); + int bch2_alloc_sectors_req(struct btree_trans *, struct alloc_request *, struct write_point_specifier, struct write_point **); diff --git a/fs/bcachefs/data/ec/init.c b/fs/bcachefs/data/ec/init.c index 18c509fa09407..69e0f16325c9f 100644 --- a/fs/bcachefs/data/ec/init.c +++ b/fs/bcachefs/data/ec/init.c @@ -126,7 +126,7 @@ static bool should_cancel_stripe(struct bch_fs *c, struct ec_stripe_new *s, stru continue; struct open_bucket *ob = c->allocator.open_buckets + s->blocks[i]; - if (ob->dev == ca->dev_idx) + if (dev_and_region_matches(ob, ca)) return true; } diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 0867f99111289..1fb7387798d09 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1339,6 +1339,7 @@ int bch2_dev_grow(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct } +// TODO: make sure everything is caught here static int tail_is_empty(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err, bool *empty) { struct bpos bp_start = bucket_pos_to_bp_start(ca, POS(ca->dev_idx, new_nbuckets)); struct bpos bp_end = bucket_pos_to_bp_start(ca, POS(ca->dev_idx, ca->mi.nbuckets)); @@ -1382,7 +1383,7 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru return bch_err_throw(c, device_already_resizing); } - /* write & commit target_nbuckets */ + /* write & commit target_nbuckets - also stops new allocations */ scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { guard(mutex)(&c->sb_lock); struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); @@ -1391,7 +1392,8 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru bch2_write_super(c); } - /* block allocation - done by setting target_nbuckets */ + /* close open buckets in the to-be-shrunk region */ + bch2_open_buckets_stop(c, ca, false); bch2_reset_alloc_cursors(c); // avoid churn /* trigger reconcile range scan -> should kick off evacuation from range */ @@ -1424,6 +1426,7 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru } } + /* make sure reconcile is actually running */ bch2_reconcile_wakeup(c); if (signal_pending(current)) @@ -1459,13 +1462,16 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru bch2_write_super(c); } + /* resize in-memory data structures */ + /* resize buckets */ ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); if (ret) { prt_printf(err, "bch2_dev_buckets_resize() error: %s\n", bch2_err_str(ret)); return ret; } + /* resize alloc info - see dev_remove */ // TODO: make this path shrink-compatible if (ca->mi.freespace_initialized) { ret = __bch2_dev_resize_alloc(ca, old_nbuckets, new_nbuckets); From 5285b7401fab367b3e629a811037ba9c9fa1bb02 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Wed, 18 Feb 2026 21:48:41 +0100 Subject: [PATCH 19/75] bcachefs: shrink: explicitly pass tail_cutoff instead of implicitly relying on ca->mi.target_nbuckets avoids possible edge cases if device is being removed mid-shrink etc. --- fs/bcachefs/alloc/background.c | 2 +- fs/bcachefs/alloc/foreground.c | 32 ++++++++++++++------------------ fs/bcachefs/alloc/foreground.h | 7 +++++-- fs/bcachefs/data/ec/init.c | 17 +++++++++++------ fs/bcachefs/data/ec/init.h | 2 +- fs/bcachefs/init/dev.c | 2 +- fs/bcachefs/init/fs.c | 2 +- 7 files changed, 34 insertions(+), 30 deletions(-) diff --git a/fs/bcachefs/alloc/background.c b/fs/bcachefs/alloc/background.c index da78e0d23a4b7..06d023276c6fa 100644 --- a/fs/bcachefs/alloc/background.c +++ b/fs/bcachefs/alloc/background.c @@ -1635,7 +1635,7 @@ void bch2_dev_allocator_remove(struct bch_fs *c, struct bch_dev *ca) */ bch2_recalc_capacity(c); - bch2_open_buckets_stop(c, ca, false); + bch2_open_buckets_stop(c, ca, false, 0); /* * Wake up threads that were blocked on allocation, so they can notice diff --git a/fs/bcachefs/alloc/foreground.c b/fs/bcachefs/alloc/foreground.c index 17c12ca66b794..1dba2309cb807 100644 --- a/fs/bcachefs/alloc/foreground.c +++ b/fs/bcachefs/alloc/foreground.c @@ -906,11 +906,6 @@ static int bucket_alloc_set_partial(struct bch_fs *c, return 0; } -/* Returns whether the @open_buckets device matches @ca, and if we're currently shrinking, whether it falls into the to-be-shrunk region */ -static bool dev_and_region_matches(struct open_bucket *ob, struct bch_dev *ca) { - return ob->dev == ca->dev_idx && (!ca->mi.target_nbuckets || ob->bucket >= ca->mi.target_nbuckets); -} - /** * should_drop_bucket - check if this is open_bucket should go away * @ob: open_bucket to predicate on @@ -919,20 +914,21 @@ static bool dev_and_region_matches(struct open_bucket *ob, struct bch_dev *ca) { * @ec: if true, we're shutting down erasure coding and killing all ec * open_buckets * otherwise, return true + * @tail_cutoff: if != 0 only drop buckets after the cutoff * Returns: true if we should kill this open_bucket * * We're killing open_buckets because we're shutting down a device, erasure * coding, or the entire filesystem - check if this open_bucket matches: */ static bool should_drop_bucket(struct open_bucket *ob, struct bch_fs *c, - struct bch_dev *ca, bool ec) + struct bch_dev *ca, bool ec, u64 tail_cutoff) { struct bch_fs_allocator *a = &c->allocator; if (ec) { return ob->ec != NULL; } else if (ca) { - bool drop = dev_and_region_matches(ob, ca); + bool drop = dev_and_region_matches(ob, ca, tail_cutoff); if (!drop && ob->ec) { guard(mutex)(&ob->ec->lock); @@ -943,7 +939,7 @@ static bool should_drop_bucket(struct open_bucket *ob, struct bch_fs *c, continue; struct open_bucket *ob2 = a->open_buckets + ob->ec->blocks[i]; - drop |= dev_and_region_matches(ob2, ca); + drop |= dev_and_region_matches(ob2, ca, tail_cutoff); } } @@ -954,7 +950,7 @@ static bool should_drop_bucket(struct open_bucket *ob, struct bch_fs *c, } static void bch2_writepoint_stop(struct bch_fs *c, struct bch_dev *ca, - bool ec, struct write_point *wp) + bool ec, struct write_point *wp, u64 tail_cutoff) { struct open_buckets ptrs = { .nr = 0 }; struct open_bucket *ob; @@ -962,27 +958,27 @@ static void bch2_writepoint_stop(struct bch_fs *c, struct bch_dev *ca, guard(mutex)(&wp->lock); open_bucket_for_each(c, &wp->ptrs, ob, i) - if (should_drop_bucket(ob, c, ca, ec)) + if (should_drop_bucket(ob, c, ca, ec, tail_cutoff)) bch2_open_bucket_put(c, ob); else ob_push(c, &ptrs, ob); wp->ptrs = ptrs; } -/* stop open buckets on @ca. If we're shrinking, only stop buckets in to-be-shrunk region */ +/* stop open buckets on @ca, if tail_cutoff isn't zero, only after the cutoff */ void bch2_open_buckets_stop(struct bch_fs *c, struct bch_dev *ca, - bool ec) + bool ec, u64 tail_cutoff) { struct bch_fs_allocator *a = &c->allocator; unsigned i; /* Next, close write points that point to this device or the to-be-shrunk region... */ for (i = 0; i < ARRAY_SIZE(a->write_points); i++) - bch2_writepoint_stop(c, ca, ec, &a->write_points[i]); + bch2_writepoint_stop(c, ca, ec, &a->write_points[i], tail_cutoff); - bch2_writepoint_stop(c, ca, ec, &c->copygc.write_point); - bch2_writepoint_stop(c, ca, ec, &a->reconcile_write_point); - bch2_writepoint_stop(c, ca, ec, &a->btree_write_point); + bch2_writepoint_stop(c, ca, ec, &c->copygc.write_point, tail_cutoff); + bch2_writepoint_stop(c, ca, ec, &a->reconcile_write_point, tail_cutoff); + bch2_writepoint_stop(c, ca, ec, &a->btree_write_point, tail_cutoff); scoped_guard(mutex, &c->btree.reserve_cache.lock) while (c->btree.reserve_cache.nr) { @@ -998,7 +994,7 @@ void bch2_open_buckets_stop(struct bch_fs *c, struct bch_dev *ca, struct open_bucket *ob = a->open_buckets + a->open_buckets_partial[i]; - if (should_drop_bucket(ob, c, ca, ec)) { + if (should_drop_bucket(ob, c, ca, ec, tail_cutoff)) { --a->open_buckets_partial_nr; swap(a->open_buckets_partial[i], a->open_buckets_partial[a->open_buckets_partial_nr]); @@ -1016,7 +1012,7 @@ void bch2_open_buckets_stop(struct bch_fs *c, struct bch_dev *ca, } } - bch2_ec_stop_dev(c, ca); + bch2_ec_stop_dev_cutoff(c, ca, tail_cutoff); } static inline struct hlist_head *writepoint_hash(struct bch_fs_allocator *a, diff --git a/fs/bcachefs/alloc/foreground.h b/fs/bcachefs/alloc/foreground.h index 273ff2c4bf129..6e2e8d09a21fa 100644 --- a/fs/bcachefs/alloc/foreground.h +++ b/fs/bcachefs/alloc/foreground.h @@ -281,7 +281,10 @@ enum bch_write_flags; int bch2_bucket_alloc_set_trans(struct btree_trans *, struct alloc_request *, struct dev_stripe_state *); -static bool dev_and_region_matches(struct open_bucket *ob, struct bch_dev *ca); +/* Returns whether the @open_buckets device matches @ca and whether the @open_bucket falls behind the cutoff */ +static inline bool dev_and_region_matches(struct open_bucket *ob, struct bch_dev *ca, u64 tail_cutoff) { + return ob->dev == ca->dev_idx && ob->bucket >= tail_cutoff; +} int bch2_alloc_sectors_req(struct btree_trans *, struct alloc_request *, struct write_point_specifier, @@ -386,7 +389,7 @@ void bch2_alloc_sectors_append_ptrs(struct bch_fs *, struct write_point *, struct bkey_i *, unsigned, bool); void bch2_alloc_sectors_done(struct bch_fs *, struct write_point *); -void bch2_open_buckets_stop(struct bch_fs *c, struct bch_dev *, bool); +void bch2_open_buckets_stop(struct bch_fs *c, struct bch_dev *, bool, u64 tail_cutoff); static inline struct write_point_specifier writepoint_hashed(unsigned long v) { diff --git a/fs/bcachefs/data/ec/init.c b/fs/bcachefs/data/ec/init.c index 69e0f16325c9f..2f897559965b2 100644 --- a/fs/bcachefs/data/ec/init.c +++ b/fs/bcachefs/data/ec/init.c @@ -116,7 +116,7 @@ int bch2_dev_remove_stripes(struct bch_fs *c, unsigned dev_idx, /* startup/shutdown */ -static bool should_cancel_stripe(struct bch_fs *c, struct ec_stripe_new *s, struct bch_dev *ca) +static bool should_cancel_stripe(struct bch_fs *c, struct ec_stripe_new *s, struct bch_dev *ca, u64 tail_cutoff) { if (!ca) return true; @@ -126,33 +126,38 @@ static bool should_cancel_stripe(struct bch_fs *c, struct ec_stripe_new *s, stru continue; struct open_bucket *ob = c->allocator.open_buckets + s->blocks[i]; - if (dev_and_region_matches(ob, ca)) + if (dev_and_region_matches(ob, ca, tail_cutoff)) return true; } return false; } -static void __bch2_ec_stop(struct bch_fs *c, struct bch_dev *ca) +static void __bch2_ec_stop(struct bch_fs *c, struct bch_dev *ca, u64 tail_cutoff) { struct ec_stripe_head *h; guard(mutex)(&c->ec.stripe_head_lock); list_for_each_entry(h, &c->ec.stripe_head_list, list) { guard(mutex)(&h->lock); - if (h->s && should_cancel_stripe(c, h->s, ca)) + if (h->s && should_cancel_stripe(c, h->s, ca, tail_cutoff)) bch2_ec_stripe_new_cancel(c, h, -BCH_ERR_erofs_no_writes); } } +void bch2_ec_stop_dev_cutoff(struct bch_fs *c, struct bch_dev *ca, u64 tail_cutoff) +{ + __bch2_ec_stop(c, ca, tail_cutoff); +} + void bch2_ec_stop_dev(struct bch_fs *c, struct bch_dev *ca) { - __bch2_ec_stop(c, ca); + __bch2_ec_stop(c, ca, 0); } void bch2_fs_ec_stop(struct bch_fs *c) { - __bch2_ec_stop(c, NULL); + __bch2_ec_stop(c, NULL, 0); } static bool bch2_fs_ec_flush_done(struct bch_fs *c) diff --git a/fs/bcachefs/data/ec/init.h b/fs/bcachefs/data/ec/init.h index f15cba83df70f..b0cf0c77e2599 100644 --- a/fs/bcachefs/data/ec/init.h +++ b/fs/bcachefs/data/ec/init.h @@ -7,6 +7,7 @@ int bch2_invalidate_stripe_to_dev(struct btree_trans *, struct btree_iter *, unsigned, struct printbuf *); int bch2_dev_remove_stripes(struct bch_fs *, unsigned, unsigned, struct printbuf *); +void bch2_ec_stop_dev_cutoff(struct bch_fs *, struct bch_dev *, u64); void bch2_ec_stop_dev(struct bch_fs *, struct bch_dev *); void bch2_fs_ec_stop(struct bch_fs *); void bch2_fs_ec_flush(struct bch_fs *); @@ -22,4 +23,3 @@ int bch2_bucket_nr_stripes(struct btree_trans *, struct bpos); int bch2_check_stripe_refs(struct btree_trans *); #endif /* _BCACHEFS_DATA_EC_INIT_H */ - diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 1fb7387798d09..96cb0b1373812 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1393,7 +1393,7 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru } /* close open buckets in the to-be-shrunk region */ - bch2_open_buckets_stop(c, ca, false); + bch2_open_buckets_stop(c, ca, false, new_nbuckets); bch2_reset_alloc_cursors(c); // avoid churn /* trigger reconcile range scan -> should kick off evacuation from range */ diff --git a/fs/bcachefs/init/fs.c b/fs/bcachefs/init/fs.c index 1bdd4c96606d2..3dbe2352a6125 100644 --- a/fs/bcachefs/init/fs.c +++ b/fs/bcachefs/init/fs.c @@ -321,7 +321,7 @@ static void __bch2_fs_read_only(struct bch_fs *c) bch2_maybe_schedule_btree_bitmap_gc_stop(c); bch2_fs_ec_stop(c); - bch2_open_buckets_stop(c, NULL, true); + bch2_open_buckets_stop(c, NULL, true, 0); bch2_reconcile_stop(c); bch2_copygc_stop(c); bch2_btree_write_buffer_stop(c); From fae49d1edcdee34be755e697316867bfd6293dc4 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Wed, 18 Feb 2026 21:57:03 +0100 Subject: [PATCH 20/75] bcachefs: dev_resize_alloc: cast new - old buckets calculation to signed integer --- fs/bcachefs/init/dev.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 96cb0b1373812..d1c3350dca4cb 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1491,7 +1491,7 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru int __bch2_dev_resize_alloc(struct bch_dev *ca, u64 old_nbuckets, u64 new_nbuckets) { struct bch_fs *c = ca->fs; - u64 v[3] = { new_nbuckets - old_nbuckets, 0, 0 }; + s64 v[3] = { (s64) new_nbuckets - (s64) old_nbuckets, 0, 0 }; return bch2_trans_commit_do(ca->fs, NULL, NULL, 0, bch2_disk_accounting_mod2(trans, false, v, dev_data_type, From 0c38949c19c01d47805c957e16dccc575ba464c8 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Sat, 21 Feb 2026 14:01:29 +0100 Subject: [PATCH 21/75] bcachefs: shrink: truncate alloc info This is done analogous to the remove alloc info path --- fs/bcachefs/alloc/background.c | 25 +++++++++++++++++++++++++ fs/bcachefs/alloc/background.h | 1 + fs/bcachefs/alloc/lru.c | 18 ++++++++++++++++++ fs/bcachefs/alloc/lru.h | 1 + fs/bcachefs/init/dev.c | 25 ++++++++++++++++--------- 5 files changed, 61 insertions(+), 9 deletions(-) diff --git a/fs/bcachefs/alloc/background.c b/fs/bcachefs/alloc/background.c index 06d023276c6fa..d58e314c35e65 100644 --- a/fs/bcachefs/alloc/background.c +++ b/fs/bcachefs/alloc/background.c @@ -1433,6 +1433,31 @@ static int bch2_dev_remove_need_discard(struct bch_fs *c, struct bch_dev *ca) })); } +int bch2_dev_truncate_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) +{ + struct bpos start = POS(ca->dev_idx, cutoff); + struct bpos end = POS(ca->dev_idx, U64_MAX); + int ret; + + /* + * We clear the LRU and need_discard btrees first so that we don't race + * with bch2_do_invalidates() and bch2_do_discards() + */ + ret = bch2_dev_truncate_lrus(c, ca, cutoff) ?: + bch2_btree_delete_range(c, BTREE_ID_need_discard, start, end, + BTREE_TRIGGER_norun) ?: + bch2_btree_delete_range(c, BTREE_ID_freespace, start, end, + BTREE_TRIGGER_norun) ?: + bch2_btree_delete_range(c, BTREE_ID_backpointers, start, end, + BTREE_TRIGGER_norun) ?: + bch2_btree_delete_range(c, BTREE_ID_bucket_gens, start, end, + BTREE_TRIGGER_norun) ?: + bch2_btree_delete_range(c, BTREE_ID_alloc, start, end, + BTREE_TRIGGER_norun) ?: + bch2_dev_usage_remove(c, ca); + bch_err_msg_dev(ca, ret, "truncating dev alloc info"); + return ret; +} int bch2_dev_remove_alloc(struct bch_fs *c, struct bch_dev *ca) { struct bpos start = POS(ca->dev_idx, 0); diff --git a/fs/bcachefs/alloc/background.h b/fs/bcachefs/alloc/background.h index 2dbf67a58a4bd..0f00207fc9da9 100644 --- a/fs/bcachefs/alloc/background.h +++ b/fs/bcachefs/alloc/background.h @@ -330,6 +330,7 @@ int bch2_trigger_alloc(struct btree_trans *, enum btree_id, unsigned, struct bkey_s_c, struct bkey_s, enum btree_iter_update_trigger_flags); +int bch2_dev_truncate_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff); int bch2_dev_remove_alloc(struct bch_fs *, struct bch_dev *); void bch2_recalc_capacity(struct bch_fs *); diff --git a/fs/bcachefs/alloc/lru.c b/fs/bcachefs/alloc/lru.c index 319a183be9465..47880f6eda71f 100644 --- a/fs/bcachefs/alloc/lru.c +++ b/fs/bcachefs/alloc/lru.c @@ -118,6 +118,24 @@ static struct bbpos lru_pos_to_bp(struct bkey_s_c lru_k) } } + +int bch2_dev_truncate_lrus(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) +{ + CLASS(btree_trans, trans)(c); + int ret = bch2_btree_write_buffer_flush_sync(trans) ?: + for_each_btree_key(trans, iter, + BTREE_ID_lru, POS_MIN, BTREE_ITER_prefetch, k, ({ + struct bbpos bp = lru_pos_to_bp(k); + + bp.btree == BTREE_ID_alloc && bp.pos.inode == ca->dev_idx && bp.pos.offset >= cutoff + ? (bch2_btree_delete_at(trans, &iter, 0) ?: + bch2_trans_commit(trans, NULL, NULL, 0)) + : 0; + })); + bch_err_fn(c, ret); + return ret; +} + int bch2_dev_remove_lrus(struct bch_fs *c, struct bch_dev *ca) { CLASS(btree_trans, trans)(c); diff --git a/fs/bcachefs/alloc/lru.h b/fs/bcachefs/alloc/lru.h index 2a9e2bf4b116d..9075b4787eed1 100644 --- a/fs/bcachefs/alloc/lru.h +++ b/fs/bcachefs/alloc/lru.h @@ -70,6 +70,7 @@ static inline int bch2_lru_change(struct btree_trans *trans, : 0; } +int bch2_dev_truncate_lrus(struct bch_fs *c, struct bch_dev *ca, u64 cutoff); int bch2_dev_remove_lrus(struct bch_fs *, struct bch_dev *); struct wb_maybe_flush; diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index d1c3350dca4cb..768320bd10493 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1339,7 +1339,8 @@ int bch2_dev_grow(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct } -// TODO: make sure everything is caught here +// TODO: make sure everything is caught here. +// Fore example journals and superblocks might need special handling static int tail_is_empty(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err, bool *empty) { struct bpos bp_start = bucket_pos_to_bp_start(ca, POS(ca->dev_idx, new_nbuckets)); struct bpos bp_end = bucket_pos_to_bp_start(ca, POS(ca->dev_idx, ca->mi.nbuckets)); @@ -1464,6 +1465,7 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru } /* resize in-memory data structures */ + /* resize buckets */ ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); if (ret) { @@ -1471,15 +1473,20 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru return ret; } - /* resize alloc info - see dev_remove */ - // TODO: make this path shrink-compatible - if (ca->mi.freespace_initialized) { - ret = __bch2_dev_resize_alloc(ca, old_nbuckets, new_nbuckets); - if (ret) { - prt_printf(err, "__bch2_dev_resize_alloc() error: %s\n", bch2_err_str(ret)); - return ret; - } + /* truncate alloc info */ + ret = bch2_dev_truncate_alloc(c, ca, new_nbuckets); + if (ret) { + prt_printf(err, "error truncating alloc info: %s\n", bch2_err_str(ret)); + return ret; } + // TODO: figure out what parts of this path still need doing + // if (ca->mi.freespace_initialized) { + // ret = __bch2_dev_resize_alloc(ca, old_nbuckets, new_nbuckets); + // if (ret) { + // prt_printf(err, "__bch2_dev_resize_alloc() error: %s\n", bch2_err_str(ret)); + // return ret; + // } + // } bch2_recalc_capacity(c); } From 9e6f21079a522ffc701dcf209370a3fa30425a9d Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Sat, 21 Feb 2026 14:15:30 +0100 Subject: [PATCH 22/75] bcachefs: shrink: run disk accounting --- fs/bcachefs/init/dev.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 768320bd10493..ba85e03e09e9c 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1479,6 +1479,19 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru prt_printf(err, "error truncating alloc info: %s\n", bch2_err_str(ret)); return ret; } + + /* account disk space */ + // see __bch2_dev_resize_alloc + s64 v[3] = { (s64) new_nbuckets - (s64) old_nbuckets, 0, 0 }; + ret = bch2_trans_commit_do(ca->fs, NULL, NULL, 0, + bch2_disk_accounting_mod2(trans, false, v, dev_data_type, + .dev = ca->dev_idx, + .data_type = BCH_DATA_free)); + if (ret) { + prt_printf(err, "error accounting disk space: %s\n", bch2_err_str(ret)); + return ret; + } + // TODO: figure out what parts of this path still need doing // if (ca->mi.freespace_initialized) { // ret = __bch2_dev_resize_alloc(ca, old_nbuckets, new_nbuckets); From a2f2b526b5c7ac5bd13ea8e78ecf04606350a05f Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Sat, 21 Feb 2026 14:28:19 +0100 Subject: [PATCH 23/75] bcachefs: shrink: disk accounting: properly handle negative changes --- fs/bcachefs/alloc/accounting.c | 2 +- fs/bcachefs/util/util.h | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/fs/bcachefs/alloc/accounting.c b/fs/bcachefs/alloc/accounting.c index 4781a6b36e041..5b54a51d158c3 100644 --- a/fs/bcachefs/alloc/accounting.c +++ b/fs/bcachefs/alloc/accounting.c @@ -124,7 +124,7 @@ int bch2_disk_accounting_mod(struct btree_trans *trans, a = (void *) bkey_next(&a->k_i)) if (bpos_eq(a->k.p, pos)) { BUG_ON(nr != bch2_accounting_counters(&a->k)); - acc_u64s(a->v.d, d, nr); + acc_s64s(a->v.d, d, nr); if (bch2_accounting_key_is_zero(accounting_i_to_s_c(a))) { unsigned offset = (u64 *) a - diff --git a/fs/bcachefs/util/util.h b/fs/bcachefs/util/util.h index 7cde5e33fa32e..e25145ee8ee58 100644 --- a/fs/bcachefs/util/util.h +++ b/fs/bcachefs/util/util.h @@ -666,6 +666,16 @@ static inline void percpu_u64_set(u64 __percpu *dst, u64 src) this_cpu_write(*dst, src); } +static inline void acc_s64s(u64 *acc, const s64 *src, unsigned nr) +{ + for (unsigned i = 0; i < nr; i++) + if (src[i] >= 0) { + acc[i] += src[i]; + } else { + acc[i] -= abs(src[i]); + } +} + static inline void acc_u64s(u64 *acc, const u64 *src, unsigned nr) { for (unsigned i = 0; i < nr; i++) From 812b9d9ec07520b366158286f120ff4d4cc79f08 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Sat, 21 Feb 2026 14:47:43 +0100 Subject: [PATCH 24/75] bcachefs: shrink: truncate alloc: remove spurious dev_usage_remove call --- fs/bcachefs/alloc/accounting.c | 2 +- fs/bcachefs/alloc/accounting_format.h | 2 +- fs/bcachefs/alloc/background.c | 3 +-- fs/bcachefs/init/dev.c | 5 +++-- fs/bcachefs/util/util.h | 10 ---------- 5 files changed, 6 insertions(+), 16 deletions(-) diff --git a/fs/bcachefs/alloc/accounting.c b/fs/bcachefs/alloc/accounting.c index 5b54a51d158c3..4781a6b36e041 100644 --- a/fs/bcachefs/alloc/accounting.c +++ b/fs/bcachefs/alloc/accounting.c @@ -124,7 +124,7 @@ int bch2_disk_accounting_mod(struct btree_trans *trans, a = (void *) bkey_next(&a->k_i)) if (bpos_eq(a->k.p, pos)) { BUG_ON(nr != bch2_accounting_counters(&a->k)); - acc_s64s(a->v.d, d, nr); + acc_u64s(a->v.d, d, nr); if (bch2_accounting_key_is_zero(accounting_i_to_s_c(a))) { unsigned offset = (u64 *) a - diff --git a/fs/bcachefs/alloc/accounting_format.h b/fs/bcachefs/alloc/accounting_format.h index dd30f050d82f6..3b048df69b8d1 100644 --- a/fs/bcachefs/alloc/accounting_format.h +++ b/fs/bcachefs/alloc/accounting_format.h @@ -10,7 +10,7 @@ * Here, the key has considerably more structure than a typical key (bpos); an * accounting key is 'struct disk_accounting_pos', which is a union of bpos. * - * More specifically: a key is just a muliword integer (where word endianness + * More specifically: a key is just a multiword integer (where word endianness * matches native byte order), so we're treating bpos as an opaque 20 byte * integer and mapping bch_accounting_key to that. * diff --git a/fs/bcachefs/alloc/background.c b/fs/bcachefs/alloc/background.c index d58e314c35e65..ae964a82f159a 100644 --- a/fs/bcachefs/alloc/background.c +++ b/fs/bcachefs/alloc/background.c @@ -1453,8 +1453,7 @@ int bch2_dev_truncate_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) bch2_btree_delete_range(c, BTREE_ID_bucket_gens, start, end, BTREE_TRIGGER_norun) ?: bch2_btree_delete_range(c, BTREE_ID_alloc, start, end, - BTREE_TRIGGER_norun) ?: - bch2_dev_usage_remove(c, ca); + BTREE_TRIGGER_norun); bch_err_msg_dev(ca, ret, "truncating dev alloc info"); return ret; } diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index ba85e03e09e9c..c6b46c21fdf2a 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1481,8 +1481,9 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru } /* account disk space */ - // see __bch2_dev_resize_alloc - s64 v[3] = { (s64) new_nbuckets - (s64) old_nbuckets, 0, 0 }; + // - BCH_DATA_free is somehow still off by 2 + // - BCH_DATA_sb isn't updated at all yet - should we relocate them or just accept that we have less copies and adjust? + s64 v[3] = { -(s64) (old_nbuckets - new_nbuckets), 0, 0 }; ret = bch2_trans_commit_do(ca->fs, NULL, NULL, 0, bch2_disk_accounting_mod2(trans, false, v, dev_data_type, .dev = ca->dev_idx, diff --git a/fs/bcachefs/util/util.h b/fs/bcachefs/util/util.h index e25145ee8ee58..7cde5e33fa32e 100644 --- a/fs/bcachefs/util/util.h +++ b/fs/bcachefs/util/util.h @@ -666,16 +666,6 @@ static inline void percpu_u64_set(u64 __percpu *dst, u64 src) this_cpu_write(*dst, src); } -static inline void acc_s64s(u64 *acc, const s64 *src, unsigned nr) -{ - for (unsigned i = 0; i < nr; i++) - if (src[i] >= 0) { - acc[i] += src[i]; - } else { - acc[i] -= abs(src[i]); - } -} - static inline void acc_u64s(u64 *acc, const u64 *src, unsigned nr) { for (unsigned i = 0; i < nr; i++) From 872f621b149b7c19d3cadaa101b8216b15ee2ca6 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Sat, 21 Feb 2026 17:37:11 +0100 Subject: [PATCH 25/75] bcachefs: shrink: drop references to cutoff superblock copies --- fs/bcachefs/init/dev.c | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index c6b46c21fdf2a..6742b1cbbd921 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -233,8 +233,10 @@ #include "init/fs.h" #include "linux/bitmap.h" +#include "linux/byteorder/generic.h" #include "linux/sched.h" #include "linux/sched/signal.h" +#include "sb/io.h" #include "sb/members.h" #include "sb/members_format.h" #include "util/util.h" @@ -1338,6 +1340,29 @@ int bch2_dev_grow(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct return 0; } +static int drop_sbs_after_cutoff(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) { + u64 cutoff_sector = bucket_to_sector(ca, cutoff); + + guard(memalloc_flags)(PF_MEMALLOC_NOFS); + guard(mutex)(&c->sb_lock); + + struct bch_sb_layout *layout = &ca->disk_sb.sb->layout; + + u64 max_sectors = 1 << layout->sb_max_size_bits; + + u8 i; + /* offsets are sorted in ascending order, see validate_sb_layout() overlapping checks for evidence */ + for (i = 0; i < layout->nr_superblocks; i++) { + u64 offset = le64_to_cpu(layout->sb_offset[i]); + if (offset + max_sectors > cutoff_sector) { + break; + } + } + + layout->nr_superblocks = i; + + return bch2_write_super(c); +} // TODO: make sure everything is caught here. // Fore example journals and superblocks might need special handling @@ -1464,7 +1489,12 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru bch2_write_super(c); } - /* resize in-memory data structures */ + /* drop references to now-truncated superblock copies */ + ret = drop_sbs_after_cutoff(c, ca, new_nbuckets); + if (ret) { + prt_printf(err, "Error dropping superblocks after cutoff: %s\n", bch2_err_str(ret)); + return ret; + } /* resize buckets */ ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); @@ -1481,7 +1511,7 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru } /* account disk space */ - // - BCH_DATA_free is somehow still off by 2 + // - BCH_DATA_free is somehow still off by the amount of superblocks // - BCH_DATA_sb isn't updated at all yet - should we relocate them or just accept that we have less copies and adjust? s64 v[3] = { -(s64) (old_nbuckets - new_nbuckets), 0, 0 }; ret = bch2_trans_commit_do(ca->fs, NULL, NULL, 0, From 8507ca39711af48605f3d0d5489fe073cf829f7e Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Sat, 21 Feb 2026 18:10:52 +0100 Subject: [PATCH 26/75] bcachefs: shrink: mark superblock buckets as metadata --- fs/bcachefs/init/dev.c | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 6742b1cbbd921..83e4ab6c1722e 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -203,6 +203,7 @@ */ #include "alloc/buckets.h" +#include "asm-generic/bug.h" #include "bcachefs.h" #include "alloc/accounting.h" @@ -1359,6 +1360,8 @@ static int drop_sbs_after_cutoff(struct bch_fs *c, struct bch_dev *ca, u64 cutof } } + BUG_ON(i == 0); + layout->nr_superblocks = i; return bch2_write_super(c); @@ -1434,7 +1437,7 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru } }; - /* wait for to-be-shrunk reason to be empty */ + /* wait for to-be-shrunk region to be empty */ while (true) { bool empty = false; try(tail_is_empty(c, ca, new_nbuckets, err, &empty)); @@ -1480,6 +1483,20 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru return -EBUSY; } + /* drop references to now-truncated superblock copies */ + ret = drop_sbs_after_cutoff(c, ca, new_nbuckets); + if (ret) { + prt_printf(err, "Error dropping superblocks after cutoff: %s\n", bch2_err_str(ret)); + return ret; + } + + /* mark superblock buckets as metadata - mirroring the grow path. TODO: not sure if this is necessary here */ + ret = bch2_trans_mark_dev_sb(c, ca, BTREE_TRIGGER_transactional); + if (ret) { + prt_printf(err, "bch2_trans_mark_dev_sb() error: %s\n", bch2_err_str(ret)); + return ret; + } + /* write & commit new_nbuckets */ scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { guard(mutex)(&c->sb_lock); @@ -1489,13 +1506,6 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru bch2_write_super(c); } - /* drop references to now-truncated superblock copies */ - ret = drop_sbs_after_cutoff(c, ca, new_nbuckets); - if (ret) { - prt_printf(err, "Error dropping superblocks after cutoff: %s\n", bch2_err_str(ret)); - return ret; - } - /* resize buckets */ ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); if (ret) { From ae100e9a0c26f07328eac410ad309d8a4675a738 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Sat, 21 Feb 2026 18:38:09 +0100 Subject: [PATCH 27/75] bcachefs: shrink: use the same usage_remove path as dev_remove --- fs/bcachefs/alloc/accounting.c | 4 ++-- fs/bcachefs/alloc/accounting.h | 2 +- fs/bcachefs/alloc/background.c | 42 +++++++++++----------------------- fs/bcachefs/alloc/background.h | 3 +-- fs/bcachefs/init/dev.c | 17 ++------------ 5 files changed, 19 insertions(+), 49 deletions(-) diff --git a/fs/bcachefs/alloc/accounting.c b/fs/bcachefs/alloc/accounting.c index 4781a6b36e041..ebfd8439f06ab 100644 --- a/fs/bcachefs/alloc/accounting.c +++ b/fs/bcachefs/alloc/accounting.c @@ -1174,7 +1174,7 @@ int bch2_accounting_read(struct bch_fs *c) return accounting_read_mem_fixups(trans); } -int bch2_dev_usage_remove(struct bch_fs *c, struct bch_dev *ca) +int bch2_dev_usage_remove(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) { CLASS(btree_trans, trans)(c); @@ -1193,7 +1193,7 @@ int bch2_dev_usage_remove(struct bch_fs *c, struct bch_dev *ca) disk_accounting_pos_to_bpos(&start), disk_accounting_pos_to_bpos(&end), BTREE_ITER_all_snapshots, k, ret) { - if (k.k->type != KEY_TYPE_accounting) + if (k.k->type != KEY_TYPE_accounting || k.k->p.offset < cutoff) continue; struct disk_accounting_pos acc; diff --git a/fs/bcachefs/alloc/accounting.h b/fs/bcachefs/alloc/accounting.h index d08b9920f5fcf..ff6ebd2fb03db 100644 --- a/fs/bcachefs/alloc/accounting.h +++ b/fs/bcachefs/alloc/accounting.h @@ -316,7 +316,7 @@ int bch2_gc_accounting_done(struct bch_fs *); int bch2_accounting_read(struct bch_fs *); -int bch2_dev_usage_remove(struct bch_fs *, struct bch_dev *); +int bch2_dev_usage_remove(struct bch_fs *, struct bch_dev *, u64); int bch2_dev_usage_init(struct bch_dev *, bool); void bch2_verify_accounting_clean(struct bch_fs *c); diff --git a/fs/bcachefs/alloc/background.c b/fs/bcachefs/alloc/background.c index ae964a82f159a..6e71e4155d289 100644 --- a/fs/bcachefs/alloc/background.c +++ b/fs/bcachefs/alloc/background.c @@ -1416,6 +1416,8 @@ int bch2_trigger_alloc(struct btree_trans *trans, /* device removal */ +<<<<<<< conflict 1 of 2 ++++++++ tkspqttk 8507ca39 "bcachefs: shrink: mark superblock buckets as metadata" (rebase destination) static int bch2_dev_remove_need_discard(struct bch_fs *c, struct bch_dev *ca) { CLASS(btree_trans, trans)(c); @@ -1433,7 +1435,7 @@ static int bch2_dev_remove_need_discard(struct bch_fs *c, struct bch_dev *ca) })); } -int bch2_dev_truncate_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) +int bch2_dev_remove_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) { struct bpos start = POS(ca->dev_idx, cutoff); struct bpos end = POS(ca->dev_idx, U64_MAX); @@ -1443,32 +1445,13 @@ int bch2_dev_truncate_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) * We clear the LRU and need_discard btrees first so that we don't race * with bch2_do_invalidates() and bch2_do_discards() */ - ret = bch2_dev_truncate_lrus(c, ca, cutoff) ?: - bch2_btree_delete_range(c, BTREE_ID_need_discard, start, end, - BTREE_TRIGGER_norun) ?: - bch2_btree_delete_range(c, BTREE_ID_freespace, start, end, - BTREE_TRIGGER_norun) ?: - bch2_btree_delete_range(c, BTREE_ID_backpointers, start, end, - BTREE_TRIGGER_norun) ?: - bch2_btree_delete_range(c, BTREE_ID_bucket_gens, start, end, - BTREE_TRIGGER_norun) ?: - bch2_btree_delete_range(c, BTREE_ID_alloc, start, end, - BTREE_TRIGGER_norun); - bch_err_msg_dev(ca, ret, "truncating dev alloc info"); - return ret; -} -int bch2_dev_remove_alloc(struct bch_fs *c, struct bch_dev *ca) -{ - struct bpos start = POS(ca->dev_idx, 0); - struct bpos end = POS(ca->dev_idx, U64_MAX); - int ret; - - /* - * We clear the LRU and need_discard btrees first so that we don't race - * with bch2_do_invalidates() and bch2_do_discards_async() - */ - ret = bch2_dev_remove_lrus(c, ca) ?: - bch2_dev_remove_need_discard(c, ca) ?: + ret = (cutoff + ? bch2_dev_truncate_lrus(c, ca, cutoff) + : bch2_dev_remove_lrus(c, ca)) ?: + (cutoff + ? bch2_btree_delete_range(c, BTREE_ID_need_discard, start, end, + BTREE_TRIGGER_norun) + : bch2_dev_remove_need_discard(c, ca)) ?: bch2_btree_delete_range(c, BTREE_ID_freespace, start, end, BTREE_TRIGGER_norun) ?: bch2_btree_delete_range(c, BTREE_ID_backpointers, start, end, @@ -1477,8 +1460,9 @@ int bch2_dev_remove_alloc(struct bch_fs *c, struct bch_dev *ca) BTREE_TRIGGER_norun) ?: bch2_btree_delete_range(c, BTREE_ID_alloc, start, end, BTREE_TRIGGER_norun) ?: - bch2_dev_usage_remove(c, ca); - bch_err_msg_dev(ca, ret, "removing dev alloc info"); + bch2_dev_usage_remove(c, ca, cutoff); + bch_err_msg_dev(ca, ret, "%s dev alloc info", + cutoff ? "truncating" : "removing"); return ret; } diff --git a/fs/bcachefs/alloc/background.h b/fs/bcachefs/alloc/background.h index 0f00207fc9da9..927fee7471cf3 100644 --- a/fs/bcachefs/alloc/background.h +++ b/fs/bcachefs/alloc/background.h @@ -330,8 +330,7 @@ int bch2_trigger_alloc(struct btree_trans *, enum btree_id, unsigned, struct bkey_s_c, struct bkey_s, enum btree_iter_update_trigger_flags); -int bch2_dev_truncate_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff); -int bch2_dev_remove_alloc(struct bch_fs *, struct bch_dev *); +int bch2_dev_remove_alloc(struct bch_fs *, struct bch_dev *, u64); void bch2_recalc_capacity(struct bch_fs *); unsigned long bch2_fs_ra_pages(struct bch_fs *); diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 83e4ab6c1722e..6434c8279183a 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -904,7 +904,7 @@ int bch2_dev_remove(struct bch_fs *c, struct bch_dev *ca, int flags, */ __bch2_dev_offline(c, ca); - ret = bch2_dev_remove_alloc(c, ca); + ret = bch2_dev_remove_alloc(c, ca, 0); if (ret) { prt_printf(err, "bch2_dev_remove_alloc() error: %s\n", bch2_err_str(ret)); goto err; @@ -1514,25 +1514,12 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru } /* truncate alloc info */ - ret = bch2_dev_truncate_alloc(c, ca, new_nbuckets); + ret = bch2_dev_remove_alloc(c, ca, new_nbuckets); if (ret) { prt_printf(err, "error truncating alloc info: %s\n", bch2_err_str(ret)); return ret; } - /* account disk space */ - // - BCH_DATA_free is somehow still off by the amount of superblocks - // - BCH_DATA_sb isn't updated at all yet - should we relocate them or just accept that we have less copies and adjust? - s64 v[3] = { -(s64) (old_nbuckets - new_nbuckets), 0, 0 }; - ret = bch2_trans_commit_do(ca->fs, NULL, NULL, 0, - bch2_disk_accounting_mod2(trans, false, v, dev_data_type, - .dev = ca->dev_idx, - .data_type = BCH_DATA_free)); - if (ret) { - prt_printf(err, "error accounting disk space: %s\n", bch2_err_str(ret)); - return ret; - } - // TODO: figure out what parts of this path still need doing // if (ca->mi.freespace_initialized) { // ret = __bch2_dev_resize_alloc(ca, old_nbuckets, new_nbuckets); From 02b86dc2f98a9ffec4de9efcdf67b903a925212d Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Sat, 21 Feb 2026 20:12:39 +0100 Subject: [PATCH 28/75] bcachefs: shrink: convert new nbuckets to little-endian before writing to superblock --- fs/bcachefs/init/dev.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 6434c8279183a..074fd583ef15f 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1501,9 +1501,9 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { guard(mutex)(&c->sb_lock); struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); - m->nbuckets = new_nbuckets; bch2_write_super(c); + m->nbuckets = cpu_to_le64(new_nbuckets); } /* resize buckets */ From 900de518302c63e61fc7beab95ac2595bba4bf82 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Sat, 21 Feb 2026 20:12:39 +0100 Subject: [PATCH 29/75] bcachefs: shrink: use alloc info to update accounting --- fs/bcachefs/alloc/accounting.c | 59 ++++++++++++++++++++++++++++++++-- fs/bcachefs/alloc/accounting.h | 5 ++- fs/bcachefs/alloc/background.c | 6 ++-- fs/bcachefs/alloc/lru.c | 20 ++---------- fs/bcachefs/alloc/lru.h | 3 +- fs/bcachefs/init/dev.c | 18 ++++++++--- 6 files changed, 79 insertions(+), 32 deletions(-) diff --git a/fs/bcachefs/alloc/accounting.c b/fs/bcachefs/alloc/accounting.c index ebfd8439f06ab..f074cebc0f489 100644 --- a/fs/bcachefs/alloc/accounting.c +++ b/fs/bcachefs/alloc/accounting.c @@ -1,14 +1,21 @@ // SPDX-License-Identifier: GPL-2.0 +#include "alloc/background.h" +#include "alloc/format.h" #include "bcachefs.h" +#include "bcachefs_format.h" #include "bcachefs_ioctl.h" +#include "alloc/accounting_format.h" #include "alloc/accounting.h" #include "alloc/buckets.h" #include "alloc/replicas.h" +#include "btree/bkey_types.h" #include "btree/cache.h" +#include "btree/iter.h" #include "btree/journal_overlay.h" +#include "btree/types.h" #include "btree/update.h" #include "btree/write_buffer.h" @@ -1174,7 +1181,55 @@ int bch2_accounting_read(struct bch_fs *c) return accounting_read_mem_fixups(trans); } -int bch2_dev_usage_remove(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) +int bch2_dev_truncate_accounting(struct bch_fs *c, struct bch_dev *ca, u64 old_nbuckets, u64 cutoff) { + struct bpos start = POS(ca->dev_idx, cutoff); + struct bpos end = POS(ca->dev_idx, old_nbuckets); + + CLASS(btree_trans, trans)(c); + + return commit_do(trans, NULL, NULL, 0, ({ + s64 delta[BCH_DATA_NR][3] = {}; + s64 keys = 0; + struct bkey_s_c k; + int ret = 0; + + for_each_btree_key_max_norestart(trans, iter, BTREE_ID_alloc, start, end, BTREE_ITER_prefetch, k, ret) { + if (!k.k->type) + continue; + + struct bch_alloc_v4 _alloc; + const struct bch_alloc_v4 *alloc = bch2_alloc_to_v4(k, &_alloc); + + enum bch_data_type data_type = alloc->data_type; + + delta[data_type][0] -= 1; + delta[data_type][1] -= bch2_bucket_sectors(*alloc); + delta[data_type][2] -= bch2_bucket_sectors_fragmented(ca, *alloc); + + s64 unstriped = bch2_bucket_sectors_unstriped(*alloc); + if (unstriped) { + delta[BCH_DATA_unstriped][0] -= 1; + delta[BCH_DATA_unstriped][1] -= unstriped; + } + + keys++; + } + if (!ret) { + delta[BCH_DATA_free][0] -= (old_nbuckets - cutoff) - keys; + + for (unsigned type = 0; type < BCH_DATA_NR; type++) { + ret = bch2_disk_accounting_mod2(trans, false, delta[type], dev_data_type, .dev = ca->dev_idx, .data_type = type); + + if (ret) + break; + } + } + + ret; + })); +} + +int bch2_dev_usage_remove(struct bch_fs *c, struct bch_dev *ca) { CLASS(btree_trans, trans)(c); @@ -1193,7 +1248,7 @@ int bch2_dev_usage_remove(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) disk_accounting_pos_to_bpos(&start), disk_accounting_pos_to_bpos(&end), BTREE_ITER_all_snapshots, k, ret) { - if (k.k->type != KEY_TYPE_accounting || k.k->p.offset < cutoff) + if (k.k->type != KEY_TYPE_accounting) continue; struct disk_accounting_pos acc; diff --git a/fs/bcachefs/alloc/accounting.h b/fs/bcachefs/alloc/accounting.h index ff6ebd2fb03db..41aa7dbd803b0 100644 --- a/fs/bcachefs/alloc/accounting.h +++ b/fs/bcachefs/alloc/accounting.h @@ -316,7 +316,10 @@ int bch2_gc_accounting_done(struct bch_fs *); int bch2_accounting_read(struct bch_fs *); -int bch2_dev_usage_remove(struct bch_fs *, struct bch_dev *, u64); + +int bch2_dev_truncate_accounting(struct bch_fs *, struct bch_dev *, u64, u64); + +int bch2_dev_usage_remove(struct bch_fs *, struct bch_dev *); int bch2_dev_usage_init(struct bch_dev *, bool); void bch2_verify_accounting_clean(struct bch_fs *c); diff --git a/fs/bcachefs/alloc/background.c b/fs/bcachefs/alloc/background.c index 6e71e4155d289..a22954a8e4667 100644 --- a/fs/bcachefs/alloc/background.c +++ b/fs/bcachefs/alloc/background.c @@ -1445,9 +1445,7 @@ int bch2_dev_remove_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) * We clear the LRU and need_discard btrees first so that we don't race * with bch2_do_invalidates() and bch2_do_discards() */ - ret = (cutoff - ? bch2_dev_truncate_lrus(c, ca, cutoff) - : bch2_dev_remove_lrus(c, ca)) ?: + ret = bch2_dev_remove_lrus(c, ca, cutoff) ?: (cutoff ? bch2_btree_delete_range(c, BTREE_ID_need_discard, start, end, BTREE_TRIGGER_norun) @@ -1460,7 +1458,7 @@ int bch2_dev_remove_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) BTREE_TRIGGER_norun) ?: bch2_btree_delete_range(c, BTREE_ID_alloc, start, end, BTREE_TRIGGER_norun) ?: - bch2_dev_usage_remove(c, ca, cutoff); + (cutoff == 0 ? bch2_dev_usage_remove(c, ca) : 0); bch_err_msg_dev(ca, ret, "%s dev alloc info", cutoff ? "truncating" : "removing"); return ret; diff --git a/fs/bcachefs/alloc/lru.c b/fs/bcachefs/alloc/lru.c index 47880f6eda71f..a62925b0f8bcb 100644 --- a/fs/bcachefs/alloc/lru.c +++ b/fs/bcachefs/alloc/lru.c @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-2.0 +#include "alloc/backpointers.h" #include "bcachefs.h" #include "alloc/background.h" @@ -119,7 +120,7 @@ static struct bbpos lru_pos_to_bp(struct bkey_s_c lru_k) } -int bch2_dev_truncate_lrus(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) +int bch2_dev_remove_lrus(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) { CLASS(btree_trans, trans)(c); int ret = bch2_btree_write_buffer_flush_sync(trans) ?: @@ -136,23 +137,6 @@ int bch2_dev_truncate_lrus(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) return ret; } -int bch2_dev_remove_lrus(struct bch_fs *c, struct bch_dev *ca) -{ - CLASS(btree_trans, trans)(c); - int ret = bch2_btree_write_buffer_flush_sync(trans) ?: - for_each_btree_key(trans, iter, - BTREE_ID_lru, POS_MIN, BTREE_ITER_prefetch, k, ({ - struct bbpos bp = lru_pos_to_bp(k); - - bp.btree == BTREE_ID_alloc && bp.pos.inode == ca->dev_idx - ? (bch2_btree_delete_at(trans, &iter, 0) ?: - bch2_trans_commit(trans, NULL, NULL, 0)) - : 0; - })); - bch_err_fn(c, ret); - return ret; -} - static u64 bkey_lru_type_idx(struct bch_fs *c, enum bch_lru_type type, struct bkey_s_c k) diff --git a/fs/bcachefs/alloc/lru.h b/fs/bcachefs/alloc/lru.h index 9075b4787eed1..fa318e0af5a89 100644 --- a/fs/bcachefs/alloc/lru.h +++ b/fs/bcachefs/alloc/lru.h @@ -70,8 +70,7 @@ static inline int bch2_lru_change(struct btree_trans *trans, : 0; } -int bch2_dev_truncate_lrus(struct bch_fs *c, struct bch_dev *ca, u64 cutoff); -int bch2_dev_remove_lrus(struct bch_fs *, struct bch_dev *); +int bch2_dev_remove_lrus(struct bch_fs *, struct bch_dev *, u64); struct wb_maybe_flush; int bch2_lru_check_set(struct btree_trans *, u16, u64, u64, struct bkey_s_c, diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 074fd583ef15f..74572e937c84a 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1501,15 +1501,16 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { guard(mutex)(&c->sb_lock); struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); - - bch2_write_super(c); m->nbuckets = cpu_to_le64(new_nbuckets); + + try(bch2_write_super(c)); } - /* resize buckets */ - ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); + + /* update accounting info - has to happen before truncating alloc info */ + ret = bch2_dev_truncate_accounting(c, ca, old_nbuckets, new_nbuckets); if (ret) { - prt_printf(err, "bch2_dev_buckets_resize() error: %s\n", bch2_err_str(ret)); + prt_printf(err, "error updating accounting info: %s\n", bch2_err_str(ret)); return ret; } @@ -1520,6 +1521,13 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru return ret; } + /* resize buckets */ + ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); + if (ret) { + prt_printf(err, "bch2_dev_buckets_resize() error: %s\n", bch2_err_str(ret)); + return ret; + } + // TODO: figure out what parts of this path still need doing // if (ca->mi.freespace_initialized) { // ret = __bch2_dev_resize_alloc(ca, old_nbuckets, new_nbuckets); From a9b2dfdfa434825f3424e281e3c64d581f73adbb Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Wed, 25 Feb 2026 19:34:48 +0100 Subject: [PATCH 30/75] bcachefs: shrink: add comment about data being left as cached --- fs/bcachefs/init/dev.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 74572e937c84a..3697ba701faf9 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1418,7 +1418,7 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); m->target_nbuckets = cpu_to_le64(new_nbuckets); - bch2_write_super(c); + try(bch2_write_super(c)); } /* close open buckets in the to-be-shrunk region */ @@ -1437,6 +1437,14 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru } }; + /* TODO: + * When we evacuate data, the original is left in place and marked as cached, just like when moving it for target/compression regions. + * We need to somehow handle this: + * - Maybe we can just not count it in tail_is_empty() and just shrink it away - but this would likely leave some metadata incorrect + * - We could just make it completely remove the data if it is evacuating, instead of marking it as cached + * - We could make passes that check for cached buckets in the tail, and close/remove those - we may actually need to handle buckets anyways + */ + /* wait for to-be-shrunk region to be empty */ while (true) { bool empty = false; From ff0d94a8454b05876d5616ca427faee3db3bce48 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Wed, 25 Feb 2026 23:25:28 +0100 Subject: [PATCH 31/75] bcachefs: shrink: drop cached data after shrink cutoff --- fs/bcachefs/data/extents.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/bcachefs/data/extents.c b/fs/bcachefs/data/extents.c index 1cae699d7e970..234f9b2873da5 100644 --- a/fs/bcachefs/data/extents.c +++ b/fs/bcachefs/data/extents.c @@ -1349,6 +1349,8 @@ static bool maybe_drop_cached_ptr(struct bch_fs *c, struct bch_inode_opts *opts, return drop_cached_pointer_trace(c, k, ptr, "pointer is stale"); if (!ca || ca->mi.state == BCH_MEMBER_STATE_evacuating) return drop_cached_pointer_trace(c, k, ptr, "device bad or evacuating"); + if (ca->mi.target_nbuckets && ca->mi.target_nbuckets <= sector_to_bucket(ca, ptr->offset)) + return drop_cached_pointer_trace(c, k, ptr, "past shrink cutoff"); unsigned target = opts->promote_target ?: opts->foreground_target; if (target && !bch2_dev_in_target_rcu(c, ptr->dev, target)) From 6574eaf44e11a1de968baa347cd4874036f2833c Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Tue, 3 Mar 2026 20:40:02 +0100 Subject: [PATCH 32/75] bcachefs: shrink: remove unnecessary sb metadata marking --- fs/bcachefs/init/dev.c | 7 ------- 1 file changed, 7 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 3697ba701faf9..12c2f0685cfce 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1498,13 +1498,6 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru return ret; } - /* mark superblock buckets as metadata - mirroring the grow path. TODO: not sure if this is necessary here */ - ret = bch2_trans_mark_dev_sb(c, ca, BTREE_TRIGGER_transactional); - if (ret) { - prt_printf(err, "bch2_trans_mark_dev_sb() error: %s\n", bch2_err_str(ret)); - return ret; - } - /* write & commit new_nbuckets */ scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { guard(mutex)(&c->sb_lock); From 6db36ba7b7c8f24f7b1487b227082a4197e95213 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Tue, 3 Mar 2026 20:45:39 +0100 Subject: [PATCH 33/75] bcachefs: shrink: flush journal --- fs/bcachefs/init/dev.c | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 12c2f0685cfce..6eb2195d072ec 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1367,7 +1367,7 @@ static int drop_sbs_after_cutoff(struct bch_fs *c, struct bch_dev *ca, u64 cutof return bch2_write_super(c); } -// TODO: make sure everything is caught here. +// TODO: make sure everything is caught here. Maybe look at bch2_dev_has_data for this // Fore example journals and superblocks might need special handling static int tail_is_empty(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err, bool *empty) { struct bpos bp_start = bucket_pos_to_bp_start(ca, POS(ca->dev_idx, new_nbuckets)); @@ -1483,6 +1483,24 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru bch2_write_super(c); } + /* flush interior updates - mirroring dev remove path */ + bch2_btree_interior_updates_flush(c); + + /* flush journal - mirroring dev remove path */ + bch2_journal_flush_all_pins(&c->journal); + + ret = bch2_journal_flush_device_pins(&c->journal, ca->dev_idx); + if (ret) { + prt_printf(err, "bch2_journal_flush_device_pins() error: %s\n", bch2_err_str(ret)); + return ret; + } + + ret = bch2_journal_flush(&c->journal); + if (ret) { + prt_printf(err, "bch2_journal_flush() error: %s\n", bch2_err_str(ret)); + return ret; + } + /* re-check that tail is really empty */ bool empty = false; try(tail_is_empty(c, ca, new_nbuckets, err, &empty)); From 34abba42f5eb7cd86268e9e8406031067407537d Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 18 Mar 2026 22:45:53 +0100 Subject: [PATCH 34/75] bcachefs: shrink: properly remove bucket gens --- fs/bcachefs/alloc/background.c | 52 ++++++++++++++++++++++++++++++++-- fs/bcachefs/alloc/background.h | 1 + 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/fs/bcachefs/alloc/background.c b/fs/bcachefs/alloc/background.c index a22954a8e4667..377e9cd8b0f48 100644 --- a/fs/bcachefs/alloc/background.c +++ b/fs/bcachefs/alloc/background.c @@ -1089,6 +1089,55 @@ int bch2_alloc_read(struct bch_fs *c) return ret; } +int bch2_dev_remove_bucket_gens(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) +{ + unsigned offset; + struct bpos pos = alloc_gens_pos(POS(ca->dev_idx, cutoff), &offset); + int ret; + + if (!offset) + return bch2_btree_delete_range(c, BTREE_ID_bucket_gens, + pos, + POS(ca->dev_idx, U64_MAX), + BTREE_TRIGGER_norun); + + { + CLASS(btree_trans, trans)(c); + bool need_commit = false; + + ret = lockrestart_do(trans, ({ + CLASS(btree_iter, iter)(trans, BTREE_ID_bucket_gens, pos, BTREE_ITER_intent); + struct bkey_s_c k = bkey_try(bch2_btree_iter_peek_slot(&iter)); + + try(bkey_err(k)); + + if (k.k->type == KEY_TYPE_bucket_gens) { + struct bkey_i_bucket_gens *g = + errptr_try(bch2_trans_kmalloc(trans, sizeof(*g))); + + bkey_reassemble(&g->k_i, k); + memset(&g->v.gens[offset], 0, sizeof(g->v.gens) - offset); + try(bch2_trans_update(trans, &iter, &g->k_i, 0)); + need_commit = true; + } + 0; + })); + if (ret) + return ret; + + if (need_commit) { + ret = bch2_trans_commit(trans, NULL, NULL, BCH_TRANS_COMMIT_no_enospc); + if (ret) + return ret; + } + } + + return bch2_btree_delete_range(c, BTREE_ID_bucket_gens, + bpos_nosnap_successor(pos), + POS(ca->dev_idx, U64_MAX), + BTREE_TRIGGER_norun); +} + /* Free space/discard btree: */ int bch2_bucket_do_freespace_index(struct btree_trans *trans, @@ -1454,8 +1503,7 @@ int bch2_dev_remove_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) BTREE_TRIGGER_norun) ?: bch2_btree_delete_range(c, BTREE_ID_backpointers, start, end, BTREE_TRIGGER_norun) ?: - bch2_btree_delete_range(c, BTREE_ID_bucket_gens, start, end, - BTREE_TRIGGER_norun) ?: + bch2_dev_remove_bucket_gens(c, ca, cutoff) ?: bch2_btree_delete_range(c, BTREE_ID_alloc, start, end, BTREE_TRIGGER_norun) ?: (cutoff == 0 ? bch2_dev_usage_remove(c, ca) : 0); diff --git a/fs/bcachefs/alloc/background.h b/fs/bcachefs/alloc/background.h index 927fee7471cf3..b57b501ac5f0e 100644 --- a/fs/bcachefs/alloc/background.h +++ b/fs/bcachefs/alloc/background.h @@ -317,6 +317,7 @@ void bch2_bucket_gens_to_text(struct printbuf *, struct bch_fs *, struct bkey_s_ }) int bch2_bucket_gens_init(struct bch_fs *); +int bch2_dev_remove_bucket_gens(struct bch_fs *, struct bch_dev *, u64); int bch2_alloc_read(struct bch_fs *); From a05fcb75cfd8eaeb1f5b8860f7e14c8d259ca8ce Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 12 Apr 2026 22:20:29 +0200 Subject: [PATCH 35/75] bcachefs: fix online shrink tail metadata cleanup Keep the shrink cutoff in force until tail alloc metadata is removed, move journal buckets out of the truncated tail before the final commit, and clear tail need_discard state so fsck/accounting do not retain bookkeeping for removed buckets. Also fix journal bucket deletion so the updated per-device journal superblock state is what gets written back. --- fs/bcachefs/alloc/background.c | 5 +- fs/bcachefs/alloc/discard.c | 57 +++++++++++++ fs/bcachefs/alloc/discard.h | 1 + fs/bcachefs/init/dev.c | 144 ++++++++++++++++++++++++++++----- fs/bcachefs/journal/init.c | 64 +++++++++------ 5 files changed, 224 insertions(+), 47 deletions(-) diff --git a/fs/bcachefs/alloc/background.c b/fs/bcachefs/alloc/background.c index 377e9cd8b0f48..ad246070506b7 100644 --- a/fs/bcachefs/alloc/background.c +++ b/fs/bcachefs/alloc/background.c @@ -1465,8 +1465,6 @@ int bch2_trigger_alloc(struct btree_trans *trans, /* device removal */ -<<<<<<< conflict 1 of 2 -+++++++ tkspqttk 8507ca39 "bcachefs: shrink: mark superblock buckets as metadata" (rebase destination) static int bch2_dev_remove_need_discard(struct bch_fs *c, struct bch_dev *ca) { CLASS(btree_trans, trans)(c); @@ -1496,8 +1494,7 @@ int bch2_dev_remove_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) */ ret = bch2_dev_remove_lrus(c, ca, cutoff) ?: (cutoff - ? bch2_btree_delete_range(c, BTREE_ID_need_discard, start, end, - BTREE_TRIGGER_norun) + ? bch2_dev_clear_need_discard(c, ca, cutoff) : bch2_dev_remove_need_discard(c, ca)) ?: bch2_btree_delete_range(c, BTREE_ID_freespace, start, end, BTREE_TRIGGER_norun) ?: diff --git a/fs/bcachefs/alloc/discard.c b/fs/bcachefs/alloc/discard.c index 3940782711dd4..7bb731a223f37 100644 --- a/fs/bcachefs/alloc/discard.c +++ b/fs/bcachefs/alloc/discard.c @@ -148,6 +148,63 @@ static int bch2_discard_one_bucket(struct btree_trans *trans, return ret; } +int bch2_dev_clear_need_discard(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) +{ + struct bpos pos = POS_MIN; + + while (1) { + struct bpos next = POS_MAX; + bool done = false; + int ret = bch2_trans_commit_do(c, NULL, NULL, + BCH_WATERMARK_reclaim| + BCH_TRANS_COMMIT_no_check_rw| + BCH_TRANS_COMMIT_no_enospc, ({ + int __ret = 0; + CLASS(btree_iter, iter)(trans, BTREE_ID_need_discard, pos, 0); + struct bkey_s_c k = bch2_btree_iter_peek(&iter); + int _ret = bkey_err(k); + + if (_ret) { + __ret = _ret; + } else if (!k.k) { + done = true; + } else { + struct bpos bucket = u64_to_bucket(k.k->p.offset); + + next = bpos_successor(k.k->p); + + if (bucket.inode == ca->dev_idx && + bucket.offset >= cutoff) { + CLASS(btree_iter, alloc_iter)(trans, BTREE_ID_alloc, + bucket, BTREE_ITER_cached); + struct bkey_s_c alloc_k = + bch2_btree_iter_peek_slot(&alloc_iter); + struct bkey_i_alloc_v4 *a; + + _ret = bkey_err(alloc_k); + if (_ret) { + __ret = _ret; + } else { + a = errptr_try(bch2_alloc_to_v4_mut(trans, alloc_k)); + if (a->v.data_type == BCH_DATA_need_discard) { + SET_BCH_ALLOC_V4_NEED_DISCARD(&a->v, false); + alloc_data_type_set(&a->v, a->v.data_type); + __ret = bch2_trans_update(trans, &alloc_iter, + &a->k_i, 0); + } + } + } + } + __ret; + })); + if (ret) + return ret; + if (done) + return 0; + pos = next; + } +} + static void calculate_discard_sectors_to_release(struct btree_trans *trans) { struct bch_fs *c = trans->c; diff --git a/fs/bcachefs/alloc/discard.h b/fs/bcachefs/alloc/discard.h index b87ea5589641a..9e971b2ea9a21 100644 --- a/fs/bcachefs/alloc/discard.h +++ b/fs/bcachefs/alloc/discard.h @@ -9,6 +9,7 @@ void bch2_fast_discard_bucket_add(struct bch_dev *, u64); void bch2_fast_discards_to_text(struct printbuf *, struct bch_dev *); void bch2_discards_to_text(struct printbuf *, struct bch_fs *, struct discard_state *); +int bch2_dev_clear_need_discard(struct bch_fs *, struct bch_dev *, u64); void bch2_dev_do_discards(struct bch_dev *); void bch2_do_discards_going_ro(struct bch_fs *); diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 6eb2195d072ec..6b639771a4aca 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1367,6 +1367,87 @@ static int drop_sbs_after_cutoff(struct bch_fs *c, struct bch_dev *ca, u64 cutof return bch2_write_super(c); } +static int move_journal_past_cutoff(struct bch_fs *c, struct bch_dev *ca, + u64 cutoff, struct printbuf *err) +{ + bool grew = false; + + while (true) { + u64 bucket_to_delete = 0; + unsigned nr = 0, nr_past_cutoff = 0; + bool cur_bucket_past_cutoff = false; + int ret; + + scoped_guard(spinlock, &c->journal.lock) { + struct journal_device *ja = &ca->journal; + + nr = ja->nr; + if (!nr) + break; + + cur_bucket_past_cutoff = ja->buckets[ja->cur_idx] >= cutoff; + + for (unsigned i = 0; i < ja->nr; i++) { + if (ja->buckets[i] < cutoff) + continue; + + nr_past_cutoff++; + if (i != ja->cur_idx && !bucket_to_delete) + bucket_to_delete = ja->buckets[i]; + } + } + + if (!nr_past_cutoff) + return 0; + + if (!grew) { + ret = bch2_set_nr_journal_buckets(c, ca, nr + nr_past_cutoff); + if (ret) { + prt_printf(err, "Failed to relocate journal buckets: %s\n", + bch2_err_str(ret)); + return ret; + } + grew = true; + continue; + } + + if (!bucket_to_delete && cur_bucket_past_cutoff) { + scoped_guard(spinlock, &c->journal.lock) { + struct journal_device *ja = &ca->journal; + + if (ja->nr && + ja->buckets[ja->cur_idx] >= cutoff) + ja->sectors_free = 0; + } + + scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { + guard(mutex)(&c->sb_lock); + ret = bch2_write_super(c); + } + if (ret) { + prt_printf(err, "Failed to advance journal off shrink tail: %s\n", + bch2_err_str(ret)); + return ret; + } + + ret = bch2_journal_flush(&c->journal); + if (ret) { + prt_printf(err, "Failed to flush relocated journal: %s\n", + bch2_err_str(ret)); + return ret; + } + continue; + } + + ret = bch2_dev_journal_bucket_delete(ca, bucket_to_delete); + if (ret) { + prt_printf(err, "Failed to drop journal bucket %llu from shrink tail: %s\n", + bucket_to_delete, bch2_err_str(ret)); + return ret; + } + } +} + // TODO: make sure everything is caught here. Maybe look at bch2_dev_has_data for this // Fore example journals and superblocks might need special handling static int tail_is_empty(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err, bool *empty) { @@ -1437,6 +1518,15 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru } }; + /* + * Move journal buckets out of the tail up front: otherwise the journal + * can keep reintroducing metadata references in the region we're trying + * to evacuate while reconcile is draining backpointers from it. + */ + ret = move_journal_past_cutoff(c, ca, new_nbuckets, err); + if (ret) + return ret; + /* TODO: * When we evacuate data, the original is left in place and marked as cached, just like when moving it for target/compression regions. * We need to somehow handle this: @@ -1472,17 +1562,17 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru schedule_timeout_killable(HZ/2); } + /* + * Re-run journal relocation as a final fence before we commit the new + * size: the current journal bucket may have advanced while we were + * waiting for the tail to drain, and we must not expose the smaller + * nbuckets while journal state can still reference the truncated tail. + */ + ret = move_journal_past_cutoff(c, ca, new_nbuckets, err); + if (ret) + return ret; scoped_guard(rwsem_write, &c->state_lock) { - /* zero target_nbuckets */ - scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { - guard(mutex)(&c->sb_lock); - struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); - m->target_nbuckets = 0; - - bch2_write_super(c); - } - /* flush interior updates - mirroring dev remove path */ bch2_btree_interior_updates_flush(c); @@ -1509,6 +1599,18 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru return -EBUSY; } + /* + * Buckets in the truncated tail may still be in NEED_DISCARD + * state. Clear that bookkeeping before we drop the alloc range so + * accounting/fsck do not retain tail-only metadata after shrink. + */ + ret = bch2_dev_clear_need_discard(c, ca, new_nbuckets); + if (ret) { + prt_printf(err, "error clearing need_discard state: %s\n", + bch2_err_str(ret)); + return ret; + } + /* drop references to now-truncated superblock copies */ ret = drop_sbs_after_cutoff(c, ca, new_nbuckets); if (ret) { @@ -1516,16 +1618,6 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru return ret; } - /* write & commit new_nbuckets */ - scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { - guard(mutex)(&c->sb_lock); - struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); - m->nbuckets = cpu_to_le64(new_nbuckets); - - try(bch2_write_super(c)); - } - - /* update accounting info - has to happen before truncating alloc info */ ret = bch2_dev_truncate_accounting(c, ca, old_nbuckets, new_nbuckets); if (ret) { @@ -1540,6 +1632,20 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru return ret; } + /* + * Commit the shrink only after the truncated tail has been + * removed from alloc metadata, so later transactions can't see + * stale tail buckets after the new size is visible. + */ + scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { + guard(mutex)(&c->sb_lock); + struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); + m->target_nbuckets = 0; + m->nbuckets = cpu_to_le64(new_nbuckets); + + try(bch2_write_super(c)); + } + /* resize buckets */ ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); if (ret) { diff --git a/fs/bcachefs/journal/init.c b/fs/bcachefs/journal/init.c index d0c5fa7df518c..19df8f8ad2dea 100644 --- a/fs/bcachefs/journal/init.c +++ b/fs/bcachefs/journal/init.c @@ -209,37 +209,53 @@ int bch2_dev_journal_bucket_delete(struct bch_dev *ca, u64 b) &new_buckets[pos + 1], (ja->nr - 1 - pos) * sizeof(new_buckets[0])); - int ret = bch2_journal_buckets_to_sb(c, ca, ja->buckets, ja->nr - 1) ?: - bch2_write_super(c); + int ret; + + scoped_guard(journal_block, &c->journal) { + ret = bch2_journal_buckets_to_sb(c, ca, new_buckets, ja->nr - 1) ?: + bch2_write_super(c); + if (ret) + break; + + scoped_guard(spinlock, &j->lock) { + if (pos < ja->discard_idx) + --ja->discard_idx; + if (pos < ja->dirty_idx_ondisk) + --ja->dirty_idx_ondisk; + if (pos < ja->dirty_idx) + --ja->dirty_idx; + if (pos < ja->cur_idx) + --ja->cur_idx; + + ja->nr--; + + memmove(&ja->buckets[pos], + &ja->buckets[pos + 1], + (ja->nr - pos) * sizeof(ja->buckets[0])); + + memmove(&ja->bucket_seq[pos], + &ja->bucket_seq[pos + 1], + (ja->nr - pos) * sizeof(ja->bucket_seq[0])); + + bch2_journal_space_available(j); + } + } + if (ret) { kfree(new_buckets); return ret; } - scoped_guard(spinlock, &j->lock) { - if (pos < ja->discard_idx) - --ja->discard_idx; - if (pos < ja->dirty_idx_ondisk) - --ja->dirty_idx_ondisk; - if (pos < ja->dirty_idx) - --ja->dirty_idx; - if (pos < ja->cur_idx) - --ja->cur_idx; - - ja->nr--; - - memmove(&ja->buckets[pos], - &ja->buckets[pos + 1], - (ja->nr - pos) * sizeof(ja->buckets[0])); - - memmove(&ja->bucket_seq[pos], - &ja->bucket_seq[pos + 1], - (ja->nr - pos) * sizeof(ja->bucket_seq[0])); - - bch2_journal_space_available(j); + kfree(new_buckets); + + { + CLASS(btree_trans, trans)(c); + ret = commit_do(trans, NULL, NULL, 0, + bch2_trans_mark_metadata_bucket(trans, ca, b, + BCH_DATA_free, 0, + BTREE_TRIGGER_transactional)); } - kfree(new_buckets); return 0; } From cf0297b4173b3b18dc601ba2eb84ba196505835d Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 13 Apr 2026 18:49:27 +0200 Subject: [PATCH 36/75] fix three-device shrink reconcile races Serialize duplicate data-update ownership in the update table so phys reconcile workers cannot race the same logical extent. Also tolerate device-scan pending data_replicas work when other reconcile bits are still set, stop background movers before globally disabling writes during RO shutdown, and avoid counting transient reconcile ENOSPC/ec_alloc_failed retries as hard data-update failures. This fixes the targeted three-device shrink tests, including the EC case. --- fs/bcachefs/data/reconcile/trigger.c | 9 ++++++++- fs/bcachefs/data/update.c | 28 +++++++++++++++++++++++++--- fs/bcachefs/init/fs.c | 14 +++++++++++--- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/fs/bcachefs/data/reconcile/trigger.c b/fs/bcachefs/data/reconcile/trigger.c index 01b748d6a9d09..c384f702b54e8 100644 --- a/fs/bcachefs/data/reconcile/trigger.c +++ b/fs/bcachefs/data/reconcile/trigger.c @@ -729,7 +729,14 @@ static int new_needs_rb_allowed(struct btree_trans *trans, if (ret) return min(ret, 0); - if (new_need_rb == BIT(BCH_RECONCILE_data_replicas)) { + /* + * Device scans can add evacuation/rereplicate work on top of existing + * reconcile state, e.g. an extent that was already waiting on + * erasure-code conversion. While that scan cookie is pending, tolerate + * missing data_replicas/hipri state even if other reconcile bits are + * also set; the scan will rewrite the full reconcile entry. + */ + if (new_need_rb & BIT(BCH_RECONCILE_data_replicas)) { ret = check_dev_reconcile_scan_cookie(trans, k, s ? &s->dev_cookie : NULL); if (ret) return min(ret, 0); diff --git a/fs/bcachefs/data/update.c b/fs/bcachefs/data/update.c index 15e540081d57f..cb8eadece5798 100644 --- a/fs/bcachefs/data/update.c +++ b/fs/bcachefs/data/update.c @@ -712,7 +712,13 @@ static inline bool should_trace_update_err(struct data_update *u, int ret) ((u->opts.type == BCH_DATA_UPDATE_reconcile || u->opts.type == BCH_DATA_UPDATE_promote) && (bch2_err_matches(ret, BCH_ERR_data_update_fail_no_rw_devs) || - bch2_err_matches(ret, BCH_ERR_insufficient_devices)))) + bch2_err_matches(ret, BCH_ERR_insufficient_devices) || + /* + * Reconcile promotes ENOSPC-class write failures to pending work + * and retries after space/stripe availability changes, so don't + * count those transient allocation misses as data update failures. + */ + bch2_err_matches(ret, ENOSPC)))) return false; return true; @@ -1400,8 +1406,24 @@ int bch2_data_update_init(struct btree_trans *trans, } } - if (!rhltable_insert_key(&c->update_table, &m->pos, &m->hash, bch_update_params)) - m->on_hashtable = true; + /* + * Phys reconcile can queue the same logical extent from multiple + * source devices. The lookup above is only a fast path; another + * mover can still win the race and claim @m->pos before we insert. + * Use the non-list insert helper so only one update owns the key. + */ + ret = rhashtable_lookup_insert_fast(&c->update_table.ht, + &m->hash.rhead, + bch_update_params); + if (ret == -EEXIST) { + event_inc(c, data_update_in_flight); + ret = bch_err_throw(c, data_update_fail_in_flight); + goto out; + } + if (ret) + goto out; + + m->on_hashtable = true; } else { if (unwritten) { ret = bch_err_throw(c, data_update_done_unwritten); diff --git a/fs/bcachefs/init/fs.c b/fs/bcachefs/init/fs.c index 3dbe2352a6125..128fa119cd945 100644 --- a/fs/bcachefs/init/fs.c +++ b/fs/bcachefs/init/fs.c @@ -320,10 +320,7 @@ static void __bch2_fs_read_only(struct bch_fs *c) u64 seq = 0; bch2_maybe_schedule_btree_bitmap_gc_stop(c); - bch2_fs_ec_stop(c); bch2_open_buckets_stop(c, NULL, true, 0); - bch2_reconcile_stop(c); - bch2_copygc_stop(c); bch2_btree_write_buffer_stop(c); bch2_fs_ec_flush(c); cancel_delayed_work_sync(&c->maybe_schedule_btree_bitmap_gc); @@ -387,6 +384,17 @@ void bch2_fs_read_only(struct bch_fs *c) bch_verbose(c, "going read-only"); + /* + * Stop background data movers before disabling writes globally: + * reconcile/copygc move writes don't hold c->writes refs, but they do + * still need journal/btree write access to finish their final index + * updates. If we shut off writes first they'll trip -EROFS during + * shutdown and spuriously account data_update failures. + */ + bch2_fs_ec_stop(c); + bch2_reconcile_stop(c); + bch2_copygc_stop(c); + /* * Block new foreground-end write operations from starting - any new * writes will return -EROFS: From 6a20513f354d9e17190e920c8351e44306585b97 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 13 Apr 2026 19:23:56 +0200 Subject: [PATCH 37/75] bcachefs: tolerate transient shrink rereplicate state During device shrink, an extent can become temporarily under-replicated as soon as one pointer lands on a bad or evacuating location, before the device scan has had a chance to rewrite the reconcile entry. Treating that window as a missing io-opts propagation cookie raises a false extent_io_opts_not_set fsck error. Skip that check when the new data_replicas work is explained by existing bad or evacuating pointers so shrink can hand the extent off to reconcile without tripping fsck. --- fs/bcachefs/data/reconcile/trigger.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fs/bcachefs/data/reconcile/trigger.c b/fs/bcachefs/data/reconcile/trigger.c index c384f702b54e8..4a64759a94787 100644 --- a/fs/bcachefs/data/reconcile/trigger.c +++ b/fs/bcachefs/data/reconcile/trigger.c @@ -707,6 +707,16 @@ static int new_needs_rb_allowed(struct btree_trans *trans, return 0; } + /* + * Shrink/evacuation can mark an extent under-replicated as soon as one + * pointer falls onto a bad/evacuating location, before the device scan + * has rewritten the reconcile entry. That's a transient physical-state + * change, not a missing io-opts propagation cookie. + */ + if ((new_need_rb & BIT(BCH_RECONCILE_data_replicas)) && + bch2_bkey_has_ptr_bad_or_evacuating(c, k)) + return 0; + /* * Either the extent data or the extent io options (from * bch_extent_reconcile) should match the io_opts from the From 34c31c9633661a2b7901a1712d8516633f4fdbb6 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 13 Apr 2026 20:00:18 +0200 Subject: [PATCH 38/75] bcachefs: update reconcile-only backpointer flags in place Reconcile-only updates can change the phys-work class encoded in rotational-data backpointers without moving any extent pointers. The fast path in bch2_trigger_extent() treated identical pointer arrays as a no-op, which left stale or missing phys backpointers behind during three-device EC shrink. Teach that path to recognize when only the backpointer reconcile flags are changing, update the existing backpointer entries in place, and adjust the reconcile_work_phys bits without routing the unchanged key through a delete+insert cycle in the write buffer. --- fs/bcachefs/alloc/buckets.c | 95 ++++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/fs/bcachefs/alloc/buckets.c b/fs/bcachefs/alloc/buckets.c index c4be7a361d613..36e4c46707283 100644 --- a/fs/bcachefs/alloc/buckets.c +++ b/fs/bcachefs/alloc/buckets.c @@ -878,6 +878,86 @@ static int __trigger_extent(struct btree_trans *trans, return 0; } +static int bch2_trigger_extent_same_backpointers(struct btree_trans *trans, + enum btree_id btree, + unsigned level, + struct bkey_s_c old, + struct bkey_s new, + bool *handled) +{ + struct bch_fs *c = trans->c; + struct bkey_i_backpointer old_bp[BCH_REPLICAS_MAX * 2]; + struct bkey_i_backpointer new_bp[BCH_REPLICAS_MAX * 2]; + struct bkey_ptrs_c ptrs; + const union bch_extent_entry *entry; + struct extent_ptr_decoded p; + unsigned nr_old = 0, nr_new = 0; + unsigned i; + + *handled = false; + if (level) + return 0; + + /* + * Reconcile-only updates do not move the extent, they only toggle the + * phys-work flag that is cached in rotational-data backpointers. Do + * not delete+reinsert those backpointers through the write buffer when + * the key itself is unchanged: update the existing entry in place and + * adjust the reconcile_work_phys bit separately. + */ + ptrs = bch2_bkey_ptrs_c(old); + bkey_for_each_ptr_decode(old.k, ptrs, p, entry) { + BUG_ON(nr_old == ARRAY_SIZE(old_bp)); + bch2_extent_ptr_to_bp(c, btree, level, old, p, entry, &old_bp[nr_old++]); + } + + ptrs = bch2_bkey_ptrs_c(new.s_c); + bkey_for_each_ptr_decode(new.k, ptrs, p, entry) { + BUG_ON(nr_new == ARRAY_SIZE(new_bp)); + bch2_extent_ptr_to_bp(c, btree, level, new.s_c, p, entry, &new_bp[nr_new++]); + } + + if (nr_old != nr_new) + return 0; + + for (i = 0; i < nr_old; i++) { + struct bch_backpointer old_v = old_bp[i].v; + struct bch_backpointer new_v = new_bp[i].v; + + SET_BACKPOINTER_RECONCILE_PHYS(&old_v, 0); + SET_BACKPOINTER_RECONCILE_PHYS(&new_v, 0); + + if (!bpos_eq(old_bp[i].k.p, new_bp[i].k.p) || + memcmp(&old_v, &new_v, sizeof(old_v))) + return 0; + } + + for (i = 0; i < nr_old; i++) { + unsigned old_phys = BACKPOINTER_RECONCILE_PHYS(&old_bp[i].v); + unsigned new_phys = BACKPOINTER_RECONCILE_PHYS(&new_bp[i].v); + + if (!old_phys && !new_phys) + continue; + + if (old_phys) + try(bch2_btree_bit_mod_buffered(trans, + reconcile_work_phys_btree[old_phys], + old_bp[i].k.p, false)); + + if (new_phys) + try(bch2_btree_bit_mod_buffered(trans, + reconcile_work_phys_btree[new_phys], + new_bp[i].k.p, true)); + + try(bch2_trans_update_buffered(trans, + backpointer_btree(&new_bp[i].v), + &new_bp[i].k_i)); + } + + *handled = true; + return 0; +} + int bch2_trigger_extent(struct btree_trans *trans, enum btree_id btree, unsigned level, struct bkey_s_c old, struct bkey_s new, @@ -891,7 +971,20 @@ int bch2_trigger_extent(struct btree_trans *trans, if (unlikely(flags & BTREE_TRIGGER_check_repair)) return bch2_check_fix_ptrs(trans, btree, level, new.s_c, flags); - /* if pointers aren't changing - nothing to do: */ + /* + * Reconcile-only updates keep the same physical pointers. Update + * their backpointer flags in place so buffered delete+insert churn + * cannot drop the key, then let reconcile accounting/work tracking + * see the logical-state change. + */ + if (level == 0) { + bool handled; + + try(bch2_trigger_extent_same_backpointers(trans, btree, level, old, new, &handled)); + if (handled) + return bch2_trigger_extent_reconcile(trans, btree, level, old, new, flags); + } + if (new_ptrs_bytes == old_ptrs_bytes && !memcmp(new_ptrs.start, old_ptrs.start, From 99be08aa0a440374299d9ba835a81ed7f2f451a8 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 13 Apr 2026 20:19:24 +0200 Subject: [PATCH 39/75] bcachefs: delete truncated backpointers in backpointer key space Shrink cleanup deletes alloc/freespace state in bucket coordinates, but backpointer keys are indexed by device+sector. Using the raw bucket cutoff for BTREE_ID_backpointers can delete live backpointers that still belong to buckets below the shrink point, which leaves offline fsck repairing missing phys backpointers in the three-device EC shrink tests. Translate the cutoff into backpointer key space before deleting the tail of the truncated device. --- fs/bcachefs/alloc/background.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/fs/bcachefs/alloc/background.c b/fs/bcachefs/alloc/background.c index ad246070506b7..6feb826a81767 100644 --- a/fs/bcachefs/alloc/background.c +++ b/fs/bcachefs/alloc/background.c @@ -1486,6 +1486,8 @@ int bch2_dev_remove_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) { struct bpos start = POS(ca->dev_idx, cutoff); struct bpos end = POS(ca->dev_idx, U64_MAX); + struct bpos bp_start = bucket_pos_to_bp_start(ca, start); + struct bpos bp_end = POS(ca->dev_idx + 1, 0); int ret; /* @@ -1498,7 +1500,13 @@ int bch2_dev_remove_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) : bch2_dev_remove_need_discard(c, ca)) ?: bch2_btree_delete_range(c, BTREE_ID_freespace, start, end, BTREE_TRIGGER_norun) ?: - bch2_btree_delete_range(c, BTREE_ID_backpointers, start, end, + /* + * Backpointer keys are indexed by device + sector, not device + + * bucket. A shrink cutoff in bucket space must be translated + * before deleting the truncated tail, otherwise we wipe + * backpointers that still belong to buckets below @cutoff. + */ + bch2_btree_delete_range(c, BTREE_ID_backpointers, bp_start, bp_end, BTREE_TRIGGER_norun) ?: bch2_dev_remove_bucket_gens(c, ca, cutoff) ?: bch2_btree_delete_range(c, BTREE_ID_alloc, start, end, From aef3e9330abc0644a92e3a9a1d5391505787215b Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 13 Apr 2026 20:35:49 +0200 Subject: [PATCH 40/75] bcachefs: close write points before stopping copygc Repeated online shrink runs can leave copygc blocked in a move write while waiting for allocator space from its dedicated write point. Stopping copygc before those open buckets are dropped can then hang the read-only transition inside kthread_stop(). Close write points before stopping the background data movers so copygc does not stay wedged behind allocator state owned by the filesystem itself. --- fs/bcachefs/init/fs.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fs/bcachefs/init/fs.c b/fs/bcachefs/init/fs.c index 128fa119cd945..f39734a62c8de 100644 --- a/fs/bcachefs/init/fs.c +++ b/fs/bcachefs/init/fs.c @@ -384,6 +384,15 @@ void bch2_fs_read_only(struct bch_fs *c) bch_verbose(c, "going read-only"); + /* + * Close write points before stopping background data movers. Shrink can + * leave copygc waiting on allocator space from its dedicated + * write_point; dropping those open buckets first breaks that dependency + * so kthread_stop() does not hang behind a move write that's blocked in + * the allocator. + */ + bch2_open_buckets_stop(c, NULL, true, 0); + /* * Stop background data movers before disabling writes globally: * reconcile/copygc move writes don't hold c->writes refs, but they do From e6a591fc05a5d13f3c7780d6e3f5324665b281ff Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 13 Apr 2026 23:47:52 +0200 Subject: [PATCH 41/75] bcachefs: start reconcile extent iterators with intent locks Reconcile always updates the extent it looks up, either to rewrite reconcile state or to queue a move. Starting those iterators in read mode forces a restart_upgrade during commit for each key, and the three-device EC shrink test can trip the slowpath counter threshold as those restarts add up. Open the logical and physical extent iterators with intent locks from the start so reconcile does not burn one upgrade restart per extent. --- fs/bcachefs/data/reconcile/work.c | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/fs/bcachefs/data/reconcile/work.c b/fs/bcachefs/data/reconcile/work.c index 42aa3bcd091a6..22e97af794981 100644 --- a/fs/bcachefs/data/reconcile/work.c +++ b/fs/bcachefs/data/reconcile/work.c @@ -708,13 +708,17 @@ static int do_reconcile_extent(struct moving_context *ctxt, struct bch_fs *c = trans->c; struct bbpos data_pos = rb_work_to_data_pos(work.pos); - /* We require holding an intent lock when calling - * bch2_stripe_handle_tryget(), to avoid racing with the stripe trigger - * deleting the stripe */ - enum btree_iter_update_trigger_flags flags = data_pos.btree == BTREE_ID_stripes - ? BTREE_ITER_intent : 0; + /* + * Reconcile always mutates the key it is looking at, either by updating + * reconcile state or by queueing a move. Start with an intent iterator + * so we do not pay one transaction restart_upgrade per extent just to + * upgrade the lock during commit. Stripe keys also require intent before + * bch2_stripe_handle_tryget() can safely race with stripe deletion. + */ + enum btree_iter_update_trigger_flags flags = + BTREE_ITER_all_snapshots|BTREE_ITER_intent; - CLASS(btree_iter, iter)(trans, data_pos.btree, data_pos.pos, BTREE_ITER_all_snapshots|flags); + CLASS(btree_iter, iter)(trans, data_pos.btree, data_pos.pos, flags); struct bkey_s_c k = bkey_try(bch2_btree_iter_peek_slot(&iter)); if (!k.k) return 0; @@ -758,11 +762,13 @@ static int do_reconcile_extent_phys(struct moving_context *ctxt, if (bch2_data_update_in_flight(c, &pos)) return 0; - /* We require holding an intent lock when calling - * bch2_stripe_handle_tryget(), to avoid racing with the stripe trigger - * deleting the stripe */ - enum btree_iter_update_trigger_flags flags = bp.v->btree_id == BTREE_ID_stripes - ? BTREE_ITER_intent : 0; + /* + * Phys reconcile reaches the owning extent through a backpointer and + * then updates that extent in the same transaction. Take the extent + * iterator in intent mode up front to avoid restart_upgrade churn when + * many adjacent extents are being reprocessed after shrink. + */ + enum btree_iter_update_trigger_flags flags = BTREE_ITER_intent; CLASS(btree_iter_uninit, iter)(trans); struct bkey_s_c k = bkey_try(bch2_backpointer_get_key(trans, bp, &iter, flags, last_flushed)); From d7a9f1c22e2924cf8994ebd1fa31f3444dbe1292 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 14 Apr 2026 00:31:16 +0200 Subject: [PATCH 42/75] bcachefs: let shrink metadata spill off shrinking targets Variable-bucket shrink can fail before reconcile moves any data when the filesystem metadata target only contains the device being shrunk. The initial reconcile_scan update, later btree node rewrites, and journal allocations all kept preferring that shrinking target, so once the remaining below-cutoff buckets ran short the shrink aborted with no_buckets_found. Teach internal btree and journal metadata allocations to fall back to the full filesystem when every rw member of the preferred metadata target is currently shrinking. Also treat allocator no_buckets_found as the same transient ENOSPC-class condition reconcile already demotes to pending work, so later shrink-triggered retries do not escalate it into a hard reconcile failure. --- fs/bcachefs/btree/interior.c | 31 ++++++++++++++++++++++++++++--- fs/bcachefs/data/reconcile/work.c | 15 +++++++++++---- fs/bcachefs/data/update.c | 7 +++++++ fs/bcachefs/journal/write.c | 30 ++++++++++++++++++++++++++++-- 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/fs/bcachefs/btree/interior.c b/fs/bcachefs/btree/interior.c index 3b2f15116d5d6..cdd64a941f651 100644 --- a/fs/bcachefs/btree/interior.c +++ b/fs/bcachefs/btree/interior.c @@ -48,6 +48,32 @@ static const char * const bch2_btree_update_modes[] = { NULL }; +static unsigned btree_alloc_target(struct bch_fs *c, unsigned target) +{ + target = target ?: c->opts.metadata_target ?: c->opts.foreground_target; + if (!target) + return 0; + + struct bch_devs_mask devs = target_rw_devs(c, BCH_DATA_btree, target); + + /* + * Shrink needs to keep rewriting btree nodes even when metadata is + * normally pinned to the device being reduced. If every rw member of + * the preferred target is currently shrinking, spill metadata anywhere + * so the shrink can update its own bookkeeping. + */ + guard(rcu)(); + unsigned i; + for_each_set_bit(i, devs.d, BCH_SB_MEMBERS_MAX) { + struct bch_dev *ca = bch2_dev_rcu_noerror(c, i); + + if (ca && !ca->mi.target_nbuckets) + return target; + } + + return 0; +} + static void bch2_btree_update_to_text(struct printbuf *, struct btree_update *); static int bch2_btree_insert_node(struct btree_update *, struct btree_trans *, @@ -1312,10 +1338,9 @@ bch2_btree_update_start(struct btree_trans *trans, struct btree_path *path, goto err; struct bch_devs_list devs_have = (struct bch_devs_list) { 0 }; + unsigned alloc_target = btree_alloc_target(c, target); struct alloc_request *req = alloc_request_get(trans, - target ?: - c->opts.metadata_target ?: - c->opts.foreground_target, + alloc_target, false, &devs_have, as->disk_res.nr_replicas, diff --git a/fs/bcachefs/data/reconcile/work.c b/fs/bcachefs/data/reconcile/work.c index 22e97af794981..de14bdb2c3ee1 100644 --- a/fs/bcachefs/data/reconcile/work.c +++ b/fs/bcachefs/data/reconcile/work.c @@ -512,10 +512,17 @@ static int check_reconcile_pending_err(struct btree_trans *trans, { struct bch_fs *c = trans->c; - if (!bch2_err_matches(err, BCH_ERR_data_update_fail_no_rw_devs) && - !bch2_err_matches(err, BCH_ERR_insufficient_devices) && - !bch2_err_matches(err, ENOSPC)) - return err; + /* + * Reconcile can park transient allocation failures on the pending list + * and retry after space becomes available. The allocator's internal + * no_buckets_found code is the same class of "try again later" failure + * as ENOSPC, but it bypasses the errno-based match. + */ + if (!bch2_err_matches(err, BCH_ERR_data_update_fail_no_rw_devs) && + !bch2_err_matches(err, BCH_ERR_insufficient_devices) && + !bch2_err_matches(err, BCH_ERR_freelist_empty) && + !bch2_err_matches(err, ENOSPC)) + return err; event_add_trace(c, reconcile_set_pending, k.k->size, buf, ({ prt_printf(&buf, "%s\n", bch2_err_str(err)); diff --git a/fs/bcachefs/data/update.c b/fs/bcachefs/data/update.c index cb8eadece5798..d176efad236ce 100644 --- a/fs/bcachefs/data/update.c +++ b/fs/bcachefs/data/update.c @@ -713,6 +713,13 @@ static inline bool should_trace_update_err(struct data_update *u, int ret) u->opts.type == BCH_DATA_UPDATE_promote) && (bch2_err_matches(ret, BCH_ERR_data_update_fail_no_rw_devs) || bch2_err_matches(ret, BCH_ERR_insufficient_devices) || + /* + * The allocator reports a fully-exhausted retry sequence as + * BCH_ERR_freelist_empty/no_buckets_found. Reconcile demotes + * that to pending work just like ENOSPC, so don't count it as a + * hard data-update failure. + */ + bch2_err_matches(ret, BCH_ERR_freelist_empty) || /* * Reconcile promotes ENOSPC-class write failures to pending work * and retries after space/stripe availability changes, so don't diff --git a/fs/bcachefs/journal/write.c b/fs/bcachefs/journal/write.c index a79f7a701d926..dcf0f88785488 100644 --- a/fs/bcachefs/journal/write.c +++ b/fs/bcachefs/journal/write.c @@ -22,9 +22,36 @@ #include "sb/clean.h" #include "sb/counters.h" +#include "sb/members.h" #include +static unsigned journal_alloc_target(struct bch_fs *c) +{ + unsigned target = c->opts.metadata_target ?: c->opts.foreground_target; + if (!target) + return 0; + + struct bch_devs_mask devs = target_rw_devs(c, BCH_DATA_journal, target); + + /* + * Shrink can temporarily make the preferred metadata target unusable + * for new journal buckets. If every rw member of that target is + * shrinking, allocate from the full filesystem until the shrink + * finishes so journal progress doesn't deadlock on target preference. + */ + guard(rcu)(); + unsigned i; + for_each_set_bit(i, devs.d, BCH_SB_MEMBERS_MAX) { + struct bch_dev *ca = bch2_dev_rcu_noerror(c, i); + + if (ca && !ca->mi.target_nbuckets) + return target; + } + + return 0; +} + static void journal_advance_devs_to_next_bucket(struct journal *j, struct dev_alloc_list *devs, unsigned sectors, __le64 seq) @@ -111,8 +138,7 @@ static int journal_write_alloc(struct journal *j, struct journal_buf *w, struct bch_devs_mask devs; struct dev_alloc_list devs_sorted; unsigned sectors = vstruct_sectors(w->data, c->block_bits); - unsigned target = c->opts.metadata_target ?: - c->opts.foreground_target; + unsigned target = journal_alloc_target(c); unsigned replicas_want = READ_ONCE(c->opts.metadata_replicas); bool advance_done = false; From b35672a97fb8eb0882235274d1d790152f903bae Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 14 Apr 2026 18:23:23 +0200 Subject: [PATCH 43/75] bcachefs: resume pending shrink during recovery `target_nbuckets` already persists an interrupted shrink target, but mount left that state stranded after a restart. Refactor the shrink path so recovery can reuse it, and resume pending shrinks from the resize-on- mount hook once btrees and normal journal reservations are available. The early resize-on-mount call still handles grow-on-mount image expansion before btrees are running. A second call after early recovery setup now requeues reconcile's device scan and finishes any pending shrink synchronously during mount. --- fs/bcachefs/init/dev.c | 50 ++++++++++++++----- fs/bcachefs/init/dev.h | 1 + fs/bcachefs/init/fs.c | 96 ++++++++++++++++++++++++++----------- fs/bcachefs/init/recovery.c | 5 +- 4 files changed, 108 insertions(+), 44 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 6b639771a4aca..584b920340100 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1469,8 +1469,10 @@ static int tail_is_empty(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, } -// TODO: resume shrink on startup -int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err) { +static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, + u64 new_nbuckets, struct printbuf *err, + bool resuming) +{ u64 old_nbuckets = ca->mi.nbuckets; int ret = 0; @@ -1489,24 +1491,32 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru } if (ca->mi.target_nbuckets) { - prt_printf(err, "Device already resizing (current target: %llu, new target: %llu\n", ca->mi.target_nbuckets, new_nbuckets); - return bch_err_throw(c, device_already_resizing); - } - - /* write & commit target_nbuckets - also stops new allocations */ - scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { - guard(mutex)(&c->sb_lock); - struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); - m->target_nbuckets = cpu_to_le64(new_nbuckets); + if (!resuming || ca->mi.target_nbuckets != new_nbuckets) { + prt_printf(err, "Device already resizing (current target: %llu, new target: %llu)\n", + ca->mi.target_nbuckets, new_nbuckets); + return bch_err_throw(c, device_already_resizing); + } + } else { + /* write & commit target_nbuckets - also stops new allocations */ + scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { + guard(mutex)(&c->sb_lock); + struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); + m->target_nbuckets = cpu_to_le64(new_nbuckets); - try(bch2_write_super(c)); + try(bch2_write_super(c)); + } } /* close open buckets in the to-be-shrunk region */ bch2_open_buckets_stop(c, ca, false, new_nbuckets); bch2_reset_alloc_cursors(c); // avoid churn - /* trigger reconcile range scan -> should kick off evacuation from range */ + /* + * target_nbuckets persists in the superblock, but the device + * scan's in-memory traversal state does not. Re-queue the + * device scan whenever shrink starts or resumes so reconcile + * reliably rediscovers the tail after a restart. + */ struct reconcile_scan s = { .type = RECONCILE_SCAN_device, // TODO(performance): make this range-based .dev = ca->dev_idx, @@ -1667,6 +1677,20 @@ int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, stru return 0; } +int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, + u64 new_nbuckets, struct printbuf *err) +{ + return __bch2_dev_shrink(c, ca, new_nbuckets, err, false); +} + +int bch2_dev_shrink_resume(struct bch_fs *c, struct bch_dev *ca, + struct printbuf *err) +{ + return ca->mi.target_nbuckets + ? __bch2_dev_shrink(c, ca, ca->mi.target_nbuckets, err, true) + : 0; +} + /* Resize on mount */ int __bch2_dev_resize_alloc(struct bch_dev *ca, u64 old_nbuckets, u64 new_nbuckets) diff --git a/fs/bcachefs/init/dev.h b/fs/bcachefs/init/dev.h index c5717f1fe3ef2..bc6b15840f7fc 100644 --- a/fs/bcachefs/init/dev.h +++ b/fs/bcachefs/init/dev.h @@ -34,6 +34,7 @@ int bch2_dev_offline(struct bch_fs *, struct bch_dev *, int, struct printbuf *); int bch2_dev_resize(struct bch_fs *, struct bch_dev *, u64, struct printbuf *); int bch2_dev_grow(struct bch_fs *, struct bch_dev *, u64, struct printbuf *); int bch2_dev_shrink(struct bch_fs *, struct bch_dev *, u64, struct printbuf *); +int bch2_dev_shrink_resume(struct bch_fs *, struct bch_dev *, struct printbuf *); int __bch2_dev_resize_alloc(struct bch_dev *, u64, u64); diff --git a/fs/bcachefs/init/fs.c b/fs/bcachefs/init/fs.c index f39734a62c8de..607befc6fb35c 100644 --- a/fs/bcachefs/init/fs.c +++ b/fs/bcachefs/init/fs.c @@ -1493,42 +1493,80 @@ static bool bch2_fs_will_resize_on_mount(struct bch_fs *c) int bch2_fs_resize_on_mount(struct bch_fs *c) { - for_each_online_member(c, ca, BCH_DEV_READ_REF_fs_resize_on_mount) { - if (bch2_dev_will_resize_on_mount(ca)) { - u64 old_nbuckets = ca->mi.nbuckets; - u64 new_nbuckets = div64_u64(get_capacity(ca->disk_sb.bdev->bd_disk), - ca->mi.bucket_size); - - bch_info_dev(ca, "resizing to size %llu", new_nbuckets * ca->mi.bucket_size); - int ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); - bch_err_fn_dev(ca, ret); - if (ret) { - enumerated_ref_put(&ca->io_ref[READ], - BCH_DEV_READ_REF_fs_resize_on_mount); - return ret; - } - - scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { - guard(mutex)(&c->sb_lock); - struct bch_member *m = - bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); - m->nbuckets = cpu_to_le64(new_nbuckets); - SET_BCH_MEMBER_RESIZE_ON_MOUNT(m, false); - - c->disk_sb.sb->features[0] &= ~cpu_to_le64(BIT_ULL(BCH_FEATURE_small_image)); - bch2_write_super(c); - } - - if (ca->mi.freespace_initialized) { - ret = __bch2_dev_resize_alloc(ca, old_nbuckets, new_nbuckets); + scoped_guard(rwsem_write, &c->state_lock) { + for_each_online_member(c, ca, BCH_DEV_READ_REF_fs_resize_on_mount) { + if (bch2_dev_will_resize_on_mount(ca)) { + u64 old_nbuckets = ca->mi.nbuckets; + u64 new_nbuckets = div64_u64(get_capacity(ca->disk_sb.bdev->bd_disk), + ca->mi.bucket_size); + + bch_info_dev(ca, "resizing to size %llu", new_nbuckets * ca->mi.bucket_size); + int ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); + bch_err_fn_dev(ca, ret); if (ret) { enumerated_ref_put(&ca->io_ref[READ], - BCH_DEV_READ_REF_fs_resize_on_mount); + BCH_DEV_READ_REF_fs_resize_on_mount); return ret; } + + scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { + guard(mutex)(&c->sb_lock); + struct bch_member *m = + bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); + m->nbuckets = cpu_to_le64(new_nbuckets); + SET_BCH_MEMBER_RESIZE_ON_MOUNT(m, false); + + c->disk_sb.sb->features[0] &= ~cpu_to_le64(BIT_ULL(BCH_FEATURE_small_image)); + bch2_write_super(c); + } + + if (ca->mi.freespace_initialized) { + ret = __bch2_dev_resize_alloc(ca, old_nbuckets, new_nbuckets); + if (ret) { + enumerated_ref_put(&ca->io_ref[READ], + BCH_DEV_READ_REF_fs_resize_on_mount); + return ret; + } + } } } } + + /* + * A pending shrink needs reconcile_scan and backpointer btree access. + * The early resize-on-mount hook runs before btree roots are read so it + * can handle grow-on-mount image expansion; defer shrink resume until a + * later call once BCH_FS_btree_running is set. + */ + if (!test_bit(BCH_FS_btree_running, &c->flags)) + return 0; + + if (c->opts.read_only || + (c->sb.features & (BIT_ULL(BCH_FEATURE_small_image) | + BIT_ULL(BCH_FEATURE_no_default_sb)))) + return 0; + + for_each_online_member(c, ca, BCH_DEV_READ_REF_fs_resize_on_mount) { + if (!ca->mi.target_nbuckets) + continue; + + CLASS(printbuf, err)(); + int ret; + + bch_info_dev(ca, "resuming shrink to size %llu", + ca->mi.target_nbuckets * ca->mi.bucket_size); + ret = bch2_dev_shrink_resume(c, ca, &err); + if (ret) { + if (err.pos) + bch_err_dev(ca, "%s", err.buf); + else + bch_err_fn_dev(ca, ret); + + enumerated_ref_put(&ca->io_ref[READ], + BCH_DEV_READ_REF_fs_resize_on_mount); + return ret; + } + } return 0; } diff --git a/fs/bcachefs/init/recovery.c b/fs/bcachefs/init/recovery.c index 2fe89cf307260..ee0d760400721 100644 --- a/fs/bcachefs/init/recovery.c +++ b/fs/bcachefs/init/recovery.c @@ -754,8 +754,7 @@ static int __bch2_fs_recovery(struct bch_fs *c) try(journal_replay_early(c, clean)); - scoped_guard(rwsem_write, &c->state_lock) - try(bch2_fs_resize_on_mount(c)); + try(bch2_fs_resize_on_mount(c)); if (c->sb.features & BIT_ULL(BCH_FEATURE_small_image)) { bch_info(c, "filesystem is an unresized image file, mounting ro"); @@ -885,6 +884,8 @@ static int __bch2_fs_recovery(struct bch_fs *c) set_bit(BCH_FS_may_go_rw, &c->flags); clear_bit(BCH_FS_in_fsck, &c->flags); + try(bch2_fs_resize_on_mount(c)); + /* in case we don't run journal replay, i.e. norecovery mode */ set_bit(BCH_FS_accounting_replay_done, &c->flags); From 557af95f7f9ec319e6085541199cc4e936a5778b Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 14 Apr 2026 22:20:39 +0200 Subject: [PATCH 44/75] bcachefs: make device resize converge to the latest target Interpret `target_nbuckets` as the single requested-size field for both shrink and grow, with helpers that normalize `0` to the current device size and make shrink-only users consult the current resize direction. This lets allocator and metadata fallback paths stop treating every nonzero target as an active shrink. Move resize execution to a per-device background kthread. Requests now persist the latest target, bump a sequence number, and wait synchronously for that sequence to finish unless a newer request supersedes it. The worker re-reads the current target at explicit restart points so stale shrink passes bail out instead of finishing obsolete work, and recovery reuses the same path to resume either shrink or grow. Also stop resize workers during read-only shutdown before clean shutdown is marked. Without that, an interrupted shrink can keep issuing transactional alloc/accounting updates during unmount and trip write-path assertions. --- fs/bcachefs/alloc/foreground.c | 3 +- fs/bcachefs/bcachefs.h | 7 + fs/bcachefs/btree/interior.c | 2 +- fs/bcachefs/data/extents.c | 3 +- fs/bcachefs/init/dev.c | 334 +++++++++++++++++++++++++++----- fs/bcachefs/init/dev.h | 5 +- fs/bcachefs/init/fs.c | 16 +- fs/bcachefs/journal/write.c | 2 +- fs/bcachefs/sb/members.c | 15 +- fs/bcachefs/sb/members.h | 27 ++- fs/bcachefs/sb/members_format.h | 2 +- fs/bcachefs/sb/members_types.h | 2 +- 12 files changed, 353 insertions(+), 65 deletions(-) diff --git a/fs/bcachefs/alloc/foreground.c b/fs/bcachefs/alloc/foreground.c index 1dba2309cb807..1af3394d35166 100644 --- a/fs/bcachefs/alloc/foreground.c +++ b/fs/bcachefs/alloc/foreground.c @@ -233,7 +233,8 @@ static struct open_bucket *__try_alloc_bucket(struct bch_fs *c, if (unlikely(is_superblock_bucket(c, ca, bucket))) return NULL; - if (unlikely(ca->mi.target_nbuckets && bucket >= ca->mi.target_nbuckets)) { + if (unlikely(bch2_dev_is_shrinking(ca) && + bucket >= bch2_dev_resize_target(ca))) { req->counters.skipped_nouse++; return NULL; } diff --git a/fs/bcachefs/bcachefs.h b/fs/bcachefs/bcachefs.h index d7259cd08f8f9..6bf1e79a72e31 100644 --- a/fs/bcachefs/bcachefs.h +++ b/fs/bcachefs/bcachefs.h @@ -33,6 +33,8 @@ #define race_fault(...) dynamic_fault("bcachefs:race") +struct task_struct; + #include #include #include @@ -521,6 +523,11 @@ struct bch_dev { unsigned nr_btree_reserve; struct work_struct invalidate_work; + spinlock_t resize_lock; + u64 resize_seq; + wait_queue_head_t resize_wait; + int resize_status; + struct task_struct *resize_thread; struct work_struct discard_fast_work; darray_u64 discard_fast; diff --git a/fs/bcachefs/btree/interior.c b/fs/bcachefs/btree/interior.c index cdd64a941f651..9b2ac90e271a4 100644 --- a/fs/bcachefs/btree/interior.c +++ b/fs/bcachefs/btree/interior.c @@ -67,7 +67,7 @@ static unsigned btree_alloc_target(struct bch_fs *c, unsigned target) for_each_set_bit(i, devs.d, BCH_SB_MEMBERS_MAX) { struct bch_dev *ca = bch2_dev_rcu_noerror(c, i); - if (ca && !ca->mi.target_nbuckets) + if (ca && !bch2_dev_is_shrinking(ca)) return target; } diff --git a/fs/bcachefs/data/extents.c b/fs/bcachefs/data/extents.c index 234f9b2873da5..bac399367e9f1 100644 --- a/fs/bcachefs/data/extents.c +++ b/fs/bcachefs/data/extents.c @@ -1349,7 +1349,8 @@ static bool maybe_drop_cached_ptr(struct bch_fs *c, struct bch_inode_opts *opts, return drop_cached_pointer_trace(c, k, ptr, "pointer is stale"); if (!ca || ca->mi.state == BCH_MEMBER_STATE_evacuating) return drop_cached_pointer_trace(c, k, ptr, "device bad or evacuating"); - if (ca->mi.target_nbuckets && ca->mi.target_nbuckets <= sector_to_bucket(ca, ptr->offset)) + if (bch2_dev_is_shrinking(ca) && + bch2_dev_resize_target(ca) <= sector_to_bucket(ca, ptr->offset)) return drop_cached_pointer_trace(c, k, ptr, "past shrink cutoff"); unsigned target = opts->promote_target ?: opts->foreground_target; diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 584b920340100..ee727490b5dcf 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -235,6 +235,7 @@ #include "linux/bitmap.h" #include "linux/byteorder/generic.h" +#include "linux/kthread.h" #include "linux/sched.h" #include "linux/sched/signal.h" #include "sb/io.h" @@ -242,6 +243,8 @@ #include "sb/members_format.h" #include "util/util.h" +static void bch2_dev_resize_thread_stop(struct bch_dev *); + #define x(n) #n, const char * const bch2_dev_read_refs[] = { BCH_DEV_READ_REFS() @@ -465,6 +468,7 @@ void bch2_dev_free(struct bch_dev *ca) WARN_ON(!enumerated_ref_is_zero(&ca->io_ref[WRITE])); WARN_ON(!enumerated_ref_is_zero(&ca->io_ref[READ])); + bch2_dev_resize_thread_stop(ca); cancel_work_sync(&ca->io_error_work); bch2_dev_unlink(ca); @@ -552,6 +556,9 @@ static struct bch_dev *__bch2_dev_alloc(struct bch_fs *c, kobject_init(&ca->kobj, &bch2_dev_ktype); init_completion(&ca->ref_completion); + spin_lock_init(&ca->resize_lock); + init_waitqueue_head(&ca->resize_wait); + ca->resize_status = 0; INIT_WORK(&ca->io_error_work, bch2_io_error_work); @@ -1257,23 +1264,157 @@ int bch2_dev_offline(struct bch_fs *c, struct bch_dev *ca, int flags, struct pri return 0; } -int bch2_dev_resize(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err) +static u64 bch2_dev_resize_seq(struct bch_dev *ca) +{ + u64 seq; + + scoped_guard(spinlock, &ca->resize_lock) + seq = ca->resize_seq; + + return seq; +} + +static bool bch2_dev_resize_wait_done(struct bch_dev *ca, u64 seq, int *status) { - u64 old_nbuckets; + bool done; + + scoped_guard(spinlock, &ca->resize_lock) { + done = ca->resize_seq != seq || + ca->resize_status != -EINPROGRESS; + if (done) + *status = ca->resize_seq != seq + ? -ECANCELED + : ca->resize_status; + } - old_nbuckets = ca->mi.nbuckets; + return done; +} - if (new_nbuckets > old_nbuckets) { - return bch2_dev_grow(c, ca, new_nbuckets, err); - } else if (new_nbuckets < old_nbuckets) { - return bch2_dev_shrink(c, ca, new_nbuckets, err); - } else { +static int bch2_dev_resize_wait(struct bch_dev *ca, u64 seq) +{ + int status = -EINPROGRESS; + int ret = wait_event_killable(ca->resize_wait, + bch2_dev_resize_wait_done(ca, seq, &status)); + + return ret ? -EINTR : status; +} + +static bool bch2_dev_resize_finish(struct bch_dev *ca, u64 seq, int status) +{ + bool is_current; + + scoped_guard(spinlock, &ca->resize_lock) { + is_current = ca->resize_seq == seq; + if (is_current) + ca->resize_status = status; + } + + if (is_current) + wake_up_all(&ca->resize_wait); + + return is_current; +} + +static int bch2_dev_resize_restart_check(struct bch_dev *ca, u64 seq) +{ + if (kthread_should_stop()) + return -EINTR; + + return bch2_dev_resize_seq(ca) != seq ? -EAGAIN : 0; +} + +static int bch2_dev_resize_thread(void *arg); + +static int bch2_dev_resize_thread_start(struct bch_dev *ca) +{ + struct bch_fs *c = ca->fs; + + lockdep_assert_held(&c->state_lock); + + if (ca->resize_thread) return 0; + + struct task_struct *p = + kthread_create(bch2_dev_resize_thread, ca, + "bch-resize/%s:%u", c->name, ca->dev_idx); + int ret = PTR_ERR_OR_ZERO(p); + if (ret) + return ret; + + get_task_struct(p); + ca->resize_thread = p; + wake_up_process(p); + return 0; +} + +static void bch2_dev_resize_thread_stop(struct bch_dev *ca) +{ + scoped_guard(spinlock, &ca->resize_lock) { + ca->resize_seq++; + ca->resize_status = -EINTR; + } + + if (ca->resize_thread) { + kthread_stop(ca->resize_thread); + put_task_struct(ca->resize_thread); + ca->resize_thread = NULL; } + + wake_up_all(&ca->resize_wait); +} + +void bch2_dev_resize_threads_stop(struct bch_fs *c) +{ + for_each_member_device(c, ca) + bch2_dev_resize_thread_stop(ca); +} + +static int bch2_dev_resize_update_target(struct bch_fs *c, struct bch_dev *ca, + u64 target_nbuckets, struct printbuf *err) +{ + lockdep_assert_held(&c->state_lock); + + u64 old_nbuckets = ca->mi.nbuckets; + + if (target_nbuckets > BCH_MEMBER_NBUCKETS_MAX) { + prt_printf(err, "New device size too big (%llu greater than max %u)\n", + target_nbuckets, BCH_MEMBER_NBUCKETS_MAX); + return bch_err_throw(c, device_size_too_big); + } + + if (target_nbuckets && + target_nbuckets < old_nbuckets && + target_nbuckets < ca->mi.first_bucket + BCH_MIN_NR_NBUCKETS) { + prt_printf(err, "New device size too small (%llu smaller than min %llu)\n", + target_nbuckets, + (u64) ca->mi.first_bucket + BCH_MIN_NR_NBUCKETS); + return bch_err_throw(c, device_size_too_small); + } + + if (target_nbuckets > old_nbuckets && + bch2_dev_is_online(ca) && + get_capacity(ca->disk_sb.bdev->bd_disk) < + ca->mi.bucket_size * target_nbuckets) { + prt_printf(err, "New size %llu larger than device size %llu\n", + ca->mi.bucket_size * target_nbuckets, + get_capacity(ca->disk_sb.bdev->bd_disk)); + return bch_err_throw(c, device_size_too_small); + } + + scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { + guard(mutex)(&c->sb_lock); + struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); + + m->target_nbuckets = cpu_to_le64(target_nbuckets); + try(bch2_write_super(c)); + } + + return 0; } /* requires write state lock */ -int bch2_dev_grow(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err) +static int __bch2_dev_grow(struct bch_fs *c, struct bch_dev *ca, + u64 new_nbuckets, struct printbuf *err) { guard(rwsem_write)(&c->state_lock); @@ -1322,6 +1463,8 @@ int bch2_dev_grow(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct guard(mutex)(&c->sb_lock); struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); m->nbuckets = cpu_to_le64(new_nbuckets); + if (bch2_dev_resize_target(ca) == new_nbuckets) + m->target_nbuckets = 0; bch2_write_super(c); } @@ -1470,8 +1613,7 @@ static int tail_is_empty(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, - u64 new_nbuckets, struct printbuf *err, - bool resuming) + u64 new_nbuckets, u64 seq, struct printbuf *err) { u64 old_nbuckets = ca->mi.nbuckets; @@ -1480,32 +1622,16 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, scoped_guard(rwsem_write, &c->state_lock) { /* validate shrink size */ if (new_nbuckets >= old_nbuckets) { - return 0; + return -EAGAIN; } - u64 first_bucket = ca->mi.first_bucket; - if (new_nbuckets < first_bucket + BCH_MIN_NR_NBUCKETS) { - prt_printf(err, "New device size too small (%llu smaller than min %llu)\n", - new_nbuckets, first_bucket + BCH_MIN_NR_NBUCKETS); - return bch_err_throw(c, device_size_too_small); - } - - if (ca->mi.target_nbuckets) { - if (!resuming || ca->mi.target_nbuckets != new_nbuckets) { - prt_printf(err, "Device already resizing (current target: %llu, new target: %llu)\n", - ca->mi.target_nbuckets, new_nbuckets); - return bch_err_throw(c, device_already_resizing); - } - } else { - /* write & commit target_nbuckets - also stops new allocations */ - scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { - guard(mutex)(&c->sb_lock); - struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); - m->target_nbuckets = cpu_to_le64(new_nbuckets); + ret = bch2_dev_resize_restart_check(ca, seq); + if (ret) + return ret; - try(bch2_write_super(c)); - } - } + if (bch2_dev_resize_target(ca) != new_nbuckets || + !bch2_dev_is_shrinking(ca)) + return -EAGAIN; /* close open buckets in the to-be-shrunk region */ bch2_open_buckets_stop(c, ca, false, new_nbuckets); @@ -1528,6 +1654,10 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, } }; + ret = bch2_dev_resize_restart_check(ca, seq); + if (ret) + return ret; + /* * Move journal buckets out of the tail up front: otherwise the journal * can keep reintroducing metadata references in the region we're trying @@ -1537,6 +1667,10 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, if (ret) return ret; + ret = bch2_dev_resize_restart_check(ca, seq); + if (ret) + return ret; + /* TODO: * When we evacuate data, the original is left in place and marked as cached, just like when moving it for target/compression regions. * We need to somehow handle this: @@ -1548,6 +1682,11 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, /* wait for to-be-shrunk region to be empty */ while (true) { bool empty = false; + + ret = bch2_dev_resize_restart_check(ca, seq); + if (ret) + return ret; + try(tail_is_empty(c, ca, new_nbuckets, err, &empty)); /* do a definitive check */ @@ -1566,10 +1705,10 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, /* make sure reconcile is actually running */ bch2_reconcile_wakeup(c); - if (signal_pending(current)) + if (kthread_should_stop()) return -EINTR; - schedule_timeout_killable(HZ/2); + schedule_timeout_interruptible(HZ/2); } /* @@ -1582,7 +1721,19 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, if (ret) return ret; + ret = bch2_dev_resize_restart_check(ca, seq); + if (ret) + return ret; + scoped_guard(rwsem_write, &c->state_lock) { + ret = bch2_dev_resize_restart_check(ca, seq); + if (ret) + return ret; + + if (bch2_dev_resize_target(ca) != new_nbuckets || + !bch2_dev_is_shrinking(ca)) + return -EAGAIN; + /* flush interior updates - mirroring dev remove path */ bch2_btree_interior_updates_flush(c); @@ -1650,8 +1801,9 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { guard(mutex)(&c->sb_lock); struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); - m->target_nbuckets = 0; m->nbuckets = cpu_to_le64(new_nbuckets); + if (bch2_dev_resize_target(ca) == new_nbuckets) + m->target_nbuckets = 0; try(bch2_write_super(c)); } @@ -1677,18 +1829,112 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, return 0; } -int bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, - u64 new_nbuckets, struct printbuf *err) +static int bch2_dev_resize_thread(void *arg) +{ + struct bch_dev *ca = arg; + struct bch_fs *c = ca->fs; + u64 seen_seq = 0; + + set_freezable(); + + while (!kthread_should_stop()) { + kthread_wait_freezable(kthread_should_stop() || + bch2_dev_resize_seq(ca) != seen_seq); + if (kthread_should_stop()) + break; + + while (!kthread_should_stop()) { + u64 seq = bch2_dev_resize_seq(ca); + u64 target = bch2_dev_resize_target(ca); + int ret; + CLASS(printbuf, err)(); + + if (target == ca->mi.nbuckets) { + ret = 0; + } else if (target > ca->mi.nbuckets) { + ret = __bch2_dev_grow(c, ca, target, &err); + } else { + ret = __bch2_dev_shrink(c, ca, target, seq, &err); + } + + if (ret == -EAGAIN) + continue; + + if (ret && err.pos) + bch_err_dev(ca, "%s", err.buf); + else if (ret && ret != -EINTR) + bch_err_fn_dev(ca, ret); + + seen_seq = bch2_dev_resize_seq(ca); + if (ret == -EINTR) + break; + if (!bch2_dev_resize_finish(ca, seq, ret)) + continue; + break; + } + } + + return 0; +} + +static int bch2_dev_resize_kick(struct bch_dev *ca) { - return __bch2_dev_shrink(c, ca, new_nbuckets, err, false); + u64 seq; + + scoped_guard(spinlock, &ca->resize_lock) { + seq = ++ca->resize_seq; + ca->resize_status = -EINPROGRESS; + } + + wake_up_process(ca->resize_thread); + return bch2_dev_resize_wait(ca, seq); } -int bch2_dev_shrink_resume(struct bch_fs *c, struct bch_dev *ca, +int bch2_dev_resize(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err) +{ + int ret = 0; + u64 target_nbuckets; + + scoped_guard(rwsem_write, &c->state_lock) { + target_nbuckets = new_nbuckets == ca->mi.nbuckets + ? 0 + : new_nbuckets; + + ret = bch2_dev_resize_thread_start(ca); + if (ret) + return ret; + + ret = bch2_dev_resize_update_target(c, ca, target_nbuckets, err); + if (ret) + return ret; + } + + ret = bch2_dev_resize_kick(ca); + if (ret == -ECANCELED) + prt_printf(err, "Resize request superseded by a newer target\n"); + else if (ret && ret != -EINTR && !err->pos) + prt_printf(err, "Resize worker failed; see kernel log for details\n"); + return ret; +} + +int bch2_dev_resize_resume(struct bch_fs *c, struct bch_dev *ca, struct printbuf *err) { - return ca->mi.target_nbuckets - ? __bch2_dev_shrink(c, ca, ca->mi.target_nbuckets, err, true) - : 0; + int ret = 0; + + if (!bch2_dev_resize_pending(ca)) + return 0; + + scoped_guard(rwsem_write, &c->state_lock) { + ret = bch2_dev_resize_thread_start(ca); + if (ret) + return ret; + } + + ret = bch2_dev_resize_kick(ca); + if (ret && ret != -ECANCELED && ret != -EINTR && !err->pos) + prt_printf(err, "Resize resume failed; see kernel log for details\n"); + return ret; } /* Resize on mount */ diff --git a/fs/bcachefs/init/dev.h b/fs/bcachefs/init/dev.h index bc6b15840f7fc..430f248ff15ce 100644 --- a/fs/bcachefs/init/dev.h +++ b/fs/bcachefs/init/dev.h @@ -32,9 +32,8 @@ int bch2_dev_add(struct bch_fs *, const char *, struct printbuf *); int bch2_dev_online(struct bch_fs *, const char *, struct printbuf *); int bch2_dev_offline(struct bch_fs *, struct bch_dev *, int, struct printbuf *); int bch2_dev_resize(struct bch_fs *, struct bch_dev *, u64, struct printbuf *); -int bch2_dev_grow(struct bch_fs *, struct bch_dev *, u64, struct printbuf *); -int bch2_dev_shrink(struct bch_fs *, struct bch_dev *, u64, struct printbuf *); -int bch2_dev_shrink_resume(struct bch_fs *, struct bch_dev *, struct printbuf *); +int bch2_dev_resize_resume(struct bch_fs *, struct bch_dev *, struct printbuf *); +void bch2_dev_resize_threads_stop(struct bch_fs *); int __bch2_dev_resize_alloc(struct bch_dev *, u64, u64); diff --git a/fs/bcachefs/init/fs.c b/fs/bcachefs/init/fs.c index 607befc6fb35c..5e321592cf0ad 100644 --- a/fs/bcachefs/init/fs.c +++ b/fs/bcachefs/init/fs.c @@ -393,6 +393,14 @@ void bch2_fs_read_only(struct bch_fs *c) */ bch2_open_buckets_stop(c, NULL, true, 0); + /* + * Stop per-device resize workers while writes are still available. + * A pending resize may be in the middle of transactional alloc/accounting + * updates; if it survives into clean shutdown it can trip write-path + * assertions while the filesystem is already going read-only. + */ + bch2_dev_resize_threads_stop(c); + /* * Stop background data movers before disabling writes globally: * reconcile/copygc move writes don't hold c->writes refs, but they do @@ -1547,15 +1555,15 @@ int bch2_fs_resize_on_mount(struct bch_fs *c) return 0; for_each_online_member(c, ca, BCH_DEV_READ_REF_fs_resize_on_mount) { - if (!ca->mi.target_nbuckets) + if (!bch2_dev_resize_pending(ca)) continue; CLASS(printbuf, err)(); int ret; - bch_info_dev(ca, "resuming shrink to size %llu", - ca->mi.target_nbuckets * ca->mi.bucket_size); - ret = bch2_dev_shrink_resume(c, ca, &err); + bch_info_dev(ca, "resuming resize to size %llu", + bch2_dev_resize_target(ca) * ca->mi.bucket_size); + ret = bch2_dev_resize_resume(c, ca, &err); if (ret) { if (err.pos) bch_err_dev(ca, "%s", err.buf); diff --git a/fs/bcachefs/journal/write.c b/fs/bcachefs/journal/write.c index dcf0f88785488..fd1f6a53079f7 100644 --- a/fs/bcachefs/journal/write.c +++ b/fs/bcachefs/journal/write.c @@ -45,7 +45,7 @@ static unsigned journal_alloc_target(struct bch_fs *c) for_each_set_bit(i, devs.d, BCH_SB_MEMBERS_MAX) { struct bch_dev *ca = bch2_dev_rcu_noerror(c, i); - if (ca && !ca->mi.target_nbuckets) + if (ca && !bch2_dev_is_shrinking(ca)) return target; } diff --git a/fs/bcachefs/sb/members.c b/fs/bcachefs/sb/members.c index f8b6de3f4b485..3a36c0100d002 100644 --- a/fs/bcachefs/sb/members.c +++ b/fs/bcachefs/sb/members.c @@ -207,23 +207,24 @@ static int validate_member(struct printbuf *err, u64 target_nbuckets = le64_to_cpu(m.target_nbuckets); if (target_nbuckets) { - if (target_nbuckets >= nbuckets) { - prt_printf(err, "device %u: target buckets >= buckets (got %llu, nbuckets %llu)", - i, target_nbuckets, nbuckets); - return -BCH_ERR_invalid_sb_members; - } - if (target_nbuckets < first_bucket) { prt_printf(err, "device %u: target buckets starts before first bucket (got %llu, first %u)", i, target_nbuckets, first_bucket); return -BCH_ERR_invalid_sb_members; } - if (target_nbuckets - first_bucket < BCH_MIN_NR_NBUCKETS) { + if (target_nbuckets < nbuckets && + target_nbuckets - first_bucket < BCH_MIN_NR_NBUCKETS) { prt_printf(err, "device %u: not enough target buckets (got %llu, min %u)", i, target_nbuckets - first_bucket, BCH_MIN_NR_NBUCKETS); return -BCH_ERR_invalid_sb_members; } + + if (target_nbuckets > BCH_MEMBER_NBUCKETS_MAX) { + prt_printf(err, "device %u: target buckets too big (got %llu, max %u)", + i, target_nbuckets, BCH_MEMBER_NBUCKETS_MAX); + return -BCH_ERR_invalid_sb_members; + } } return 0; diff --git a/fs/bcachefs/sb/members.h b/fs/bcachefs/sb/members.h index 224c6adfa4207..ae6481daf7e48 100644 --- a/fs/bcachefs/sb/members.h +++ b/fs/bcachefs/sb/members.h @@ -258,10 +258,35 @@ static inline bool bch2_dev_bad_or_evacuating(struct bch_fs *c, unsigned dev) return bch2_dev_bad_or_evacuating_rcu(c, dev); } +static inline u64 bch2_dev_resize_target(const struct bch_dev *ca) +{ + u64 target = READ_ONCE(ca->mi.target_nbuckets); + + return target ?: READ_ONCE(ca->mi.nbuckets); +} + +static inline bool bch2_dev_resize_pending(const struct bch_dev *ca) +{ + return bch2_dev_resize_target(ca) != READ_ONCE(ca->mi.nbuckets); +} + +static inline bool bch2_dev_is_shrinking(const struct bch_dev *ca) +{ + return bch2_dev_resize_target(ca) < READ_ONCE(ca->mi.nbuckets); +} + +static inline bool bch2_dev_is_growing(const struct bch_dev *ca) +{ + return bch2_dev_resize_target(ca) > READ_ONCE(ca->mi.nbuckets); +} + static inline bool bch2_ptr_bad_or_evacuating_rcu(struct bch_fs *c, const struct bch_extent_ptr *ptr) { struct bch_dev *ca = bch2_dev_rcu_noerror(c, ptr->dev); - return !ca || bch2_dev_bad_or_evacuating_rcu(c, ptr->dev) || (ca->mi.target_nbuckets && ca->mi.target_nbuckets <= div_u64(ptr->offset, ca->mi.bucket_size)); + return !ca || + bch2_dev_bad_or_evacuating_rcu(c, ptr->dev) || + (bch2_dev_is_shrinking(ca) && + bch2_dev_resize_target(ca) <= div_u64(ptr->offset, ca->mi.bucket_size)); } static inline bool bch2_ptr_bad_or_evacuating(struct bch_fs *c, const struct bch_extent_ptr *ptr) { diff --git a/fs/bcachefs/sb/members_format.h b/fs/bcachefs/sb/members_format.h index d87366d239e69..d39ba5c8fb630 100644 --- a/fs/bcachefs/sb/members_format.h +++ b/fs/bcachefs/sb/members_format.h @@ -76,7 +76,7 @@ struct bch_member { __u8 device_model[64] __nonstring; __le64 flush_errors; __u8 device_serial[64] __nonstring; - __le64 target_nbuckets; /* 0 => no pending resize, (first_bucket + BCH_MIN_NR_NBUCKETS)..nbuckets => shrink, other => illegal */ + __le64 target_nbuckets; /* 0 => idle, nbuckets => idle, < nbuckets => shrink target, > nbuckets => grow target */ }; /* diff --git a/fs/bcachefs/sb/members_types.h b/fs/bcachefs/sb/members_types.h index 3711cbd92139e..1fcdacb8a3aa0 100644 --- a/fs/bcachefs/sb/members_types.h +++ b/fs/bcachefs/sb/members_types.h @@ -18,7 +18,7 @@ struct bch_member_cpu { u8 valid; u8 btree_bitmap_shift; u64 btree_allocated_bitmap; - u64 target_nbuckets; /* 0 => no pending resize, [first_bucket + BCH_MIN_NR_NBUCKETS, nbuckets) => shrink, other => illegal */ + u64 target_nbuckets; /* 0 => idle, nbuckets => idle, < nbuckets => shrink target, > nbuckets => grow target */ }; #endif /* _BCACHEFS_SB_MEMBERS_H */ From 59c4d95e5d277c77c8c1390b8b33f342032b2d03 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 15 Apr 2026 19:16:36 +0200 Subject: [PATCH 45/75] bcachefs: discard: drop need_discard iter before commit bch2_do_discards() now clears need_discard buckets via alloc updates that commit the transaction and remove the corresponding need_discard entry. Holding the need_discard iterator open across that commit can deadlock against the alloc/freespace updates while shrink is reclaiming truncated tails. Fetch one need_discard key at a time, remember the successor position, and reopen the iterator after each bucket is processed. This keeps the upstream need_discard worker model intact while avoiding the lock cycle seen in shrink tests after the rebase. --- fs/bcachefs/alloc/discard.c | 61 ++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/fs/bcachefs/alloc/discard.c b/fs/bcachefs/alloc/discard.c index 7bb731a223f37..8e781e5ae100f 100644 --- a/fs/bcachefs/alloc/discard.c +++ b/fs/bcachefs/alloc/discard.c @@ -171,6 +171,11 @@ int bch2_dev_clear_need_discard(struct bch_fs *c, struct bch_dev *ca, u64 cutoff } else { struct bpos bucket = u64_to_bucket(k.k->p.offset); + /* + * need_discard is ordered by (journal seq, encoded bucket), + * not by device bucket space, so tail truncation has to + * scan and filter by the decoded bucket position. + */ next = bpos_successor(k.k->p); if (bucket.inode == ca->dev_idx && @@ -273,31 +278,59 @@ static void bch2_do_discards(struct bch_fs *c) memset(s, 0, sizeof(*s)); struct bpos discard_pos_done = POS_MAX; + struct bpos pos = POS_MIN; /* * Iterate need_discard btree (sorted by journal_seq). * Stop when we hit a seq beyond rewind_seq_ondisk. */ - ret = for_each_btree_key(trans, iter, - BTREE_ID_need_discard, POS_MIN, 0, k, ({ - u64 journal_seq = k.k->p.inode; - struct bpos bucket = u64_to_bucket(k.k->p.offset); - int _ret = 0; + while (!ret) { + struct bpos next = POS_MAX; + struct bpos bucket = POS_MAX; + u64 journal_seq = 0; + bool done = false; - s->pos = iter.pos; + /* + * Drop the need_discard iterator before we update alloc and + * commit: the discard path removes the corresponding index + * entry, and keeping the original iterator pinned across that + * commit can deadlock against the alloc/freespace updates. + */ + ret = lockrestart_do(trans, ({ + int __ret = 0; + CLASS(btree_iter, iter)(trans, BTREE_ID_need_discard, pos, 0); + struct bkey_s_c k = bch2_btree_iter_peek(&iter); + int _ret = bkey_err(k); + + if (_ret) { + __ret = _ret; + } else if (!k.k) { + done = true; + } else { + journal_seq = k.k->p.inode; + bucket = u64_to_bucket(k.k->p.offset); + next = bpos_successor(k.k->p); + s->pos = next; + } + __ret; + })); + if (ret || done) + break; if (journal_seq >= min(c->journal.rewind_seq_ondisk, c->journal.flushed_seq_ondisk + 1)) break; - _ret = bch2_discard_one_bucket(trans, bucket, - &discard_pos_done, - s, false); - - if (!_ret) - s->seen += dev_bucket_size(c, bucket.inode); - _ret; - })); + ret = lockrestart_do(trans, ({ + int __ret = bch2_discard_one_bucket(trans, bucket, + &discard_pos_done, + s, false); + if (!__ret) + s->seen += dev_bucket_size(c, bucket.inode); + __ret; + })); + pos = next; + } /* * Rewind buffer policy: advance rewind_seq when free space From 47cbc8808c4be29d2711bfe72a95c8beef674cee Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 15 Apr 2026 21:41:24 +0200 Subject: [PATCH 46/75] fs/bcachefs: drop cached reconcile paths before move work Do not keep reconcile_scan iterators or other cached search paths alive across the actual move/rewrite operations. Return the next reconcile_scan entry without exiting from inside the iterator helper, and explicitly restart the transaction before the normal and phys reconcile workers begin mutating extents. This avoids deadlocks where cached reconcile paths stay pinned while move work needs alloc/freespace updates and btree node rewrites. --- fs/bcachefs/data/reconcile/work.c | 40 +++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/fs/bcachefs/data/reconcile/work.c b/fs/bcachefs/data/reconcile/work.c index de14bdb2c3ee1..83f846d11590d 100644 --- a/fs/bcachefs/data/reconcile/work.c +++ b/fs/bcachefs/data/reconcile/work.c @@ -211,14 +211,23 @@ static struct bkey_s_c next_reconcile_entry(struct btree_trans *trans, if (work_pos->btree == BTREE_ID_reconcile_scan) { buf->nr = 0; - int ret = for_each_btree_key_max(trans, iter, work_pos->btree, work_pos->pos, end, - flags, k, ({ - bkey_reassemble(&darray_top(*buf), k); - return bkey_i_to_s_c(&darray_top(*buf)); - 0; - })); + /* + * Do not return out of a for_each_btree_key_* body here: we want + * the iterator fully dropped before reconcile starts mutating the + * extent that this work item points at. Keeping the reconcile_scan + * path pinned across the later move/rewrite can deadlock against + * alloc/freespace updates from the same transaction. + */ + CLASS(btree_iter, iter)(trans, work_pos->btree, work_pos->pos, flags); + struct bkey_s_c k = bch2_btree_iter_peek(&iter); + + if (bkey_err(k)) + return bkey_s_c_err(bkey_err(k)); + if (!k.k || bpos_gt(k.k->p, end)) + return bkey_s_c_null; - return ret ? bkey_s_c_err(ret) : bkey_s_c_null; + bkey_reassemble(&darray_top(*buf), k); + return bkey_i_to_s_c(&darray_top(*buf)); } if (unlikely(!buf->nr)) { @@ -1217,6 +1226,15 @@ static CLOSURE_CALLBACK(do_reconcile_phys_thread) k.k->p.inode != thr->dev) break; + /* + * Drop any cached search paths from next_reconcile_entry() before + * we start the actual move work: otherwise reconcile_scan or + * backpointer paths looked up to choose the work item can stay + * pinned in this transaction and deadlock against alloc/freespace + * updates during btree node rewrites. + */ + bch2_trans_begin(trans); + int ret = lockrestart_do(trans, do_reconcile_extent_phys(&ctxt, &snapshot_io_opts, BBPOS(work_pos.btree, k.k->p), @@ -1370,6 +1388,14 @@ static int do_reconcile(struct moving_context *ctxt) reconcile_scan_decode(c, k.k->p.offset).type == RECONCILE_SCAN_pending) bkey_reassemble(&pending_cookie.k_i, k); + /* + * next_reconcile_entry() may have touched reconcile_scan or other + * work btrees to select this item. Start the actual reconcile work + * in a fresh transaction so those cached paths are gone before we + * mutate alloc/freespace state via move_extent(). + */ + bch2_trans_begin(trans); + if (k.k->type == KEY_TYPE_cookie) { ret = do_reconcile_scan(ctxt, &snapshot_io_opts, k.k->p, From 197ab69bdbb1af5547363f21cb3d303717b8a9b6 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 18 Apr 2026 13:48:32 +0200 Subject: [PATCH 47/75] fs/bcachefs: keep shrink discards out of the evacuated tail Shrink still needs discard progress on retained buckets, but discards in the tail being evacuated can deadlock against resize/reconcile and journal pin flushing. Only defer need_discard entries at or beyond the current shrink target, drain any in-flight discard workers before the shrink path starts, and re-kick discards once the current resize request reaches a terminal state. Also clear tail need_discard state by deleting the derived index entries directly instead of mutating alloc keys that are about to be truncated anyway. --- fs/bcachefs/alloc/discard.c | 112 +++++++++++++++++------------------- fs/bcachefs/init/dev.c | 24 ++++++++ 2 files changed, 78 insertions(+), 58 deletions(-) diff --git a/fs/bcachefs/alloc/discard.c b/fs/bcachefs/alloc/discard.c index 8e781e5ae100f..fb2096ae697a5 100644 --- a/fs/bcachefs/alloc/discard.c +++ b/fs/bcachefs/alloc/discard.c @@ -150,64 +150,33 @@ static int bch2_discard_one_bucket(struct btree_trans *trans, int bch2_dev_clear_need_discard(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) { - struct bpos pos = POS_MIN; - - while (1) { - struct bpos next = POS_MAX; - bool done = false; - int ret = bch2_trans_commit_do(c, NULL, NULL, - BCH_WATERMARK_reclaim| - BCH_TRANS_COMMIT_no_check_rw| - BCH_TRANS_COMMIT_no_enospc, ({ - int __ret = 0; - CLASS(btree_iter, iter)(trans, BTREE_ID_need_discard, pos, 0); - struct bkey_s_c k = bch2_btree_iter_peek(&iter); - int _ret = bkey_err(k); - - if (_ret) { - __ret = _ret; - } else if (!k.k) { - done = true; - } else { - struct bpos bucket = u64_to_bucket(k.k->p.offset); - - /* - * need_discard is ordered by (journal seq, encoded bucket), - * not by device bucket space, so tail truncation has to - * scan and filter by the decoded bucket position. - */ - next = bpos_successor(k.k->p); - - if (bucket.inode == ca->dev_idx && - bucket.offset >= cutoff) { - CLASS(btree_iter, alloc_iter)(trans, BTREE_ID_alloc, - bucket, BTREE_ITER_cached); - struct bkey_s_c alloc_k = - bch2_btree_iter_peek_slot(&alloc_iter); - struct bkey_i_alloc_v4 *a; - - _ret = bkey_err(alloc_k); - if (_ret) { - __ret = _ret; - } else { - a = errptr_try(bch2_alloc_to_v4_mut(trans, alloc_k)); - if (a->v.data_type == BCH_DATA_need_discard) { - SET_BCH_ALLOC_V4_NEED_DISCARD(&a->v, false); - alloc_data_type_set(&a->v, a->v.data_type); - __ret = bch2_trans_update(trans, &alloc_iter, - &a->k_i, 0); - } - } - } - } - __ret; - })); - if (ret) - return ret; - if (done) - return 0; - pos = next; - } + /* + * Shrink is about to delete the tail's alloc keys entirely, so we only + * need to remove the derived need_discard index entries. Avoid updating + * alloc keys here: that would also fire freespace/reconcile triggers + * while reconcile is still draining the shrinking device, which can + * deadlock with btree node rewrites in the move path. + * + * need_discard is ordered by (journal seq, encoded bucket), not by + * device bucket space, so tail truncation has to scan and filter by the + * decoded bucket position. + */ + CLASS(btree_trans, trans)(c); + unsigned dev_idx = ca->dev_idx; + + return for_each_btree_key_commit(trans, iter, + BTREE_ID_need_discard, POS_MIN, + BTREE_ITER_intent|BTREE_ITER_prefetch, k, + NULL, NULL, + BCH_WATERMARK_reclaim| + BCH_TRANS_COMMIT_no_check_rw| + BCH_TRANS_COMMIT_no_enospc, ({ + struct bpos bucket = u64_to_bucket(k.k->p.offset); + + (bucket.inode == dev_idx && bucket.offset >= cutoff) + ? bch2_btree_delete_at(trans, &iter, BTREE_TRIGGER_norun) + : 0; + })); } static void calculate_discard_sectors_to_release(struct btree_trans *trans) @@ -263,6 +232,16 @@ static void calculate_discard_sectors_to_release(struct btree_trans *trans) s->r.flush_journal = true; } +static bool bch2_discard_blocked_by_resize(struct bch_fs *c, struct bpos bucket) +{ + guard(rcu)(); + struct bch_dev *ca = bch2_dev_rcu_noerror(c, bucket.inode); + + return ca && + bch2_dev_is_shrinking(ca) && + bucket.offset >= bch2_dev_resize_target(ca); +} + static void bch2_do_discards(struct bch_fs *c) { int ret = 0; @@ -321,6 +300,23 @@ static void bch2_do_discards(struct bch_fs *c) c->journal.flushed_seq_ondisk + 1)) break; + /* + * need_discard is only advisory trim bookkeeping. If this + * bucket sits in the tail of a device that is currently + * shrinking, leave the entry queued for the tail cleanup + * pass: discard's alloc update can deadlock with + * resize/reconcile on the region being evacuated. + * + * Buckets that remain below the shrink target still need + * discard progress so the allocator can reuse retained + * space on the shrinking device during the long-running + * evacuation. + */ + if (bch2_discard_blocked_by_resize(c, bucket)) { + pos = next; + continue; + } + ret = lockrestart_do(trans, ({ int __ret = bch2_discard_one_bucket(trans, bucket, &discard_pos_done, diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index ee727490b5dcf..8fa3f5ddcd885 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1312,6 +1312,15 @@ static bool bch2_dev_resize_finish(struct bch_dev *ca, u64 seq, int status) if (is_current) wake_up_all(&ca->resize_wait); + /* + * Discards are deferred while resize is active to avoid lock/allocator + * deadlocks with reconcile and journal-pin flushing. Kick the worker + * once the latest resize request reaches a terminal state so queued + * need_discard entries can drain again. + */ + if (is_current) + bch2_do_discards_async(ca->fs); + return is_current; } @@ -1654,6 +1663,21 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, } }; + ret = bch2_dev_resize_restart_check(ca, seq); + if (ret) + return ret; + + /* + * Shrink can start while a discard worker from earlier freespace + * churn is still in flight. Drain that work before we begin the + * evacuation/journal-flush path: once shrink has started, later + * discard passes skip the shrinking device, but an already-running + * discard can still race in and deadlock against resize/reconcile's + * allocator and btree rewrite work. + */ + flush_work(&c->discards.work); + flush_work(&ca->discard_fast_work); + ret = bch2_dev_resize_restart_check(ca, seq); if (ret) return ret; From 15c35f69d4b8ad5dff55a58e5829bb66ffc6c471 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Sun, 19 Apr 2026 19:25:21 +0200 Subject: [PATCH 48/75] bcachefs: shrink: bch2_dev_resize_wait_done: improve readability --- fs/bcachefs/init/dev.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 8fa3f5ddcd885..8fc59f13e894d 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1276,18 +1276,18 @@ static u64 bch2_dev_resize_seq(struct bch_dev *ca) static bool bch2_dev_resize_wait_done(struct bch_dev *ca, u64 seq, int *status) { - bool done; - scoped_guard(spinlock, &ca->resize_lock) { - done = ca->resize_seq != seq || - ca->resize_status != -EINPROGRESS; - if (done) - *status = ca->resize_seq != seq - ? -ECANCELED - : ca->resize_status; + if (ca->resize_seq != seq) { + *status = -ECANCELED; + return true; + } + if (ca->resize_status != -EINPROGRESS) { + *status = ca->resize_status; + return true; + } } - return done; + return false; } static int bch2_dev_resize_wait(struct bch_dev *ca, u64 seq) From 8386a41972bae9783c1698fc1e7fd92612678e1d Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Sun, 19 Apr 2026 19:38:55 +0200 Subject: [PATCH 49/75] bcachefs: shrink: small code cleanups --- fs/bcachefs/init/dev.c | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 8fc59f13e894d..2d0aa5fa1a5e0 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1266,12 +1266,8 @@ int bch2_dev_offline(struct bch_fs *c, struct bch_dev *ca, int flags, struct pri static u64 bch2_dev_resize_seq(struct bch_dev *ca) { - u64 seq; - scoped_guard(spinlock, &ca->resize_lock) - seq = ca->resize_seq; - - return seq; + return ca->resize_seq; } static bool bch2_dev_resize_wait_done(struct bch_dev *ca, u64 seq, int *status) @@ -1309,17 +1305,12 @@ static bool bch2_dev_resize_finish(struct bch_dev *ca, u64 seq, int status) ca->resize_status = status; } - if (is_current) + if (is_current) { wake_up_all(&ca->resize_wait); - /* - * Discards are deferred while resize is active to avoid lock/allocator - * deadlocks with reconcile and journal-pin flushing. Kick the worker - * once the latest resize request reaches a terminal state so queued - * need_discard entries can drain again. - */ - if (is_current) + /* Discards are deferred during resize to avoid allocator/journal deadlocks, restart them now that we are done */ bch2_do_discards_async(ca->fs); + } return is_current; } @@ -1383,6 +1374,7 @@ static int bch2_dev_resize_update_target(struct bch_fs *c, struct bch_dev *ca, { lockdep_assert_held(&c->state_lock); + /* validate target_nbuckets */ u64 old_nbuckets = ca->mi.nbuckets; if (target_nbuckets > BCH_MEMBER_NBUCKETS_MAX) { @@ -1410,6 +1402,7 @@ static int bch2_dev_resize_update_target(struct bch_fs *c, struct bch_dev *ca, return bch_err_throw(c, device_size_too_small); } + /* commit target_nbuckets */ scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { guard(mutex)(&c->sb_lock); struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); @@ -1421,7 +1414,6 @@ static int bch2_dev_resize_update_target(struct bch_fs *c, struct bch_dev *ca, return 0; } -/* requires write state lock */ static int __bch2_dev_grow(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err) { @@ -1512,6 +1504,8 @@ static int drop_sbs_after_cutoff(struct bch_fs *c, struct bch_dev *ca, u64 cutof } } + /* this should never happen, as we only call to this function after checking the cutoff against the minimum fs size, + * which includes at least the first sb copy */ BUG_ON(i == 0); layout->nr_superblocks = i; From ca44f040ad97b1266925a91a9bd0804ab2b85451 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 19 Apr 2026 21:53:10 +0200 Subject: [PATCH 50/75] bcachefs: fail impossible shrink after reconcile drains Track completed reconcile kicks so shrink can wait for a full reconcile pass instead of polling forever for the tail to become empty. Shrink now queues a device scan plus a pending pass, retries pending work once more, and returns `-ENOSPC` if the tail is still occupied after both passes. Before returning that failure, clear the persisted shrink target and wake pending reconcile work so remount no longer retries a known-impossible shrink under the stale cutoff. --- fs/bcachefs/data/reconcile/types.h | 6 +- fs/bcachefs/data/reconcile/work.c | 33 ++++++-- fs/bcachefs/data/reconcile/work.h | 15 +++- fs/bcachefs/init/dev.c | 131 ++++++++++++++++++++++++----- 4 files changed, 154 insertions(+), 31 deletions(-) diff --git a/fs/bcachefs/data/reconcile/types.h b/fs/bcachefs/data/reconcile/types.h index cc09e8ec0950e..a0e77addf848d 100644 --- a/fs/bcachefs/data/reconcile/types.h +++ b/fs/bcachefs/data/reconcile/types.h @@ -2,13 +2,17 @@ #ifndef _BCACHEFS_REBALANCE_TYPES_H #define _BCACHEFS_REBALANCE_TYPES_H +#include + #include "btree/bbpos_types.h" #include "data/move_types.h" #include "init/progress.h" struct bch_fs_reconcile { struct task_struct __rcu *thread; - u32 kick; + atomic_t kick; + wait_queue_head_t wait; + atomic_t completed_kick; bool running; u64 wait_iotime_start; diff --git a/fs/bcachefs/data/reconcile/work.c b/fs/bcachefs/data/reconcile/work.c index 83f846d11590d..d2f53774faed2 100644 --- a/fs/bcachefs/data/reconcile/work.c +++ b/fs/bcachefs/data/reconcile/work.c @@ -1324,8 +1324,9 @@ static int do_reconcile(struct moving_context *ctxt) struct bch_fs *c = trans->c; struct bch_fs_reconcile *r = &c->reconcile; u64 sectors_scanned = 0; - u32 kick = r->kick; + u32 kick = atomic_read(&r->kick); u32 copygc_run_count = c->copygc.run_count; + bool pass_complete = false; int ret = 0; CLASS(darray_reconcile_work, work)(); @@ -1359,8 +1360,8 @@ static int do_reconcile(struct moving_context *ctxt) break; } - if (kick != r->kick) { - kick = r->kick; + if (kick != atomic_read(&r->kick)) { + kick = atomic_read(&r->kick); work.nr = 0; r->phase = 0; reconcile_phase_start(c); @@ -1376,8 +1377,10 @@ static int do_reconcile(struct moving_context *ctxt) break; if (!k.k) { - if (!reconcile_phase_next(c, ctxt, &pending_cookie)) + if (!reconcile_phase_next(c, ctxt, &pending_cookie)) { + pass_complete = true; break; + } continue; } @@ -1463,13 +1466,27 @@ static int do_reconcile(struct moving_context *ctxt) try(bch2_clear_reconcile_needs_scan(trans, pending_cookie.k.p, pending_cookie.v.cookie)); + /* + * A completed kick means reconcile drained every phase for the current + * request generation without being superseded by a newer wakeup. + * Shrink uses this as the point where all scan-generated downstream + * work has been attempted before it decides a tail is still impossible + * to evacuate. + */ + if (!ret && + pass_complete && + kick == atomic_read(&r->kick)) { + atomic_set(&r->completed_kick, kick); + wake_up_all(&r->wait); + } + bch2_move_stats_exit(&r->work_stats, c); if (!ret && !kthread_should_stop() && !atomic64_read(&r->work_stats.sectors_seen) && !sectors_scanned && - kick == r->kick) { + kick == atomic_read(&r->kick)) { bch2_moving_ctxt_flush_all(ctxt); bch2_trans_unlock_long(trans); reconcile_wait(c); @@ -1658,9 +1675,13 @@ void bch2_fs_reconcile_exit(struct bch_fs *c) int bch2_fs_reconcile_init(struct bch_fs *c) { -#ifdef CONFIG_POWER_SUPPLY struct bch_fs_reconcile *r = &c->reconcile; + atomic_set(&r->kick, 0); + init_waitqueue_head(&r->wait); + atomic_set(&r->completed_kick, 0); + +#ifdef CONFIG_POWER_SUPPLY r->power_notifier.notifier_call = bch2_reconcile_power_notifier; try(power_supply_reg_notifier(&r->power_notifier)); diff --git a/fs/bcachefs/data/reconcile/work.h b/fs/bcachefs/data/reconcile/work.h index d45d88a770685..5da3f504d2222 100644 --- a/fs/bcachefs/data/reconcile/work.h +++ b/fs/bcachefs/data/reconcile/work.h @@ -31,13 +31,24 @@ int bch2_set_reconcile_needs_scan_trans(struct btree_trans *, struct reconcile_s int bch2_set_reconcile_needs_scan(struct bch_fs *, struct reconcile_scan, bool); int bch2_set_fs_needs_reconcile(struct bch_fs *); -static inline void bch2_reconcile_wakeup(struct bch_fs *c) +static inline u32 bch2_reconcile_kick(struct bch_fs *c) { - c->reconcile.kick++; + u32 kick = atomic_inc_return(&c->reconcile.kick); guard(rcu)(); struct task_struct *p = rcu_dereference(c->reconcile.thread); if (p) wake_up_process(p); + return kick; +} + +static inline void bch2_reconcile_wakeup(struct bch_fs *c) +{ + bch2_reconcile_kick(c); +} + +static inline u32 bch2_reconcile_completed_kick(struct bch_fs *c) +{ + return atomic_read(&c->reconcile.completed_kick); } static inline int bch2_reconcile_pending_wakeup(struct bch_fs *c) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 2d0aa5fa1a5e0..9fc1d5bbdb274 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1614,6 +1614,93 @@ static int tail_is_empty(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, return 0; } +static int bch2_dev_shrink_queue_reconcile(struct bch_fs *c, struct bch_dev *ca, + bool scan_device, u32 *kick, + struct printbuf *err) +{ + if (scan_device) { + struct reconcile_scan s = { + .type = RECONCILE_SCAN_device, // TODO(performance): make this range-based + .dev = ca->dev_idx, + }; + + int ret = bch2_set_reconcile_needs_scan(c, s, false); + if (ret) { + prt_printf(err, "Failed to queue device reconcile scan: %s\n", + bch2_err_str(ret)); + return ret; + } + } + + /* + * Shrink waits for a completed reconcile pass, not just for the device + * scan cookie to disappear. Queue the pending phase alongside the scan + * so the same pass also retries any work that evacuation demoted to the + * pending list. + */ + int ret = bch2_set_reconcile_needs_scan(c, + (struct reconcile_scan) { .type = RECONCILE_SCAN_pending }, false); + if (ret) { + prt_printf(err, "Failed to queue pending reconcile scan: %s\n", + bch2_err_str(ret)); + return ret; + } + + *kick = bch2_reconcile_kick(c); + return 0; +} + +static int bch2_dev_shrink_wait_reconcile(struct bch_dev *ca, u64 seq, u32 kick) +{ + struct bch_fs *c = ca->fs; + + while (true) { + int ret = bch2_dev_resize_restart_check(ca, seq); + if (ret) + return ret; + + if (bch2_reconcile_completed_kick(c) >= kick) + return 0; + + ret = wait_event_killable(c->reconcile.wait, + bch2_reconcile_completed_kick(c) >= kick || + bch2_dev_resize_seq(ca) != seq || + kthread_should_stop()); + if (ret) + return -EINTR; + } +} + +static int bch2_dev_shrink_clear_target(struct bch_fs *c, struct bch_dev *ca, + u64 new_nbuckets, u64 seq, + struct printbuf *err) +{ + int ret = 0; + + scoped_guard(rwsem_write, &c->state_lock) { + ret = bch2_dev_resize_restart_check(ca, seq); + if (ret) + return ret; + + if (bch2_dev_resize_target(ca) != new_nbuckets || + !bch2_dev_is_shrinking(ca)) + return -EAGAIN; + + try(bch2_dev_resize_update_target(c, ca, 0, err)); + } + + /* + * Canceling the persisted shrink target lifts the shrink cutoff. Retry + * pending reconcile work so anything that was parked behind the failed + * shrink gets re-evaluated under the restored placement rules. + */ + ret = bch2_reconcile_pending_wakeup(c); + if (ret) + bch_err_fn(c, ret); + + return 0; +} + static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, u64 seq, struct printbuf *err) @@ -1639,22 +1726,6 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, /* close open buckets in the to-be-shrunk region */ bch2_open_buckets_stop(c, ca, false, new_nbuckets); bch2_reset_alloc_cursors(c); // avoid churn - - /* - * target_nbuckets persists in the superblock, but the device - * scan's in-memory traversal state does not. Re-queue the - * device scan whenever shrink starts or resumes so reconcile - * reliably rediscovers the tail after a restart. - */ - struct reconcile_scan s = { - .type = RECONCILE_SCAN_device, // TODO(performance): make this range-based - .dev = ca->dev_idx, - }; - ret = bch2_set_reconcile_needs_scan(c, s, true); - if (ret) { - prt_printf(err, "Failed to run device reconcile scan: %s\n", bch2_err_str(ret)); - return ret; - } }; ret = bch2_dev_resize_restart_check(ca, seq); @@ -1698,8 +1769,9 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, */ /* wait for to-be-shrunk region to be empty */ - while (true) { + for (unsigned pass = 0; ; pass++) { bool empty = false; + u32 kick; ret = bch2_dev_resize_restart_check(ca, seq); if (ret) @@ -1720,13 +1792,28 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, } } - /* make sure reconcile is actually running */ - bch2_reconcile_wakeup(c); + if (pass >= 2) { + prt_printf(err, + "Shrink failed: insufficient space or replicas to evacuate data from the shrink tail\n"); + ret = bch2_dev_shrink_clear_target(c, ca, new_nbuckets, seq, err); + if (ret) + return ret; + return -ENOSPC; + } - if (kthread_should_stop()) - return -EINTR; + /* + * A consumed scan cookie only means reconcile observed the scan + * request and queued downstream work. Wait for a completed pass + * so the scan-generated normal and pending work has been + * attempted before we decide the tail still cannot drain. + */ + ret = bch2_dev_shrink_queue_reconcile(c, ca, pass == 0, &kick, err); + if (ret) + return ret; - schedule_timeout_interruptible(HZ/2); + ret = bch2_dev_shrink_wait_reconcile(ca, seq, kick); + if (ret) + return ret; } /* From 3341e0fa84ec6b9dcef6dacdf40967cba17239e7 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 19 Apr 2026 23:05:04 +0200 Subject: [PATCH 51/75] bcachefs: keep shrink reconcile focused on the truncating tail Shrink was queuing a full-device RECONCILE_SCAN_device pass and then starting the final journal flush as soon as the requested kick drained. In online_three_device_variable_buckets_shrink that could requeue metadata below the retained region for tens of seconds, and once the kick completed the resize worker could still collide with reconcile's cached paths during key-cache pin flushing. Start shrink-triggered backpointer scans at target_nbuckets translated into backpointer key space, and wait for reconcile to report idle before the final shrink flush. That keeps the resize worker focused on the tail that will actually be truncated and avoids the long post-resize stalls. --- fs/bcachefs/data/reconcile/work.c | 31 ++++++++++++++++++++++++++----- fs/bcachefs/data/reconcile/work.h | 6 ++++++ fs/bcachefs/init/dev.c | 10 ++++++++-- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/fs/bcachefs/data/reconcile/work.c b/fs/bcachefs/data/reconcile/work.c index d2f53774faed2..abeee892d411c 100644 --- a/fs/bcachefs/data/reconcile/work.c +++ b/fs/bcachefs/data/reconcile/work.c @@ -920,14 +920,29 @@ static int do_reconcile_scan_bps(struct moving_context *ctxt, struct btree_trans *trans = ctxt->trans; struct bch_fs *c = trans->c; struct bch_fs_reconcile *r = &c->reconcile; + struct bpos start = POS(s.dev, 0); + struct bch_dev *ca; - r->scan_start = BBPOS(BTREE_ID_backpointers, POS(s.dev, 0)); + /* + * Shrink only needs reconcile work for the tail that will be truncated. + * Scanning the whole device requeues metadata below the retained region, + * which can churn btree rewrites for tens of seconds before the resize + * worker ever gets to commit the smaller size. + */ + scoped_guard(rcu) { + ca = bch2_dev_rcu_noerror(c, s.dev); + if (ca && bch2_dev_is_shrinking(ca)) + start = bucket_pos_to_bp_start(ca, + POS(s.dev, bch2_dev_resize_target(ca))); + } + + r->scan_start = BBPOS(BTREE_ID_backpointers, start); r->scan_end = BBPOS(BTREE_ID_backpointers, POS(s.dev, U64_MAX)); bch2_btree_write_buffer_flush_sync(trans); return backpointer_scan_for_each(trans, iter, BTREE_ID_backpointers, - POS(s.dev, 0), POS(s.dev, U64_MAX), + start, POS(s.dev, U64_MAX), last_flushed, NULL, bp, ({ ctxt->stats->pos = BBPOS(BTREE_ID_backpointers, iter.pos); @@ -1100,10 +1115,16 @@ static void reconcile_wait(struct bch_fs *c) r->wait_iotime_end = now + (min_member_capacity >> 6); - if (r->running) { + if (READ_ONCE(r->running)) { r->wait_iotime_start = now; r->wait_wallclock_start = ktime_get_real_ns(); - r->running = false; + WRITE_ONCE(r->running, false); + /* + * Shrink waits for reconcile to go idle before its final + * journal/key-cache flush, otherwise reconcile can still hold + * cached search paths while it unwinds from the completed kick. + */ + wake_up_all(&r->wait); } bch2_kthread_io_clock_wait_once(clock, r->wait_iotime_end, MAX_SCHEDULE_TIMEOUT); @@ -1384,7 +1405,7 @@ static int do_reconcile(struct moving_context *ctxt) continue; } - r->running = true; + WRITE_ONCE(r->running, true); r->work_pos.pos = k.k->p; if (k.k->type == KEY_TYPE_cookie && diff --git a/fs/bcachefs/data/reconcile/work.h b/fs/bcachefs/data/reconcile/work.h index 5da3f504d2222..437cc5ee59e69 100644 --- a/fs/bcachefs/data/reconcile/work.h +++ b/fs/bcachefs/data/reconcile/work.h @@ -51,6 +51,12 @@ static inline u32 bch2_reconcile_completed_kick(struct bch_fs *c) return atomic_read(&c->reconcile.completed_kick); } +static inline bool bch2_reconcile_kick_idle(struct bch_fs *c, u32 kick) +{ + return bch2_reconcile_completed_kick(c) >= kick && + !READ_ONCE(c->reconcile.running); +} + static inline int bch2_reconcile_pending_wakeup(struct bch_fs *c) { return bch2_set_reconcile_needs_scan(c, diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 9fc1d5bbdb274..2ed927c9df14e 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1659,11 +1659,17 @@ static int bch2_dev_shrink_wait_reconcile(struct bch_dev *ca, u64 seq, u32 kick) if (ret) return ret; - if (bch2_reconcile_completed_kick(c) >= kick) + /* + * A completed kick only says the requested reconcile pass + * drained. Wait for the worker to reach its idle state too, or + * shrink's final journal flush can still race with reconcile's + * cached btree paths and stall behind key-cache pin flushing. + */ + if (bch2_reconcile_kick_idle(c, kick)) return 0; ret = wait_event_killable(c->reconcile.wait, - bch2_reconcile_completed_kick(c) >= kick || + bch2_reconcile_kick_idle(c, kick) || bch2_dev_resize_seq(ca) != seq || kthread_should_stop()); if (ret) From 9c1f0618984ee931b8a615947b57671b245edf40 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 19 Apr 2026 23:32:07 +0200 Subject: [PATCH 52/75] bcachefs: stop shrink waiting once the tail is empty A shrink-triggered reconcile kick can keep draining unrelated global reconcile work long after the truncating tail has already been evacuated. In the variable-bucket shrink ktest that turned occasional runs into multi-minute stalls even though the tail was already empty. Poll tail emptiness while waiting for reconcile so shrink can move on as soon as evacuation has actually cleared the tail. Keep the final cutoff path as a separate helper so the control flow stays structured and does not need gotos. --- fs/bcachefs/data/reconcile/work.c | 6 - fs/bcachefs/data/reconcile/work.h | 6 - fs/bcachefs/init/dev.c | 291 ++++++++++++++++-------------- 3 files changed, 160 insertions(+), 143 deletions(-) diff --git a/fs/bcachefs/data/reconcile/work.c b/fs/bcachefs/data/reconcile/work.c index abeee892d411c..986e0f271664d 100644 --- a/fs/bcachefs/data/reconcile/work.c +++ b/fs/bcachefs/data/reconcile/work.c @@ -1119,12 +1119,6 @@ static void reconcile_wait(struct bch_fs *c) r->wait_iotime_start = now; r->wait_wallclock_start = ktime_get_real_ns(); WRITE_ONCE(r->running, false); - /* - * Shrink waits for reconcile to go idle before its final - * journal/key-cache flush, otherwise reconcile can still hold - * cached search paths while it unwinds from the completed kick. - */ - wake_up_all(&r->wait); } bch2_kthread_io_clock_wait_once(clock, r->wait_iotime_end, MAX_SCHEDULE_TIMEOUT); diff --git a/fs/bcachefs/data/reconcile/work.h b/fs/bcachefs/data/reconcile/work.h index 437cc5ee59e69..5da3f504d2222 100644 --- a/fs/bcachefs/data/reconcile/work.h +++ b/fs/bcachefs/data/reconcile/work.h @@ -51,12 +51,6 @@ static inline u32 bch2_reconcile_completed_kick(struct bch_fs *c) return atomic_read(&c->reconcile.completed_kick); } -static inline bool bch2_reconcile_kick_idle(struct bch_fs *c, u32 kick) -{ - return bch2_reconcile_completed_kick(c) >= kick && - !READ_ONCE(c->reconcile.running); -} - static inline int bch2_reconcile_pending_wakeup(struct bch_fs *c) { return bch2_set_reconcile_needs_scan(c, diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 2ed927c9df14e..b5e7c461c71a1 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1650,29 +1650,38 @@ static int bch2_dev_shrink_queue_reconcile(struct bch_fs *c, struct bch_dev *ca, return 0; } -static int bch2_dev_shrink_wait_reconcile(struct bch_dev *ca, u64 seq, u32 kick) +static int bch2_dev_shrink_wait_reconcile(struct bch_dev *ca, u64 new_nbuckets, + u64 seq, u32 kick, + struct printbuf *err) { struct bch_fs *c = ca->fs; while (true) { + bool empty = false; int ret = bch2_dev_resize_restart_check(ca, seq); if (ret) return ret; /* - * A completed kick only says the requested reconcile pass - * drained. Wait for the worker to reach its idle state too, or - * shrink's final journal flush can still race with reconcile's - * cached btree paths and stall behind key-cache pin flushing. + * We only need reconcile to keep running until the shrink tail is + * actually empty. A kick can continue chewing through unrelated + * global reconcile work for minutes after the truncating region is + * already evacuated, especially with fragmented variable-bucket + * workloads. Poll tail emptiness so shrink can move on as soon as + * the region being truncated is clear. */ - if (bch2_reconcile_kick_idle(c, kick)) + try(tail_is_empty(c, ca, new_nbuckets, err, &empty)); + + if (empty || + bch2_reconcile_completed_kick(c) >= kick) return 0; - ret = wait_event_killable(c->reconcile.wait, - bch2_reconcile_kick_idle(c, kick) || + ret = wait_event_killable_timeout(c->reconcile.wait, + bch2_reconcile_completed_kick(c) >= kick || bch2_dev_resize_seq(ca) != seq || - kthread_should_stop()); - if (ret) + kthread_should_stop(), + HZ); + if (ret < 0) return -EINTR; } } @@ -1707,12 +1716,147 @@ static int bch2_dev_shrink_clear_target(struct bch_fs *c, struct bch_dev *ca, return 0; } +static int bch2_dev_shrink_finish(struct bch_fs *c, struct bch_dev *ca, + u64 old_nbuckets, u64 new_nbuckets, + u64 seq, struct printbuf *err) +{ + int ret; + + /* + * Re-run journal relocation as a final fence before we commit the new + * size: the current journal bucket may have advanced while we were + * waiting for the tail to drain, and we must not expose the smaller + * nbuckets while journal state can still reference the truncated tail. + */ + ret = move_journal_past_cutoff(c, ca, new_nbuckets, err); + if (ret) + return ret; + + ret = bch2_dev_resize_restart_check(ca, seq); + if (ret) + return ret; + + scoped_guard(rwsem_write, &c->state_lock) { + bool empty = false; + + ret = bch2_dev_resize_restart_check(ca, seq); + if (ret) + return ret; + + if (bch2_dev_resize_target(ca) != new_nbuckets || + !bch2_dev_is_shrinking(ca)) + return -EAGAIN; + + /* flush interior updates - mirroring dev remove path */ + bch2_btree_interior_updates_flush(c); + + /* flush journal - mirroring dev remove path */ + bch2_journal_flush_all_pins(&c->journal); + + ret = bch2_journal_flush_device_pins(&c->journal, ca->dev_idx); + if (ret) { + prt_printf(err, "bch2_journal_flush_device_pins() error: %s\n", + bch2_err_str(ret)); + return ret; + } + + ret = bch2_journal_flush(&c->journal); + if (ret) { + prt_printf(err, "bch2_journal_flush() error: %s\n", + bch2_err_str(ret)); + return ret; + } + + /* re-check that tail is really empty */ + ret = tail_is_empty(c, ca, new_nbuckets, err, &empty); + if (ret) + return ret; + if (!empty) { + prt_printf(err, "Shrink failed: still has data\n"); + return -EBUSY; + } + + /* + * Buckets in the truncated tail may still be in NEED_DISCARD + * state. Clear that bookkeeping before we drop the alloc range so + * accounting/fsck do not retain tail-only metadata after shrink. + */ + ret = bch2_dev_clear_need_discard(c, ca, new_nbuckets); + if (ret) { + prt_printf(err, "error clearing need_discard state: %s\n", + bch2_err_str(ret)); + return ret; + } + + /* drop references to now-truncated superblock copies */ + ret = drop_sbs_after_cutoff(c, ca, new_nbuckets); + if (ret) { + prt_printf(err, "Error dropping superblocks after cutoff: %s\n", + bch2_err_str(ret)); + return ret; + } + + /* update accounting info - has to happen before truncating alloc info */ + ret = bch2_dev_truncate_accounting(c, ca, old_nbuckets, new_nbuckets); + if (ret) { + prt_printf(err, "error updating accounting info: %s\n", + bch2_err_str(ret)); + return ret; + } + + /* truncate alloc info */ + ret = bch2_dev_remove_alloc(c, ca, new_nbuckets); + if (ret) { + prt_printf(err, "error truncating alloc info: %s\n", + bch2_err_str(ret)); + return ret; + } + + /* + * Commit the shrink only after the truncated tail has been + * removed from alloc metadata, so later transactions can't see + * stale tail buckets after the new size is visible. + */ + scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { + guard(mutex)(&c->sb_lock); + struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); + m->nbuckets = cpu_to_le64(new_nbuckets); + if (bch2_dev_resize_target(ca) == new_nbuckets) + m->target_nbuckets = 0; + + ret = bch2_write_super(c); + if (ret) + return ret; + } + + /* resize buckets */ + ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); + if (ret) { + prt_printf(err, "bch2_dev_buckets_resize() error: %s\n", + bch2_err_str(ret)); + return ret; + } + + // TODO: figure out what parts of this path still need doing + // if (ca->mi.freespace_initialized) { + // ret = __bch2_dev_resize_alloc(ca, old_nbuckets, new_nbuckets); + // if (ret) { + // prt_printf(err, "__bch2_dev_resize_alloc() error: %s\n", bch2_err_str(ret)); + // return ret; + // } + // } + + bch2_recalc_capacity(c); + } + + return 0; +} + static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, u64 seq, struct printbuf *err) { u64 old_nbuckets = ca->mi.nbuckets; - int ret = 0; scoped_guard(rwsem_write, &c->state_lock) { @@ -1809,135 +1953,20 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, /* * A consumed scan cookie only means reconcile observed the scan - * request and queued downstream work. Wait for a completed pass - * so the scan-generated normal and pending work has been - * attempted before we decide the tail still cannot drain. + * request and queued downstream work. Wait until either the + * requested pass completes or the tail is already empty before we + * decide whether shrink needs another evacuation pass. */ ret = bch2_dev_shrink_queue_reconcile(c, ca, pass == 0, &kick, err); if (ret) return ret; - ret = bch2_dev_shrink_wait_reconcile(ca, seq, kick); + ret = bch2_dev_shrink_wait_reconcile(ca, new_nbuckets, seq, kick, err); if (ret) return ret; } - /* - * Re-run journal relocation as a final fence before we commit the new - * size: the current journal bucket may have advanced while we were - * waiting for the tail to drain, and we must not expose the smaller - * nbuckets while journal state can still reference the truncated tail. - */ - ret = move_journal_past_cutoff(c, ca, new_nbuckets, err); - if (ret) - return ret; - - ret = bch2_dev_resize_restart_check(ca, seq); - if (ret) - return ret; - - scoped_guard(rwsem_write, &c->state_lock) { - ret = bch2_dev_resize_restart_check(ca, seq); - if (ret) - return ret; - - if (bch2_dev_resize_target(ca) != new_nbuckets || - !bch2_dev_is_shrinking(ca)) - return -EAGAIN; - - /* flush interior updates - mirroring dev remove path */ - bch2_btree_interior_updates_flush(c); - - /* flush journal - mirroring dev remove path */ - bch2_journal_flush_all_pins(&c->journal); - - ret = bch2_journal_flush_device_pins(&c->journal, ca->dev_idx); - if (ret) { - prt_printf(err, "bch2_journal_flush_device_pins() error: %s\n", bch2_err_str(ret)); - return ret; - } - - ret = bch2_journal_flush(&c->journal); - if (ret) { - prt_printf(err, "bch2_journal_flush() error: %s\n", bch2_err_str(ret)); - return ret; - } - - /* re-check that tail is really empty */ - bool empty = false; - try(tail_is_empty(c, ca, new_nbuckets, err, &empty)); - if (!empty) { - prt_printf(err, "Shrink failed: still has data\n"); - return -EBUSY; - } - - /* - * Buckets in the truncated tail may still be in NEED_DISCARD - * state. Clear that bookkeeping before we drop the alloc range so - * accounting/fsck do not retain tail-only metadata after shrink. - */ - ret = bch2_dev_clear_need_discard(c, ca, new_nbuckets); - if (ret) { - prt_printf(err, "error clearing need_discard state: %s\n", - bch2_err_str(ret)); - return ret; - } - - /* drop references to now-truncated superblock copies */ - ret = drop_sbs_after_cutoff(c, ca, new_nbuckets); - if (ret) { - prt_printf(err, "Error dropping superblocks after cutoff: %s\n", bch2_err_str(ret)); - return ret; - } - - /* update accounting info - has to happen before truncating alloc info */ - ret = bch2_dev_truncate_accounting(c, ca, old_nbuckets, new_nbuckets); - if (ret) { - prt_printf(err, "error updating accounting info: %s\n", bch2_err_str(ret)); - return ret; - } - - /* truncate alloc info */ - ret = bch2_dev_remove_alloc(c, ca, new_nbuckets); - if (ret) { - prt_printf(err, "error truncating alloc info: %s\n", bch2_err_str(ret)); - return ret; - } - - /* - * Commit the shrink only after the truncated tail has been - * removed from alloc metadata, so later transactions can't see - * stale tail buckets after the new size is visible. - */ - scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS) { - guard(mutex)(&c->sb_lock); - struct bch_member *m = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); - m->nbuckets = cpu_to_le64(new_nbuckets); - if (bch2_dev_resize_target(ca) == new_nbuckets) - m->target_nbuckets = 0; - - try(bch2_write_super(c)); - } - - /* resize buckets */ - ret = bch2_dev_buckets_resize(c, ca, new_nbuckets); - if (ret) { - prt_printf(err, "bch2_dev_buckets_resize() error: %s\n", bch2_err_str(ret)); - return ret; - } - - // TODO: figure out what parts of this path still need doing - // if (ca->mi.freespace_initialized) { - // ret = __bch2_dev_resize_alloc(ca, old_nbuckets, new_nbuckets); - // if (ret) { - // prt_printf(err, "__bch2_dev_resize_alloc() error: %s\n", bch2_err_str(ret)); - // return ret; - // } - // } - - bch2_recalc_capacity(c); - } - return 0; + return bch2_dev_shrink_finish(c, ca, old_nbuckets, new_nbuckets, seq, err); } static int bch2_dev_resize_thread(void *arg) From 6161dce3de104a8c621c68554ef03b284e87084a Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 20 Apr 2026 00:16:26 +0200 Subject: [PATCH 53/75] bcachefs: bound shrink's final journal pin flush Shrink's final commit path only needs to flush journal pins that were already outstanding when it proved the tail empty. Waiting for bch2_journal_flush_all_pins() lets unrelated reconcile/key-cache work keep adding newer pins, which turns the resize ioctl into a multi-minute stall even though the shrinking tail is already clear. Flush only journal_cur_seq() before the device-specific pin flush. That still drains the journal state shrink must fence before it commits the smaller nbuckets, without waiting on unrelated future journal traffic. --- fs/bcachefs/init/dev.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index b5e7c461c71a1..7c6d316b4d719 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1750,8 +1750,15 @@ static int bch2_dev_shrink_finish(struct bch_fs *c, struct bch_dev *ca, /* flush interior updates - mirroring dev remove path */ bch2_btree_interior_updates_flush(c); - /* flush journal - mirroring dev remove path */ - bch2_journal_flush_all_pins(&c->journal); + /* + * Only flush pins that were already outstanding when shrink + * entered the final commit path. Reconcile can continue + * generating unrelated key-cache journal pins on newer + * sequences while the tail is already empty; waiting for every + * future pin here can turn shrink into an unbounded global + * journal drain. + */ + bch2_journal_flush_outstanding_pins(&c->journal); ret = bch2_journal_flush_device_pins(&c->journal, ca->dev_idx); if (ret) { From 19cfd754fdf87c28256ca042ed3e226c80c0a30e Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 20 Apr 2026 20:51:45 +0200 Subject: [PATCH 54/75] bcachefs: add comment --- fs/bcachefs/init/dev.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 7c6d316b4d719..fd66623b721e9 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1315,6 +1315,7 @@ static bool bch2_dev_resize_finish(struct bch_dev *ca, u64 seq, int status) return is_current; } +/* checks for kthread interruption, and resize seq having changed */ static int bch2_dev_resize_restart_check(struct bch_dev *ca, u64 seq) { if (kthread_should_stop()) From f70b79b798379ef518c4e60a1a28bba1a7eb02c2 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 20 Apr 2026 21:05:39 +0200 Subject: [PATCH 55/75] bcachefs: shrink_finish: remove redundant journal moving and restart check restart check is done below after acquiring the lock anyways. the journal move is not needed as we already move the journal at the start of the shrink, and new allocations (and thus journal buckets) are blocked in the tail, so it can't re-appear. --- fs/bcachefs/init/dev.c | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index fd66623b721e9..8f82d6c9dd8f7 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1723,20 +1723,6 @@ static int bch2_dev_shrink_finish(struct bch_fs *c, struct bch_dev *ca, { int ret; - /* - * Re-run journal relocation as a final fence before we commit the new - * size: the current journal bucket may have advanced while we were - * waiting for the tail to drain, and we must not expose the smaller - * nbuckets while journal state can still reference the truncated tail. - */ - ret = move_journal_past_cutoff(c, ca, new_nbuckets, err); - if (ret) - return ret; - - ret = bch2_dev_resize_restart_check(ca, seq); - if (ret) - return ret; - scoped_guard(rwsem_write, &c->state_lock) { bool empty = false; From 9f3a8b5a736990e66d4d96a5e2adfe737bad5b60 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 20 Apr 2026 21:11:04 +0200 Subject: [PATCH 56/75] bcachefs: rename bch2_dev_shrink_finish -> bch2_dev_shrink_finalize imo more descriptive --- fs/bcachefs/init/dev.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 8f82d6c9dd8f7..93e0be83a75e6 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1717,7 +1717,7 @@ static int bch2_dev_shrink_clear_target(struct bch_fs *c, struct bch_dev *ca, return 0; } -static int bch2_dev_shrink_finish(struct bch_fs *c, struct bch_dev *ca, +static int bch2_dev_shrink_finalize(struct bch_fs *c, struct bch_dev *ca, u64 old_nbuckets, u64 new_nbuckets, u64 seq, struct printbuf *err) { @@ -1960,7 +1960,7 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, return ret; } - return bch2_dev_shrink_finish(c, ca, old_nbuckets, new_nbuckets, seq, err); + return bch2_dev_shrink_finalize(c, ca, old_nbuckets, new_nbuckets, seq, err); } static int bch2_dev_resize_thread(void *arg) From 11c8da53a124a3adb3d8ab02dba247c80b43c01b Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 20 Apr 2026 21:17:54 +0200 Subject: [PATCH 57/75] bcachefs: shorten comment about waking pending reconcile on shrink cancel --- fs/bcachefs/init/dev.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 93e0be83a75e6..05b992243bd35 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1705,11 +1705,7 @@ static int bch2_dev_shrink_clear_target(struct bch_fs *c, struct bch_dev *ca, try(bch2_dev_resize_update_target(c, ca, 0, err)); } - /* - * Canceling the persisted shrink target lifts the shrink cutoff. Retry - * pending reconcile work so anything that was parked behind the failed - * shrink gets re-evaluated under the restored placement rules. - */ + /* allocations are now no longer blocked after the cutoff, so there may now be more usable space */ ret = bch2_reconcile_pending_wakeup(c); if (ret) bch_err_fn(c, ret); From 5b983240a3e2516fa8914cb916f37accff35af35 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 20 Apr 2026 21:26:35 +0200 Subject: [PATCH 58/75] bcachefs: shrink: add TODO --- fs/bcachefs/init/dev.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 05b992243bd35..33867f7426e02 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1706,7 +1706,7 @@ static int bch2_dev_shrink_clear_target(struct bch_fs *c, struct bch_dev *ca, } /* allocations are now no longer blocked after the cutoff, so there may now be more usable space */ - ret = bch2_reconcile_pending_wakeup(c); + ret = bch2_reconcile_pending_wakeup(c); // TODO: also do this when a user requests a shrink cancel (aka a resize to the current size) if (ret) bch_err_fn(c, ret); From 3f605d30a057bc86e1aef328cc3be471c1651ba4 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 20 Apr 2026 21:29:48 +0200 Subject: [PATCH 59/75] bcachefs: shrink: change not-enough-space error message --- fs/bcachefs/init/dev.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 33867f7426e02..e20830203c8ef 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1934,7 +1934,7 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, if (pass >= 2) { prt_printf(err, - "Shrink failed: insufficient space or replicas to evacuate data from the shrink tail\n"); + "Shrink failed: insufficient space on the filesystem to evacuate data from the shrink tail\n"); ret = bch2_dev_shrink_clear_target(c, ca, new_nbuckets, seq, err); if (ret) return ret; From a2c5355977eeeff6c78b444746f448b673689cea Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 20 Apr 2026 21:40:56 +0200 Subject: [PATCH 60/75] bcachefs: shrink: bound stalled tail detection on reconcile work Shrink still used a wall-clock no-progress deadline after moving to tail head plus aggregate backpointer tracking. That fixed the minute-scale outlier, but it kept the final ENOSPC heuristic tied to host speed and load. Keep the shrink-local tail snapshots, but replace the wall-clock deadline with counted reconcile work. Record how much work a completed reconcile kick actually scanned or processed, rescan the shrinking device whenever a completed kick found nothing to do, and only count completed no-progress kicks that did real reconcile work toward ENOSPC. Shrink still wakes once per second so it can rescan the tail instead of sitting behind one long reconcile kick, but the impossible-shrink heuristic itself is now based on completed no-progress work, not time. --- fs/bcachefs/data/reconcile/types.h | 1 + fs/bcachefs/data/reconcile/work.c | 4 + fs/bcachefs/data/reconcile/work.h | 5 + fs/bcachefs/init/dev.c | 341 ++++++++++++++++++++++++++--- 4 files changed, 320 insertions(+), 31 deletions(-) diff --git a/fs/bcachefs/data/reconcile/types.h b/fs/bcachefs/data/reconcile/types.h index a0e77addf848d..74204083f4ba5 100644 --- a/fs/bcachefs/data/reconcile/types.h +++ b/fs/bcachefs/data/reconcile/types.h @@ -13,6 +13,7 @@ struct bch_fs_reconcile { atomic_t kick; wait_queue_head_t wait; atomic_t completed_kick; + atomic64_t completed_work_units; bool running; u64 wait_iotime_start; diff --git a/fs/bcachefs/data/reconcile/work.c b/fs/bcachefs/data/reconcile/work.c index 986e0f271664d..31584cb6936e5 100644 --- a/fs/bcachefs/data/reconcile/work.c +++ b/fs/bcachefs/data/reconcile/work.c @@ -1491,6 +1491,9 @@ static int do_reconcile(struct moving_context *ctxt) if (!ret && pass_complete && kick == atomic_read(&r->kick)) { + atomic64_set(&r->completed_work_units, + atomic64_read(&r->work_stats.sectors_seen) + + sectors_scanned); atomic_set(&r->completed_kick, kick); wake_up_all(&r->wait); } @@ -1695,6 +1698,7 @@ int bch2_fs_reconcile_init(struct bch_fs *c) atomic_set(&r->kick, 0); init_waitqueue_head(&r->wait); atomic_set(&r->completed_kick, 0); + atomic64_set(&r->completed_work_units, 0); #ifdef CONFIG_POWER_SUPPLY r->power_notifier.notifier_call = bch2_reconcile_power_notifier; diff --git a/fs/bcachefs/data/reconcile/work.h b/fs/bcachefs/data/reconcile/work.h index 5da3f504d2222..77fdad35f35d7 100644 --- a/fs/bcachefs/data/reconcile/work.h +++ b/fs/bcachefs/data/reconcile/work.h @@ -51,6 +51,11 @@ static inline u32 bch2_reconcile_completed_kick(struct bch_fs *c) return atomic_read(&c->reconcile.completed_kick); } +static inline u64 bch2_reconcile_completed_work_units(struct bch_fs *c) +{ + return atomic64_read(&c->reconcile.completed_work_units); +} + static inline int bch2_reconcile_pending_wakeup(struct bch_fs *c) { return bch2_set_reconcile_needs_scan(c, diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index e20830203c8ef..08bf8d2c0c472 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -215,13 +215,16 @@ #include "alloc/foreground.h" #include "bcachefs_format.h" +#include "btree/bkey_methods.h" #include "btree/bkey_types.h" #include "btree/interior.h" #include "btree/iter.h" +#include "btree/update.h" #include "btree/types.h" #include "btree/write_buffer.h" #include "data/ec/init.h" +#include "data/extents.h" #include "data/migrate.h" #include "data/reconcile/work.h" @@ -1595,9 +1598,60 @@ static int move_journal_past_cutoff(struct bch_fs *c, struct bch_dev *ca, } } +struct shrink_tail_head { + struct bpos bucket; + struct bpos first_bp; + unsigned nr_backpointers; +}; + +struct shrink_tail_progress { + struct shrink_tail_head head; + u64 nr_backpointers; +}; + +static inline bool shrink_tail_head_empty(const struct shrink_tail_head *head) +{ + return bpos_eq(head->first_bp, SPOS_MAX); +} + +static bool shrink_tail_head_progressed(const struct shrink_tail_head *old, + const struct shrink_tail_head *new) +{ + if (shrink_tail_head_empty(new)) + return true; + + if (shrink_tail_head_empty(old)) + return false; + + if (!bpos_eq(new->bucket, old->bucket)) + return bpos_gt(new->bucket, old->bucket); + + if (!bpos_eq(new->first_bp, old->first_bp)) + return bpos_gt(new->first_bp, old->first_bp); + + return new->nr_backpointers < old->nr_backpointers; +} + +static bool shrink_tail_progressed(const struct shrink_tail_progress *old, + const struct shrink_tail_progress *new) +{ + if (shrink_tail_head_empty(&new->head)) + return true; + + if (shrink_tail_head_empty(&old->head)) + return false; + + if (new->nr_backpointers < old->nr_backpointers) + return true; + + return shrink_tail_head_progressed(&old->head, &new->head); +} + // TODO: make sure everything is caught here. Maybe look at bch2_dev_has_data for this // Fore example journals and superblocks might need special handling -static int tail_is_empty(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct printbuf *err, bool *empty) { +static int tail_head_snapshot(struct bch_fs *c, struct bch_dev *ca, + u64 new_nbuckets, struct shrink_tail_head *head) +{ struct bpos bp_start = bucket_pos_to_bp_start(ca, POS(ca->dev_idx, new_nbuckets)); struct bpos bp_end = bucket_pos_to_bp_start(ca, POS(ca->dev_idx, ca->mi.nbuckets)); @@ -1611,10 +1665,168 @@ static int tail_is_empty(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, try(bkey_err(bp)); - *empty = !bp.k; + *head = (struct shrink_tail_head) { + .bucket = SPOS_MAX, + .first_bp = SPOS_MAX, + }; + + if (!bp.k) + return 0; + + head->bucket = bp_pos_to_bucket(ca, bp.k->p); + head->first_bp = bp.k->p; + + do { + head->nr_backpointers++; + bch2_bp_scan_iter_advance(&iter); + bp = bch2_bp_scan_iter_peek(trans, &iter, bp_end, &last_flushed); + try(bkey_err(bp)); + } while (bp.k && bpos_eq(bp_pos_to_bucket(ca, bp.k->p), head->bucket)); + + return 0; +} + +static int tail_progress_snapshot(struct bch_fs *c, struct bch_dev *ca, + u64 new_nbuckets, + struct shrink_tail_progress *progress) +{ + struct bpos bp_start = bucket_pos_to_bp_start(ca, POS(ca->dev_idx, new_nbuckets)); + struct bpos bp_end = bucket_pos_to_bp_start(ca, POS(ca->dev_idx, ca->mi.nbuckets)); + + CLASS(btree_trans, trans)(c); + CLASS(backpointer_scan_iter, iter)(BTREE_ID_backpointers, bp_start, NULL); + + struct wb_maybe_flush last_flushed __cleanup(wb_maybe_flush_exit); + wb_maybe_flush_init(&last_flushed); + + *progress = (struct shrink_tail_progress) { + .head.bucket = SPOS_MAX, + .head.first_bp = SPOS_MAX, + }; + + while (true) { + struct bkey_s_c_backpointer bp = + bch2_bp_scan_iter_peek(trans, &iter, bp_end, &last_flushed); + + try(bkey_err(bp)); + if (!bp.k) + return 0; + + if (shrink_tail_head_empty(&progress->head)) { + progress->head.bucket = bp_pos_to_bucket(ca, bp.k->p); + progress->head.first_bp = bp.k->p; + } + + if (bpos_eq(bp_pos_to_bucket(ca, bp.k->p), progress->head.bucket)) + progress->head.nr_backpointers++; + + progress->nr_backpointers++; + bch2_bp_scan_iter_advance(&iter); + } +} + +static int tail_is_empty(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, + bool *empty) +{ + struct shrink_tail_head head; + + try(tail_head_snapshot(c, ca, new_nbuckets, &head)); + *empty = shrink_tail_head_empty(&head); + return 0; +} + +static int bch2_dev_shrink_invalidate_bp(struct btree_trans *trans, + struct bch_dev *ca, + struct bkey_s_c_backpointer bp, + struct wb_maybe_flush *last_flushed) +{ + struct bch_fs *c = trans->c; + + CLASS(btree_iter_uninit, iter)(trans); + struct bkey_s_c k = bkey_try(bch2_backpointer_get_key(trans, bp, &iter, 0, last_flushed)); + if (!k.k) + return 0; + + struct bkey_i *n = errptr_try(bch2_bkey_make_mut(trans, &iter, &k, + BTREE_UPDATE_internal_snapshot_node)); + + bch2_bkey_drop_device_noerror(c, bkey_i_to_s(n), ca->dev_idx); + + if (!bch2_bkey_can_read(c, bkey_i_to_s_c(n))) + bch2_set_bkey_error(c, n, KEY_TYPE_ERROR_device_removed); + return 0; } +static int bch2_dev_shrink_invalidate_cached_bucket(struct btree_trans *trans, + struct bch_dev *ca, + struct bpos bucket, + u8 gen, + struct wb_maybe_flush *last_flushed) +{ + struct bpos bp_start = bucket_pos_to_bp_start(ca, bucket); + struct bpos bp_end = bucket_pos_to_bp_end(ca, bucket); + + return for_each_btree_key_max_commit(trans, iter, BTREE_ID_backpointers, + bp_start, bp_end, 0, k, + NULL, NULL, + BCH_WATERMARK_btree| + BCH_TRANS_COMMIT_no_enospc, ({ + if (k.k->type != KEY_TYPE_backpointer) + continue; + + struct bkey_s_c_backpointer bp = bkey_s_c_to_backpointer(k); + + if (bp.v->bucket_gen != gen) + continue; + + try(bch2_dev_shrink_invalidate_bp(trans, ca, bp, last_flushed)); + 0; + })); +} + +static int bch2_dev_shrink_invalidate_tail_cached(struct bch_fs *c, struct bch_dev *ca, + u64 new_nbuckets, bool *invalidated) +{ + struct bpos start = POS(ca->dev_idx, new_nbuckets); + struct bpos end = POS(ca->dev_idx, ca->mi.nbuckets - 1); + CLASS(btree_trans, trans)(c); + + struct wb_maybe_flush last_flushed __cleanup(wb_maybe_flush_exit); + wb_maybe_flush_init(&last_flushed); + + /* + * Shrink evacuation rewrites live data elsewhere but leaves the old + * buckets behind as cached copies. Those cached-only tail buckets don't + * need reconcile, yet they still keep backpointers alive and make + * tail_is_empty() fail. Drop them directly so shrink only waits on + * buckets that still hold durable data or metadata. + */ + *invalidated = false; + + try(bch2_btree_write_buffer_flush_sync(trans)); + + return for_each_btree_key_max_commit(trans, iter, BTREE_ID_alloc, + start, end, BTREE_ITER_prefetch, k, + NULL, NULL, + BCH_WATERMARK_btree| + BCH_TRANS_COMMIT_no_enospc, ({ + struct bch_alloc_v4 a_convert; + const struct bch_alloc_v4 *a = bch2_alloc_to_v4(k, &a_convert); + + if (a->data_type != BCH_DATA_cached || !a->cached_sectors) + continue; + + if (bch2_bucket_is_open_safe(c, k.k->p.inode, k.k->p.offset)) + continue; + + try(bch2_dev_shrink_invalidate_cached_bucket(trans, ca, k.k->p, + a->gen, &last_flushed)); + *invalidated = true; + 0; + })); +} + static int bch2_dev_shrink_queue_reconcile(struct bch_fs *c, struct bch_dev *ca, bool scan_device, u32 *kick, struct printbuf *err) @@ -1653,12 +1865,14 @@ static int bch2_dev_shrink_queue_reconcile(struct bch_fs *c, struct bch_dev *ca, static int bch2_dev_shrink_wait_reconcile(struct bch_dev *ca, u64 new_nbuckets, u64 seq, u32 kick, + const struct shrink_tail_head *head_before, + bool *kick_complete, struct printbuf *err) { struct bch_fs *c = ca->fs; while (true) { - bool empty = false; + bool completed; int ret = bch2_dev_resize_restart_check(ca, seq); if (ret) return ret; @@ -1668,14 +1882,21 @@ static int bch2_dev_shrink_wait_reconcile(struct bch_dev *ca, u64 new_nbuckets, * actually empty. A kick can continue chewing through unrelated * global reconcile work for minutes after the truncating region is * already evacuated, especially with fragmented variable-bucket - * workloads. Poll tail emptiness so shrink can move on as soon as - * the region being truncated is clear. + * workloads. Poll the head of the shrink tail and bound each wait + * to a one-second slice so shrink can rescan its own state instead + * of sitting behind one long reconcile kick. */ - try(tail_is_empty(c, ca, new_nbuckets, err, &empty)); + struct shrink_tail_head head_after; - if (empty || - bch2_reconcile_completed_kick(c) >= kick) + try(tail_head_snapshot(c, ca, new_nbuckets, &head_after)); + completed = bch2_reconcile_completed_kick(c) >= kick; + + if (shrink_tail_head_empty(&head_after) || + shrink_tail_head_progressed(head_before, &head_after) || + completed) { + *kick_complete = completed; return 0; + } ret = wait_event_killable_timeout(c->reconcile.wait, bch2_reconcile_completed_kick(c) >= kick || @@ -1684,6 +1905,10 @@ static int bch2_dev_shrink_wait_reconcile(struct bch_dev *ca, u64 new_nbuckets, HZ); if (ret < 0) return -EINTR; + if (!ret) { + *kick_complete = false; + return 0; + } } } @@ -1758,7 +1983,7 @@ static int bch2_dev_shrink_finalize(struct bch_fs *c, struct bch_dev *ca, } /* re-check that tail is really empty */ - ret = tail_is_empty(c, ca, new_nbuckets, err, &empty); + ret = tail_is_empty(c, ca, new_nbuckets, &empty); if (ret) return ret; if (!empty) { @@ -1900,45 +2125,75 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, if (ret) return ret; - /* TODO: - * When we evacuate data, the original is left in place and marked as cached, just like when moving it for target/compression regions. - * We need to somehow handle this: - * - Maybe we can just not count it in tail_is_empty() and just shrink it away - but this would likely leave some metadata incorrect - * - We could just make it completely remove the data if it is evacuating, instead of marking it as cached - * - We could make passes that check for cached buckets in the tail, and close/remove those - we may actually need to handle buckets anyways - */ - /* wait for to-be-shrunk region to be empty */ + const unsigned stalled_kicks_limit = 32; + struct shrink_tail_progress best_progress = { + .head.bucket = SPOS_MAX, + .head.first_bp = SPOS_MAX, + }; + bool scan_device = true; + unsigned stalled_kicks = 0; + for (unsigned pass = 0; ; pass++) { - bool empty = false; + bool invalidated_cached = false; + bool kick_complete; + struct shrink_tail_head head; + struct shrink_tail_progress progress; u32 kick; ret = bch2_dev_resize_restart_check(ca, seq); if (ret) return ret; - try(tail_is_empty(c, ca, new_nbuckets, err, &empty)); + try(tail_head_snapshot(c, ca, new_nbuckets, &head)); /* do a definitive check */ - if (empty) { + if (shrink_tail_head_empty(&head)) { { CLASS(btree_trans, trans)(c); try(bch2_btree_write_buffer_flush_sync(trans)); } - try(tail_is_empty(c, ca, new_nbuckets, err, &empty)); - if (empty) { + try(tail_head_snapshot(c, ca, new_nbuckets, &head)); + if (shrink_tail_head_empty(&head)) { break; } } - if (pass >= 2) { - prt_printf(err, - "Shrink failed: insufficient space on the filesystem to evacuate data from the shrink tail\n"); - ret = bch2_dev_shrink_clear_target(c, ca, new_nbuckets, seq, err); - if (ret) - return ret; - return -ENOSPC; + ret = bch2_dev_shrink_invalidate_tail_cached(c, ca, new_nbuckets, + &invalidated_cached); + if (ret) { + prt_printf(err, "Failed to invalidate cached shrink-tail data: %s\n", + bch2_err_str(ret)); + return ret; + } + + if (invalidated_cached) { + { + CLASS(btree_trans, trans)(c); + try(bch2_btree_write_buffer_flush_sync(trans)); + } + + try(tail_head_snapshot(c, ca, new_nbuckets, &head)); + if (shrink_tail_head_empty(&head)) + break; + } + + /* + * Live foreground IO can require several shrink/reconcile passes + * before the leading tail bucket drains, and the head bucket can + * stay put while work elsewhere in the tail is still making + * space for it. Track both the leading bucket and the total tail + * backpointer count, then only fail after many completed + * no-progress reconcile kicks that actually scanned or processed + * shrink work instead of using a wall-clock timeout. + */ + try(tail_progress_snapshot(c, ca, new_nbuckets, &progress)); + + if (shrink_tail_progressed(&best_progress, &progress)) { + best_progress = progress; + scan_device = false; + stalled_kicks = 0; } /* @@ -1947,13 +2202,37 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, * requested pass completes or the tail is already empty before we * decide whether shrink needs another evacuation pass. */ - ret = bch2_dev_shrink_queue_reconcile(c, ca, pass == 0, &kick, err); + ret = bch2_dev_shrink_queue_reconcile(c, ca, pass == 0 || scan_device, + &kick, err); if (ret) return ret; - ret = bch2_dev_shrink_wait_reconcile(ca, new_nbuckets, seq, kick, err); + ret = bch2_dev_shrink_wait_reconcile(ca, new_nbuckets, seq, kick, + &head, &kick_complete, err); if (ret) return ret; + + try(tail_progress_snapshot(c, ca, new_nbuckets, &progress)); + + if (shrink_tail_head_empty(&progress.head)) + break; + + if (shrink_tail_progressed(&best_progress, &progress)) { + best_progress = progress; + scan_device = false; + stalled_kicks = 0; + } else if (kick_complete) { + scan_device = !bch2_reconcile_completed_work_units(c); + if (bch2_reconcile_completed_work_units(c) && + ++stalled_kicks >= stalled_kicks_limit) { + prt_printf(err, + "Shrink failed: insufficient space on the filesystem to evacuate data from the shrink tail\n"); + ret = bch2_dev_shrink_clear_target(c, ca, new_nbuckets, seq, err); + if (ret) + return ret; + return -ENOSPC; + } + } } return bch2_dev_shrink_finalize(c, ca, old_nbuckets, new_nbuckets, seq, err); From f4c7603ef0c095c79cf882dec7bb01b8fb3d0c06 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 21 Apr 2026 00:10:30 +0200 Subject: [PATCH 61/75] bcachefs: shrink: only fail after quiescent stalled rescans A shrink tail can stay flat for many reconcile kicks while foreground IO or reconcile itself is still changing metadata. Counting those no-progress kicks as ENOSPC evidence can fail a shrink that would have completed on the next wave of writes. Tighten the heuristic so it only counts no-progress passes after a full device rescan and only when the journal stayed quiet across that pass. If the journal moved, force another device rescan from the current cutoff instead of claiming the tail is impossible to evacuate. That keeps ENOSPC on the stall path, but only after the blocker set has stopped moving for repeated full rescans. --- fs/bcachefs/data/reconcile/types.h | 1 - fs/bcachefs/data/reconcile/work.c | 4 ---- fs/bcachefs/data/reconcile/work.h | 5 ----- fs/bcachefs/init/dev.c | 32 ++++++++++++++++++++++-------- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/fs/bcachefs/data/reconcile/types.h b/fs/bcachefs/data/reconcile/types.h index 74204083f4ba5..a0e77addf848d 100644 --- a/fs/bcachefs/data/reconcile/types.h +++ b/fs/bcachefs/data/reconcile/types.h @@ -13,7 +13,6 @@ struct bch_fs_reconcile { atomic_t kick; wait_queue_head_t wait; atomic_t completed_kick; - atomic64_t completed_work_units; bool running; u64 wait_iotime_start; diff --git a/fs/bcachefs/data/reconcile/work.c b/fs/bcachefs/data/reconcile/work.c index 31584cb6936e5..986e0f271664d 100644 --- a/fs/bcachefs/data/reconcile/work.c +++ b/fs/bcachefs/data/reconcile/work.c @@ -1491,9 +1491,6 @@ static int do_reconcile(struct moving_context *ctxt) if (!ret && pass_complete && kick == atomic_read(&r->kick)) { - atomic64_set(&r->completed_work_units, - atomic64_read(&r->work_stats.sectors_seen) + - sectors_scanned); atomic_set(&r->completed_kick, kick); wake_up_all(&r->wait); } @@ -1698,7 +1695,6 @@ int bch2_fs_reconcile_init(struct bch_fs *c) atomic_set(&r->kick, 0); init_waitqueue_head(&r->wait); atomic_set(&r->completed_kick, 0); - atomic64_set(&r->completed_work_units, 0); #ifdef CONFIG_POWER_SUPPLY r->power_notifier.notifier_call = bch2_reconcile_power_notifier; diff --git a/fs/bcachefs/data/reconcile/work.h b/fs/bcachefs/data/reconcile/work.h index 77fdad35f35d7..5da3f504d2222 100644 --- a/fs/bcachefs/data/reconcile/work.h +++ b/fs/bcachefs/data/reconcile/work.h @@ -51,11 +51,6 @@ static inline u32 bch2_reconcile_completed_kick(struct bch_fs *c) return atomic_read(&c->reconcile.completed_kick); } -static inline u64 bch2_reconcile_completed_work_units(struct bch_fs *c) -{ - return atomic64_read(&c->reconcile.completed_work_units); -} - static inline int bch2_reconcile_pending_wakeup(struct bch_fs *c) { return bch2_set_reconcile_needs_scan(c, diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 08bf8d2c0c472..f225323cca421 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -231,6 +231,7 @@ #include "debug/sysfs.h" #include "journal/init.h" +#include "journal/journal.h" #include "journal/reclaim.h" #include "init/dev.h" @@ -2184,9 +2185,11 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, * before the leading tail bucket drains, and the head bucket can * stay put while work elsewhere in the tail is still making * space for it. Track both the leading bucket and the total tail - * backpointer count, then only fail after many completed - * no-progress reconcile kicks that actually scanned or processed - * shrink work instead of using a wall-clock timeout. + * backpointer count, but only count no-progress passes after a + * full device rescan on a journal-quiescent state; if the + * journal is still moving then foreground IO or reconcile is + * still mutating metadata and a no-progress pass is not evidence + * that the shrink tail is impossible to evacuate. */ try(tail_progress_snapshot(c, ca, new_nbuckets, &progress)); @@ -2202,10 +2205,13 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, * requested pass completes or the tail is already empty before we * decide whether shrink needs another evacuation pass. */ - ret = bch2_dev_shrink_queue_reconcile(c, ca, pass == 0 || scan_device, - &kick, err); + bool did_scan = pass == 0 || scan_device; + u64 journal_seq_before; + + ret = bch2_dev_shrink_queue_reconcile(c, ca, did_scan, &kick, err); if (ret) return ret; + journal_seq_before = journal_cur_seq(&c->journal); ret = bch2_dev_shrink_wait_reconcile(ca, new_nbuckets, seq, kick, &head, &kick_complete, err); @@ -2222,9 +2228,19 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, scan_device = false; stalled_kicks = 0; } else if (kick_complete) { - scan_device = !bch2_reconcile_completed_work_units(c); - if (bch2_reconcile_completed_work_units(c) && - ++stalled_kicks >= stalled_kicks_limit) { + /* + * A no-progress pass only counts toward ENOSPC after a + * full device rescan on a quiescent journal state. If + * metadata is still being committed then the set of tail + * blockers is still moving, so rescan from the current + * tail instead of treating the shrink as impossible. + */ + if (journal_cur_seq(&c->journal) != journal_seq_before) { + scan_device = true; + stalled_kicks = 0; + } else if (!did_scan) { + scan_device = true; + } else if (++stalled_kicks >= stalled_kicks_limit) { prt_printf(err, "Shrink failed: insufficient space on the filesystem to evacuate data from the shrink tail\n"); ret = bch2_dev_shrink_clear_target(c, ca, new_nbuckets, seq, err); From d2a62b04dbdf826f75c734329003dbb927941266 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 21 Apr 2026 01:02:10 +0200 Subject: [PATCH 62/75] bcachefs: shrink: document journal as imperfect progress signal --- fs/bcachefs/init/dev.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index f225323cca421..6e0590fbdf507 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -2234,6 +2234,12 @@ static int __bch2_dev_shrink(struct bch_fs *c, struct bch_dev *ca, * metadata is still being committed then the set of tail * blockers is still moving, so rescan from the current * tail instead of treating the shrink as impossible. + * + * This is intentionally conservative but the signal is + * filesystem-global: unrelated metadata-writing IO can + * advance the journal and keep the heuristic from ever + * counting a stalled pass. A later cleanup should narrow + * this to a shrink-local churn signal. */ if (journal_cur_seq(&c->journal) != journal_seq_before) { scan_device = true; From ea79de8e9a4c58e6a7685071231ed60a2ddc945f Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 15 Jun 2026 14:18:06 +0200 Subject: [PATCH 63/75] bcachefs: shrink: add shrinking docs --- fs/bcachefs/init/dev.c | 167 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 165 insertions(+), 2 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 6e0590fbdf507..ec55089668b54 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -167,14 +167,177 @@ * * \subsubsection{Resize} * - * \texttt{bcachefs device resize} grows a device to use additional space - * (shrinking is not yet supported). If no size is specified, the device grows to + * \texttt{bcachefs device resize} resizes a device to use more or less space. + * If no size is specified, the device grows to * fill its underlying block device. Resize works online---no unmount required. + * + * \subsubsubsection{Growing} * The new size is subject to a maximum bucket count * (\texttt{BCH\_MEMBER\_NBUCKETS\_MAX}); resize will fail if the requested size * would exceed this limit. After resize, the reconcile subsystem is notified to * account for the newly available space. * + * \subsubsubsection{Shrinking} + * Shrinking removes the tail of the device's bucket range, evacuates any data + * living there, and commits the smaller \texttt{nbuckets}. The truncating region is called the + * \emph{shrink tail}: buckets \texttt{[target\_nbuckets, nbuckets)}. + * + * Once \texttt{target\_nbuckets < nbuckets} is persisted, several subsystems + * react to the pending shrink before the worker has finished evacuation: + * + * \begin{itemize} + * \item The \hyperref[sec:write-points]{allocator} refuses new open-bucket + * allocations at or beyond the cutoff. + * \item Cached extent pointers past the cutoff are treated as stale and + * dropped on lookup. + * \item Metadata allocation (journal buckets, btree node rewrites) falls back + * to the full filesystem when the preferred metadata target consists only of + * shrinking devices, avoiding a deadlock on metadata writes the shrink + * itself needs. + * \item \hyperref[sec:backpointers]{Backpointer} scans for reconcile start at + * the cutoff instead of bucket zero, avoiding needless requeue of work on + * the retained region. + * \end{itemize} + * + * These immediate effects are what make the shrink cutoff effective as soon as + * the target is persisted. They are driven by normalized helper checks + * (\texttt{bch2\_dev\_is\_shrinking()}) and flip automatically when + * userspace retargets away from shrink. + * + * \textbf{Worker model.} + * Each device has a dedicated kthread (\texttt{bch2\_dev\_resize\_thread()}) + * that processes resize requests. On each wakeup it snapshots the current + * request sequence number and the normalized target, then dispatches to + * either \texttt{__bch2\_dev\_grow()} or \texttt{__bch2\_dev\_shrink()}. + * The thread is restartable rather than strictly cancelable---a newer + * request increments \texttt{resize\_seq} and causes the active pass to + * return \texttt{-EAGAIN} and restart with the latest target. + * The common restart check is \texttt{bch2\_dev\_resize\_restart\_check()}, + * which also handles kthread shutdown via \texttt{-EINTR}. + * + * \textbf{Shrink algorithm.} + * Shrink proceeds through four phases: + * + * \textbf{Phase 1---Enter shrink mode.} + * The worker takes \texttt{state\_lock}, validates the target still matches + * the live request, closes any open buckets in the truncating tail + * (\texttt{bch2\_open\_buckets\_stop()}), and resets alloc cursors. After + * dropping the lock, it flushes the discard workqueues so that an older + * discard pass on the same region does not deadlock the evacuation path. + * + * \textbf{Phase 2---Move journal state out of the tail.} + * \texttt{move\_journal\_past\_cutoff()} runs before the main drain loop. + * The journal is handled explicitly because journal buckets are not something + * shrink should wait for reconcile to discover and evacuate indirectly. + * The function counts journal buckets in the tail, temporarily grows the + * journal if more journal buckets are needed for relocation, forces the + * current journal bucket to advance if it still sits in the tail, flushes + * the journal to persist the relocation, and deletes the now-obsolete journal + * buckets from the tail. Without this step, journal activity can keep + * reintroducing metadata references into the very region shrink is trying to + * empty. + * + * \textbf{Phase 3---Drain the tail.} + * The core loop tracks tail liveness by scanning the backpointer btree in the + * truncating region. The loop is built around several key helpers: + * + * \begin{itemize} + * \item \texttt{tail\_head\_snapshot()} records the first tail bucket, its + * first backpointer, and the number of backpointers in that bucket. + * \item \texttt{tail\_progress\_snapshot()} records the same head data plus + * the total backpointer count across the full tail. + * \item \texttt{tail\_is\_empty()} performs a definitive emptiness check + * after flushing the btree write buffer (to expose buffered updates). + * \end{itemize} + * + * On each pass the loop: + * + * \begin{enumerate} + * \item Snapshots the tail head. If it looks empty, flushes the write buffer + * and rechecks before declaring emptiness. + * \item Invalidates cached-only tail buckets via + * \texttt{bch2\_dev\_shrink\_invalidate\_tail\_cached()}. Cached copies in + * the tail are not durable data---they can be dropped directly rather than + * requiring reconcile to evacuate them. + * \item Queues reconcile work with + * \texttt{bch2\_dev\_shrink\_queue\_reconcile()}: a device backpointer scan + * (starting at the cutoff, not bucket zero) plus a pending scan. + * \item Waits using \texttt{bch2\_dev\_shrink\_wait\_reconcile()}, polling + * once per second and returning when the tail is empty, the requested + * reconcile kick completes, or head progress is detected. This avoids + * waiting for unrelated global reconcile work after the tail is already + * empty. + * \end{enumerate} + * + * The progress heuristic tracks two things: the leading tail bucket and its + * first backpointer, and the total tail backpointer count. A pass that makes + * progress on either metric resets the stall counter. Passes without progress + * are counted only after a full device rescan on a journal-quiescent state; + * if the journal advanced during the pass, the blocker set is still changing + * and the pass is not evidence of impossibility. After + * \texttt{stalled\_kicks\_limit} (32) no-progress full rescans on a + * quiescent journal, shrink declares \texttt{-ENOSPC} and clears the pending + * target (\texttt{bch2\_dev\_shrink\_clear\_target()}) so allocations are no + * longer blocked past the old cutoff. + * + * The journal-quiescence check is filesystem-global: unrelated + * metadata-writing IO elsewhere can keep advancing the journal sequence + * number and suppress the stall counter indefinitely. The intended follow-up + * is a shrink-local churn signal. + * + * \textbf{Phase 4---Finalize the shrink.} + * \texttt{bch2\_dev\_shrink\_finalize()} runs under \texttt{state\_lock} and + * commits the truncation. The ordering is critical: + * + * \begin{enumerate} + * \item Revalidate that the live request still matches this pass. + * \item Flush btree interior updates. + * \item Flush outstanding journal pins (device-specific, then a flush of + * \texttt{journal\_cur\_seq()}---not \texttt{bch2\_journal\_flush\_all\_pins()}, + * which can wait behind unrelated reconcile or key-cache pins indefinitely). + * \item Recheck tail emptiness with \texttt{tail\_is\_empty()}. + * \item Clear \texttt{NEED\_DISCARD} entries for the truncated tail. This must + * scan and filter by decoded bucket position because + * \texttt{BTREE\_ID\_need\_discard} is keyed by journal sequence, not by + * \texttt{(dev, bucket)}. + * \item Drop superblock copies whose offsets are beyond the cutoff with + * \texttt{drop\_sbs\_after\_cutoff()}. + * \item Truncate accounting with \texttt{bch2\_dev\_truncate\_accounting()}. + * \item Remove alloc metadata for the tail with + * \texttt{bch2\_dev\_remove\_alloc()}. + * \item Commit \texttt{nbuckets = new\_nbuckets} in the superblock and clear + * \texttt{target\_nbuckets} if it still matches the committed target. + * \item Resize in-memory bucket arrays with + * \texttt{bch2\_dev\_buckets\_resize()}. + * \item Recalculate filesystem capacity. + * \end{enumerate} + * + * The central invariant: shrink may only commit the smaller \texttt{nbuckets} + * while holding \texttt{state\_lock}, after rechecking that the current + * normalized target still equals the target used by this pass and is still a + * shrink target. This prevents an obsolete pass from truncating the device + * after userspace has already requested a different target or cancellation. + * After finalization completes, \texttt{bch2\_dev\_resize\_finish()} wakes + * waiters and restarts async discards on the device (which were deferred + * during the shrink to avoid deadlocks with the evacuation path). + * + * \textbf{Recovery and remount behavior.} + * A pending shrink that was persisted to the superblock but interrupted + * (e.g., by a crash or unmount) resumes automatically on the next read-write + * mount. \texttt{bch2\_fs\_resize\_on\_mount()} detects devices whose + * normalized target still differs from \texttt{nbuckets} once the btree and + * reconcile infrastructure is running, and calls + * \texttt{bch2\_dev\_resize\_resume()} for each such device. No userspace + * reissue is required. + * + * \textbf{Shutdown ordering.} + * Resize workers are stopped in \texttt{bch2\_fs\_read\_only()} via + * \texttt{bch2\_dev\_resize\_threads\_stop()}, before the filesystem enters + * clean shutdown. This ordering matters because a persisted shrink may still + * be performing transactional alloc/accounting work; if the filesystem starts + * going read-only first, the worker's writes can trip write-path assertions. + * + * * \texttt{bcachefs device resize-journal} adjusts the per-device journal size * independently of the data area. * From 0a69452e2c3bb1e91f6c4797d35ded1f77708d79 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Wed, 24 Jun 2026 21:47:55 +0200 Subject: [PATCH 64/75] bcachefs: shrink: docs: remove unnecessary detail --- fs/bcachefs/init/dev.c | 181 ++++++----------------------------------- 1 file changed, 27 insertions(+), 154 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index ec55089668b54..4812bef1b3656 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -178,164 +178,37 @@ * account for the newly available space. * * \subsubsubsection{Shrinking} - * Shrinking removes the tail of the device's bucket range, evacuates any data - * living there, and commits the smaller \texttt{nbuckets}. The truncating region is called the - * \emph{shrink tail}: buckets \texttt{[target\_nbuckets, nbuckets)}. + * Shrinking removes the tail of a device\'s bucket range, evacuates any data + * located there, and commits the smaller \texttt{nbuckets}. The truncating + * region is called the \emph{shrink tail}: buckets + * \texttt{[target\_nbuckets, nbuckets)}. * - * Once \texttt{target\_nbuckets < nbuckets} is persisted, several subsystems - * react to the pending shrink before the worker has finished evacuation: + * Once \texttt{target\_nbuckets < nbuckets} is persisted, the allocator + * refuses new allocations in the tail, cached pointers past the cutoff are + * treated as stale, and metadata allocation spills to non-shrinking devices + * so shrink does not deadlock on its own journal or btree-rewrite needs. + * Reconcile is then used to discover and evacuate the remaining data; + * its backpointer scans start at the cutoff rather than bucket zero. * - * \begin{itemize} - * \item The \hyperref[sec:write-points]{allocator} refuses new open-bucket - * allocations at or beyond the cutoff. - * \item Cached extent pointers past the cutoff are treated as stale and - * dropped on lookup. - * \item Metadata allocation (journal buckets, btree node rewrites) falls back - * to the full filesystem when the preferred metadata target consists only of - * shrinking devices, avoiding a deadlock on metadata writes the shrink - * itself needs. - * \item \hyperref[sec:backpointers]{Backpointer} scans for reconcile start at - * the cutoff instead of bucket zero, avoiding needless requeue of work on - * the retained region. - * \end{itemize} - * - * These immediate effects are what make the shrink cutoff effective as soon as - * the target is persisted. They are driven by normalized helper checks - * (\texttt{bch2\_dev\_is\_shrinking()}) and flip automatically when - * userspace retargets away from shrink. - * - * \textbf{Worker model.} - * Each device has a dedicated kthread (\texttt{bch2\_dev\_resize\_thread()}) - * that processes resize requests. On each wakeup it snapshots the current - * request sequence number and the normalized target, then dispatches to - * either \texttt{__bch2\_dev\_grow()} or \texttt{__bch2\_dev\_shrink()}. - * The thread is restartable rather than strictly cancelable---a newer - * request increments \texttt{resize\_seq} and causes the active pass to - * return \texttt{-EAGAIN} and restart with the latest target. - * The common restart check is \texttt{bch2\_dev\_resize\_restart\_check()}, - * which also handles kthread shutdown via \texttt{-EINTR}. - * - * \textbf{Shrink algorithm.} - * Shrink proceeds through four phases: - * - * \textbf{Phase 1---Enter shrink mode.} - * The worker takes \texttt{state\_lock}, validates the target still matches - * the live request, closes any open buckets in the truncating tail - * (\texttt{bch2\_open\_buckets\_stop()}), and resets alloc cursors. After - * dropping the lock, it flushes the discard workqueues so that an older - * discard pass on the same region does not deadlock the evacuation path. - * - * \textbf{Phase 2---Move journal state out of the tail.} - * \texttt{move\_journal\_past\_cutoff()} runs before the main drain loop. - * The journal is handled explicitly because journal buckets are not something - * shrink should wait for reconcile to discover and evacuate indirectly. - * The function counts journal buckets in the tail, temporarily grows the - * journal if more journal buckets are needed for relocation, forces the - * current journal bucket to advance if it still sits in the tail, flushes - * the journal to persist the relocation, and deletes the now-obsolete journal - * buckets from the tail. Without this step, journal activity can keep - * reintroducing metadata references into the very region shrink is trying to - * empty. - * - * \textbf{Phase 3---Drain the tail.} - * The core loop tracks tail liveness by scanning the backpointer btree in the - * truncating region. The loop is built around several key helpers: - * - * \begin{itemize} - * \item \texttt{tail\_head\_snapshot()} records the first tail bucket, its - * first backpointer, and the number of backpointers in that bucket. - * \item \texttt{tail\_progress\_snapshot()} records the same head data plus - * the total backpointer count across the full tail. - * \item \texttt{tail\_is\_empty()} performs a definitive emptiness check - * after flushing the btree write buffer (to expose buffered updates). - * \end{itemize} - * - * On each pass the loop: - * - * \begin{enumerate} - * \item Snapshots the tail head. If it looks empty, flushes the write buffer - * and rechecks before declaring emptiness. - * \item Invalidates cached-only tail buckets via - * \texttt{bch2\_dev\_shrink\_invalidate\_tail\_cached()}. Cached copies in - * the tail are not durable data---they can be dropped directly rather than - * requiring reconcile to evacuate them. - * \item Queues reconcile work with - * \texttt{bch2\_dev\_shrink\_queue\_reconcile()}: a device backpointer scan - * (starting at the cutoff, not bucket zero) plus a pending scan. - * \item Waits using \texttt{bch2\_dev\_shrink\_wait\_reconcile()}, polling - * once per second and returning when the tail is empty, the requested - * reconcile kick completes, or head progress is detected. This avoids - * waiting for unrelated global reconcile work after the tail is already - * empty. - * \end{enumerate} - * - * The progress heuristic tracks two things: the leading tail bucket and its - * first backpointer, and the total tail backpointer count. A pass that makes - * progress on either metric resets the stall counter. Passes without progress - * are counted only after a full device rescan on a journal-quiescent state; - * if the journal advanced during the pass, the blocker set is still changing - * and the pass is not evidence of impossibility. After - * \texttt{stalled\_kicks\_limit} (32) no-progress full rescans on a - * quiescent journal, shrink declares \texttt{-ENOSPC} and clears the pending - * target (\texttt{bch2\_dev\_shrink\_clear\_target()}) so allocations are no - * longer blocked past the old cutoff. - * - * The journal-quiescence check is filesystem-global: unrelated - * metadata-writing IO elsewhere can keep advancing the journal sequence - * number and suppress the stall counter indefinitely. The intended follow-up - * is a shrink-local churn signal. - * - * \textbf{Phase 4---Finalize the shrink.} - * \texttt{bch2\_dev\_shrink\_finalize()} runs under \texttt{state\_lock} and - * commits the truncation. The ordering is critical: - * - * \begin{enumerate} - * \item Revalidate that the live request still matches this pass. - * \item Flush btree interior updates. - * \item Flush outstanding journal pins (device-specific, then a flush of - * \texttt{journal\_cur\_seq()}---not \texttt{bch2\_journal\_flush\_all\_pins()}, - * which can wait behind unrelated reconcile or key-cache pins indefinitely). - * \item Recheck tail emptiness with \texttt{tail\_is\_empty()}. - * \item Clear \texttt{NEED\_DISCARD} entries for the truncated tail. This must - * scan and filter by decoded bucket position because - * \texttt{BTREE\_ID\_need\_discard} is keyed by journal sequence, not by - * \texttt{(dev, bucket)}. - * \item Drop superblock copies whose offsets are beyond the cutoff with - * \texttt{drop\_sbs\_after\_cutoff()}. - * \item Truncate accounting with \texttt{bch2\_dev\_truncate\_accounting()}. - * \item Remove alloc metadata for the tail with - * \texttt{bch2\_dev\_remove\_alloc()}. - * \item Commit \texttt{nbuckets = new\_nbuckets} in the superblock and clear - * \texttt{target\_nbuckets} if it still matches the committed target. - * \item Resize in-memory bucket arrays with - * \texttt{bch2\_dev\_buckets\_resize()}. - * \item Recalculate filesystem capacity. - * \end{enumerate} - * - * The central invariant: shrink may only commit the smaller \texttt{nbuckets} - * while holding \texttt{state\_lock}, after rechecking that the current - * normalized target still equals the target used by this pass and is still a - * shrink target. This prevents an obsolete pass from truncating the device - * after userspace has already requested a different target or cancellation. - * After finalization completes, \texttt{bch2\_dev\_resize\_finish()} wakes - * waiters and restarts async discards on the device (which were deferred - * during the shrink to avoid deadlocks with the evacuation path). + * Shrink is executed by a per-device kthread that can be restarted when a + * newer request supersedes an in-progress pass. Before draining the tail, + * the worker relocates any journal buckets in the truncating region explicitly + * (\texttt{move\_journal\_past\_cutoff()}) so journal activity does not keep + * reintroducing references into the region being evacuated. * - * \textbf{Recovery and remount behavior.} - * A pending shrink that was persisted to the superblock but interrupted - * (e.g., by a crash or unmount) resumes automatically on the next read-write - * mount. \texttt{bch2\_fs\_resize\_on\_mount()} detects devices whose - * normalized target still differs from \texttt{nbuckets} once the btree and - * reconcile infrastructure is running, and calls - * \texttt{bch2\_dev\_resize\_resume()} for each such device. No userspace - * reissue is required. + * Once the tail is empty, the worker finalises under \texttt{state\_lock}: + * it flushes journal pins, clears \texttt{NEED\_DISCARD} bookkeeping for + * the removed buckets, drops superblock copies and alloc metadata past the + * cutoff, then commits \texttt{nbuckets = target\_nbuckets} to the + * superblock. If the tail cannot be drained (e.g. insufficient space on + * remaining devices), \texttt{target\_nbuckets} is cleared so allocations + * are no longer blocked past the old cutoff. * - * \textbf{Shutdown ordering.} - * Resize workers are stopped in \texttt{bch2\_fs\_read\_only()} via - * \texttt{bch2\_dev\_resize\_threads\_stop()}, before the filesystem enters - * clean shutdown. This ordering matters because a persisted shrink may still - * be performing transactional alloc/accounting work; if the filesystem starts - * going read-only first, the worker's writes can trip write-path assertions. + * A pending shrink persisted to the superblock resumes automatically on the + * next read-write mount without requiring userspace to reissue the ioctl. + * Resize workers are stopped in \texttt{bch2\_fs\_read\_only()} before the + * filesystem enters clean shutdown, so that in-flight transactional work + * does not trip write-path assertions. * * * \texttt{bcachefs device resize-journal} adjusts the per-device journal size From d294b8d3e670ce73e23093e05a92747e0701bf8d Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Wed, 24 Jun 2026 21:55:09 +0200 Subject: [PATCH 65/75] bcachefs: shrink: make docs more concise --- fs/bcachefs/init/dev.c | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 4812bef1b3656..533e9ef884bad 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -170,6 +170,9 @@ * \texttt{bcachefs device resize} resizes a device to use more or less space. * If no size is specified, the device grows to * fill its underlying block device. Resize works online---no unmount required. + * Resize is executed by a per-device kthread, which is restarted when a + * newer request overwrites an in-progress resize, automatically cancelling the old resize. + * Resizes are persisted as \texttt{target\_nbuckets} and will be automatically resumed across restarts. * * \subsubsubsection{Growing} * The new size is subject to a maximum bucket count @@ -178,21 +181,16 @@ * account for the newly available space. * * \subsubsubsection{Shrinking} - * Shrinking removes the tail of a device\'s bucket range, evacuates any data - * located there, and commits the smaller \texttt{nbuckets}. The truncating - * region is called the \emph{shrink tail}: buckets - * \texttt{[target\_nbuckets, nbuckets)}. + * Shrinking removes the tail region of a device and evacuates any data + * located there. If this is not possible, the operation will fail with \texttt{-ENOSPC}. * * Once \texttt{target\_nbuckets < nbuckets} is persisted, the allocator * refuses new allocations in the tail, cached pointers past the cutoff are * treated as stale, and metadata allocation spills to non-shrinking devices * so shrink does not deadlock on its own journal or btree-rewrite needs. - * Reconcile is then used to discover and evacuate the remaining data; - * its backpointer scans start at the cutoff rather than bucket zero. + * Reconcile is then used to discover and evacuate the remaining data. * - * Shrink is executed by a per-device kthread that can be restarted when a - * newer request supersedes an in-progress pass. Before draining the tail, - * the worker relocates any journal buckets in the truncating region explicitly + * Before draining the tail, the worker relocates any journal buckets in the truncating region explicitly * (\texttt{move\_journal\_past\_cutoff()}) so journal activity does not keep * reintroducing references into the region being evacuated. * @@ -200,16 +198,7 @@ * it flushes journal pins, clears \texttt{NEED\_DISCARD} bookkeeping for * the removed buckets, drops superblock copies and alloc metadata past the * cutoff, then commits \texttt{nbuckets = target\_nbuckets} to the - * superblock. If the tail cannot be drained (e.g. insufficient space on - * remaining devices), \texttt{target\_nbuckets} is cleared so allocations - * are no longer blocked past the old cutoff. - * - * A pending shrink persisted to the superblock resumes automatically on the - * next read-write mount without requiring userspace to reissue the ioctl. - * Resize workers are stopped in \texttt{bch2\_fs\_read\_only()} before the - * filesystem enters clean shutdown, so that in-flight transactional work - * does not trip write-path assertions. - * + * superblock. * * \texttt{bcachefs device resize-journal} adjusts the per-device journal size * independently of the data area. From a2f5aebe9d4ec6938e59954378b7f9ed7456a2db Mon Sep 17 00:00:00 2001 From: Darafei Praliaskouski Date: Sun, 28 Jun 2026 06:20:30 +0400 Subject: [PATCH 66/75] bcachefs: shrink: address review cleanups --- fs/bcachefs/init/dev.c | 16 +++++----------- fs/bcachefs/sb/members.h | 18 ++++++++++++------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 533e9ef884bad..5c7fd5738d754 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -1673,8 +1673,11 @@ static bool shrink_tail_progressed(const struct shrink_tail_progress *old, return shrink_tail_head_progressed(&old->head, &new->head); } -// TODO: make sure everything is caught here. Maybe look at bch2_dev_has_data for this -// Fore example journals and superblocks might need special handling +/* + * Make sure everything is caught here: this snapshots backpointer-visible tail + * data. Journal buckets and superblock copies in the shrink tail are handled by + * move_journal_past_cutoff() and drop_sbs_after_cutoff(). + */ static int tail_head_snapshot(struct bch_fs *c, struct bch_dev *ca, u64 new_nbuckets, struct shrink_tail_head *head) { @@ -2078,15 +2081,6 @@ static int bch2_dev_shrink_finalize(struct bch_fs *c, struct bch_dev *ca, return ret; } - // TODO: figure out what parts of this path still need doing - // if (ca->mi.freespace_initialized) { - // ret = __bch2_dev_resize_alloc(ca, old_nbuckets, new_nbuckets); - // if (ret) { - // prt_printf(err, "__bch2_dev_resize_alloc() error: %s\n", bch2_err_str(ret)); - // return ret; - // } - // } - bch2_recalc_capacity(c); } diff --git a/fs/bcachefs/sb/members.h b/fs/bcachefs/sb/members.h index ae6481daf7e48..78016bfadcfad 100644 --- a/fs/bcachefs/sb/members.h +++ b/fs/bcachefs/sb/members.h @@ -280,16 +280,22 @@ static inline bool bch2_dev_is_growing(const struct bch_dev *ca) return bch2_dev_resize_target(ca) > READ_ONCE(ca->mi.nbuckets); } -static inline bool bch2_ptr_bad_or_evacuating_rcu(struct bch_fs *c, const struct bch_extent_ptr *ptr) { +static inline bool bch2_ptr_bad_or_evacuating_rcu(struct bch_fs *c, const struct bch_extent_ptr *ptr) +{ struct bch_dev *ca = bch2_dev_rcu_noerror(c, ptr->dev); + u64 resize_target; + + if (!ca || bch2_dev_bad_or_evacuating_rcu(c, ptr->dev)) + return true; - return !ca || - bch2_dev_bad_or_evacuating_rcu(c, ptr->dev) || - (bch2_dev_is_shrinking(ca) && - bch2_dev_resize_target(ca) <= div_u64(ptr->offset, ca->mi.bucket_size)); + resize_target = bch2_dev_resize_target(ca); + + return resize_target < READ_ONCE(ca->mi.nbuckets) && + ptr->offset >= resize_target * ca->mi.bucket_size; } -static inline bool bch2_ptr_bad_or_evacuating(struct bch_fs *c, const struct bch_extent_ptr *ptr) { +static inline bool bch2_ptr_bad_or_evacuating(struct bch_fs *c, const struct bch_extent_ptr *ptr) +{ guard(rcu)(); return bch2_ptr_bad_or_evacuating_rcu(c, ptr); } From 749abf0fbc71010819fa71771aaa6b2181cbf1e7 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 29 Jun 2026 20:52:01 +0200 Subject: [PATCH 67/75] bcachefs: shrink: remove redundant bch2_dev_clear_need_discard call This is already internally called by bch2_dev_remove_alloc --- fs/bcachefs/init/dev.c | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 5c7fd5738d754..187d7f5ef7204 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -2020,18 +2020,6 @@ static int bch2_dev_shrink_finalize(struct bch_fs *c, struct bch_dev *ca, return -EBUSY; } - /* - * Buckets in the truncated tail may still be in NEED_DISCARD - * state. Clear that bookkeeping before we drop the alloc range so - * accounting/fsck do not retain tail-only metadata after shrink. - */ - ret = bch2_dev_clear_need_discard(c, ca, new_nbuckets); - if (ret) { - prt_printf(err, "error clearing need_discard state: %s\n", - bch2_err_str(ret)); - return ret; - } - /* drop references to now-truncated superblock copies */ ret = drop_sbs_after_cutoff(c, ca, new_nbuckets); if (ret) { From a06045042a294eaf47b58e58eeb7f7efa31c6ef6 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 29 Jun 2026 20:52:01 +0200 Subject: [PATCH 68/75] bcachefs: remove bch2_dev_clear_need_discard and merge cutoff awareness into bch2_dev_remove_need_discard --- fs/bcachefs/alloc/background.c | 13 ++++++------- fs/bcachefs/alloc/discard.c | 31 ------------------------------- fs/bcachefs/alloc/discard.h | 1 - 3 files changed, 6 insertions(+), 39 deletions(-) diff --git a/fs/bcachefs/alloc/background.c b/fs/bcachefs/alloc/background.c index 6feb826a81767..f26d709bba77c 100644 --- a/fs/bcachefs/alloc/background.c +++ b/fs/bcachefs/alloc/background.c @@ -1463,9 +1463,9 @@ int bch2_trigger_alloc(struct btree_trans *trans, return ret; } -/* device removal */ +/* device removal / shrinking */ -static int bch2_dev_remove_need_discard(struct bch_fs *c, struct bch_dev *ca) +static int bch2_dev_remove_need_discard(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) { CLASS(btree_trans, trans)(c); unsigned dev_idx = ca->dev_idx; @@ -1473,9 +1473,10 @@ static int bch2_dev_remove_need_discard(struct bch_fs *c, struct bch_dev *ca) return for_each_btree_key_commit(trans, iter, BTREE_ID_need_discard, POS_MIN, BTREE_ITER_intent|BTREE_ITER_prefetch, k, - NULL, NULL, BCH_TRANS_COMMIT_no_enospc, ({ + NULL, NULL, BCH_WATERMARK_reclaim| + BCH_TRANS_COMMIT_no_enospc, ({ struct bpos bucket = u64_to_bucket(k.k->p.offset); - (bucket.inode == dev_idx) + (bucket.inode == dev_idx && bucket.offset >= cutoff) ? bch2_btree_delete_at(trans, &iter, BTREE_TRIGGER_norun) : 0; @@ -1495,9 +1496,7 @@ int bch2_dev_remove_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) * with bch2_do_invalidates() and bch2_do_discards() */ ret = bch2_dev_remove_lrus(c, ca, cutoff) ?: - (cutoff - ? bch2_dev_clear_need_discard(c, ca, cutoff) - : bch2_dev_remove_need_discard(c, ca)) ?: + bch2_dev_remove_need_discard(c, ca, cutoff) ?: bch2_btree_delete_range(c, BTREE_ID_freespace, start, end, BTREE_TRIGGER_norun) ?: /* diff --git a/fs/bcachefs/alloc/discard.c b/fs/bcachefs/alloc/discard.c index fb2096ae697a5..5394b1165075f 100644 --- a/fs/bcachefs/alloc/discard.c +++ b/fs/bcachefs/alloc/discard.c @@ -148,37 +148,6 @@ static int bch2_discard_one_bucket(struct btree_trans *trans, return ret; } -int bch2_dev_clear_need_discard(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) -{ - /* - * Shrink is about to delete the tail's alloc keys entirely, so we only - * need to remove the derived need_discard index entries. Avoid updating - * alloc keys here: that would also fire freespace/reconcile triggers - * while reconcile is still draining the shrinking device, which can - * deadlock with btree node rewrites in the move path. - * - * need_discard is ordered by (journal seq, encoded bucket), not by - * device bucket space, so tail truncation has to scan and filter by the - * decoded bucket position. - */ - CLASS(btree_trans, trans)(c); - unsigned dev_idx = ca->dev_idx; - - return for_each_btree_key_commit(trans, iter, - BTREE_ID_need_discard, POS_MIN, - BTREE_ITER_intent|BTREE_ITER_prefetch, k, - NULL, NULL, - BCH_WATERMARK_reclaim| - BCH_TRANS_COMMIT_no_check_rw| - BCH_TRANS_COMMIT_no_enospc, ({ - struct bpos bucket = u64_to_bucket(k.k->p.offset); - - (bucket.inode == dev_idx && bucket.offset >= cutoff) - ? bch2_btree_delete_at(trans, &iter, BTREE_TRIGGER_norun) - : 0; - })); -} - static void calculate_discard_sectors_to_release(struct btree_trans *trans) { struct bch_fs *c = trans->c; diff --git a/fs/bcachefs/alloc/discard.h b/fs/bcachefs/alloc/discard.h index 9e971b2ea9a21..b87ea5589641a 100644 --- a/fs/bcachefs/alloc/discard.h +++ b/fs/bcachefs/alloc/discard.h @@ -9,7 +9,6 @@ void bch2_fast_discard_bucket_add(struct bch_dev *, u64); void bch2_fast_discards_to_text(struct printbuf *, struct bch_dev *); void bch2_discards_to_text(struct printbuf *, struct bch_fs *, struct discard_state *); -int bch2_dev_clear_need_discard(struct bch_fs *, struct bch_dev *, u64); void bch2_dev_do_discards(struct bch_dev *); void bch2_do_discards_going_ro(struct bch_fs *); From 049aa83a325a7311199d96b8b81a462cd1752219 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 29 Jun 2026 21:11:21 +0200 Subject: [PATCH 69/75] bcachefs: move bch2_dev_usage_remove() from bch2_dev_remove_alloc() into caller bch2_dev_remove_alloc() now does the same operations wheter it is passed a cutoff or not. Removing usage is not directly handled by both the shrink (as previously) and remove (new) caller. --- fs/bcachefs/alloc/background.c | 3 +-- fs/bcachefs/init/dev.c | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/fs/bcachefs/alloc/background.c b/fs/bcachefs/alloc/background.c index f26d709bba77c..31922cc351a88 100644 --- a/fs/bcachefs/alloc/background.c +++ b/fs/bcachefs/alloc/background.c @@ -1509,8 +1509,7 @@ int bch2_dev_remove_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) BTREE_TRIGGER_norun) ?: bch2_dev_remove_bucket_gens(c, ca, cutoff) ?: bch2_btree_delete_range(c, BTREE_ID_alloc, start, end, - BTREE_TRIGGER_norun) ?: - (cutoff == 0 ? bch2_dev_usage_remove(c, ca) : 0); + BTREE_TRIGGER_norun); bch_err_msg_dev(ca, ret, "%s dev alloc info", cutoff ? "truncating" : "removing"); return ret; diff --git a/fs/bcachefs/init/dev.c b/fs/bcachefs/init/dev.c index 187d7f5ef7204..26353fd540eac 100644 --- a/fs/bcachefs/init/dev.c +++ b/fs/bcachefs/init/dev.c @@ -945,6 +945,11 @@ int bch2_dev_remove(struct bch_fs *c, struct bch_dev *ca, int flags, prt_printf(err, "bch2_dev_remove_alloc() error: %s\n", bch2_err_str(ret)); goto err; } + ret = bch2_dev_usage_remove(c, ca); + if (ret) { + prt_printf(err, "bch2_dev_usage_remove() error: %s\n", bch2_err_str(ret)); + goto err; + } /* * We need to flush the entire journal to get rid of keys that reference From 85280d7a2f6cf8e648d19fe8af18351f6b87c636 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 29 Jun 2026 21:14:00 +0200 Subject: [PATCH 70/75] bcachefs: bch2_dev_remove_alloc(): remove unnecessary comment explanation not needed --- fs/bcachefs/alloc/background.c | 6 ------ 1 file changed, 6 deletions(-) diff --git a/fs/bcachefs/alloc/background.c b/fs/bcachefs/alloc/background.c index 31922cc351a88..a8d60dd117a76 100644 --- a/fs/bcachefs/alloc/background.c +++ b/fs/bcachefs/alloc/background.c @@ -1499,12 +1499,6 @@ int bch2_dev_remove_alloc(struct bch_fs *c, struct bch_dev *ca, u64 cutoff) bch2_dev_remove_need_discard(c, ca, cutoff) ?: bch2_btree_delete_range(c, BTREE_ID_freespace, start, end, BTREE_TRIGGER_norun) ?: - /* - * Backpointer keys are indexed by device + sector, not device + - * bucket. A shrink cutoff in bucket space must be translated - * before deleting the truncated tail, otherwise we wipe - * backpointers that still belong to buckets below @cutoff. - */ bch2_btree_delete_range(c, BTREE_ID_backpointers, bp_start, bp_end, BTREE_TRIGGER_norun) ?: bch2_dev_remove_bucket_gens(c, ca, cutoff) ?: From 2906ebc4ea806c55be43c8ed90c059135453f0d5 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 29 Jun 2026 22:45:29 +0200 Subject: [PATCH 71/75] bcachefs: simplify bch2_dev_remove_bucket_gens() use helper macros and add comments --- fs/bcachefs/alloc/background.c | 42 ++++++++++++++-------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/fs/bcachefs/alloc/background.c b/fs/bcachefs/alloc/background.c index a8d60dd117a76..fdf5e0c37ddfe 100644 --- a/fs/bcachefs/alloc/background.c +++ b/fs/bcachefs/alloc/background.c @@ -1093,45 +1093,37 @@ int bch2_dev_remove_bucket_gens(struct bch_fs *c, struct bch_dev *ca, u64 cutoff { unsigned offset; struct bpos pos = alloc_gens_pos(POS(ca->dev_idx, cutoff), &offset); - int ret; + /* cutoff on key boundary */ if (!offset) return bch2_btree_delete_range(c, BTREE_ID_bucket_gens, pos, POS(ca->dev_idx, U64_MAX), BTREE_TRIGGER_norun); - { - CLASS(btree_trans, trans)(c); - bool need_commit = false; + /* Cutoff mid-key */ - ret = lockrestart_do(trans, ({ - CLASS(btree_iter, iter)(trans, BTREE_ID_bucket_gens, pos, BTREE_ITER_intent); - struct bkey_s_c k = bkey_try(bch2_btree_iter_peek_slot(&iter)); + CLASS(btree_trans, trans)(c); - try(bkey_err(k)); + /* zero tail gens */ + try(commit_do(trans, NULL, NULL, BCH_TRANS_COMMIT_no_enospc, ({ + CLASS(btree_iter, iter)(trans, BTREE_ID_bucket_gens, pos, BTREE_ITER_intent); + struct bkey_s_c k = bkey_try(bch2_btree_iter_peek_slot(&iter)); - if (k.k->type == KEY_TYPE_bucket_gens) { - struct bkey_i_bucket_gens *g = - errptr_try(bch2_trans_kmalloc(trans, sizeof(*g))); + try(bkey_err(k)); - bkey_reassemble(&g->k_i, k); - memset(&g->v.gens[offset], 0, sizeof(g->v.gens) - offset); - try(bch2_trans_update(trans, &iter, &g->k_i, 0)); - need_commit = true; - } - 0; - })); - if (ret) - return ret; + if (k.k->type == KEY_TYPE_bucket_gens) { + struct bkey_i_bucket_gens *g = + errptr_try(bch2_trans_kmalloc(trans, sizeof(*g))); - if (need_commit) { - ret = bch2_trans_commit(trans, NULL, NULL, BCH_TRANS_COMMIT_no_enospc); - if (ret) - return ret; + bkey_reassemble(&g->k_i, k); + memset(&g->v.gens[offset], 0, sizeof(g->v.gens) - offset); + try(bch2_trans_update(trans, &iter, &g->k_i, 0)); } - } + 0; + }))); + /* delete remaining keys */ return bch2_btree_delete_range(c, BTREE_ID_bucket_gens, bpos_nosnap_successor(pos), POS(ca->dev_idx, U64_MAX), From 3d91eb2d4c83a418435510bf3f5851ddeb7418c9 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 29 Jun 2026 22:52:50 +0200 Subject: [PATCH 72/75] bcachefs: shrink: add comment --- fs/bcachefs/alloc/accounting.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/bcachefs/alloc/accounting.c b/fs/bcachefs/alloc/accounting.c index f074cebc0f489..a6eeb38c4374d 100644 --- a/fs/bcachefs/alloc/accounting.c +++ b/fs/bcachefs/alloc/accounting.c @@ -1187,6 +1187,7 @@ int bch2_dev_truncate_accounting(struct bch_fs *c, struct bch_dev *ca, u64 old_n CLASS(btree_trans, trans)(c); + /* accumulate deltas per data type, then apply them */ return commit_do(trans, NULL, NULL, 0, ({ s64 delta[BCH_DATA_NR][3] = {}; s64 keys = 0; From 805df711de480a2a3b708443f70c575f9c8b71ae Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Mon, 29 Jun 2026 23:02:00 +0200 Subject: [PATCH 73/75] bcachefs: shrink: add bch2_target_has_non_shrinking_dev() helper extracts some duplicate logic --- fs/bcachefs/alloc/disk_groups.h | 17 +++++++++++++++++ fs/bcachefs/btree/interior.c | 24 +++--------------------- fs/bcachefs/journal/write.c | 24 +++--------------------- 3 files changed, 23 insertions(+), 42 deletions(-) diff --git a/fs/bcachefs/alloc/disk_groups.h b/fs/bcachefs/alloc/disk_groups.h index e8e5636995a9e..e953b6e579075 100644 --- a/fs/bcachefs/alloc/disk_groups.h +++ b/fs/bcachefs/alloc/disk_groups.h @@ -3,6 +3,7 @@ #define _BCACHEFS_DISK_GROUPS_H #include "disk_groups_types.h" +#include "sb/members.h" extern const struct bch_sb_field_ops bch_sb_field_ops_disk_groups; @@ -78,6 +79,22 @@ static inline bool bch2_target_accepts_data(struct bch_fs *c, return !bitmap_empty(rw_devs.d, BCH_SB_MEMBERS_MAX); } +static inline bool bch2_target_has_non_shrinking_dev(struct bch_fs *c, + enum bch_data_type data_type, + u16 target) +{ + struct bch_devs_mask devs = target_rw_devs(c, data_type, target); + + guard(rcu)(); + unsigned i; + for_each_set_bit(i, devs.d, BCH_SB_MEMBERS_MAX) { + struct bch_dev *ca = bch2_dev_rcu_noerror(c, i); + if (ca && !bch2_dev_is_shrinking(ca)) + return true; + } + return false; +} + bool bch2_dev_in_target_rcu(struct bch_fs *, unsigned, unsigned); static inline bool bch2_dev_in_target(struct bch_fs *c, unsigned dev, unsigned target) diff --git a/fs/bcachefs/btree/interior.c b/fs/bcachefs/btree/interior.c index 9b2ac90e271a4..b03f20701f9f6 100644 --- a/fs/bcachefs/btree/interior.c +++ b/fs/bcachefs/btree/interior.c @@ -51,27 +51,9 @@ static const char * const bch2_btree_update_modes[] = { static unsigned btree_alloc_target(struct bch_fs *c, unsigned target) { target = target ?: c->opts.metadata_target ?: c->opts.foreground_target; - if (!target) - return 0; - - struct bch_devs_mask devs = target_rw_devs(c, BCH_DATA_btree, target); - - /* - * Shrink needs to keep rewriting btree nodes even when metadata is - * normally pinned to the device being reduced. If every rw member of - * the preferred target is currently shrinking, spill metadata anywhere - * so the shrink can update its own bookkeeping. - */ - guard(rcu)(); - unsigned i; - for_each_set_bit(i, devs.d, BCH_SB_MEMBERS_MAX) { - struct bch_dev *ca = bch2_dev_rcu_noerror(c, i); - - if (ca && !bch2_dev_is_shrinking(ca)) - return target; - } - - return 0; + return target && bch2_target_has_non_shrinking_dev(c, BCH_DATA_btree, target) + ? target + : 0; } static void bch2_btree_update_to_text(struct printbuf *, struct btree_update *); diff --git a/fs/bcachefs/journal/write.c b/fs/bcachefs/journal/write.c index fd1f6a53079f7..aa58069434f3f 100644 --- a/fs/bcachefs/journal/write.c +++ b/fs/bcachefs/journal/write.c @@ -29,27 +29,9 @@ static unsigned journal_alloc_target(struct bch_fs *c) { unsigned target = c->opts.metadata_target ?: c->opts.foreground_target; - if (!target) - return 0; - - struct bch_devs_mask devs = target_rw_devs(c, BCH_DATA_journal, target); - - /* - * Shrink can temporarily make the preferred metadata target unusable - * for new journal buckets. If every rw member of that target is - * shrinking, allocate from the full filesystem until the shrink - * finishes so journal progress doesn't deadlock on target preference. - */ - guard(rcu)(); - unsigned i; - for_each_set_bit(i, devs.d, BCH_SB_MEMBERS_MAX) { - struct bch_dev *ca = bch2_dev_rcu_noerror(c, i); - - if (ca && !bch2_dev_is_shrinking(ca)) - return target; - } - - return 0; + return target && bch2_target_has_non_shrinking_dev(c, BCH_DATA_journal, target) + ? target + : 0; } static void journal_advance_devs_to_next_bucket(struct journal *j, From d3c3cc61c9901c4e26f719f9b905c6766e8ca06d Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Wed, 1 Jul 2026 10:39:42 +0200 Subject: [PATCH 74/75] bcachefs: rename bch2_trigger_extent_same_backpointers to bch2_trigger_extent_reconcile_phys_update and update comments --- fs/bcachefs/alloc/buckets.c | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/fs/bcachefs/alloc/buckets.c b/fs/bcachefs/alloc/buckets.c index 36e4c46707283..03ccb546018b3 100644 --- a/fs/bcachefs/alloc/buckets.c +++ b/fs/bcachefs/alloc/buckets.c @@ -878,9 +878,9 @@ static int __trigger_extent(struct btree_trans *trans, return 0; } -static int bch2_trigger_extent_same_backpointers(struct btree_trans *trans, +/* in-place update RECONCILE_PHYS if it is the only thing that changed */ +static int bch2_trigger_extent_reconcile_phys_update(struct btree_trans *trans, enum btree_id btree, - unsigned level, struct bkey_s_c old, struct bkey_s new, bool *handled) @@ -895,16 +895,7 @@ static int bch2_trigger_extent_same_backpointers(struct btree_trans *trans, unsigned i; *handled = false; - if (level) - return 0; - /* - * Reconcile-only updates do not move the extent, they only toggle the - * phys-work flag that is cached in rotational-data backpointers. Do - * not delete+reinsert those backpointers through the write buffer when - * the key itself is unchanged: update the existing entry in place and - * adjust the reconcile_work_phys bit separately. - */ ptrs = bch2_bkey_ptrs_c(old); bkey_for_each_ptr_decode(old.k, ptrs, p, entry) { BUG_ON(nr_old == ARRAY_SIZE(old_bp)); @@ -971,16 +962,11 @@ int bch2_trigger_extent(struct btree_trans *trans, if (unlikely(flags & BTREE_TRIGGER_check_repair)) return bch2_check_fix_ptrs(trans, btree, level, new.s_c, flags); - /* - * Reconcile-only updates keep the same physical pointers. Update - * their backpointer flags in place so buffered delete+insert churn - * cannot drop the key, then let reconcile accounting/work tracking - * see the logical-state change. - */ + /* optimization for in-place updates to reconcile_phys to avoid delete-insert churn */ if (level == 0) { bool handled; - try(bch2_trigger_extent_same_backpointers(trans, btree, level, old, new, &handled)); + try(bch2_trigger_extent_reconcile_phys_update(trans, btree, old, new, &handled)); if (handled) return bch2_trigger_extent_reconcile(trans, btree, level, old, new, flags); } From f74372111084f9b50dc52eaeb9e8ec40399b2609 Mon Sep 17 00:00:00 2001 From: Jul Lang Date: Wed, 1 Jul 2026 10:51:53 +0200 Subject: [PATCH 75/75] bcachefs: clean up bch2_do_discards --- fs/bcachefs/alloc/discard.c | 59 +++++++++++++------------------------ 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/fs/bcachefs/alloc/discard.c b/fs/bcachefs/alloc/discard.c index 5394b1165075f..022cd37a803a1 100644 --- a/fs/bcachefs/alloc/discard.c +++ b/fs/bcachefs/alloc/discard.c @@ -231,36 +231,30 @@ static void bch2_do_discards(struct bch_fs *c) /* * Iterate need_discard btree (sorted by journal_seq). * Stop when we hit a seq beyond rewind_seq_ondisk. + * + * Drop need_discard iterator before we update alloc and + * commit to avoid deadlock against alloc/freespace updates + * when removing the corresponding index entry */ while (!ret) { - struct bpos next = POS_MAX; - struct bpos bucket = POS_MAX; - u64 journal_seq = 0; + struct bpos bucket; + u64 journal_seq; bool done = false; - /* - * Drop the need_discard iterator before we update alloc and - * commit: the discard path removes the corresponding index - * entry, and keeping the original iterator pinned across that - * commit can deadlock against the alloc/freespace updates. - */ ret = lockrestart_do(trans, ({ - int __ret = 0; CLASS(btree_iter, iter)(trans, BTREE_ID_need_discard, pos, 0); struct bkey_s_c k = bch2_btree_iter_peek(&iter); - int _ret = bkey_err(k); + int ret = bkey_err(k); - if (_ret) { - __ret = _ret; - } else if (!k.k) { - done = true; - } else { + if (!ret && k.k) { journal_seq = k.k->p.inode; bucket = u64_to_bucket(k.k->p.offset); - next = bpos_successor(k.k->p); - s->pos = next; + pos = bpos_successor(k.k->p); + s->pos = pos; + } else if (!ret) { + done = true; } - __ret; + ret; })); if (ret || done) break; @@ -270,31 +264,20 @@ static void bch2_do_discards(struct bch_fs *c) break; /* - * need_discard is only advisory trim bookkeeping. If this - * bucket sits in the tail of a device that is currently - * shrinking, leave the entry queued for the tail cleanup - * pass: discard's alloc update can deadlock with - * resize/reconcile on the region being evacuated. - * - * Buckets that remain below the shrink target still need - * discard progress so the allocator can reuse retained - * space on the shrinking device during the long-running - * evacuation. + * Leave buckets in the shrink tail queued, as the + * alloc update can deadlock with reconcile. */ - if (bch2_discard_blocked_by_resize(c, bucket)) { - pos = next; + if (bch2_discard_blocked_by_resize(c, bucket)) continue; - } ret = lockrestart_do(trans, ({ - int __ret = bch2_discard_one_bucket(trans, bucket, - &discard_pos_done, - s, false); - if (!__ret) + int ret = bch2_discard_one_bucket(trans, bucket, + &discard_pos_done, + s, false); + if (!ret) s->seen += dev_bucket_size(c, bucket.inode); - __ret; + ret; })); - pos = next; } /*