1 import Base 2 import ./Keys.bend as Keys 3 import ./MemTable.bend as MemTable 4 import ./Sstable.bend as Sstable 5 import ./SortedRun.bend as SortedRun 6 import ./SstStreamIo.bend as SstStreamIo 7 import ./Wal.bend as Wal 8 import ./Manifest.bend as Manifest 9 import ./StorageBytes.bend as StorageBytes 10 import ./Fs.bend as Fs 11 import ./DbIo.bend as DbIo 12 import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes 13 import ./Db.bend as Db 14 import ./CrashPoint.bend as CrashPoint 15 import ./FlushPolicy.bend as Policy 16 17 # Represent ManifestReadState data used by the memtable flush. 18 type ManifestReadState is Type: 19 ManifestNeed{file: File, remaining: Nat, chunks: List<&1, Bytes.Bytes>} 20 ManifestGot{pair: File & Result<&1, &1, U32 & String, Bytes.Bytes>, remaining: Nat, chunks: List<&1, Bytes.Bytes>} 21 22 # Flush (Task 8): sealed memtable -> L0 SSTable file + manifest + WAL reset. 23 # 24 # Crash protocol (each step durable before the next, tmp + rename for 25 # atomic visibility): 26 # 1. ensure /l0; write .tmp; fsync; rename to ; fsync l0. 27 # 2. load manifest (absent file = fresh M{Nil}; corrupt = ABORT Fail). 28 # 3. write MANIFEST.tmp; fsync; rename; fsync dir. 29 # 4. remove wal.log (failure ignored: a stale WAL replays idempotently — 30 # same versions, mem-first reads return equal values; merge dedups). 31 # A crash before step 3 leaves the table file orphaned (recovery ignores 32 # files absent from the manifest; Task 12 sweeps orphans). A crash after 33 # step 3 with a live WAL replays duplicates harmlessly (see above). 34 # Empty memtable flush is a no-op (no files touched). 35 # CrashPoint calls are test-only host effects. Unset or unequal checkpoints are 36 # successful no-ops; signal delivery and filesystem ordering are not Bend proofs. 37 38 # Handle l0 add in the memtable flush. 39 def l0_add(+levels: List<&2, List<&2, Sstable.Table>>, +tbl: Sstable.Table) -> List<&2, List<&2, Sstable.Table>>: 40 match levels: 41 case Nil{}: 42 Con{Con{tbl, Nil{}}, Nil{}} 43 case Con{l0, rest}: 44 Con{Con{tbl, l0}, rest} 45 46 # Handle mfst add in the memtable flush. 47 def mfst_add(+mfst: Manifest.Manifest, +name: String) -> Manifest.Manifest: 48 match mfst: 49 case Manifest.M{lvs}: 50 match lvs: 51 case Nil{}: 52 Manifest.M{Con{Con{name, Nil{}}, Nil{}}} 53 case Con{l0, rest}: 54 Manifest.M{Con{Con{name, l0}, rest}} 55 56 # --- File helpers (pair-splits contained; straight-line callers above) --- 57 58 def write_tail.chmod( 59 result: Result<&1, &1, U32 & String, Unit>, 60 +path: String 61 ) -> IO(Result<&1, &1, U32 & String, Unit>): 62 match result: 63 case Fail{error}: 64 IO.pure(Result<&1, &1, U32 & String, Unit>, Fail{error}) 65 case Done{Unit{}}: 66 Fs.chmod(path, U32.from_nat(384n)) 67 68 def write_tail.fsync( 69 result: Result<&1, &1, U32 & String, Unit>, 70 +path: String 71 ) -> IO(Result<&1, &1, U32 & String, Unit>): 72 match result: 73 case Fail{error}: 74 IO.pure(Result<&1, &1, U32 & String, Unit>, Fail{error}) 75 case Done{Unit{}}: 76 do IO>: 77 synced : Result<&1, &1, U32 & String, Unit> <- Fs.fsync(path) 78 write_tail.chmod(synced, path) 79 80 # Writes the remaining bytes of a file buffer. 81 def write_tail( 82 fr: File & Result<&1, &1, U32 & String, Unit>, 83 +path: String 84 ) -> IO(Result<&1, &1, U32 & String, Unit>): 85 match fr: 86 case (file, result): 87 do IO>: 88 _closed : Unit <- File.close(file) 89 write_tail.fsync(result, path) 90 91 def write_file.opened( 92 opened: Result<&1, &1, U32 & String, File>, 93 +path: String, 94 +data: String 95 ) -> IO(Result<&1, &1, U32 & String, Unit>): 96 match opened: 97 case Fail{error}: 98 IO.pure(Result<&1, &1, U32 & String, Unit>, Fail{error}) 99 case Done{file}: 100 do IO>: 101 written : File & Result<&1, &1, U32 & String, Unit> <- File.write(file, data) 102 write_tail(written, path) 103 104 # Creates and syncs one storage file. 105 def write_file(+path: String, +data: String) -> IO(Result<&1, &1, U32 & String, Unit>): 106 do IO>: 107 opened : Result<&1, &1, U32 & String, File> <- File.open(path, "w") 108 write_file.opened(opened, path, data) 109 110 def write_manifest.bytes.opened( 111 opened: Result<&1, &1, U32 & String, File>, 112 +path: String, 113 bytes: Bytes.Bytes 114 ) -> IO(Result<&1, &1, U32 & String, Unit>): 115 match opened: 116 case Fail{error}: 117 IO.pure(Result<&1, &1, U32 & String, Unit>, Fail{error}) 118 case Done{file}: 119 do IO>: 120 written : File & Result<&1, &1, U32 & String, Unit> <- Fs.write_bytes(file, bytes) 121 write_tail(written, path) 122 123 def write_manifest.bytes(+path: String, bytes: Bytes.Bytes) -> IO(Result<&1, &1, U32 & String, Unit>): 124 do IO>: 125 opened : Result<&1, &1, U32 & String, File> <- File.open(path, "w") 126 write_manifest.bytes.opened(opened, path, bytes) 127 128 def write_manifest.encoded( 129 path: String, 130 encoded: Result<&1, &1, Manifest.Error, Bytes.Bytes> 131 ) -> IO(Result<&1, &1, U32 & String, Unit>): 132 match encoded: 133 case Fail{_}: 134 IO.pure(Result<&1, &1, U32 & String, Unit>, Fail{(U32.from_nat(1n), "cannot encode manifest")}) 135 case Done{bytes}: 136 write_manifest.bytes(path, bytes) 137 138 # Write manifest for the memtable flush. 139 def write_manifest(path: String, manifest: Manifest.Manifest) -> IO(Result<&1, &1, U32 & String, Unit>): 140 write_manifest.encoded(path, Manifest.serialize(manifest)) 141 142 # Read tail3 for the memtable flush. 143 def read_tail3(fr: (File & Result<&1, &1, U32 & String, String>)) -> IO(Result<&1, &1, U32 & String, String>): 144 match fr: 145 case (f3, r1): 146 do IO>: 147 _cls : Unit <- File.close(f3) 148 IO.pure(Result<&1, &1, U32 & String, String>, r1) 149 150 151 # Base File.read decodes each returned chunk independently. A chunk boundary can 152 # split a multi-byte UTF-8 character, so table files are read in one bounded call 153 # Reads a bounded storage file and returns its bytes. 154 def read_file.opened( 155 opened: Result<&1, &1, U32 & String, File> 156 ) -> IO(Result<&1, &1, U32 & String, String>): 157 match opened: 158 case Fail{error}: 159 IO.pure(Result<&1, &1, U32 & String, String>, Fail{error}) 160 case Done{file}: 161 do IO>: 162 result : File & Result<&1, &1, U32 & String, String> <- File.read(file, U32.from_nat(4294967295n)) 163 read_tail3(result) 164 165 # Reads a bounded storage file and returns its bytes. 166 def read_file(+path: String) -> IO(Result<&1, &1, U32 & String, String>): 167 do IO>: 168 opened : Result<&1, &1, U32 & String, File> <- File.open(path, "r") 169 read_file.opened(opened) 170 171 # Classifies the Manifest parse result before recovery. 172 def mfst_parsed(parsed: Manifest.ParseResult) -> Result<&1, &1, U32 & String, Manifest.Manifest>: 173 match parsed: 174 case Manifest.Parsed{manifest}: 175 Done{manifest} 176 case Manifest.UnsupportedVersion{_}: 177 Fail{(Manifest.unsupported_version_error_code(), "unsupported manifest version")} 178 case Manifest.Malformed{}: 179 Fail{(Manifest.malformed_error_code(), "malformed manifest")} 180 case Manifest.ParseLimitExceeded{}: 181 Fail{(Manifest.parse_limit_error_code(), "manifest size limit exceeded")} 182 183 def manifest.read.result( 184 pair: File & Result<&1, &1, U32 & String, Bytes.Bytes> 185 ) -> IO(Result<&1, &1, U32 & String, Manifest.Manifest>): 186 match pair: 187 case (file, Fail{error}): 188 do IO>: 189 _closed : Unit <- File.close(file) 190 return Fail{error} 191 case (file, Done{bytes}): 192 do IO>: 193 _closed : Unit <- File.close(file) 194 return mfst_parsed(Manifest.parse_result(bytes)) 195 196 def manifest.read.loop(fuel: Nat, state: ManifestReadState) -> IO(File & Result<&1, &1, U32 & String, Bytes.Bytes>): 197 match fuel: 198 case 0n: 199 match state: 200 case ManifestNeed{file, +remaining, chunks}: 201 match remaining: 202 case 0n: 203 IO.pure(File & Result<&1, &1, U32 & String, Bytes.Bytes>, (file, Done{Bytes.concat(List.reverse(&1, Bytes.Bytes, chunks))})) 204 case 1n+_: 205 IO.pure(File & Result<&1, &1, U32 & String, Bytes.Bytes>, (file, Fail{(U32.from_nat(7n), "bounded manifest read exhausted")})) 206 case ManifestGot{pair, _, _}: 207 match pair: 208 case (file, Fail{error}): 209 IO.pure(File & Result<&1, &1, U32 & String, Bytes.Bytes>, (file, Fail{error})) 210 case (file, Done{_}): 211 IO.pure(File & Result<&1, &1, U32 & String, Bytes.Bytes>, (file, Fail{(U32.from_nat(7n), "bounded manifest read exhausted")})) 212 case 1n+rest: 213 match state: 214 case ManifestNeed{file, +remaining, chunks}: 215 match remaining: 216 case 0n: 217 IO.pure(File & Result<&1, &1, U32 & String, Bytes.Bytes>, (file, Done{Bytes.concat(List.reverse(&1, Bytes.Bytes, chunks))})) 218 case 1n+_: 219 do IO>: 220 pair : File & Result<&1, &1, U32 & String, Bytes.Bytes> <- Fs.read_bytes(file, U32.from_nat(remaining)) 221 manifest.read.loop(rest, ManifestGot{pair, remaining, chunks}) 222 case ManifestGot{pair, +remaining, chunks}: 223 match pair: 224 case (file, Fail{error}): 225 IO.pure(File & Result<&1, &1, U32 & String, Bytes.Bytes>, (file, Fail{error})) 226 case (file, Done{Bytes.Bytes{0, _}}): 227 IO.pure(File & Result<&1, &1, U32 & String, Bytes.Bytes>, (file, Fail{(U32.from_nat(8n), "truncated manifest file")})) 228 case (file, Done{Bytes.Bytes{+len, buf}}): 229 manifest.read.loop(rest, ManifestNeed{file, Nat.sub(remaining, U32.to_nat(len)), Con{Bytes.Bytes{len, buf}, chunks}}) 230 231 def manifest.read.size.run(file: File, +size: Nat) -> IO(Result<&1, &1, U32 & String, Manifest.Manifest>): 232 do IO>: 233 pair : File & Result<&1, &1, U32 & String, Bytes.Bytes> <- manifest.read.loop(Nat.add(size, 1n), ManifestNeed{file, size, Nil{}}) 234 manifest.read.result(pair) 235 236 def manifest.read.size.valid( 237 file: File, 238 size: Nat, 239 within: Bool 240 ) -> IO(Result<&1, &1, U32 & String, Manifest.Manifest>): 241 match within: 242 case False{}: 243 do IO>: 244 _closed : Unit <- File.close(file) 245 return Fail{(U32.from_nat(9n), "manifest too large")} 246 case True{}: 247 manifest.read.size.run(file, size) 248 249 def manifest.read.size( 250 file: File, 251 result: Result<&1, &1, U32 & String, Nat> 252 ) -> IO(Result<&1, &1, U32 & String, Manifest.Manifest>): 253 match result: 254 case Fail{error}: 255 do IO>: 256 _closed : Unit <- File.close(file) 257 return Fail{error} 258 case Done{+size}: 259 manifest.read.size.valid(file, size, Nat.is_le(size, U32.to_nat(StorageBytes.MAX_MANIFEST_BYTES()))) 260 261 # Handle mfst open error in the memtable flush. 262 def mfst_open_error( 263 missing: Bool, 264 +code: U32 265 ) -> IO(Result<&1, &1, U32 & String, Manifest.Manifest>): 266 match missing: 267 case True{}: 268 IO.pure(Result<&1, &1, U32 & String, Manifest.Manifest>, Done{Manifest.M{Nil{}}}) 269 case False{}: 270 IO.pure(Result<&1, &1, U32 & String, Manifest.Manifest>, Fail{(code, "cannot open manifest")}) 271 272 # Handle mfst opened in the memtable flush. 273 def mfst_opened( 274 path: String, 275 result: Result<&1, &1, U32 & String, File> 276 ) -> IO(Result<&1, &1, U32 & String, Manifest.Manifest>): 277 match result: 278 case Fail{(+code, _)}: 279 mfst_open_error(U32.is_eq(code, 2), code) 280 case Done{file}: 281 do IO>: 282 size : Result<&1, &1, U32 & String, Nat> <- Fs.file_size(path) 283 manifest.read.size(file, size) 284 285 # Handle load mfst in the memtable flush. 286 def load_mfst(+path: String) -> IO(Result<&1, &1, U32 & String, Manifest.Manifest>): 287 do IO>: 288 opened : Result<&1, &1, U32 & String, File> <- File.open(path, "r") 289 mfst_opened(path, opened) 290 291 # Handle dir tail in the memtable flush. 292 def dir_tail(res: Result<&1, &1, U32 & String, Nat>, +path: String) -> IO(Result<&1, &1, U32 & String, Unit>): 293 match res: 294 case Done{n}: 295 IO.pure(Result<&1, &1, U32 & String, Unit>, Done{Unit{}}) 296 case Fail{e}: 297 Fs.make_dir(path) 298 299 # Handle ensure dir in the memtable flush. 300 def ensure_dir(+path: String) -> IO(Result<&1, &1, U32 & String, Unit>): 301 do IO>: 302 r : Result<&1, &1, U32 & String, Nat> <- Fs.read_dir_count(path) 303 dir_tail(r, path) 304 305 # --- Flush chain (defined bottom-up: tails first, entry last) --- 306 307 def flush_goB.wal( 308 result: Result<&1, &1, U32 & String, Unit>, 309 +dir: String, 310 +m2: Manifest.Manifest, 311 +tbl: Sstable.Table, 312 +levels: List<&2, List<&2, Sstable.Table>>, 313 +flushed: Nat, 314 +mem: MemTable.MemTable, 315 +batch_cap: Nat 316 ) -> IO(Result<&1, &1, U32 & String, Db.Db>): 317 match result: 318 case Fail{error}: 319 IO.pure(Result<&1, &1, U32 & String, Db.Db>, Fail{error}) 320 case Done{Unit{}}: 321 IO.pure(Result<&1, &1, U32 & String, Db.Db>, 322 Done{Db.Db{dir, mem, MemTable.MT{Nil{}}, batch_cap, Nil{}, 323 l0_add(levels, tbl), Nat.add(flushed, 1n), Manifest.token(m2), 0n, 0n}}) 324 325 def flush_goB.synced( 326 result: Result<&1, &1, U32 & String, Unit>, 327 +dir: String, 328 +m2: Manifest.Manifest, 329 +tbl: Sstable.Table, 330 +levels: List<&2, List<&2, Sstable.Table>>, 331 +flushed: Nat, 332 +mem: MemTable.MemTable, 333 +batch_cap: Nat 334 ) -> IO(Result<&1, &1, U32 & String, Db.Db>): 335 match result: 336 case Fail{error}: 337 IO.pure(Result<&1, &1, U32 & String, Db.Db>, Fail{error}) 338 case Done{Unit{}}: 339 do IO>: 340 _checkpoint : Unit <- IO.try(Unit, CrashPoint.hit("flush.manifest_published")) 341 _removed : Result<&1, &1, U32 & String, Unit> <- Fs.remove(Db.wal_path(dir)) 342 initialized : Result<&1, &1, U32 & String, Unit> <- DbIo.wal_initialize(dir) 343 flush_goB.wal(initialized, dir, m2, tbl, levels, flushed, mem, batch_cap) 344 345 def flush_goB.renamed( 346 result: Result<&1, &1, U32 & String, Unit>, 347 +dir: String, 348 +m2: Manifest.Manifest, 349 +tbl: Sstable.Table, 350 +levels: List<&2, List<&2, Sstable.Table>>, 351 +flushed: Nat, 352 +mem: MemTable.MemTable, 353 +batch_cap: Nat 354 ) -> IO(Result<&1, &1, U32 & String, Db.Db>): 355 match result: 356 case Fail{error}: 357 IO.pure(Result<&1, &1, U32 & String, Db.Db>, Fail{error}) 358 case Done{Unit{}}: 359 do IO>: 360 synced : Result<&1, &1, U32 & String, Unit> <- Fs.fsync(dir) 361 flush_goB.synced(synced, dir, m2, tbl, levels, flushed, mem, batch_cap) 362 363 def flush_goB.written( 364 result: Result<&1, &1, U32 & String, Unit>, 365 +dir: String, 366 +mpath: String, 367 +mtmp: String, 368 +m2: Manifest.Manifest, 369 +tbl: Sstable.Table, 370 +levels: List<&2, List<&2, Sstable.Table>>, 371 +flushed: Nat, 372 +mem: MemTable.MemTable, 373 +batch_cap: Nat 374 ) -> IO(Result<&1, &1, U32 & String, Db.Db>): 375 match result: 376 case Fail{error}: 377 IO.pure(Result<&1, &1, U32 & String, Db.Db>, Fail{error}) 378 case Done{Unit{}}: 379 do IO>: 380 _checkpoint : Unit <- IO.try(Unit, CrashPoint.hit("flush.manifest_synced")) 381 renamed : Result<&1, &1, U32 & String, Unit> <- Fs.rename(mtmp, mpath) 382 flush_goB.renamed(renamed, dir, m2, tbl, levels, flushed, mem, batch_cap) 383 384 # Publishes flushed SSTables and updates the Manifest. 385 def flush_goB( 386 +dir: String, 387 +mpath: String, 388 +mtmp: String, 389 +m2: Manifest.Manifest, 390 +tbl: Sstable.Table, 391 +levels: List<&2, List<&2, Sstable.Table>>, 392 +flushed: Nat, 393 +mem: MemTable.MemTable, 394 +batch_cap: Nat, 395 ) -> IO(Result<&1, &1, U32 & String, Db.Db>): 396 do IO>: 397 written : Result<&1, &1, U32 & String, Unit> <- write_manifest(mtmp, m2) 398 flush_goB.written(written, dir, mpath, mtmp, m2, tbl, levels, flushed, mem, batch_cap) 399 400 # Flush drift for the memtable flush. 401 def flush_drift( 402 ok: Bool, 403 +dir: String, 404 +mpath: String, 405 +mtmp: String, 406 +mfst: Manifest.Manifest, 407 +tbl: Sstable.Table, 408 +name: String, 409 +levels: List<&2, List<&2, Sstable.Table>>, 410 +flushed: Nat, 411 +mem: MemTable.MemTable, 412 +batch_cap: Nat, 413 ) -> IO(Result<&1, &1, U32 & String, Db.Db>): 414 match ok: 415 case False{}: 416 IO.pure(Result<&1, &1, U32 & String, Db.Db>, Fail{(U32.from_nat(2n), "manifest drift")}) 417 case True{}: 418 flush_goB(dir, mpath, mtmp, mfst_add(mfst, name), tbl, levels, flushed, mem, batch_cap) 419 420 def flush_goA.loaded( 421 result: Result<&1, &1, U32 & String, Manifest.Manifest>, 422 +dir: String, 423 +mpath: String, 424 +mtmp: String, 425 +tbl: Sstable.Table, 426 +name: String, 427 +levels: List<&2, List<&2, Sstable.Table>>, 428 +flushed: Nat, 429 +manifest_token: String, 430 +mem: MemTable.MemTable, 431 +batch_cap: Nat 432 ) -> IO(Result<&1, &1, U32 & String, Db.Db>): 433 match result: 434 case Fail{error}: 435 IO.pure(Result<&1, &1, U32 & String, Db.Db>, Fail{error}) 436 case Done{+current}: 437 flush_drift(String.eq(Manifest.token(current), manifest_token), dir, 438 mpath, mtmp, current, tbl, name, levels, flushed, mem, batch_cap) 439 440 def flush_goA.synced( 441 result: Result<&1, &1, U32 & String, Unit>, 442 +dir: String, 443 +mpath: String, 444 +mtmp: String, 445 +tbl: Sstable.Table, 446 +name: String, 447 +levels: List<&2, List<&2, Sstable.Table>>, 448 +flushed: Nat, 449 +manifest_token: String, 450 +mem: MemTable.MemTable, 451 +batch_cap: Nat 452 ) -> IO(Result<&1, &1, U32 & String, Db.Db>): 453 match result: 454 case Fail{error}: 455 IO.pure(Result<&1, &1, U32 & String, Db.Db>, Fail{error}) 456 case Done{Unit{}}: 457 do IO>: 458 _checkpoint : Unit <- IO.try(Unit, CrashPoint.hit("flush.table_published")) 459 current : Result<&1, &1, U32 & String, Manifest.Manifest> <- load_mfst(mpath) 460 flush_goA.loaded(current, dir, mpath, mtmp, tbl, name, levels, 461 flushed, manifest_token, mem, batch_cap) 462 463 def flush_goA.renamed( 464 result: Result<&1, &1, U32 & String, Unit>, 465 +dir: String, 466 +l0dir: String, 467 +mpath: String, 468 +mtmp: String, 469 +tbl: Sstable.Table, 470 +name: String, 471 +levels: List<&2, List<&2, Sstable.Table>>, 472 +flushed: Nat, 473 +manifest_token: String, 474 +mem: MemTable.MemTable, 475 +batch_cap: Nat 476 ) -> IO(Result<&1, &1, U32 & String, Db.Db>): 477 match result: 478 case Fail{error}: 479 IO.pure(Result<&1, &1, U32 & String, Db.Db>, Fail{error}) 480 case Done{Unit{}}: 481 do IO>: 482 synced : Result<&1, &1, U32 & String, Unit> <- Fs.fsync(l0dir) 483 flush_goA.synced(synced, dir, mpath, mtmp, tbl, name, levels, 484 flushed, manifest_token, mem, batch_cap) 485 486 # Writes the MemTable to an SSTable and completes the flush. 487 def flush_goA.written( 488 result: Result<&1, &1, U32 & String, Unit>, 489 +dir: String, 490 +l0dir: String, 491 +tmp: String, 492 +final: String, 493 +mpath: String, 494 +mtmp: String, 495 +tbl: Sstable.Table, 496 +name: String, 497 +levels: List<&2, List<&2, Sstable.Table>>, 498 +flushed: Nat, 499 +manifest_token: String, 500 +mem: MemTable.MemTable, 501 +batch_cap: Nat, 502 ) -> IO(Result<&1, &1, U32 & String, Db.Db>): 503 match result: 504 case Fail{error}: 505 IO.pure(Result<&1, &1, U32 & String, Db.Db>, Fail{error}) 506 case Done{Unit{}}: 507 do IO>: 508 _cp1 : Unit <- IO.try(Unit, CrashPoint.hit("flush.table_synced")) 509 renamed : Result<&1, &1, U32 & String, Unit> <- Fs.rename(tmp, final) 510 flush_goA.renamed(renamed, dir, l0dir, mpath, mtmp, tbl, name, levels, flushed, manifest_token, mem, batch_cap) 511 512 def flush_goA.ensured( 513 result: Result<&1, &1, U32 & String, Unit>, 514 +dir: String, 515 +l0dir: String, 516 +tmp: String, 517 +final: String, 518 +mpath: String, 519 +mtmp: String, 520 +entries: List<&2, MemTable.Entry>, 521 +level: U32, 522 +tbl: Sstable.Table, 523 +name: String, 524 +levels: List<&2, List<&2, Sstable.Table>>, 525 +flushed: Nat, 526 +manifest_token: String, 527 +mem: MemTable.MemTable, 528 +batch_cap: Nat, 529 ) -> IO(Result<&1, &1, U32 & String, Db.Db>): 530 match result: 531 case Fail{error}: 532 IO.pure(Result<&1, &1, U32 & String, Db.Db>, Fail{error}) 533 case Done{Unit{}}: 534 do IO>: 535 written : Result<&1, &1, U32 & String, Unit> <- SstStreamIo.write_table(tmp, entries, level) 536 flush_goA.written(written, dir, l0dir, tmp, final, mpath, mtmp, tbl, name, levels, flushed, manifest_token, mem, batch_cap) 537 538 # Writes the MemTable to an SSTable and completes the flush. 539 def flush_goA( 540 +dir: String, 541 +l0dir: String, 542 +tmp: String, 543 +final: String, 544 +mpath: String, 545 +mtmp: String, 546 +entries: List<&2, MemTable.Entry>, 547 +level: U32, 548 +tbl: Sstable.Table, 549 +name: String, 550 +levels: List<&2, List<&2, Sstable.Table>>, 551 +flushed: Nat, 552 +manifest_token: String, 553 +mem: MemTable.MemTable, 554 +batch_cap: Nat, 555 ) -> IO(Result<&1, &1, U32 & String, Db.Db>): 556 do IO>: 557 ensured : Result<&1, &1, U32 & String, Unit> <- ensure_dir(l0dir) 558 flush_goA.ensured(ensured, dir, l0dir, tmp, final, mpath, mtmp, entries, level, tbl, name, levels, flushed, manifest_token, mem, batch_cap) 559 560 # Flush pre for the memtable flush. 561 def flush_pre( 562 +dir: String, 563 +entries: List<&2, MemTable.Entry>, 564 +levels: List<&2, List<&2, Sstable.Table>>, 565 +flushed: Nat, 566 +manifest_token: String, 567 +batch_cap: Nat, 568 ) -> IO(Result<&1, &1, U32 & String, Db.Db>): 569 +tbl = Sstable.build(entries, 0n, List.length(&2, MemTable.Entry, entries)) 570 +name = Policy.table_name(flushed) 571 +l0dir = dir ++ "/l0" 572 flush_goA(dir, l0dir, l0dir ++ "/" ++ name ++ ".tmp", l0dir ++ "/" ++ name, dir ++ "/MANIFEST", dir ++ "/MANIFEST.tmp", Db.table_entries(tbl), 0, tbl, name, levels, flushed, 573 manifest_token, MemTable.MT{Nil{}}, batch_cap) 574 575 # Frozen-path flush MERGES mem (newer) over frozen (older) into one L0 table 576 # and drains both: the WAL is truncated on publish, so anything left in mem 577 # would otherwise exist nowhere on disk (loss window caught by 20485 smoke). 578 def flush_frozen( 579 +dir: String, 580 +mem: MemTable.MemTable, 581 +entries: List<&2, MemTable.Entry>, 582 +levels: List<&2, List<&2, Sstable.Table>>, 583 +flushed: Nat, 584 +manifest_token: String, 585 +batch_cap: Nat, 586 ) -> IO(Result<&1, &1, U32 & String, Db.Db>): 587 match mem: 588 case MemTable.MT{live}: 589 +merged = SortedRun.merge_many_newest(Con{live, Con{entries, Nil{}}}) 590 +tbl = Sstable.build(merged, 0n, List.length(&2, MemTable.Entry, merged)) 591 +name = Policy.table_name(flushed) 592 +l0dir = dir ++ "/l0" 593 flush_goA(dir, l0dir, l0dir ++ "/" ++ name ++ ".tmp", l0dir ++ "/" ++ name, dir ++ "/MANIFEST", dir ++ "/MANIFEST.tmp", Db.table_entries(tbl), 0, tbl, name, levels, flushed, 594 manifest_token, MemTable.MT{Nil{}}, batch_cap) 595 596 # Flush pick for the memtable flush. 597 def flush_pick( 598 +frozen: MemTable.MemTable, 599 +mem: MemTable.MemTable, 600 +dir: String, 601 +levels: List<&2, List<&2, Sstable.Table>>, 602 +flushed: Nat, 603 +manifest_token: String, 604 +batch_cap: Nat, 605 +bcache: List<&2, Db.BEntry>, 606 ) -> IO(Result<&1, &1, U32 & String, Db.Db>): 607 match frozen mem: 608 case MemTable.MT{Nil{}} MemTable.MT{Nil{}}: 609 IO.pure(Result<&1, &1, U32 & String, Db.Db>, Done{Db.Db{dir, MemTable.MT{Nil{}}, MemTable.MT{Nil{}}, batch_cap, bcache, levels, flushed, manifest_token, 0n, 0n}}) 610 case MemTable.MT{Nil{}} MemTable.MT{Con{e, t}}: 611 flush_pre(dir, Con{e, t}, levels, flushed, manifest_token, batch_cap) 612 case MemTable.MT{Con{fe, ft}} _: 613 flush_frozen(dir, mem, Con{fe, ft}, levels, flushed, manifest_token, batch_cap) 614 615 # Handle flush in the memtable flush. 616 def flush(+db: Db.Db) -> IO(Result<&1, &1, U32 & String, Db.Db>): 617 match db: 618 case Db.Db{dir, mem, frozen, batch_cap, bcache, levels, flushed, manifest_token, mem_count, frozen_count}: 619 flush_pick(frozen, mem, dir, levels, flushed, manifest_token, batch_cap, bcache) 620 621 # --- Closed-vector laws live in laws/Flush.bend (spec law 6) ---