/// a full ref [0, N) from file A and partial refs [X, X+Y) from
/// file B — all of which need FIDEDUPERANGE calls to fully replace
/// the old extent.
-static vector<BeesRangePair>
+///
+/// Returns an empty optional when a planned source window has no
+/// usable ref: the plan was built from a previous transid cycle's
+/// snapshot, and the covering ref has since vanished from the
+/// refetched refs list (file range overwritten, file or snapshot
+/// deleted) or stopped opening. The caller must abandon the dst
+/// plan without executing any pairs — partial execution would create
+/// refs without freeing the dst extent, breaking the cost model the
+/// plan was accepted under. The extent is rescanned in a later
+/// cycle once the churn settles.
+static optional<vector<BeesRangePair>>
scan_next_sweep_join_pairs(BeesContext &ctx,
const vector<BeesTreeRefSlice> &dst_refs,
const BeesExtent &src_tree,
uint64_t cursor = dst_ref.begin();
while (cursor < dst_ref.end()) {
bool covered = false;
+ bool open_failed = false;
uint64_t best_overlap_end = cursor;
const auto src_sub_slice = BeesExtentSlice(src_tree, cursor, dst_ref.end());
src_sub_slice.refs(ctx.layer().get(), [&](BeesExtentLayer *, const BeesTreeRefSlice &src_ref) {
const auto src_fd = src_ref.ref().open(ctx);
if (!src_fd) {
+ open_failed = true;
return true; // Keep walking to try other overlapping fallbacks
}
const auto overlap_end = min(src_ref.end(), dst_ref.end());
}, BeesVisitorPath::src);
if (!covered) {
- THROW_ERROR(runtime_error, "Plan incomplete: no valid source reference covers range ["
- << to_hex(cursor) << ".." << to_hex(dst_ref.end()) << ") for dst " << dst_ref);
+ // Concurrent filesystem modification invalidated the
+ // plan: expected churn, not an error, so no exception
+ // and no trace dump — count, log one line, abandon.
+ if (open_failed) {
+ BEESCOUNT(plan_src_open_fail);
+ } else {
+ BEESCOUNT(plan_src_gone);
+ }
+ BEESLOGC(DEBUG, Plan, "scan_next abandon dst plan: no "
+ << (open_failed ? "openable" : "covering")
+ << " src ref for [" << to_hex(cursor)
+ << ".." << to_hex(dst_ref.end())
+ << ") of dst " << dst_ref);
+ return {};
}
cursor = best_overlap_end;
return rv;
}
-vector<BeesRangePair>
+optional<vector<BeesRangePair>>
scan_next_build_tree_pairs(BeesContext &ctx, const BeesExtent &prepared,
const BeesExtent &src_tree, uint64_t reachable_size,
optional<bool> want_prealloc = {})
// Build dedupe pairs: prealloc first, then data. Pass reachable_size
// so dst-ref enumeration stops at the mid-block EOF and never demands a
- // source for the bare past-EOF tail hole.
- auto prealloc_pairs = scan_next_build_tree_pairs(*ctx, dst,
+ // source for the bare past-EOF tail hole. An empty optional means a
+ // planned src window lost its last usable ref to concurrent filesystem
+ // modification (counted and logged in the sweep join) — abandon this
+ // dst before executing anything; it can be rescanned in a later cycle.
+ auto prealloc_pairs_opt = scan_next_build_tree_pairs(*ctx, dst,
src_tree, reachable_size, true);
- auto data_pairs = scan_next_build_tree_pairs(*ctx, dst,
+ if (!prealloc_pairs_opt) {
+ return false;
+ }
+ auto data_pairs_opt = scan_next_build_tree_pairs(*ctx, dst,
src_tree, reachable_size, false);
+ if (!data_pairs_opt) {
+ return false;
+ }
+ auto &prealloc_pairs = *prealloc_pairs_opt;
+ auto &data_pairs = *data_pairs_opt;
BEESLOGC(DEBUG, Plan, "scan_next_execute_dst_plan dst "
<< to_hex(dst.bytenr())