Claimable-stripe admission for stripe_alloc

Draft design, 2026-08-09. Addresses the churn-time writeback ENOSPC storms: ~5.2-5.5k reserved buffered writebacks dropped per fsstress churn run (~20 MB), silently for non-fsync writers. Stock refuses the same writes at write() time, so this is the one regime where stripe_alloc is strictly worse than stock.

The problem, precisely

Measured on the 8x512M raid5 fill+churn workload (dumps at every failing find_free_extent):

Why the existing accounting cannot close this

The reservation side is derived: free_space minus bytes_stripe_unusable minus bytes_stripe_open, minus a pessimistic per-extent margin held until writeback. For that arithmetic to guarantee writeback success, bytes_stripe_unusable may never under-count trapped bytes mid-window. The incremental accounting (stripe_unusable_account_add) is documented to over-count returns ("bounded overcount in the safe direction"), but there are paths that can leave it under reality between commit rescans:

  1. Tail claims (btrfs_claim_stripe_tail): a sub-stripe piece is taken out of the free space cache to extend a run's frontier stripe. The remaining free bytes of that stripe just became trapped fragments, but nothing adds them to unusable until the next rescan.
  2. Whole-stripe middles of merged returns: two adjacent returns can complete a stripe whose halves were both counted trapped; the rescan lowers unusable (safe), but the mirror case -- a counted- usable whole stripe losing wholeness through a tail claim -- has no incremental hook (case 1 again, from the other side).
  3. Any future allocation-side consumer of sub-stripe free space repeats the pattern. The invariant "only frees change fragmentation" is false as soon as any claim takes less than a whole stripe.

Even with perfect unusable accounting there is a structural gap: the ledger is global while the constraint is per-block-group and per-stripe-width. A global availability of one full stripe can be spread across block groups as sub-stripe claimable remainders in each (mixed 320K/448K widths make this worse). Global arithmetic cannot see that.

Proposal: admit against measured claimable supply

Stop deriving what a writeback will find; count it.

Per-block-group counter: bg->stripe_claimable = bytes of fully free, stripe-aligned whole stripes in the free space cache (exactly what btrfs_claim_free_stripe_run can take; the fast path's definition). Complementary to bg->stripe_unusable: every free byte in a stripe is claimable iff the whole stripe is free, trapped otherwise. The commit rescan (stripe_unusable_scan) already computes per-stripe free totals in its freep[] array -- it can emit both counters from one pass at no extra cost, and is the authority that corrects drift each commit.

Incremental maintenance shares the sites (and the touched-stripe classification) with the unusable accounting:

Accuracy rule: any site that cannot cheaply classify may err by counting claimable LOW (move bytes to unusable) -- early write()-time ENOSPC, corrected by the next rescan; never high.

Admission: sinfo->bytes_stripe_claimable = sum over armed bgs (same aggregation as bytes_stripe_unusable, same rescan reconciliation). The data reservation path admits only while

bytes_stripe_claimable >= bytes_stripe_margin + request

i.e. the claimable supply covers every already-admitted-not-yet- allocated write's worst case (that is what bytes_stripe_margin already tracks) plus this one. The existing derived check stays as a second gate; effective admission is min(derived, claimable-based).

This closes the straggler window by construction: a write is admitted only if, at admission time, enough actually claimable whole-stripe supply exists to cover it and everyone admitted before it, and that supply can only be consumed by those same admitted writes (plus metadata/log claims -- see open questions).

Not counted (deliberately, v1): joinable open-run remainders. They can satisfy same-class writers but not others (LOG/NOCOW/RELOC are private or class-bound); ignoring them under-counts supply, which is the safe direction. If admission proves too tight in fsync-heavy workloads, a per-class refinement can add the COW-class shared-run remainder.

Alternatives considered

Costs and risks

Validation plan

  1. Counter-only patch first (maintain + rescan-verify, WARN on drift): run fill/churn/balance suites; drift warnings localize any missed site. This alone also fixes the tail-claim undercount of bytes_stripe_unusable.
  2. Enable the admission gate; acceptance: fsstress churn run with zero "returned ENOSPC despite reservation margin" warnings and zero cow_file_range -28 on reserved COW writes, at a churn throughput within ~10% of today's.
  3. Regression battery: why-rmw (seed reads must stay 0), fill-edge, balance-reclaim, ENOSPC dataloss matrix, aligned control, raid6.

Open questions for review