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) {
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
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
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;
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
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
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;