]> git.hungrycats.org Git - bees/commitdiff
scan_next: retire over-limit extents in refs_fetch, drop the ceiling gate
authorZygo Blaxell <bees@furryterror.org>
Wed, 8 Jul 2026 05:18:42 +0000 (01:18 -0400)
committerZygo Blaxell <bees@furryterror.org>
Sat, 5 Sep 2026 04:04:13 +0000 (00:04 -0400)
An extent whose reference count reaches rewrite.refs-max is unusable: deduping
onto it would push it past the limit, and the LOGICAL_INO buffer (sized to
refs-max) can no longer enumerate it in full.  scan_one handles this in its own
logical_ino wrapper — an over-limit resolve is treated as a failure so the
extent's refs are never inserted and it disappears from consideration
(bees-roots.cc).  The method-1 layer path had no such retirement: refs_fetch
returned every ref it got, so a full extent stayed a live candidate.

Combined with the additive ceiling gate this produced the reference-stacking
blow-up the t10 validation caught.  The gate stopped a dedupe at src+dst >
refs-max, so an extent parked one ref short of the limit — still reporting its
refs, still eligible — and thousands of them churned at the wall (a staircase of
extents pinned at refs-max, absorbing and draining ~refs-max references each)
instead of a full extent retiring as a stable canonical sink.

Retire the extent at logical_ino time instead: when the reference count reaches
refs-max, refs_fetch reports it as having no references.  Nothing can use it —
it reads exactly like an extent whose refs were all deleted, and the callers'
existing 0-ref handling (scan_next's early exit for the start, the candidate
drop that also dodges the datacow_fetch throw) makes it inert: not locatable as
a src, no refs to redirect as a dst.  A dedupe may now run to completion even
when it takes the src to at most 2*refs-max references, after which the src
itself retires; refs-max bounds the inputs, not the output.

With retirement doing the work, the additive gate (scan_next_refs_would_exceed
and its two process_candidate sites) and the truncated-refs dst guard are both
redundant — they were compensating for the missing retirement at the wrong
layer — so remove them, along with the now-obsolete test_refs_ceiling_gate.
The high-ref-src tie-break stays: with saturated srcs retired, it consolidates
onto the highest non-full src, which then fills and retires, converging a
duplicate cluster onto a series of full canonical sinks.

Assisted-by: Claude-Code:claude-opus-4-8
src/bees-extent-layer.cc
src/bees-plan.h
src/bees-scan-next.cc
test/test-bees-plan.cc

index d6693ef6208f906b088a1195a94c6c1b1178f4ce..483a2cd1f69efdd4c6e058ecead0e7a73c104d5a 100644 (file)
@@ -281,6 +281,25 @@ BeesBtrfsExtentLayer::refs_fetch(BeesBtrfsExtentNode &node) const
        if (lio->m_iors.size() == 0) {
                BEESCOUNT(refs_zero);
        }
+
+       // Retirement.  An extent at or above the configured reference ceiling is
+       // unusable: deduping onto it would push it past refs_max, and the
+       // LOGICAL_INO buffer (sized to refs_max) can no longer enumerate it in
+       // full.  Report it as having no references — nothing can use it, so it
+       // reads exactly like an extent whose refs were all deleted, and the
+       // callers' existing 0-ref handling (scan_next's early-exit for the start,
+       // the candidate drop that also dodges the datacow_fetch throw) makes it
+       // inert: not locatable as a src, no refs to redirect as a dst.  This is
+       // what scan_one built into its own logical_ino wrapper (bees-roots.cc);
+       // method 1 was missing it, which let clusters churn at the ceiling instead
+       // of retiring a full extent as a permanent canonical sink.
+       if (count >= m_ctx.refs_max()) {
+               BEESCOUNT(refs_overflow);
+               const auto empty = make_shared<const vector<BeesRef>>();
+               atomic_store(&node.m_refs, empty);
+               return empty;
+       }
+
        vector<BeesRef> rv;
        rv.reserve(lio->m_iors.size());
        for (const auto &ior : lio->m_iors) {
index af6ad441760da9bedc1b7653ddcc1b43eaa9c0dc..cf9509c127d0f591058749badf0e14dbd9f4d09d 100644 (file)
@@ -230,19 +230,6 @@ scan_next_covering_debt(const PlanSearchInputs &in,
        const BeesRewritePolicy &policy,
        size_t dedupe_ops, size_t copy_ops, size_t matched_blocks);
 
-/// Would deduping an extent that has @p dst_refs references onto a src that has
-/// @p src_refs references push the src past @p refs_max?  Dedupe redirects every
-/// reference of the dst onto the src, so the src ends up with the sum of the two
-/// counts; reject only when that sum *exceeds* the ceiling (landing exactly on
-/// it is allowed).  This is the per-plan admission gate that keeps a single
-/// dedupe from overshooting rewrite.refs-max, unlike a binary "src already full"
-/// test which a many-ref dst can still blow past.
-inline bool
-scan_next_refs_would_exceed(uint64_t src_refs, uint64_t dst_refs, uint64_t refs_max)
-{
-       return src_refs + dst_refs > refs_max;
-}
-
 /// Lazy-sorted candidate queue.  Priority is the ratio of
 /// matching-hashes to candidate phys_size (higher first); the sort is
 /// deferred to pick_best_candidate() since the queue size is bounded
index 0885d68faa9333b77ed25bfe318235ad6f823a48..38e4b060e75ce80c8ff4bf2983e91bfa2b1f3205 100644 (file)
@@ -2028,16 +2028,11 @@ BeesStartAsDstPlan::process_candidate(
                return;
        }
 
-       // Don't let this dedupe push the src past the reference ceiling: the src
-       // (candidate) absorbs all of the dst's (m_start's) references, so gate on
-       // the resulting combined count, not just the src's current one.
-       {
-               auto &layer = Borrower::current().layer();
-               if (scan_next_refs_would_exceed(candidate.refs(layer)->size(),
-                               m_start.refs(layer)->size(), m_policy.m_refs_max)) {
-                       return;
-               }
-       }
+       // No reference-ceiling gate here: a candidate at or above refs_max already
+       // reads as 0-ref (retired at logical_ino time — see refs_fetch) and was
+       // dropped as not-accepted before it ever reached a plan.  Letting the
+       // dedupe run means the src can grow to at most 2*refs_max references, then
+       // it too retires — refs_max bounds the inputs, not the output.
 
        if (!matches || matches->m_start_as_dst.empty()) {
                // This candidate has no matches against m_start in our
@@ -2418,15 +2413,9 @@ BeesStartAsSrcPlan::process_candidate(
                return;
        }
 
-       // Same reference-ceiling gate as Plan A with the roles swapped: here the
-       // src is m_start and it absorbs the dst's (candidate's) references.
-       {
-               auto &layer = Borrower::current().layer();
-               if (scan_next_refs_would_exceed(m_start.refs(layer)->size(),
-                               candidate.refs(layer)->size(), m_policy.m_refs_max)) {
-                       return;
-               }
-       }
+       // No reference-ceiling gate (see Plan A): a candidate at refs_max is
+       // already retired to 0-ref by refs_fetch and dropped before it reaches a
+       // plan; the src (m_start) grows to at most 2*refs_max and then retires.
 
        if (!matches || matches->m_candidate_as_dst.empty()) {
                return;
@@ -3294,13 +3283,9 @@ Planner::run(const BtrfsTreeItem &bti,
        auto &layer = Borrower::current().layer();
        BEESTRACE("scan_next accept_dst");
        m_start_accept_dst = m_filter_cache->accept_dst(m_start);
-       if (m_start_accept_dst) {
-               if (m_start.refs(layer)->size() >= m_rewrite_policy.m_refs_max) {
-                       m_start_accept_dst = false;
-                       BEESLOGC(INFO, Plan, "scan_next reject truncated refs list for dst "
-                               << to_hex(m_start.bytenr()) << " (refs size: " << m_start.refs(layer)->size() << ")");
-               }
-       }
+       // No truncated-refs guard here: a start at or above refs_max now reads as
+       // 0-ref (retired in refs_fetch) and already took the early exit above, so
+       // any start reaching this point has a full, in-range reference list.
        // Per-extent force-rewrite flag for the start (do-nothing-plan.md
        // §1).  When set, the floor of the cost space is no longer
        // do-nothing but a forced FullRewrite of the start: it must be
index b22b798caf21f3a6369b9cda2457b1f746f4cad2..3b1329bc17f50d9033fe72044a298566bea9d9ef 100644 (file)
@@ -844,29 +844,6 @@ test_search_v_budget_greedy_abandons_over_limit()
        assert(!greedy.has_value());
 }
 
-// The additive reference-ceiling admission gate (scan_next_refs_would_exceed).
-// A dedupe redirects every dst reference onto the src, so the src's resulting
-// count is src_refs + dst_refs; reject only when that sum exceeds refs-max
-// (landing exactly on the ceiling is allowed).  Pins the boundary (> not >=)
-// and the summation that a binary "src already full" test would miss.
-static void
-test_refs_ceiling_gate()
-{
-       const uint64_t refs_max = 9999;
-       // Exact fit at the ceiling is accepted.
-       assert(!scan_next_refs_would_exceed(5000, 4999, refs_max)); // sum == 9999
-       assert(!scan_next_refs_would_exceed(9999, 0, refs_max));
-       assert(!scan_next_refs_would_exceed(0, 9999, refs_max));
-       // One past the ceiling is rejected.
-       assert( scan_next_refs_would_exceed(5000, 5000, refs_max)); // sum == 10000
-       // A many-ref dst overshoots even when the src is well under the limit —
-       // exactly the overshoot a binary "src already full" gate let through.
-       assert( scan_next_refs_would_exceed(9000, 5000, refs_max)); // 9000<max, 14000>max
-       // Either side already at the ceiling rejects (real extents have >= 1 ref).
-       assert( scan_next_refs_would_exceed(refs_max, 1, refs_max));
-       assert( scan_next_refs_would_exceed(1, refs_max, refs_max));
-}
-
 // ref-op-cost prices every reference a plan rewrites, so a whole-extent (F=1)
 // dedupe that moves many references to free one small extent — a net win under
 // the pure do-nothing model — becomes a net loss once the per-reference work is
@@ -980,7 +957,6 @@ main(int, char **)
        RUN_A_TEST(test_search_large_input_smoke());
        RUN_A_TEST(test_search_v_budget_greedy_fallback());
        RUN_A_TEST(test_search_v_budget_greedy_abandons_over_limit());
-       RUN_A_TEST(test_refs_ceiling_gate());
        RUN_A_TEST(test_ref_op_cost_prices_whole_extent_dedupe());
        RUN_A_TEST(test_search_prefers_high_ref_src());
        return 0;