~/bend-docscommunity

src/Recover.bend source

src/Recover.bend on the hub · documented module

import Baseimport ./Keys.bend as Keysimport ./MemTable.bend as MemTableimport ./Sstable.bend as Sstableimport ./SstStreamIo.bend as SstStreamIoimport ./Wal.bend as Walimport ./Manifest.bend as Manifestimport ./Flush.bend as Flushimport ./Compact.bend as Compactimport ./CompactIo.bend as CompactIoimport ./Fs.bend as Fsimport ./Db.bend as Dbimport ./DbIo.bend as DbIoimport ./RecoverPure.bend as Pureimport bend-kit-bytes@0.3.2.0/bytes.bend as Bytes# Represent ExactResult data used by the database recovery effects.type ExactResult is Type:  ExactResult{bytes: Bytes.Bytes, complete: Bool}# Represent ExactState data used by the database recovery effects.type ExactState is Type:  ExactNeed{file: File, remaining: Nat, chunks: List<&1, Bytes.Bytes>}  ExactRead{pair: File & Result<&1, &1, U32 & String, Bytes.Bytes>, remaining: Nat, chunks: List<&1, Bytes.Bytes>}# Represent WalProgress data used by the database recovery effects.type WalProgress is Type:  WalMore{file: File, offset: Nat, state: Db.RotRes}  WalStop{result: Result<&1, &1, U32 & String, Db.RotRes>}# Represent WalState data used by the database recovery effects.type WalState is Type:  WalAt{file: File, offset: Nat, state: Db.RotRes}  WalAfter{progress: WalProgress}def exact.max(large: Bool, +remaining: Nat) -> U32:  match large:    case True{}:      1048576    case False{}:      U32.from_nat(remaining)def exact.result(  file: File,  chunks: List<&1, Bytes.Bytes>,  complete: Bool) -> IO(File & Result<&1, &1, U32 & String, ExactResult>):  IO.pure(File & Result<&1, &1, U32 & String, ExactResult>,    (file, Done{ExactResult{Bytes.concat(List.reverse(&1, Bytes.Bytes, chunks)), complete}}))def exact.loop(  fuel: Nat,  state: ExactState) -> IO(File & Result<&1, &1, U32 & String, ExactResult>):  match fuel:    case 0n:      match state:        case ExactNeed{file, remaining, chunks}:          match remaining:            case 0n:              exact.result(file, chunks, True{})            case 1n+_:              IO.pure(File & Result<&1, &1, U32 & String, ExactResult>,                (file, Fail{(U32.from_nat(7n), "bounded read exhausted")}))        case ExactRead{pair, _, _}:          match pair:            case (file, Fail{error}):              IO.pure(File & Result<&1, &1, U32 & String, ExactResult>, (file, Fail{error}))            case (file, Done{_}):              IO.pure(File & Result<&1, &1, U32 & String, ExactResult>,                (file, Fail{(U32.from_nat(7n), "bounded read exhausted")}))    case 1n+rest:      match state:        case ExactNeed{file, +remaining, chunks}:          match remaining:            case 0n:              exact.result(file, chunks, True{})            case 1n+_:              do IO<File & Result<&1, &1, U32 & String, ExactResult>>:                next : File & Result<&1, &1, U32 & String, Bytes.Bytes> <-                  Fs.read_bytes(file, exact.max(Nat.is_le(1048577n, remaining), remaining))                exact.loop(rest, ExactRead{next, remaining, chunks})        case ExactRead{pair, remaining, chunks}:          match pair:            case (file, Fail{error}):              IO.pure(File & Result<&1, &1, U32 & String, ExactResult>, (file, Fail{error}))            case (file, Done{Bytes.Bytes{0, _}}):              exact.result(file, chunks, False{})            case (file, Done{Bytes.Bytes{+len, buf}}):              exact.loop(rest, ExactNeed{file,                (remaining - U32.to_nat(len) : Nat),                Con{Bytes.Bytes{len, buf}, chunks}})# Recovery + maintenance (Task 10).## Open: ensure dir; sweep *.tmp (best-effort); load manifest (absent =# fresh; corrupt = fatal); shape-check all names; load tables positionally# (listed-but-missing/corrupt = fatal, never silent); restore the flush# counter from the greatest table-name generation; replay + truncate# the WAL; return the Db.## Maintenance (synchronous, observably identical ordering to background):# after every acked batch: drain while L0 >= 8 (stall = blocking# maintenance), flush when mem >= 4096, compact (self-gated at L0 > 4).# True fork-based background (frozen mem + IO.spawn) is a benchmark-gated# follow-up; ordering and stall semantics are already exact.# --- Loaders (self-recursive IO; fatal via IO.die, never silent) ---# Handle load tables in the database recovery effects.def load_tables(  names: List<&2, String>,  +ldir: String,  +dir: String,  +acc: List<&2, Sstable.Table>) -> IO(List<&2, Sstable.Table>):  match names:    case Nil{}:      IO.pure(List<&2, Sstable.Table>, List.reverse(&2, Sstable.Table, acc))    case Con{+n, t}:      do IO<List<&2, Sstable.Table>>:        tbl : Sstable.Table <- IO.try(Sstable.Table, SstStreamIo.read_table(dir ++ "/" ++ ldir ++ "/" ++ n))        load_tables(t, ldir, dir, Con{tbl, acc})# Handle ldir io in the database recovery effects.def ldir_io(+opt: Maybe<&2, String>) -> IO(String):  match opt:    case None{}:      IO.die(String, U32.from_nat(4n), "too many levels")    case Some{d}:      IO.pure(String, d)# Handle ldir of in the database recovery effects.def ldir_of(idx: Nat) -> Maybe<&2, String>:  match idx:    case 0n:      Some{"l0"}    case 1n+m:      match m:        case 0n:          Some{"l1"}        case 1n+p:          match p:            case 0n:              Some{"l2"}            case 1n+q:              match q:                case 0n:                  Some{"l3"}                case 1n+r:                  None{}# Handle load levels in the database recovery effects.def load_levels(  lvls: List<&2, List<&2, String>>,  +idx: Nat,  +dir: String,  +acc: List<&2, List<&2, Sstable.Table>>) -> IO(List<&2, List<&2, Sstable.Table>>):  match lvls:    case Nil{}:      IO.pure(List<&2, List<&2, Sstable.Table>>, List.reverse(&2, List<&2, Sstable.Table>, acc))    case Con{ns, t}:      do IO<List<&2, List<&2, Sstable.Table>>>:        ld : String <- ldir_io(ldir_of(idx))        tabs : List<&2, Sstable.Table> <- load_tables(ns, ld, dir, Nil{})        load_levels(t, Nat.add(idx, 1n), dir, Con{tabs, acc})def wal.empty() -> Db.RotRes:  Db.Rot{MemTable.empty(), MemTable.empty(), 0n, 0n}def exact.start(file: File, +needed: Nat) -> IO(File & Result<&1, &1, U32 & String, ExactResult>):  exact.loop(Nat.add(Nat.mul(2n, needed), 1n), ExactNeed{file, needed, Nil{}})def wal.truncate(  +path: String,  +offset: Nat,  state: Db.RotRes) -> IO(WalProgress):  do IO<WalProgress>:    _truncated : Unit <- IO.try(Unit, Fs.truncate(path, offset))    _synced : Unit <- IO.try(Unit, Fs.fsync(path))    return WalStop{Done{state}}def wal.u32.pair(pair: Bytes.Cursor & Maybe<&2, U32>) -> Maybe<&2, U32>:  match pair:    case (_, value):      valuedef wal.u32(bytes: Bytes.Bytes) -> Maybe<&2, U32>:  wal.u32.pair(Bytes.Cursor.u32be(Bytes.Cursor.new(bytes)))def wal.apply(batch: Wal.Batch, state: Db.RotRes) -> Db.RotRes:  match batch state:    case Wal.Batch{muts} Db.Rot{mem, frozen, mem_count, frozen_count}:      Db.apply_and_rotate(muts, mem, frozen, mem_count, frozen_count)def wal.frames.decoded(  result: Result<&1, &1, Wal.Error, Wal.Batch>,  file: File,  +next_offset: Nat,  state: Db.RotRes) -> IO(WalProgress):  match result:    case Fail{_}:      do IO<WalProgress>:        _closed : Unit <- File.close(file)        return WalStop{Fail{(U32.from_nat(3n), "corrupt WAL frame")}}    case Done{batch}:      IO.pure(WalProgress,        WalMore{file, next_offset, wal.apply(batch, state)})def wal.frames.decode(  pair: File & Result<&1, &1, U32 & String, ExactResult>,  prefix: Bytes.Bytes,  +offset: Nat,  +next_offset: Nat,  state: Db.RotRes,  +path: String) -> IO(WalProgress):  match pair:    case (file, Fail{error}):      do IO<WalProgress>:        _closed : Unit <- File.close(file)        return WalStop{Fail{error}}    case (file, Done{ExactResult{_, False{}}}):      do IO<WalProgress>:        _closed : Unit <- File.close(file)        wal.truncate(path, offset, state)    case (file, Done{ExactResult{body, True{}}}):      wal.frames.decoded(Wal.decode_frame(Bytes.concat([prefix, body])),        file, next_offset, state)def wal.frames.body.bound(  enough: Bool,  file: File,  prefix: Bytes.Bytes,  +frame_size: Nat,  +offset: Nat,  +next_offset: Nat,  state: Db.RotRes,  +path: String) -> IO(WalProgress):  match enough:    case False{}:      do IO<WalProgress>:        _closed : Unit <- File.close(file)        wal.truncate(path, offset, state)    case True{}:      do IO<WalProgress>:        pair : File & Result<&1, &1, U32 & String, ExactResult> <- exact.start(file, frame_size)        wal.frames.decode(pair, prefix, offset, next_offset, state, path)def wal.frames.body(  file: File,  prefix: Bytes.Bytes,  +frame_size: Nat,  +size: Nat,  +offset: Nat,  state: Db.RotRes,  +path: String) -> IO(WalProgress):  +next_offset = Nat.add(offset, Nat.add(4n, frame_size))  wal.frames.body.bound(Nat.is_le(next_offset, size), file, prefix,    frame_size, offset, next_offset, state, path)def wal.frames.bound(  within: Bool,  file: File,  bytes: Bytes.Bytes,  frame_len: U32,  +size: Nat,  +offset: Nat,  state: Db.RotRes,  +path: String) -> IO(WalProgress):  match within:    case False{}:      do IO<WalProgress>:        _closed : Unit <- File.close(file)        return WalStop{Fail{(U32.from_nat(3n), "WAL frame length out of bounds")}}    case True{}:      wal.frames.body(file, bytes, U32.to_nat(frame_len), size,        offset, state, path)def wal.frames.length.checked(  enough: Bool,  within: Bool,  file: File,  prefix: Bytes.Bytes,  frame_len: U32,  +size: Nat,  +offset: Nat,  state: Db.RotRes,  +path: String) -> IO(WalProgress):  match enough:    case False{}:      do IO<WalProgress>:        _closed : Unit <- File.close(file)        wal.truncate(path, offset, state)    case True{}:      wal.frames.bound(within, file, prefix, frame_len, size,        offset, state, path)def wal.frames.length.value(  file: File,  maybe_len: Maybe<&2, U32>,  +size: Nat,  +offset: Nat,  state: Db.RotRes,  +path: String) -> IO(WalProgress):  match maybe_len:    case None{}:      do IO<WalProgress>:        _closed : Unit <- File.close(file)        return WalStop{Fail{(U32.from_nat(3n), "malformed WAL frame length")}}    case Some{+frame_len}:      +next_offset = Nat.add(offset, Nat.add(4n, U32.to_nat(frame_len)))      wal.frames.length.checked(Nat.is_le(next_offset, size),        U32.is_le(36, frame_len) && U32.is_le(frame_len, 1073741860),        file, Wal.u32_bytes(frame_len), frame_len, size, offset,        state, path)def wal.frames.length(  pair: File & Result<&1, &1, U32 & String, ExactResult>,  +size: Nat,  +offset: Nat,  state: Db.RotRes,  +path: String) -> IO(WalProgress):  match pair:    case (file, Fail{error}):      do IO<WalProgress>:        _closed : Unit <- File.close(file)        return WalStop{Fail{error}}    case (file, Done{ExactResult{bytes, complete}}):      match complete:        case False{}:          do IO<WalProgress>:            _closed : Unit <- File.close(file)            wal.truncate(path, offset, state)        case True{}:          wal.frames.length.value(file, wal.u32(bytes),            size, offset, state, path)def wal.frames.remaining(  file: File,  +size: Nat,  +offset: Nat,  state: Db.RotRes,  +path: String,  remaining: Nat) -> IO(WalProgress):  match remaining:    case 0n:      do IO<WalProgress>:        _closed : Unit <- File.close(file)        return WalStop{Done{state}}    case 1n+p:      match p:        case 0n:          do IO<WalProgress>:            _closed : Unit <- File.close(file)            wal.truncate(path, offset, state)        case 1n+q:          match q:            case 0n:              do IO<WalProgress>:                _closed : Unit <- File.close(file)                wal.truncate(path, offset, state)            case 1n+_:              do IO<WalProgress>:                pair : File & Result<&1, &1, U32 & String, ExactResult> <- exact.start(file, 4n)                wal.frames.length(pair, size, offset, state, path)def wal.frames.end(  at_end: Bool,  file: File,  +size: Nat,  +offset: Nat,  state: Db.RotRes,  +path: String) -> IO(WalProgress):  match at_end:    case True{}:      do IO<WalProgress>:        _closed : Unit <- File.close(file)        return WalStop{Done{state}}    case False{}:      wal.frames.remaining(file, size, offset, state, path,        (size - offset : Nat))def wal.frames(  +fuel: Nat,  +size: Nat,  +path: String,  state: WalState) -> IO(Result<&1, &1, U32 & String, Db.RotRes>):  match fuel:    case 0n:      match state:        case WalAt{file, _, _}:          do IO<Result<&1, &1, U32 & String, Db.RotRes>>:            _closed : Unit <- File.close(file)            return Fail{(U32.from_nat(7n), "WAL frame limit exceeded")}        case WalAfter{WalStop{result}}:          IO.pure(Result<&1, &1, U32 & String, Db.RotRes>, result)        case WalAfter{WalMore{file, _, _}}:          do IO<Result<&1, &1, U32 & String, Db.RotRes>>:            _closed : Unit <- File.close(file)            return Fail{(U32.from_nat(7n), "WAL frame limit exceeded")}    case 1n+rest:      match state:        case WalAt{file, +offset, res}:          do IO<Result<&1, &1, U32 & String, Db.RotRes>>:            progress : WalProgress <- wal.frames.end(              Nat.is_eq(offset, size), file, size, offset, res, path)            wal.frames(rest, size, path, WalAfter{progress})        case WalAfter{WalStop{result}}:          IO.pure(Result<&1, &1, U32 & String, Db.RotRes>, result)        case WalAfter{WalMore{file, offset, res}}:          wal.frames(rest, size, path, WalAt{file, offset, res})def wal.header.decoded(  result: Result<&1, &1, Wal.Error, Unit>,  file: File,  +size: Nat,  +path: String) -> IO(Result<&1, &1, U32 & String, Db.RotRes>):  match result:    case Fail{_}:      do IO<Result<&1, &1, U32 & String, Db.RotRes>>:        _closed : Unit <- File.close(file)        return Fail{(U32.from_nat(3n), "unsupported WAL format")}    case Done{Unit{}}:      wal.frames(Nat.add(Nat.div(size, 18n), 4n), size, path,        WalAt{file, 8n, wal.empty()})def wal.header(  pair: File & Result<&1, &1, U32 & String, ExactResult>,  +size: Nat,  +path: String) -> IO(Result<&1, &1, U32 & String, Db.RotRes>):  match pair:    case (file, Fail{error}):      do IO<Result<&1, &1, U32 & String, Db.RotRes>>:        _closed : Unit <- File.close(file)        return Fail{error}    case (file, Done{ExactResult{_, False{}}}):      do IO<Result<&1, &1, U32 & String, Db.RotRes>>:        _closed : Unit <- File.close(file)        _truncated : Unit <- IO.try(Unit, Fs.truncate(path, 0n))        _synced : Unit <- IO.try(Unit, Fs.fsync(path))        return Done{wal.empty()}    case (file, Done{ExactResult{bytes, True{}}}):      wal.header.decoded(Wal.decode_header(bytes), file, size, path)def wal.open.present(  present: Bool,  +path: String) -> IO(Result<&1, &1, U32 & String, Db.RotRes>):  match present:    case False{}:      IO.pure(Result<&1, &1, U32 & String, Db.RotRes>, Done{wal.empty()})    case True{}:      do IO<Result<&1, &1, U32 & String, Db.RotRes>>:        size : Nat <- IO.try(Nat, Fs.file_size(path))        file : File <- IO.try(File, File.open(path, "r"))        pair : File & Result<&1, &1, U32 & String, ExactResult> <- exact.start(file, 8n)        wal.header(pair, size, path)# Handle wal open in the database recovery effects.def wal_open(+dir: String) -> IO(Result<&1, &1, U32 & String, Db.RotRes>):  do IO<Result<&1, &1, U32 & String, Db.RotRes>>:    present : Bool <- IO.try(Bool, Fs.exists(Db.wal_path(dir)))    wal.open.present(present, Db.wal_path(dir))# --- Tmp sweep (best-effort) ---# Handle tmp pick in the database recovery effects.def tmp_pick(keep: Bool, +name: String, +acc: List<&2, String>) -> List<&2, String>:  match keep:    case True{}:      Con{name, acc}    case False{}:      acc# Handle tmp some in the database recovery effects.def tmp_some(+ok: Bool, +name: String) -> Maybe<&2, String>:  match ok:    case True{}:      Some{name}    case False{}:      None{}# Handle tmp map list in the database recovery effects.def tmp_map_list(xs: List<&1, String>) -> List<&2, Maybe<&2, String>>:  match xs:    case Nil{}:      Nil{}    case Con{+h, t}:      Con{tmp_some(String.ends_with(h, ".tmp"), h), tmp_map_list(t)}# Handle cat go in the database recovery effects.def cat_go(xs: List<&2, Maybe<&2, String>>) -> List<&2, String>:  match xs:    case Nil{}:      Nil{}    case Con{h, t}:      match h:        case None{}:          cat_go(t)        case Some{s}:          Con{s, cat_go(t)}# Handle tmp names in the database recovery effects.def tmp_names(xs: List<&1, String>) -> List<&2, String>:  cat_go(tmp_map_list(xs))# Handle sweep one in the database recovery effects.def sweep_one(res: Result<&1, &1, U32 & String, List<String>>, +dir: String) -> IO(Unit):  match res:    case Fail{e}:      IO.pure(Unit, Unit{})    case Done{ns}:      do IO<Unit>:        _ign : Result<&1, &1, U32 & String, Unit> <- CompactIo.remove_list(Compact.prefix(tmp_names(ns), dir))        IO.pure(Unit, Unit{})# Handle sweep get in the database recovery effects.def sweep_get(+dir: String, +sub: String) -> IO(Unit):  do IO<Unit>:    r : Result<&1, &1, U32 & String, List<String>> <- Fs.read_dir(dir ++ "/" ++ sub)    sweep_one(r, dir ++ "/" ++ sub)# Handle sweep cnt in the database recovery effects.def sweep_cnt(res: Result<&1, &1, U32 & String, Nat>, +dir: String, +sub: String) -> IO(Unit):  match res:    case Fail{e}:      IO.pure(Unit, Unit{})    case Done{n}:      sweep_get(dir, sub)# Handle sweep dir in the database recovery effects.def sweep_dir(+dir: String, +sub: String) -> IO(Unit):  do IO<Unit>:    c : Result<&1, &1, U32 & String, Nat> <- Fs.read_dir_count(dir ++ "/" ++ sub)    sweep_cnt(c, dir, sub)# --- Open assembly ---# Handle wal part in the database recovery effects.def wal_part(+dir: String) -> IO(Result<&1, &1, U32 & String, Db.RotRes>):  do IO<Result<&1, &1, U32 & String, Db.RotRes>>:    _initialized : Unit <- IO.try(Unit, DbIo.wal_initialize(dir))    wal_open(dir)# Open assemble for the database recovery effects.def open_assemble(  res: Db.RotRes,  +dir: String,  +mfst: Manifest.Manifest,  levels: List<&2, List<&2, Sstable.Table>>) -> IO(Result<&1, &1, U32 & String, Db.Db>):  match res:    case Db.Rot{mem, frozen, mc, fc}:      do IO<Result<&1, &1, U32 & String, Db.Db>>:        return Done{Db.Db{dir, mem, frozen, Db.default_batch_cap(), Nil{}, levels, Pure.count_gen(Pure.mfst_lists(mfst)), Manifest.token(mfst), mc, fc}}# Open levels checked for the database recovery effects.def open_levels_checked(  ok: Bool,  +dir: String,  +mfst: Manifest.Manifest,  levels: List<&2, List<&2, Sstable.Table>>) -> IO(Result<&1, &1, U32 & String, Db.Db>):  match ok:    case False{}:      IO.pure(Result<&1, &1, U32 & String, Db.Db>, Fail{(U32.from_nat(6n), "overlapping tables in L1+ manifest")})    case True{}:      do IO<Result<&1, &1, U32 & String, Db.Db>>:        res : Db.RotRes <- IO.try(Db.RotRes, wal_part(dir))        open_assemble(res, dir, mfst, levels)# Open levels for the database recovery effects.def open_levels(+dir: String, +mfst: Manifest.Manifest) -> IO(Result<&1, &1, U32 & String, Db.Db>):  do IO<Result<&1, &1, U32 & String, Db.Db>>:    +levels : List<&2, List<&2, Sstable.Table>> <- load_levels(Pure.mfst_lists(mfst), 0n, dir, Nil{})    open_levels_checked(Pure.l1_plus_disjoint(levels), dir, mfst, levels)# Open names go for the database recovery effects.def open_names_go(ok: Bool, +dir: String, +mfst: Manifest.Manifest) -> IO(Result<&1, &1, U32 & String, Db.Db>):  match ok:    case True{}:      open_levels(dir, mfst)    case False{}:      IO.pure(Result<&1, &1, U32 & String, Db.Db>, Fail{(U32.from_nat(5n), "bad manifest names")})# Handle sweep root go in the database recovery effects.def sweep_root_go(+dir: String) -> IO(Unit):  do IO<Unit>:    r : Result<&1, &1, U32 & String, List<String>> <- Fs.read_dir(dir)    sweep_one(r, dir)# Handle sweep root cnt in the database recovery effects.def sweep_root_cnt(res: Result<&1, &1, U32 & String, Nat>, +dir: String) -> IO(Unit):  match res:    case Fail{e}:      IO.pure(Unit, Unit{})    case Done{n}:      sweep_root_go(dir)# Handle sweep root in the database recovery effects.def sweep_root(+dir: String) -> IO(Unit):  do IO<Unit>:    c : Result<&1, &1, U32 & String, Nat> <- Fs.read_dir_count(dir)    sweep_root_cnt(c, dir)# Open db for the database recovery effects.def open_db(+dir: String) -> IO(Result<&1, &1, U32 & String, Db.Db>):  do IO<Result<&1, &1, U32 & String, Db.Db>>:    _u0 : Unit <- IO.try(Unit, Flush.ensure_dir(dir))    _s0 : Unit <- sweep_dir(dir, "l0")    _s1 : Unit <- sweep_dir(dir, "l1")    _s2 : Unit <- sweep_dir(dir, "l2")    _s3 : Unit <- sweep_dir(dir, "l3")    _s4 : Unit <- sweep_root(dir)    +m : Manifest.Manifest <- IO.try(Manifest.Manifest, Flush.load_mfst(dir ++ "/MANIFEST"))    open_names_go(Pure.manifest_names_ok(Pure.mfst_lists(m), 0n), dir, m)# --- Maintenance (synchronous auto flush/compact + stall) ---# One drain round always suffices (L0 >= 8 > 4, so the gated compaction# fires and empties L0) — no loop, no cycle.# Flush dec for the database recovery effects.def flush_dec(full: Bool, +db: Db.Db) -> IO(Result<&1, &1, U32 & String, Db.Db>):  match full:    case True{}:      Flush.flush(db)    case False{}:      IO.pure(Result<&1, &1, U32 & String, Db.Db>, Done{db})# Flush gate for the database recovery effects.def flush_gate(db: Db.Db) -> IO(Result<&1, &1, U32 & String, Db.Db>):  match db:    case Db.Db{dir, mem, frozen, batch_cap, bcache, levels, flushed, manifest_token, +mem_count, frozen_count}:      flush_dec(Nat.is_lt(4096n, mem_count), Db.Db{dir, mem, frozen, batch_cap, bcache, levels, flushed, manifest_token, mem_count, frozen_count})# Handle l0 full in the database recovery effects.def l0_full(db: Db.Db) -> Bool:  match db:    case Db.Db{dir, mem, frozen, batch_cap, bcache, levels, flushed, manifest_token, mem_count, frozen_count}:      Nat.is_lt(7n, List.length(&2, Sstable.Table, Compact.l0list(levels)))# Handle drain once in the database recovery effects.def drain_once(+db: Db.Db) -> IO(Result<&1, &1, U32 & String, Db.Db>):  do IO<Result<&1, &1, U32 & String, Db.Db>>:    f : Db.Db <- IO.try(Db.Db, Flush.flush(db))    c : Db.Db <- IO.try(Db.Db, CompactIo.compact(f))    return Done{c}# Handle drain pick in the database recovery effects.def drain_pick(full: Bool, +db: Db.Db) -> IO(Result<&1, &1, U32 & String, Db.Db>):  match full:    case True{}:      drain_once(db)    case False{}:      IO.pure(Result<&1, &1, U32 & String, Db.Db>, Done{db})# Handle drain go in the database recovery effects.def drain_go(+db: Db.Db) -> IO(Result<&1, &1, U32 & String, Db.Db>):  drain_pick(l0_full(db), db)# Handle maintain in the database recovery effects.def maintain(+db: Db.Db) -> IO(Result<&1, &1, U32 & String, Db.Db>):  do IO<Result<&1, &1, U32 & String, Db.Db>>:    d : Db.Db <- IO.try(Db.Db, drain_go(db))    f : Db.Db <- IO.try(Db.Db, flush_gate(d))    CompactIo.compact(f)# Handle maintain unwrap in the database recovery effects.def maintain_unwrap(res: Result<&1, &1, U32 & String, Db.Db>) -> IO(Result<&1, &1, U32 & String, Db.Db>):  match res:    case Fail{e}:      IO.pure(Result<&1, &1, U32 & String, Db.Db>, Fail{e})    case Done{db}:      maintain(db)# Handle write in the database recovery effects.def write(+db: Db.Db, +batch: Wal.Batch) -> IO(Result<&1, &1, U32 & String, Db.Db>):  do IO<Result<&1, &1, U32 & String, Db.Db>>:    r : Result<&1, &1, U32 & String, Db.Db> <- DbIo.db_write(db, batch)    maintain_unwrap(r)# Handle put in the database recovery effects.def put(+db: Db.Db, +key: String, +val: String) -> IO(Result<&1, &1, U32 & String, Db.Db>):  write(db, Wal.Batch{Con{Wal.Put{key, val}, Nil{}}})# Handle del in the database recovery effects.def del(+db: Db.Db, +key: String) -> IO(Result<&1, &1, U32 & String, Db.Db>):  write(db, Wal.Batch{Con{Wal.Del{key}, Nil{}}})# --- Closed-vector laws live in laws/Recover.bend (spec law 7) ---