]> git.hungrycats.org Git - bees/commitdiff
extent-layer: exclude unopenable refs from block map coverage
authorZygo Blaxell <bees@furryterror.org>
Mon, 3 Aug 2026 03:28:53 +0000 (23:28 -0400)
committerZygo Blaxell <bees@furryterror.org>
Sat, 5 Sep 2026 04:04:14 +0000 (00:04 -0400)
Refs in deleted files or snapshots linger in an extent's ref list
until the last open fd closes or the subvol cleaner runs.  Blocks
covered only by such refs cannot be read through any ref, and
block_map_fetch pass 2 would fail the whole block map with a noisy
"no usable ref covers extent offset" exception for a condition that
is ordinary filesystem churn.

Require data refs to open successfully before they contribute to
pass 1's coverage union.  Blocks whose only covering refs fail to
open are now classified as unreachable, the same as blocks with no
covering ref at all, and map generation proceeds quietly.  Prealloc
classification is unchanged: it is purely structural and pass 2
never reads prealloc-only ranges.

Probe cost is bounded: a ref that adds no new coverage beyond
already-covered blocks is skipped without probing, so heavily
overlapping ref clusters do not open every referencing file, and
the fd cache stores negative results, so a dead (root, ino) pair
costs one failed resolution however many refs point to it.

Count weeded refs as block_map_ref_unopenable.  The residual
pread_no_ref_open exception now only indicates a file or subvol
that disappeared between pass 1's probe and pass 2's read.

Assisted-by: Claude-Code:claude-fable-5
docs/event-counters.md
src/bees-extent-layer.cc

index 06b65a0caa958ff1d627f1acf0b5d4bd64f92373..c281b9685484bd262f23f3cb5d01205e5be8904c 100644 (file)
@@ -83,6 +83,7 @@ The `block` event group consists of operations related to reading data blocks fr
 
  * `block_bytes`: Number of data bytes read.
  * `block_hash`: Number of block hashes computed.
+ * `block_map_ref_unopenable`: A data ref was excluded from block map coverage because its file could not be opened — typically a ref in a deleted file or snapshot that lingers in the ref list until the last open fd closes or the subvol cleaner runs.  Blocks covered only by such refs are classified as unreachable.
  * `block_ms`: Total time reading data blocks.
  * `block_read`: Number of data blocks read.
  * `block_zero`: Number of data blocks read with zero contents (i.e. candidates for replacement with a hole).
@@ -345,7 +346,7 @@ block map in `BeesBtrfsExtentLayer::block_map_fetch`.
  * `pread_fail`: A `pread()` call failed with an error other than `EINTR`.
  * `pread_ms`: Total wallclock time spent in the `pread()` loop for a chunk.
  * `pread_no_ref_gap`: A read failed because no ref covered the extent offset at all — the caller's coverage model disagreed with the ref list.  The read is abandoned with an exception.
- * `pread_no_ref_open`: A read failed because every ref covering the extent offset failed to open — typically the referencing files or subvols were deleted between the metadata fetch and the read.  The read is abandoned with an exception.
+ * `pread_no_ref_open`: A read failed because every ref covering the extent offset failed to open.  Block map generation excludes refs that fail to open (`block_map_ref_unopenable`) before reading, so this counter indicates a referencing file or subvol that disappeared while the block map was being built.  The read is abandoned with an exception.
  * `pread_ok`: A `pread()` call returned successfully.
  * `pread_ref_open_fail`: A ref covering the current read offset failed to open.  Not fatal by itself — another covering ref may open successfully.
  * `pread_try`: A `pread()` call was attempted (including retries after `EINTR`).
index 37403e8de78001cf4e4d81a0eccea8347350af39..d7932f51ad5e086e06044ad46a80032f651e1a02 100644 (file)
@@ -569,10 +569,13 @@ BeesBtrfsExtentLayer::block_map_fetch(BeesBtrfsExtentNode &node,
        eof_analysis_fetch(node);
 
        // Pass 1 — walk refs to compute the prealloc mask and the union of
-       // data-ref ranges.  No I/O here: prealloc state is purely structural,
-       // and we use the data-ref union to avoid invoking extent-level pread
-       // over ranges that have only prealloc refs (which would have no
-       // usable ref to read from).
+       // openable data-ref ranges.  Prealloc state is purely structural.
+       // Data coverage additionally requires the ref to open: refs in
+       // deleted files or snapshots linger in the ref list until the last
+       // open fd closes or the subvol cleaner runs, and blocks covered
+       // only by such refs are unreachable, not readable.  The data-ref
+       // union then keeps pass 2 from invoking extent-level pread over
+       // ranges with no usable ref to read from.
        auto refs_sp = atomic_load(&node.m_refs);
        if (!refs_sp) refs_sp = refs_fetch(node);
        vector<bool> has_data(block_count, false);
@@ -606,7 +609,25 @@ BeesBtrfsExtentLayer::block_map_fetch(BeesBtrfsExtentNode &node,
                                }
                        }
                } else {
-                       for (size_t i = first; i < min(last, block_count); ++i) {
+                       // Skip the openability probe when the ref adds no new
+                       // coverage, so heavily-overlapping ref clusters don't
+                       // probe every referencing file.
+                       const auto limit = min(last, block_count);
+                       bool new_coverage = false;
+                       for (size_t i = first; i < limit; ++i) {
+                               if (!has_data[i]) {
+                                       new_coverage = true;
+                                       break;
+                               }
+                       }
+                       if (!new_coverage) {
+                               continue;
+                       }
+                       if (!ref.open(self)) {
+                               BEESCOUNT(block_map_ref_unopenable);
+                               continue;
+                       }
+                       for (size_t i = first; i < limit; ++i) {
                                has_data[i] = true;
                        }
                }