1 # Append-only journal: checksummed frames, fsync'd appends, and crash recovery of the committed prefix. Source: https://github.com/paymog/bend-kit/tree/main/journal 2 import Base 3 import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes 4 import bend-kit-hash@0.2.0.0/hash.bend as Hash 5 6 # A frame is one record and one commit: three u32 little-endian fields, then the payload. 7 # len, then U32.not(len), then the CRC-32 of the payload, then len payload octets. 8 # A frame is committed when all 12 + len of its octets are in the file. 9 10 # Clean: the input ends on a frame boundary. 11 # Torn: the input ends inside a frame. A crash cut the last append short. 12 # Corrupt: a whole header or a whole frame fails its check. Recovery does not go past it. 13 type Tail is Data: 14 Clean{} 15 Torn{} 16 Corrupt{why: String} 17 18 # One decoding step. Short, Bad, and End give the input back untouched, at the frame start. 19 type Step<-P: Type, -S: Type> is Type: 20 End{s: S} 21 Short{s: S} 22 Bad{why: String, s: S} 23 Got{p: P, s: S} 24 25 # The committed records in order, how the input ends, and the input from the first frame not taken. 26 type Recovery is Type: 27 Recovery{records: List, tail: Tail, rest: S} 28 29 # The state after every committed record, how the input ends, and the input from the first frame not taken. 30 type Replay<-A: Type, -S: Type> is Type: 31 Replay{acc: A, tail: Tail, rest: S} 32 33 # The decoder is generic over the input S and the payload P. Bytes runs it; a list of octets proves it. 34 # fits(s, at, n): n octets from at fit in s. u32(s, at): the field at at. cut(s, hs, len): a copy of 35 # [hs, hs + len). skip(s, hs, len): s from hs + len. sum(p): the checksum of p. 36 37 def next.verdict(~P: Type, ~S: Type, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, ok: Bool, +len: U32, s: S, p: P) -> Step: 38 match ok: 39 case True{}: 40 Got{p, skip(s, 12, len)} 41 case False{}: 42 Bad{"checksum mismatch", s} 43 44 def next.summed(~P: Type, ~S: Type, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, +want: U32, +len: U32, s: S, r: P & U32) -> Step: 45 (p, +got) = r 46 next.verdict(~P, ~S, ~skip, U32.is_eq(got, want), len, s, p) 47 48 def next.cut(~P: Type, ~S: Type, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, ~sum: P -> P & U32, +want: U32, +len: U32, r: S & P) -> Step: 49 (s, p) = r 50 next.summed(~P, ~S, ~skip, want, len, s, sum(p)) 51 52 def next.want(~P: Type, ~S: Type, ~cut: S -> @+hs:U32 -> @+len:U32 -> S & P, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, ~sum: P -> P & U32, +len: U32, r: S & U32) -> Step: 53 (s, +want) = r 54 next.cut(~P, ~S, ~skip, ~sum, want, len, cut(s, 12, len)) 55 56 def next.body(~P: Type, ~S: Type, ~u32: S -> @+at:U32 -> S & U32, ~cut: S -> @+hs:U32 -> @+len:U32 -> S & P, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, ~sum: P -> P & U32, +len: U32, r: S & Bool) -> Step: 57 (s, whole) = r 58 match whole: 59 case True{}: 60 next.want(~P, ~S, ~cut, ~skip, ~sum, len, u32(s, 8)) 61 case False{}: 62 Short{s} 63 64 def next.size(~P: Type, ~S: Type, ~fits: S -> @+at:U32 -> @+n:U32 -> S & Bool, ~u32: S -> @+at:U32 -> S & U32, ~cut: S -> @+hs:U32 -> @+len:U32 -> S & P, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, ~sum: P -> P & U32, over: Bool, +len: U32, s: S) -> Step: 65 match over: 66 case True{}: 67 Bad{"length over max", s} 68 case False{}: 69 next.body(~P, ~S, ~u32, ~cut, ~skip, ~sum, len, fits(s, 12, len)) 70 71 def next.head(~P: Type, ~S: Type, ~fits: S -> @+at:U32 -> @+n:U32 -> S & Bool, ~u32: S -> @+at:U32 -> S & U32, ~cut: S -> @+hs:U32 -> @+len:U32 -> S & P, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, ~sum: P -> P & U32, ok: Bool, +max: U32, +len: U32, s: S) -> Step: 72 match ok: 73 case True{}: 74 next.size(~P, ~S, ~fits, ~u32, ~cut, ~skip, ~sum, U32.is_gt(len, max), len, s) 75 case False{}: 76 Bad{"length check mismatch", s} 77 78 def next.lenx(~P: Type, ~S: Type, ~fits: S -> @+at:U32 -> @+n:U32 -> S & Bool, ~u32: S -> @+at:U32 -> S & U32, ~cut: S -> @+hs:U32 -> @+len:U32 -> S & P, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, ~sum: P -> P & U32, +max: U32, +len: U32, r: S & U32) -> Step: 79 (s, +lenx) = r 80 next.head(~P, ~S, ~fits, ~u32, ~cut, ~skip, ~sum, U32.is_eq(lenx, U32.not(len)), max, len, s) 81 82 def next.len(~P: Type, ~S: Type, ~fits: S -> @+at:U32 -> @+n:U32 -> S & Bool, ~u32: S -> @+at:U32 -> S & U32, ~cut: S -> @+hs:U32 -> @+len:U32 -> S & P, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, ~sum: P -> P & U32, +max: U32, r: S & U32) -> Step: 83 (s, +len) = r 84 next.lenx(~P, ~S, ~fits, ~u32, ~cut, ~skip, ~sum, max, len, u32(s, 4)) 85 86 # The two length fields are checked before the payload is waited for, so a damaged length is 87 # Bad and never a Torn tail that hides the frames after it. 88 def next.start(~P: Type, ~S: Type, ~fits: S -> @+at:U32 -> @+n:U32 -> S & Bool, ~u32: S -> @+at:U32 -> S & U32, ~cut: S -> @+hs:U32 -> @+len:U32 -> S & P, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, ~sum: P -> P & U32, +max: U32, r: S & Bool) -> Step: 89 (s, both) = r 90 match both: 91 case True{}: 92 next.len(~P, ~S, ~fits, ~u32, ~cut, ~skip, ~sum, max, u32(s, 0)) 93 case False{}: 94 Short{s} 95 96 def next.any(~P: Type, ~S: Type, ~fits: S -> @+at:U32 -> @+n:U32 -> S & Bool, ~u32: S -> @+at:U32 -> S & U32, ~cut: S -> @+hs:U32 -> @+len:U32 -> S & P, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, ~sum: P -> P & U32, +max: U32, r: S & Bool) -> Step: 97 (s, some) = r 98 match some: 99 case True{}: 100 next.start(~P, ~S, ~fits, ~u32, ~cut, ~skip, ~sum, max, fits(s, 0, 8)) 101 case False{}: 102 End{s} 103 104 # One frame from s. max caps a payload's length. 105 def next(~P: Type, ~S: Type, ~fits: S -> @+at:U32 -> @+n:U32 -> S & Bool, ~u32: S -> @+at:U32 -> S & U32, ~cut: S -> @+hs:U32 -> @+len:U32 -> S & P, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, ~sum: P -> P & U32, +max: U32, s: S) -> Step: 106 next.any(~P, ~S, ~fits, ~u32, ~cut, ~skip, ~sum, max, fits(s, 0, 1)) 107 108 def recover.con(~a: Quant, ~P: Kind(a), ~S: Type, p: P, r: Recovery) -> Recovery: 109 Recovery{rs, t, s} = r 110 Recovery{Con{p, rs}, t, s} 111 112 # Every frame from st on. Once n frames are taken, the next one is the last: the tail is Corrupt, 113 # and rest starts after it. Callers give n at least the frame count, so this never happens. 114 def recover.go(~a: Quant, ~P: Kind(a), ~S: Type, ~fits: S -> @+at:U32 -> @+n:U32 -> S & Bool, ~u32: S -> @+at:U32 -> S & U32, ~cut: S -> @+hs:U32 -> @+len:U32 -> S & P, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, ~sum: P -> P & U32, n: Nat, +max: U32, st: Step) -> Recovery: 115 match n st: 116 case 0n End{s}: 117 Recovery{Nil{}, Clean{}, s} 118 case 0n Short{s}: 119 Recovery{Nil{}, Torn{}, s} 120 case 0n Bad{why, s}: 121 Recovery{Nil{}, Corrupt{why}, s} 122 case 0n Got{p, s}: 123 Recovery{Con{p, Nil{}}, Corrupt{"frame budget exhausted"}, s} 124 case 1n+m End{s}: 125 Recovery{Nil{}, Clean{}, s} 126 case 1n+m Short{s}: 127 Recovery{Nil{}, Torn{}, s} 128 case 1n+m Bad{why, s}: 129 Recovery{Nil{}, Corrupt{why}, s} 130 case 1n+m Got{p, s}: 131 recover.con(~a, ~P, ~S, p, recover.go(~a, ~P, ~S, ~fits, ~u32, ~cut, ~skip, ~sum, m, max, next(~P, ~S, ~fits, ~u32, ~cut, ~skip, ~sum, max, s))) 132 133 # Every committed record of s, in order. 134 def recover(~a: Quant, ~P: Kind(a), ~S: Type, ~fits: S -> @+at:U32 -> @+n:U32 -> S & Bool, ~u32: S -> @+at:U32 -> S & U32, ~cut: S -> @+hs:U32 -> @+len:U32 -> S & P, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, ~sum: P -> P & U32, n: Nat, +max: U32, s: S) -> Recovery: 135 recover.go(~a, ~P, ~S, ~fits, ~u32, ~cut, ~skip, ~sum, n, max, next(~P, ~S, ~fits, ~u32, ~cut, ~skip, ~sum, max, s)) 136 137 # step folded over every frame from st on, without a list of the records. n as in recover.go. 138 def replay.go(~P: Type, ~S: Type, ~A: Type, ~fits: S -> @+at:U32 -> @+n:U32 -> S & Bool, ~u32: S -> @+at:U32 -> S & U32, ~cut: S -> @+hs:U32 -> @+len:U32 -> S & P, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, ~sum: P -> P & U32, ~step: A -> P -> A, n: Nat, +max: U32, st: Step, acc: A) -> Replay: 139 match n st: 140 case 0n End{s}: 141 Replay{acc, Clean{}, s} 142 case 0n Short{s}: 143 Replay{acc, Torn{}, s} 144 case 0n Bad{why, s}: 145 Replay{acc, Corrupt{why}, s} 146 case 0n Got{p, s}: 147 Replay{step(acc, p), Corrupt{"frame budget exhausted"}, s} 148 case 1n+m End{s}: 149 Replay{acc, Clean{}, s} 150 case 1n+m Short{s}: 151 Replay{acc, Torn{}, s} 152 case 1n+m Bad{why, s}: 153 Replay{acc, Corrupt{why}, s} 154 case 1n+m Got{p, s}: 155 replay.go(~P, ~S, ~A, ~fits, ~u32, ~cut, ~skip, ~sum, ~step, m, max, next(~P, ~S, ~fits, ~u32, ~cut, ~skip, ~sum, max, s), step(acc, p)) 156 157 def replay(~P: Type, ~S: Type, ~A: Type, ~fits: S -> @+at:U32 -> @+n:U32 -> S & Bool, ~u32: S -> @+at:U32 -> S & U32, ~cut: S -> @+hs:U32 -> @+len:U32 -> S & P, ~skip: S -> @+hs:U32 -> @+len:U32 -> S, ~sum: P -> P & U32, ~step: A -> P -> A, n: Nat, +max: U32, s: S, acc: A) -> Replay: 158 replay.go(~P, ~S, ~A, ~fits, ~u32, ~cut, ~skip, ~sum, ~step, n, max, next(~P, ~S, ~fits, ~u32, ~cut, ~skip, ~sum, max, s), acc) 159 160 # The model: a list of octets. The laws are stated over it. 161 # A model field holds its value in the first of its four cells and 0 in the others. Bytes holds it 162 # as four little-endian octets. Both take four cells, so offsets agree; check.bend tests the octets. 163 164 def list.fits(xs: List<&2, U32>, +at: U32, +n: U32) -> List<&2, U32> & Bool: 165 +ys = xs 166 (ys, Nat.is_le(Nat.add(U32.to_nat(at), U32.to_nat(n)), List.length(&2, U32, ys))) 167 168 def list.field(xs: List<&2, U32>) -> U32: 169 match xs: 170 case Con{x, t}: 171 x 172 case Nil{}: 173 0 174 175 def list.u32(xs: List<&2, U32>, +at: U32) -> List<&2, U32> & U32: 176 +ys = xs 177 (ys, list.field(List.drop(&2, U32, ys, U32.to_nat(at)))) 178 179 def list.cut(xs: List<&2, U32>, +hs: U32, +len: U32) -> List<&2, U32> & List<&2, U32>: 180 +ys = xs 181 (ys, List.take(&2, U32, List.drop(&2, U32, ys, U32.to_nat(hs)), U32.to_nat(len))) 182 183 def list.skip(xs: List<&2, U32>, +hs: U32, +len: U32) -> List<&2, U32>: 184 List.drop(&2, U32, xs, Nat.add(U32.to_nat(hs), U32.to_nat(len))) 185 186 def crc.bit(n: Nat, +c: U32) -> U32: 187 match n: 188 case 0n: 189 c 190 case 1n+m: 191 crc.bit(m, Bool.pick(U32, U32.is_eq((c .&. 1 : U32), 1), ((c >> 1n) .^. 3988292384 : U32), (c >> 1n : U32))) 192 193 def crc.go(xs: List<&2, U32>, +c: U32) -> U32: 194 match xs: 195 case Nil{}: 196 c 197 case Con{+x, t}: 198 crc.go(t, crc.bit(8n, (c .^. (x .&. 255 : U32) : U32))) 199 200 # CRC-32 (ISO-HDLC, as zlib and Hash.crc32) of a list of octets, bit by bit. 201 def list.crc(xs: List<&2, U32>) -> U32: 202 U32.not(crc.go(xs, 4294967295)) 203 204 def list.sum(xs: List<&2, U32>) -> List<&2, U32> & U32: 205 +ys = xs 206 (ys, list.crc(ys)) 207 208 def list.next(+max: U32, xs: List<&2, U32>) -> Step, List<&2, U32>>: 209 next(~List<&2, U32>, ~List<&2, U32>, ~list.fits, ~list.u32, ~list.cut, ~list.skip, ~list.sum, max, xs) 210 211 def list.recover(n: Nat, +max: U32, xs: List<&2, U32>) -> Recovery<&2, List<&2, U32>, List<&2, U32>>: 212 recover(~&2, ~List<&2, U32>, ~List<&2, U32>, ~list.fits, ~list.u32, ~list.cut, ~list.skip, ~list.sum, n, max, xs) 213 214 def list.replay(~A: Type, ~step: A -> List<&2, U32> -> A, n: Nat, +max: U32, xs: List<&2, U32>, acc: A) -> Replay>: 215 replay(~List<&2, U32>, ~List<&2, U32>, ~A, ~list.fits, ~list.u32, ~list.cut, ~list.skip, ~list.sum, ~step, n, max, xs, acc) 216 217 def list.head(+len: U32, +sum: U32) -> List<&2, U32>: 218 [len, 0, 0, 0, U32.not(len), 0, 0, 0, sum, 0, 0, 0] 219 220 def list.frame(+p: List<&2, U32>) -> List<&2, U32>: 221 List.append(&2, U32, list.head(U32.from_nat(List.length(&2, U32, p)), list.crc(p)), p) 222 223 # The frames of rs, one after another: what appending each record writes. 224 def list.log(rs: List<&2, List<&2, U32>>) -> List<&2, U32>: 225 match rs: 226 case Nil{}: 227 Nil{} 228 case Con{p, t}: 229 List.append(&2, U32, list.frame(p), list.log(t)) 230 231 # The Bytes instance. A cursor reads b from pos; pos only moves forward, past whole frames. 232 type Cur is Type: 233 Cur{b: Bytes.Bytes, pos: U32} 234 235 def cur.fits(c: Cur, +at: U32, +n: U32) -> Cur & Bool: 236 Cur{Bytes.Bytes{+len, buf}, +pos} = c 237 (Cur{Bytes.Bytes{len, buf}, pos}, Bytes.fits((len - pos : U32), at, n)) 238 239 def cur.field(m: Maybe<&2, U32>) -> U32: 240 match m: 241 case Some{x}: 242 x 243 case None{}: 244 0 245 246 def cur.u32.fin(+pos: U32, r: Bytes.Bytes & Maybe<&2, U32>) -> Cur & U32: 247 (b, m) = r 248 (Cur{b, pos}, cur.field(m)) 249 250 def cur.u32(c: Cur, +at: U32) -> Cur & U32: 251 Cur{b, +pos} = c 252 cur.u32.fin(pos, Bytes.get.u32le(b, (pos + at : U32))) 253 254 def cur.cut.fin(+pos: U32, r: Bytes.Bytes & Bytes.Bytes) -> Cur & Bytes.Bytes: 255 (b, p) = r 256 (Cur{b, pos}, p) 257 258 def cur.cut(c: Cur, +hs: U32, +len: U32) -> Cur & Bytes.Bytes: 259 Cur{b, +pos} = c 260 cur.cut.fin(pos, Bytes.slice(b, (pos + hs : U32), len)) 261 262 def cur.skip(c: Cur, +hs: U32, +len: U32) -> Cur: 263 Cur{b, +pos} = c 264 Cur{b, (pos + hs + len : U32)} 265 266 # ponytail: Hash.crc32 builds its 256-entry table per record; share one table if recovery gets hot. 267 def cur.sum(p: Bytes.Bytes) -> Bytes.Bytes & U32: 268 Hash.crc32(p) 269 270 # Every frame of b holds at least 12 octets, so len / 12 + 1 steps reach the end. 271 def fuel(+len: U32) -> Nat: 272 U32.to_nat((len / 12 + 1 : U32)) 273 274 # step folded over every committed record of b, in order. rest.pos is where the committed prefix ends. 275 def replay.bytes(~A: Type, ~step: A -> Bytes.Bytes -> A, +max: U32, b: Bytes.Bytes, acc: A) -> Replay: 276 Bytes.Bytes{+len, buf} = b 277 replay(~Bytes.Bytes, ~Cur, ~A, ~cur.fits, ~cur.u32, ~cur.cut, ~cur.skip, ~cur.sum, ~step, fuel(len), max, Cur{Bytes.Bytes{len, buf}, 0}, acc) 278 279 def frame.of(+len: U32, r: Bytes.Bytes & U32) -> Bytes.Bytes: 280 (p, +sum) = r 281 Bytes.append(Bytes.set.u32le(Bytes.set.u32le(Bytes.set.u32le(Bytes.new(12), 0, len), 4, U32.not(len)), 8, sum), p) 282 283 # The frame that appending p writes. 284 def frame(p: Bytes.Bytes) -> Bytes.Bytes: 285 Bytes.Bytes{+len, buf} = p 286 frame.of(len, Hash.crc32(Bytes.Bytes{len, buf})) 287 288 # Effects. Each one finishes its write with fsync (F_FULLFSYNC on macOS in C) before it answers. 289 290 # Opens path for reading and appending, creating it, and fsyncs its directory. Answers the file and 291 # its whole contents. Fails with EFBIG when the file holds more than limit octets. 292 def open.words(path: String, limit: U32) -> IO(Result<&1, &1, U32 & String, File & U32 & Array>): 293 import "./effs/journal.c" 294 import "./effs/journal.js" 295 296 # Writes len octets of words at the end of file, then fsyncs it. 297 def append.words(file: File, len: U32, words: Array) -> IO(File & Result<&1, &1, U32 & String, Unit>): 298 import "./effs/journal.c" 299 import "./effs/journal.js" 300 301 # Cuts file to len octets, then fsyncs it. 302 def truncate(file: File, len: U32) -> IO(File & Result<&1, &1, U32 & String, Unit>): 303 import "./effs/journal.c" 304 import "./effs/journal.js" 305 306 # An open journal. max caps a record's length. end is the file's length. ok turns False when an 307 # append fails: what reached the disk is then unknown, so appends stop until the journal is reopened. 308 type Journal is Type: 309 Journal{file: File, max: U32, end: U32, ok: Bool} 310 311 def open.cut(r: File & Result<&1, &1, U32 & String, Unit>, +max: U32, +end: U32, -A: Type, acc: A) -> IO(Result<&1, &1, U32 & String, Journal & A>): 312 match r: 313 case (file, Fail{e}): 314 do IO>: 315 File.close(file) 316 return Fail{e} 317 case (file, Done{u}): 318 do IO>: 319 return Done{(Journal{file, max, end, True{}}, acc)} 320 321 def open.tail(t: Tail, file: File, +max: U32, +pos: U32, -A: Type, acc: A) -> IO(Result<&1, &1, U32 & String, Journal & A>): 322 match t: 323 case Clean{}: 324 do IO>: 325 return Done{(Journal{file, max, pos, True{}}, acc)} 326 case Torn{}: 327 do IO>: 328 r : File & Result<&1, &1, U32 & String, Unit> <- truncate(file, pos) 329 open.cut(r, max, pos, A, acc) 330 case Corrupt{why}: 331 do IO>: 332 File.close(file) 333 return Fail{(22, "journal: corrupt frame at offset " ++ U32.show(pos) ++ ": " ++ why)} 334 335 def open.replayed(file: File, +max: U32, -A: Type, r: Replay) -> IO(Result<&1, &1, U32 & String, Journal & A>): 336 Replay{acc, t, rest} = r 337 Cur{b, +pos} = rest 338 open.tail(t, file, max, pos, A, acc) 339 340 def open.read(~A: Type, ~step: A -> Bytes.Bytes -> A, r: Result<&1, &1, U32 & String, File & U32 & Array>, +max: U32, acc: A) -> IO(Result<&1, &1, U32 & String, Journal & A>): 341 match r: 342 case Fail{e}: 343 do IO>: 344 return Fail{e} 345 case Done{(file, (len, words))}: 346 open.replayed(file, max, A, replay.bytes(~A, ~step, max, Bytes.Bytes{len, words}, acc)) 347 348 # Opens the journal at path and folds step over its committed records, from acc. 349 # A torn tail is cut off and fsynced before open answers. A corrupt frame fails with EINVAL and 350 # its offset; the file is left as it is. max caps a record's length; limit caps the file's size. 351 def open(~A: Type, ~step: A -> Bytes.Bytes -> A, path: String, +max: U32, limit: U32, acc: A) -> IO(Result<&1, &1, U32 & String, Journal & A>): 352 do IO>: 353 r : Result<&1, &1, U32 & String, File & U32 & Array> <- open.words(path, limit) 354 open.read(~A, ~step, r, max, acc) 355 356 def append.back(+max: U32, +end: U32, +len: U32, r: File & Result<&1, &1, U32 & String, Unit>) -> Journal & Result<&1, &1, U32 & String, Unit>: 357 match r: 358 case (file, Done{u}): 359 (Journal{file, max, (end + len : U32), True{}}, Done{u}) 360 case (file, Fail{e}): 361 (Journal{file, max, end, False{}}, Fail{e}) 362 363 def append.write(file: File, +max: U32, +end: U32, f: Bytes.Bytes) -> IO(Journal & Result<&1, &1, U32 & String, Unit>): 364 Bytes.Bytes{+len, words} = f 365 do IO>: 366 r : File & Result<&1, &1, U32 & String, Unit> <- append.words(file, len, words) 367 return append.back(max, end, len, r) 368 369 def append.refuse(file: File, +max: U32, +end: U32, +ok: Bool, e: U32 & String) -> IO(Journal & Result<&1, &1, U32 & String, Unit>): 370 do IO>: 371 return (Journal{file, max, end, ok}, Fail{e}) 372 373 # The file stays under 4 GiB, so every offset and the next open's read fit a U32. 374 def append.size(over: Bool, big: Bool, file: File, +max: U32, +end: U32, p: Bytes.Bytes) -> IO(Journal & Result<&1, &1, U32 & String, Unit>): 375 match over big: 376 case True{} _: 377 append.refuse(file, max, end, True{}, (22, "journal: record over max")) 378 case False{} True{}: 379 append.refuse(file, max, end, True{}, (27, "journal: file would reach 4 GiB")) 380 case False{} False{}: 381 append.write(file, max, end, frame(p)) 382 383 def append.ok(ok: Bool, file: File, +max: U32, +end: U32, p: Bytes.Bytes) -> IO(Journal & Result<&1, &1, U32 & String, Unit>): 384 match ok: 385 case False{}: 386 append.refuse(file, max, end, False{}, (5, "journal: an append failed; reopen the journal")) 387 case True{}: 388 Bytes.Bytes{+len, buf} = p 389 append.size(U32.is_gt(len, max), Bool.or(U32.is_gt(end, 4294967283), U32.is_lt((4294967283 - end : U32), len)), file, max, end, Bytes.Bytes{len, buf}) 390 391 # Appends p as one frame and answers once it is fsynced: Done means durable, under the trust 392 # assumptions in README.md. After a Fail the record may or may not survive a reopen. 393 def append(j: Journal, p: Bytes.Bytes) -> IO(Journal & Result<&1, &1, U32 & String, Unit>): 394 Journal{file, +max, +end, ok} = j 395 append.ok(ok, file, max, end, p) 396 397 def close(j: Journal) -> IO(Unit): 398 Journal{file, max, end, ok} = j 399 File.close(file)