src/Compact.bend source
src/Compact.bend on the hub · documented module
import Baseimport ./Keys.bend as Keysimport ./MemTable.bend as MemTableimport ./Sstable.bend as Sstableimport ./SortedRun.bend as SortedRunimport ./SstFile.bend as SstFileimport ./Wal.bend as Walimport ./Manifest.bend as Manifestimport ./Db.bend as Db# Tiered compaction (Task 9): L0-drain + L1 overlap-closure.## Invariants (Task 7/8 read path depends on them):# - L0-DRAIN: every compaction consumes ALL of L0 (post L0 = []), so every# L0 table is strictly newer than every L1+ table.# - L1-DISJOINT: output is disjoint from every remaining L1 table, via# overlap-closure over the final hull (rounds = len(l1)+1 unconditionally;# rounds past fixpoint are idempotent, so no done-flag is needed).# - READ-ORDER MERGE: inputs merge in exact read order (L0 stored order ++# absorbed-L1 stored order), so first-wins == read first-match.# - TOMBSTONE-SAFE: all winning tombstones are retained at this checkpoint.# This conservative policy prevents resurrection without requiring zero-output# publication support.# Trigger: L0 count > 4 (spec: backpressure stall at 8 = 2T, Task 10).# CrashPoint calls are test-only host effects. Unset or unequal checkpoints are# successful no-ops; signal delivery and filesystem ordering are not Bend proofs.# Soundness sketch (spec law 11): keys resolving outside the closure are# untouched (a key in both closure-inputs and a remainder table would put# it in the hull and in a hull-disjoint range — contradiction); keys inside# resolve to the same newest version (subset order == read order).# Handle ov and in the level compaction.def ov_and(lhs: Bool, rhs: Bool) -> Bool: match lhs: case True{}: rhs case False{}: False{}# Handle le cmp in the level compaction.def le_cmp(ord: Cmp) -> Bool: match ord: case LT{}: True{} case EQ{}: True{} case GT{}: False{}# Handle ov12 in the level compaction.def ov12(fa: Maybe<&2, String>, lb: Maybe<&2, String>) -> Bool: match fa lb: case None{} _: False{} case _ None{}: False{} case Some{x} Some{y}: le_cmp(Keys.cmp(x, y))# Return the table overlap for the level compaction.def table_overlap(+ta: Sstable.Table, +tb: Sstable.Table) -> Bool: ov_and(ov12(Sstable.table_smallest(ta), Sstable.table_largest(tb)), ov12(Sstable.table_smallest(tb), Sstable.table_largest(ta)))# Hull bound folds (pump+fuel+leaf; single-Maybe state, no pairs).def pick_lo(ord: Cmp, k1: String, k2: String) -> Maybe<&2, String>: match ord: case LT{}: Some{k1} case EQ{}: Some{k1} case GT{}: Some{k2}# Handle min step in the level compaction.def min_step(fm: Maybe<&2, String>, cur: Maybe<&2, String>) -> Maybe<&2, String>: match fm cur: case None{} _: cur case _ None{}: fm case Some{+a} Some{+b}: pick_lo(Keys.cmp(a, b), a, b)# Handle min go in the level compaction.def min_go(fuel: Nat, +cur: Maybe<&2, String>, tabs: List<&2, Sstable.Table>) -> Maybe<&2, String>: match fuel: case 0n: cur case 1n+f: match tabs: case Nil{}: cur case Con{h, t}: min_go(f, min_step(Sstable.table_smallest(h), cur), t)# Handle min first in the level compaction.def min_first(+tabs: List<&2, Sstable.Table>) -> Maybe<&2, String>: min_go(List.length(&2, Sstable.Table, tabs), None{}, tabs)# Select hi for the level compaction.def pick_hi(ord: Cmp, k1: String, k2: String) -> Maybe<&2, String>: match ord: case LT{}: Some{k2} case EQ{}: Some{k2} case GT{}: Some{k1}# Handle max step in the level compaction.def max_step(lm: Maybe<&2, String>, cur: Maybe<&2, String>) -> Maybe<&2, String>: match lm cur: case None{} _: cur case _ None{}: lm case Some{+a} Some{+b}: pick_hi(Keys.cmp(a, b), a, b)# Handle max go in the level compaction.def max_go(fuel: Nat, +cur: Maybe<&2, String>, tabs: List<&2, Sstable.Table>) -> Maybe<&2, String>: match fuel: case 0n: cur case 1n+f: match tabs: case Nil{}: cur case Con{h, t}: max_go(f, max_step(Sstable.table_largest(h), cur), t)# Handle max last in the level compaction.def max_last(+tabs: List<&2, Sstable.Table>) -> Maybe<&2, String>: max_go(List.length(&2, Sstable.Table, tabs), None{}, tabs)# Overlap of one table against a hull (bounds as params).def hull_step(keep: Bool, +tab: Sstable.Table, +acc: List<&2, Sstable.Table>) -> List<&2, Sstable.Table>: match keep: case True{}: Con{tab, acc} case False{}: acc# Handle hull neg in the level compaction.def hull_neg(keep: Bool, +tab: Sstable.Table, +acc: List<&2, Sstable.Table>) -> List<&2, Sstable.Table>: match keep: case True{}: acc case False{}: Con{tab, acc}# Handle ov hull in the level compaction.def ov_hull(+tab: Sstable.Table, +lo: Maybe<&2, String>, +hi: Maybe<&2, String>) -> Bool: ov_and(ov12(Sstable.table_smallest(tab), hi), ov12(lo, Sstable.table_largest(tab)))# Handle filt go in the level compaction.def filt_go( fuel: Nat, +lo: Maybe<&2, String>, +hi: Maybe<&2, String>, +acc: List<&2, Sstable.Table>, rest: List<&2, Sstable.Table>) -> List<&2, Sstable.Table>: match fuel: case 0n: List.reverse(&2, Sstable.Table, acc) case 1n+f: match rest: case Nil{}: List.reverse(&2, Sstable.Table, acc) case Con{+h, t}: filt_go(f, lo, hi, hull_step(ov_hull(h, lo, hi), h, acc), t)# Handle filter pos in the level compaction.def filter_pos( +rest: List<&2, Sstable.Table>, +lo: Maybe<&2, String>, +hi: Maybe<&2, String>) -> List<&2, Sstable.Table>: filt_go(List.length(&2, Sstable.Table, rest), lo, hi, Nil{}, rest)# Handle filt neg go in the level compaction.def filt_neg_go( fuel: Nat, +lo: Maybe<&2, String>, +hi: Maybe<&2, String>, +acc: List<&2, Sstable.Table>, rest: List<&2, Sstable.Table>) -> List<&2, Sstable.Table>: match fuel: case 0n: List.reverse(&2, Sstable.Table, acc) case 1n+f: match rest: case Nil{}: List.reverse(&2, Sstable.Table, acc) case Con{+h, t}: filt_neg_go(f, lo, hi, hull_neg(ov_hull(h, lo, hi), h, acc), t)# Handle filter neg in the level compaction.def filter_neg( +rest: List<&2, Sstable.Table>, +lo: Maybe<&2, String>, +hi: Maybe<&2, String>) -> List<&2, Sstable.Table>: filt_neg_go(List.length(&2, Sstable.Table, rest), lo, hi, Nil{}, rest)# The production policy is conservative: every winning tombstone is retained# regardless of the lower-level shadow list.def drop_none( +key: String, +_shadow: List<&2, MemTable.Entry>, +acc: List<&2, MemTable.Entry>) -> List<&2, MemTable.Entry>: Con{MemTable.Entry{key, None{}}, acc}# Handle drop go in the level compaction.def drop_go( fuel: Nat, +shadow: List<&2, MemTable.Entry>, +acc: List<&2, MemTable.Entry>, xs: List<&2, MemTable.Entry>) -> List<&2, MemTable.Entry>: match fuel: case 0n: List.reverse(&2, MemTable.Entry, acc) case 1n+f: match xs: case Nil{}: List.reverse(&2, MemTable.Entry, acc) case Con{MemTable.Entry{k, v}, t}: match v: case None{}: drop_go(f, shadow, drop_none(k, shadow, acc), t) case Some{s}: drop_go(f, shadow, Con{MemTable.Entry{k, Some{s}}, acc}, t)# Handle shadow drop in the level compaction.def shadow_drop(+xs: List<&2, MemTable.Entry>, +shadow: List<&2, MemTable.Entry>) -> List<&2, MemTable.Entry>: drop_go(List.length(&2, MemTable.Entry, xs), shadow, Nil{}, xs)# Preserve each table's strict sorted-run boundary and stored read order.def table_runs(tabs: List<&2, Sstable.Table>) -> List<&2, List<&2, MemTable.Entry>>: match tabs: case Nil{}: Nil{} case Con{h, rest}: Con{Db.table_entries(h), table_runs(rest)}# Production output: merge L0 in chronological stored order, merge absorbed L1# in its original stored order, then merge L0 as strictly newer than L1.# Tombstones are retained, so a triggered compaction publishes exactly one table.def finish_runs( +l0_runs: List<&2, List<&2, MemTable.Entry>>, +l1_runs: List<&2, List<&2, MemTable.Entry>>, +rest: List<&2, Sstable.Table>) -> List<&2, Sstable.Table>: merged_l0 merged_l1 = SortedRun.merge_many_newest(l0_runs) SortedRun.merge_many_newest(l1_runs) +merged = SortedRun.merge_many_newest(Con{merged_l0, Con{merged_l1, Nil{}}}) Con{Sstable.from_sorted_unique(shadow_drop(merged, Nil{}), 1n), rest}# Closure rounds retain the original L1 list so the final absorbed runs are# selected in exact stored read order, independent of the round that found them.def closed_runs_go( major: Nat, +lo: Maybe<&2, String>, +hi: Maybe<&2, String>, +l0_runs: List<&2, List<&2, MemTable.Entry>>, +all_l1: List<&2, Sstable.Table>, +rest: List<&2, Sstable.Table>,) -> List<&2, Sstable.Table>: match major: case 0n: finish_runs(l0_runs, table_runs(filter_pos(all_l1, lo, hi)), rest) case 1n+m: +abs_new +rem_new = filter_pos(rest, lo, hi) filter_neg(rest, lo, hi) closed_runs_go(m, min_step(min_first(abs_new), lo), max_step(max_last(abs_new), hi), l0_runs, all_l1, rem_new)# Top dispatch (all matches on params; count gate inside).def compact_snd( +l0: List<&2, Sstable.Table>, rest1: List<&2, List<&2, Sstable.Table>>) -> List<&2, List<&2, Sstable.Table>>: match rest1: case Nil{}: Con{Nil{}, Con{finish_runs(table_runs(l0), Nil{}, Nil{}), Nil{}}} case Con{+l1, +below}: Con{Nil{}, Con{closed_runs_go(Nat.add(List.length(&2, Sstable.Table, l1), 1n), min_first(l0), max_last(l0), table_runs(l0), l1, l1), below}}# Compact dec for the level compaction.def compact_dec( gt: Bool, +l0: List<&2, Sstable.Table>, +rest1: List<&2, List<&2, Sstable.Table>>) -> List<&2, List<&2, Sstable.Table>>: match gt: case True{}: compact_snd(l0, rest1) case False{}: Con{l0, rest1}# Compact levels for the level compaction.def compact_levels(levels: List<&2, List<&2, Sstable.Table>>) -> List<&2, List<&2, Sstable.Table>>: match levels: case Nil{}: Nil{} case Con{+l0, rest1}: compact_dec(Nat.is_lt(4n, List.length(&2, Sstable.Table, l0)), l0, rest1)# --- IO helpers (Task 9b): remainder-membership, name splits, level surgery.# Remainder test via ranges (sound by old-L1 disjointness, spec law 12):# in a disjoint family, overlap with a remainder table means identity.# Combine leaf for the level compaction.def or_leaf(+hit: Bool, +acc: Bool) -> Bool: match hit: case True{}: True{} case False{}: acc# Handle in go in the level compaction.def in_go(fuel: Nat, +tab: Sstable.Table, +acc: Bool, rem: List<&2, Sstable.Table>) -> Bool: match fuel: case 0n: acc case 1n+f: match rem: case Nil{}: acc case Con{h, tt}: in_go(f, tab, or_leaf(table_overlap(tab, h), acc), tt)# Handle in rem in the level compaction.def in_rem(+tab: Sstable.Table, +rem: List<&2, Sstable.Table>) -> Bool: in_go(List.length(&2, Sstable.Table, rem), tab, False{}, rem)# Handle abs pick in the level compaction.def abs_pick(+inr: Bool, +name: String, +acc: List<&2, String>) -> List<&2, String>: match inr: case True{}: acc case False{}: Con{name, acc}# Handle abs go in the level compaction.def abs_go( fuel: Nat, +rem: List<&2, Sstable.Table>, +acc: List<&2, String>, tabs: List<&2, Sstable.Table>, names: List<&2, String>) -> List<&2, String>: match fuel: case 0n: List.reverse(&2, String, acc) case 1n+f: match tabs names: case Nil{} Nil{}: List.reverse(&2, String, acc) case Nil{} Con{nh, nt}: List.reverse(&2, String, acc) case Con{th, tt} Nil{}: List.reverse(&2, String, acc) case Con{th, tt} Con{nh, nt}: abs_go(f, rem, abs_pick(in_rem(th, rem), nh, acc), tt, nt)# Handle abs names in the level compaction.def abs_names( +tabs: List<&2, Sstable.Table>, +names: List<&2, String>, +rem: List<&2, Sstable.Table>) -> List<&2, String>: abs_go(List.length(&2, Sstable.Table, tabs), rem, Nil{}, tabs, names)# Handle rem pick in the level compaction.def rem_pick(+inr: Bool, +name: String, +acc: List<&2, String>) -> List<&2, String>: match inr: case True{}: Con{name, acc} case False{}: acc# Handle rem go in the level compaction.def rem_go( fuel: Nat, +rem: List<&2, Sstable.Table>, +acc: List<&2, String>, tabs: List<&2, Sstable.Table>, names: List<&2, String>) -> List<&2, String>: match fuel: case 0n: List.reverse(&2, String, acc) case 1n+f: match tabs names: case Nil{} Nil{}: List.reverse(&2, String, acc) case Nil{} Con{nh, nt}: List.reverse(&2, String, acc) case Con{th, tt} Nil{}: List.reverse(&2, String, acc) case Con{th, tt} Con{nh, nt}: rem_go(f, rem, rem_pick(in_rem(th, rem), nh, acc), tt, nt)# Handle rem names in the level compaction.def rem_names( +tabs: List<&2, Sstable.Table>, +names: List<&2, String>, +rem: List<&2, Sstable.Table>) -> List<&2, String>: rem_go(List.length(&2, Sstable.Table, tabs), rem, Nil{}, tabs, names)# --- Closed-vector laws live in laws/Compact.bend (spec laws 6, 11, 12) ---# --- IO chain (Task 9b): output file + manifest rewrite + input removal.# Crash discipline mirrors flush: output durable, then manifest (tmp +# rename + dir fsync), then input files become orphans (recovery ignores# files absent from the manifest; Task 12 sweeps them). Manifest/table# drift (exact serialized Manifest identity mismatch) aborts fail-closed;# names stay positionally aligned with tables (output head, remainder tail).# Handle l1hd in the level compaction.def l1hd(r1: List<&2, List<&2, Sstable.Table>>) -> List<&2, Sstable.Table>: match r1: case Nil{}: Nil{} case Con{l1, below}: l1# Handle l1list in the level compaction.def l1list(lvls: List<&2, List<&2, Sstable.Table>>) -> List<&2, Sstable.Table>: match lvls: case Nil{}: Nil{} case Con{l0, r1}: l1hd(r1)# Handle l0list in the level compaction.def l0list(lvls: List<&2, List<&2, Sstable.Table>>) -> List<&2, Sstable.Table>: match lvls: case Nil{}: Nil{} case Con{l0, r1}: l0# Handle hd tbl in the level compaction.def hd_tbl(tabs: List<&2, Sstable.Table>) -> Sstable.Table: match tabs: case Nil{}: Sstable.from_sorted_unique(Nil{}, 1n) case Con{h, t}: h# Process the remaining of for the level compaction.def tail_of(tabs: List<&2, Sstable.Table>) -> List<&2, Sstable.Table>: match tabs: case Nil{}: Nil{} case Con{h, t}: t# Handle ml0 in the level compaction.def ml0(mfst: Manifest.Manifest) -> List<&2, String>: match mfst: case Manifest.M{lvs}: match lvs: case Nil{}: Nil{} case Con{l0n, r1}: l0n# Handle ml1b in the level compaction.def ml1b(r1: List<&2, List<&2, String>>) -> List<&2, String>: match r1: case Nil{}: Nil{} case Con{l1n, below}: l1n# Handle ml1 in the level compaction.def ml1(mfst: Manifest.Manifest) -> List<&2, String>: match mfst: case Manifest.M{lvs}: match lvs: case Nil{}: Nil{} case Con{l0n, r1}: ml1b(r1)# Handle mbelb in the level compaction.def mbelb(r1: List<&2, List<&2, String>>) -> List<&2, List<&2, String>>: match r1: case Nil{}: Nil{} case Con{l1n, below}: below# Handle mbelow in the level compaction.def mbelow(mfst: Manifest.Manifest) -> List<&2, List<&2, String>>: match mfst: case Manifest.M{lvs}: match lvs: case Nil{}: Nil{} case Con{l0n, r1}: mbelb(r1)# Handle mfst new in the level compaction.def mfst_new(+mfst: Manifest.Manifest, +remnames: List<&2, String>, +outname: String) -> Manifest.Manifest: Manifest.M{Con{Nil{}, Con{Con{outname, remnames}, mbelow(mfst)}}}# Handle prefix go in the level compaction.def prefix_go(names: List<&2, String>, +dir: String, +acc: List<&2, String>) -> List<&2, String>: match names: case Nil{}: List.reverse(&2, String, acc) case Con{h, t}: prefix_go(t, dir, Con{dir ++ "/" ++ h, acc})# Handle prefix in the level compaction.def prefix(+names: List<&2, String>, +dir: String) -> List<&2, String>: prefix_go(names, dir, Nil{})