Skip to main content

fusee_sim/
env.rs

1//! [`SimEnv`]: the shard runtime's I/O on a simulated disk and clock.
2
3use crate::disk::SimDisk;
4use crate::rng::Rng;
5use fusee::env::{Env, Notify};
6use fusee_core::Lsn;
7use fusee_core::segment::Batch;
8use fusee_core::store::{Checkpoint, store_header};
9use std::cell::RefCell;
10use std::collections::BTreeMap;
11use std::io;
12use std::rc::Rc;
13
14/// Scheduled I/O completions, owned by the world.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
16pub enum Io {
17    Flush {
18        shard: u32,
19        epoch: u64,
20    },
21    Checkpoint {
22        shard: u32,
23        epoch: u64,
24    },
25    /// One of the shared log's writes in flight may now sync.
26    LogFlush {
27        epoch: u64,
28        seq: u64,
29    },
30    /// The shared log's blob writes through job `id` may now sync.
31    BlobFlush {
32        epoch: u64,
33        id: u64,
34    },
35}
36
37/// The shared log's segment files on the simulated disk (`wal/`), or its
38/// blob area's files (`blob/`, which grow by appends; a crash resolves
39/// their pending writes like any others', independently of the log's).
40pub struct SimDevice {
41    m: MachineRef,
42    prefix: &'static str,
43    /// The 4 KiB blocks each `write` call touched, as (segment, first,
44    /// last), for [`SimLog`] to attribute to the write in flight.
45    touched: Rc<RefCell<Vec<(u64, u64, u64)>>>,
46}
47
48impl SimDevice {
49    fn blobs(m: &MachineRef) -> Self {
50        Self {
51            m: m.clone(),
52            prefix: "blob/",
53            touched: Rc::new(RefCell::new(Vec::new())),
54        }
55    }
56
57    fn name(&self, number: u64) -> String {
58        format!("{}{number:020}", self.prefix)
59    }
60}
61
62impl LogDevice for SimDevice {
63    fn segments(&self) -> io::Result<Vec<u64>> {
64        let m = self.m.borrow();
65        let mut v: Vec<u64> = m
66            .disk
67            .names_with_prefix(self.prefix)
68            .iter()
69            .filter_map(|n| n.strip_prefix(self.prefix)?.parse().ok())
70            .collect();
71        v.sort_unstable();
72        Ok(v)
73    }
74    fn len(&self, segment: u64) -> io::Result<u64> {
75        let m = self.m.borrow();
76        let f = m
77            .disk
78            .get(&self.name(segment))
79            .ok_or_else(|| io::Error::other("missing log segment"))?;
80        Ok(f.view().len() as u64)
81    }
82    fn read(&self, segment: u64, offset: u64, len: usize) -> io::Result<Vec<u8>> {
83        let m = self.m.borrow();
84        let img = m
85            .disk
86            .get(&self.name(segment))
87            .ok_or_else(|| io::Error::other("missing log segment"))?
88            .view();
89        let start = (offset as usize).min(img.len());
90        let end = start.saturating_add(len).min(img.len());
91        Ok(img[start..end].to_vec())
92    }
93    fn create(&self, segment: u64, len: u64) -> io::Result<()> {
94        let mut m = self.m.borrow_mut();
95        m.disk.remove(&self.name(segment));
96        let f = m.disk.file(&self.name(segment));
97        f.write(0, &vec![0; len as usize]);
98        f.flush();
99        Ok(())
100    }
101    fn write(&self, segment: u64, offset: u64, bytes: &[u8]) -> io::Result<()> {
102        let block = fusee_core::wal::BLOCK;
103        let last = (offset + (bytes.len() as u64).max(1) - 1) / block;
104        if self.prefix == "wal/" {
105            self.touched
106                .borrow_mut()
107                .push((segment, offset / block, last));
108        }
109        self.m
110            .borrow_mut()
111            .disk
112            .file(&self.name(segment))
113            .write(offset, bytes);
114        Ok(())
115    }
116    fn sync(&self, segment: u64) -> io::Result<()> {
117        self.m.borrow_mut().disk.file(&self.name(segment)).flush();
118        Ok(())
119    }
120    fn rename(&self, from: u64, to: u64) -> io::Result<()> {
121        let mut m = self.m.borrow_mut();
122        let f = m
123            .disk
124            .files
125            .remove(&self.name(from))
126            .ok_or_else(|| io::Error::other("missing log segment"))?;
127        m.disk.files.insert(self.name(to), f);
128        Ok(())
129    }
130    fn remove(&self, segment: u64) -> io::Result<()> {
131        self.m.borrow_mut().disk.remove(&self.name(segment));
132        Ok(())
133    }
134}
135
136/// How the simulated shared log treats records over its offload threshold.
137#[derive(Clone, Copy, Debug, Default)]
138pub struct Offload {
139    /// Records whose canonical encoding is longer than this go to the blob
140    /// area; 0: none.
141    pub threshold: u64,
142    /// Sabotage: sections are released to batches as soon as their blobs
143    /// are written, before the blobs are durable.
144    pub early: bool,
145}
146
147/// The shared log of one simulated machine (log format 3): one write in
148/// flight, completed by the world after a random latency.
149pub struct SimLog {
150    pub wal: Wal<SimDevice>,
151    /// Blob files written to since their last sync.
152    blob_files: Vec<u64>,
153    offload: Offload,
154    in_flight: Vec<InFlight>,
155    failed: bool,
156    epoch: u64,
157    touched: Rc<RefCell<Vec<(u64, u64, u64)>>>,
158    /// Blocks each write in flight touched: (seq, segment, first, last).
159    blocks: Vec<(u64, u64, u64, u64)>,
160    /// Two writes in flight together touched one block. Under `O_DIRECT`
161    /// each rewrites its blocks whole, so one could erase the other's
162    /// bytes; the simulated disk merges them, so this is checked instead.
163    violation: Option<String>,
164}
165
166pub type SimLogRef = Rc<RefCell<SimLog>>;
167
168impl SimLog {
169    pub fn open(
170        m: &MachineRef,
171        shards: u32,
172        segment_bytes: u64,
173        in_flight: usize,
174        spares: usize,
175        offload: Offload,
176    ) -> io::Result<SimLogRef> {
177        let (now, life, epoch) = {
178            let mut mm = m.borrow_mut();
179            (mm.now, mm.rng.next() | 1, mm.epoch)
180        };
181        let opts = WalOptions {
182            segment_bytes,
183            first_segment_bytes: (segment_bytes / 4).max(16 << 10),
184            in_flight,
185            spares,
186            ..WalOptions::default()
187        };
188        let touched = Rc::new(RefCell::new(Vec::new()));
189        let dev = SimDevice {
190            m: m.clone(),
191            prefix: "wal/",
192            touched: touched.clone(),
193        };
194        let wal = Wal::open_with_blobs(
195            dev,
196            Blobs {
197                dev: SimDevice::blobs(m),
198                threshold: offload.threshold,
199            },
200            shards,
201            opts,
202            life,
203            now,
204        )?;
205        touched.borrow_mut().clear();
206        Ok(Rc::new(RefCell::new(Self {
207            wal,
208            blob_files: Vec::new(),
209            offload,
210            in_flight: Vec::new(),
211            failed: false,
212            epoch,
213            touched,
214            blocks: Vec::new(),
215            violation: None,
216        })))
217    }
218
219    /// Two writes in flight shared a block, if they ever did.
220    pub fn violation(&self) -> Option<&str> {
221        self.violation.as_deref()
222    }
223
224    /// Issues the blob writes staged sections need, and schedules their
225    /// sync after a random latency (sections that reference them are staged
226    /// once it completes, or at once under the `early` sabotage).
227    fn start_blobs(&mut self, m: &MachineRef) {
228        if self.failed {
229            return;
230        }
231        let jobs = self.wal.take_blob_jobs();
232        let Some(last) = jobs.last().map(|j| j.id) else {
233            return;
234        };
235        let Some(dev) = self.wal.blob_device() else {
236            return;
237        };
238        for j in &jobs {
239            if j.perform(dev).is_err() {
240                self.failed = true;
241                return;
242            }
243            if !self.blob_files.contains(&j.file) {
244                self.blob_files.push(j.file);
245            }
246        }
247        if self.offload.early && self.wal.blobs_written(last).is_err() {
248            self.failed = true;
249        }
250        let mut mm = m.borrow_mut();
251        let after = mm.latency();
252        let epoch = self.epoch;
253        mm.schedule(after, Io::BlobFlush { epoch, id: last });
254    }
255
256    /// The blob writes through job `id` may sync now, or fail.
257    pub fn complete_blobs(log: &SimLogRef, m: &MachineRef, id: u64) {
258        let mut l = log.borrow_mut();
259        let fail = {
260            let mut mm = m.borrow_mut();
261            let p = mm.flush_fail_per_mille;
262            let f = mm.rng.chance(p);
263            if f {
264                mm.log.push("blob area flush failed".to_string());
265            }
266            f
267        };
268        if fail || l.failed {
269            l.failed = true;
270            return;
271        }
272        let files = std::mem::take(&mut l.blob_files);
273        let synced = match l.wal.blob_device() {
274            Some(dev) => files.iter().try_for_each(|f| dev.sync(*f)),
275            None => Ok(()),
276        };
277        if synced.and_then(|()| l.wal.blobs_written(id)).is_err() {
278            l.failed = true;
279            return;
280        }
281        l.start(m);
282    }
283
284    /// Starts writes of everything staged while the log allows another in
285    /// flight (two at once: their completions arrive in any order).
286    fn start(&mut self, m: &MachineRef) {
287        self.start_blobs(m);
288        while !self.failed && self.wal.has_staged() && self.wal.can_begin() {
289            // Like the engine: with nothing in flight a write is sometimes
290            // packed (the inline path), which leaves the next batch starting
291            // inside a block; otherwise it is padded so another may follow.
292            let (now, overlap) = {
293                let mut mm = m.borrow_mut();
294                let packed = self.in_flight.is_empty() && mm.rng.chance(500);
295                (mm.now, !packed)
296            };
297            match self.wal.begin_write(now, overlap) {
298                Ok(Some(w)) => {
299                    let seq = w.seq();
300                    let touched: Vec<_> = self.touched.borrow_mut().drain(..).collect();
301                    for (segment, first, last) in touched {
302                        if let Some(other) = self
303                            .blocks
304                            .iter()
305                            .find(|b| b.0 != seq && b.1 == segment && b.2 <= last && b.3 >= first)
306                            && self.violation.is_none()
307                        {
308                            self.violation = Some(format!(
309                                "log writes {} and {seq} were in flight together and both \
310                                 touched segment {segment} block {}",
311                                other.0,
312                                first.max(other.2)
313                            ));
314                        }
315                        self.blocks.push((seq, segment, first, last));
316                    }
317                    self.in_flight.push(w);
318                    let mut mm = m.borrow_mut();
319                    let after = mm.latency();
320                    let epoch = self.epoch;
321                    mm.schedule(after, Io::LogFlush { epoch, seq });
322                }
323                Ok(None) => break,
324                Err(_) => self.failed = true,
325            }
326        }
327    }
328
329    /// Writes in flight.
330    pub fn in_flight(&self) -> usize {
331        self.in_flight.len()
332    }
333
334    /// The write `seq` may sync now, or fail.
335    pub fn complete(log: &SimLogRef, m: &MachineRef, seq: u64) {
336        let mut l = log.borrow_mut();
337        let Some(i) = l.in_flight.iter().position(|w| w.seq() == seq) else {
338            return;
339        };
340        let w = l.in_flight.remove(i);
341        l.blocks.retain(|b| b.0 != seq);
342        let fail = {
343            let mut mm = m.borrow_mut();
344            let p = mm.flush_fail_per_mille;
345            let f = mm.rng.chance(p);
346            if f {
347                mm.log.push("shared log flush failed".to_string());
348            }
349            f
350        };
351        if fail || l.failed || l.wal.finish_write(w).is_err() {
352            // The outcome of the writes is unknown: every shard stops.
353            l.failed = true;
354            return;
355        }
356        l.start(m);
357    }
358}
359
360use fusee::wal::{Blobs, InFlight, LogDevice, Wal, WalOptions};
361
362/// State shared by all shards of one simulated machine.
363pub struct Machine {
364    pub disk: SimDisk,
365    pub now: u64,
366    pub rng: Rng,
367    /// (due time, insertion order) -> completion
368    pub io: BTreeMap<(u64, u64), Io>,
369    pub io_seq: u64,
370    pub epoch: u64,
371    pub flush_fail_per_mille: u32,
372    pub max_io_ms: u64,
373    pub log: Vec<String>,
374}
375
376pub type MachineRef = Rc<RefCell<Machine>>;
377
378impl Machine {
379    pub fn schedule(&mut self, after: u64, io: Io) {
380        let t = self.now + after;
381        let s = self.io_seq;
382        self.io_seq += 1;
383        self.io.insert((t, s), io);
384    }
385
386    pub fn latency(&mut self) -> u64 {
387        let max = self.max_io_ms.max(1);
388        self.rng.below(max + 1)
389    }
390}
391
392pub struct SimEnv {
393    /// Log format 3: the machine's shared log, and the last LSN submitted.
394    log: Option<SimLogRef>,
395    log_in_flight: Option<Lsn>,
396    /// Sections of this shard the runtime may have in flight at once, and
397    /// the durable position last reported to it. Half the shards of an
398    /// epoch keep two in flight, the others one.
399    log_max_in_flight: usize,
400    log_reported: Lsn,
401    /// Checkpoints submitted, and what the two newest durable ones cover.
402    log_checkpoints: Vec<Lsn>,
403    log_retained: Vec<Lsn>,
404    shard: u32,
405    epoch: u64,
406    m: MachineRef,
407    rng: Rng,
408    flushing: Option<Batch>,
409    flushed: Option<io::Result<Lsn>>,
410    ckpt: Option<(Checkpoint, Option<u64>)>,
411    ckpt_done: Option<io::Result<Lsn>>,
412    store_gen: u64,
413    store_len: u64,
414    /// The prefix of the loaded checkpoint, when recovery reads its
415    /// persisted index instead of scanning the store (most opens; the
416    /// others scan, so both ways are exercised).
417    indexed_prefix: Option<(u64, u64)>,
418    failed: bool,
419}
420
421fn seg_name(shard: u32, seg: u64) -> String {
422    format!("s{shard:03}/log/{seg:020}")
423}
424
425fn store_name(shard: u32, generation: u64) -> String {
426    format!("s{shard:03}/runs-{generation:010}")
427}
428
429fn ckpt_name(shard: u32, lsn: Lsn) -> String {
430    format!("s{shard:03}/cp-{lsn:020}")
431}
432
433impl SimEnv {
434    pub fn new(shard: u32, m: MachineRef) -> Self {
435        let (epoch, rng) = {
436            let mut mm = m.borrow_mut();
437            (mm.epoch, mm.rng.fork())
438        };
439        Self {
440            log: None,
441            log_in_flight: None,
442            log_max_in_flight: 1,
443            log_reported: 0,
444            log_checkpoints: Vec::new(),
445            log_retained: Vec::new(),
446            shard,
447            epoch,
448            m,
449            rng,
450            flushing: None,
451            flushed: None,
452            ckpt: None,
453            ckpt_done: None,
454            store_gen: 0,
455            store_len: 0,
456            indexed_prefix: None,
457            failed: false,
458        }
459    }
460
461    /// A shard whose records go to the machine's shared log.
462    pub fn with_log(shard: u32, m: MachineRef, log: SimLogRef) -> Self {
463        let mut env = Self::new(shard, m);
464        // Not from the random stream, so that adding this left every
465        // seed's other choices as they were.
466        env.log_max_in_flight = 1 + ((env.epoch + u64::from(shard)) % 2) as usize;
467        env.log_reported = log.borrow().wal.durable(shard);
468        env.log = Some(log);
469        env
470    }
471
472    fn report_retained(&self) -> io::Result<()> {
473        if let (Some(log), Some(min)) = (&self.log, self.log_retained.iter().copied().min()) {
474            log.borrow_mut().wal.set_retain(self.shard, min)?;
475        }
476        Ok(())
477    }
478
479    /// The scheduled flush completed (or failed).
480    pub fn complete_flush(&mut self) {
481        let Some(batch) = self.flushing.take() else {
482            return;
483        };
484        let mut m = self.m.borrow_mut();
485        let fail = {
486            let p = m.flush_fail_per_mille;
487            m.rng.chance(p)
488        };
489        if fail || self.failed {
490            // After a failed fsync the kernel may already have dropped the
491            // dirty pages: the outcome of these writes is unknown. The
492            // runtime must stop using this log.
493            self.failed = true;
494            m.log.push(format!(
495                "shard {} flush failed (batch ..={})",
496                self.shard, batch.last_lsn
497            ));
498            self.flushed = Some(Err(io::Error::other("simulated fsync failure")));
499            return;
500        }
501        let mut names: Vec<String> = Vec::new();
502        for c in &batch.chunks {
503            let n = seg_name(self.shard, c.segment);
504            if !names.contains(&n) {
505                names.push(n);
506            }
507        }
508        for n in names {
509            m.disk.file(&n).flush();
510        }
511        self.flushed = Some(Ok(batch.last_lsn));
512    }
513
514    pub fn complete_checkpoint(&mut self) {
515        let Some((cp, delete)) = self.ckpt.take() else {
516            return;
517        };
518        let mut m = self.m.borrow_mut();
519        let shard = self.shard;
520        m.disk.file(&store_name(shard, cp.store_generation)).flush();
521        let bytes = cp.encode();
522        let name = ckpt_name(shard, cp.lsn);
523        let f = m.disk.file(&name);
524        f.truncate(0);
525        f.write(0, &bytes);
526        f.flush();
527        let mut cps = m.disk.names_with_prefix(&format!("s{shard:03}/cp-"));
528        cps.sort();
529        if cps.len() > 2 {
530            for old in &cps[..cps.len() - 2] {
531                m.disk.remove(old);
532            }
533        }
534        let saved: Vec<_> = cps
535            .iter()
536            .rev()
537            .take(2)
538            .filter_map(|name| m.disk.get(name))
539            .filter_map(|f| Checkpoint::decode(&f.view()).ok())
540            .collect();
541        for name in m.disk.names_with_prefix(&format!("s{shard:03}/runs-")) {
542            if !saved
543                .iter()
544                .any(|c| store_name(shard, c.store_generation) == name)
545            {
546                m.disk.remove(&name);
547            }
548        }
549        if self.log.is_some() {
550            if let Some(i) = self.log_checkpoints.iter().position(|l| *l == cp.lsn) {
551                self.log_checkpoints.remove(i);
552                self.log_retained.push(cp.lsn);
553                if self.log_retained.len() > 2 {
554                    self.log_retained.remove(0);
555                }
556            }
557            drop(m);
558            self.ckpt_done = Some(self.report_retained().map(|()| cp.lsn));
559            return;
560        }
561        if let (Some(through), Some(oldest)) = (delete, saved.last()) {
562            let mut segments = m.disk.names_with_prefix(&format!("s{shard:03}/log/"));
563            segments.sort();
564            let boundary = segments.iter().rev().find_map(|path| {
565                let number = path.rsplit('/').next()?.parse::<u64>().ok()?;
566                if number > through.saturating_add(1) {
567                    return None;
568                }
569                let bytes = m.disk.get(path)?.read(0, 44)?;
570                let header = fusee_core::segment::SegmentHeader::decode(&bytes).ok()??;
571                (header.segment == number && header.base_lsn <= oldest.lsn.saturating_add(1))
572                    .then_some(path.clone())
573            });
574            if let Some(boundary) = boundary {
575                for name in segments.into_iter().take_while(|n| *n < boundary) {
576                    m.disk.remove(&name);
577                }
578            }
579        }
580        self.ckpt_done = Some(Ok(cp.lsn));
581    }
582
583    pub fn epoch(&self) -> u64 {
584        self.epoch
585    }
586}
587
588impl Env for SimEnv {
589    fn now_ms(&self) -> u64 {
590        self.m.borrow().now
591    }
592
593    fn random(&mut self) -> u64 {
594        self.rng.next()
595    }
596
597    fn set_notify(&mut self, _notify: Notify) {}
598
599    fn max_in_flight(&self) -> usize {
600        if self.log.is_some() {
601            self.log_max_in_flight
602        } else {
603            1
604        }
605    }
606
607    fn submit(&mut self, batch: Batch) {
608        if let Some(log) = &self.log {
609            self.log_in_flight = Some(batch.last_lsn);
610            let mut l = log.borrow_mut();
611            if l.wal.stage(self.shard, batch).is_err() {
612                l.failed = true;
613            }
614            l.start(&self.m);
615            return;
616        }
617        let mut m = self.m.borrow_mut();
618        for c in &batch.chunks {
619            m.disk
620                .file(&seg_name(self.shard, c.segment))
621                .write(c.offset, &c.bytes);
622        }
623        let after = m.latency();
624        let (shard, epoch) = (self.shard, self.epoch);
625        m.schedule(after, Io::Flush { shard, epoch });
626        self.flushing = Some(batch);
627    }
628
629    fn poll_flushed(&mut self) -> Option<io::Result<Lsn>> {
630        if let Some(log) = &self.log {
631            let want = self.log_in_flight?;
632            let l = log.borrow();
633            if l.failed {
634                self.log_in_flight = None;
635                return Some(Err(io::Error::other("simulated shared log failure")));
636            }
637            let durable = l.wal.durable(self.shard);
638            if durable >= want {
639                self.log_in_flight = None;
640                self.log_reported = durable;
641                return Some(Ok(durable));
642            }
643            if durable > self.log_reported {
644                self.log_reported = durable;
645                return Some(Ok(durable));
646            }
647            return None;
648        }
649        self.flushed.take()
650    }
651
652    fn store_append(&mut self, frame: &[u8]) -> io::Result<u64> {
653        let off = self.store_len;
654        let name = store_name(self.shard, self.store_gen);
655        self.m.borrow_mut().disk.file(&name).write(off, frame);
656        self.store_len += frame.len() as u64;
657        Ok(off)
658    }
659
660    fn store_read(&mut self, offset: u64, len: u32) -> io::Result<Vec<u8>> {
661        let name = store_name(self.shard, self.store_gen);
662        self.m
663            .borrow()
664            .disk
665            .get(&name)
666            .and_then(|f| f.read(offset, len as usize))
667            .ok_or_else(|| io::Error::other("run store read past end"))
668    }
669
670    fn store_len(&self) -> u64 {
671        self.store_len
672    }
673
674    fn store_generation(&self) -> u64 {
675        self.store_gen
676    }
677
678    fn compact_store(
679        &mut self,
680        live: &[(fusee_core::RunId, fusee_core::shard::BlobRef)],
681    ) -> io::Result<bool> {
682        let mut generation = self.store_gen;
683        let name = loop {
684            generation = generation
685                .checked_add(1)
686                .ok_or_else(|| io::Error::other("store generation overflow"))?;
687            let name = store_name(self.shard, generation);
688            if self.m.borrow().disk.get(&name).is_none() {
689                break name;
690            }
691        };
692        let header = store_header();
693        self.m.borrow_mut().disk.file(&name).write(0, &header);
694        let mut length = header.len() as u64;
695        for &(run, blob) in live {
696            let frame = self.store_read(blob.offset, blob.len)?;
697            fusee::env::check_live_frame(&frame, run)?;
698            self.m.borrow_mut().disk.file(&name).write(length, &frame);
699            length += u64::from(blob.len);
700        }
701        self.store_gen = generation;
702        self.store_len = length;
703        Ok(true)
704    }
705
706    fn checkpoint(&mut self, checkpoint: Checkpoint, delete_through_segment: Option<u64>) {
707        if self.log.is_some() {
708            self.log_checkpoints.push(checkpoint.lsn);
709        }
710        let mut m = self.m.borrow_mut();
711        let after = m.latency();
712        let (shard, epoch) = (self.shard, self.epoch);
713        m.schedule(after, Io::Checkpoint { shard, epoch });
714        self.ckpt = Some((checkpoint, delete_through_segment));
715    }
716
717    fn poll_checkpoint(&mut self) -> Option<io::Result<Lsn>> {
718        self.ckpt_done.take()
719    }
720
721    fn load_checkpoint(&mut self) -> io::Result<Option<Checkpoint>> {
722        let cp = self.load_newest_checkpoint()?;
723        self.indexed_prefix = cp
724            .as_ref()
725            .filter(|c| {
726                c.index.is_some() && !(self.epoch + u64::from(self.shard)).is_multiple_of(4)
727            })
728            .map(|c| (c.store_generation, c.store_len));
729        if self.log.is_some() {
730            let m = self.m.borrow();
731            let mut cps = m.disk.names_with_prefix(&format!("s{:03}/cp-", self.shard));
732            cps.sort();
733            let mut positions: Vec<u64> = cps
734                .iter()
735                .rev()
736                .filter_map(|n| m.disk.get(n))
737                .filter_map(|f| Checkpoint::decode(&f.view()).ok())
738                .map(|c| c.lsn)
739                .take(2)
740                .collect();
741            positions.reverse();
742            drop(m);
743            self.log_retained = if cp.is_some() { positions } else { vec![0] };
744            self.report_retained()?;
745        }
746        Ok(cp)
747    }
748
749    fn shared_log(&self) -> bool {
750        self.log.is_some()
751    }
752
753    fn recover_log(
754        &mut self,
755        after: Lsn,
756    ) -> io::Result<(Vec<(Lsn, fusee_core::record::RecordBuf)>, Lsn)> {
757        match &self.log {
758            Some(log) => log.borrow_mut().wal.take_recovered(self.shard, after),
759            None => Err(io::Error::other("this shard has per-shard segments")),
760        }
761    }
762
763    fn log_position(&self) -> u64 {
764        self.log.as_ref().map_or(0, |l| l.borrow().wal.position())
765    }
766
767    fn log_segment(&self) -> u64 {
768        self.log
769            .as_ref()
770            .map_or(0, |l| l.borrow().wal.active_segment())
771    }
772
773    fn checkpoint_wanted(&self) -> bool {
774        self.log
775            .as_ref()
776            .is_some_and(|l| l.borrow().wal.pins(self.shard))
777    }
778    fn checkpoint_wanted_on_close(&self) -> bool {
779        self.log
780            .as_ref()
781            .is_some_and(|l| l.borrow().wal.holds_old(self.shard))
782    }
783    fn recover_store(
784        &mut self,
785        generation: u64,
786        len: u64,
787        visit: &mut dyn FnMut(u64, u32, fusee_core::store::StoreEntry),
788    ) -> io::Result<()> {
789        let indexed = generation != 0 && self.indexed_prefix.take() == Some((generation, len));
790        let bytes = self.load_store(generation, len)?;
791        if indexed {
792            return Ok(());
793        }
794        fusee_core::store::scan_store(&bytes, visit).map_err(io::Error::other)
795    }
796
797    fn load_store(&mut self, generation: u64, len: u64) -> io::Result<Vec<u8>> {
798        let mut m = self.m.borrow_mut();
799        if generation == 0 {
800            let name = store_name(self.shard, 1);
801            m.disk.remove(&name);
802            let f = m.disk.file(&name);
803            let h = store_header();
804            f.write(0, &h);
805            f.flush();
806            self.store_gen = 1;
807            self.store_len = h.len() as u64;
808            return Ok(h);
809        }
810        let name = store_name(self.shard, generation);
811        let f = m.disk.file(&name);
812        f.truncate(len);
813        let bytes = f.view();
814        if (bytes.len() as u64) < len {
815            return Err(io::Error::other(
816                "run store shorter than its checkpoint says",
817            ));
818        }
819        self.store_gen = generation;
820        self.store_len = len;
821        Ok(bytes)
822    }
823
824    fn segments(&mut self) -> io::Result<Vec<u64>> {
825        let m = self.m.borrow();
826        let mut v: Vec<u64> = m
827            .disk
828            .names_with_prefix(&format!("s{:03}/log/", self.shard))
829            .iter()
830            .filter_map(|n| n.rsplit('/').next()?.parse().ok())
831            .collect();
832        v.sort_unstable();
833        Ok(v)
834    }
835
836    fn read_segment(&mut self, segment: u64) -> io::Result<Vec<u8>> {
837        let m = self.m.borrow();
838        m.disk
839            .get(&seg_name(self.shard, segment))
840            .map(|f| f.view())
841            .ok_or_else(|| io::Error::other("missing segment"))
842    }
843
844    fn sync_recovered(&mut self) -> io::Result<()> {
845        let mut m = self.m.borrow_mut();
846        for n in m
847            .disk
848            .names_with_prefix(&format!("s{:03}/log/", self.shard))
849        {
850            m.disk.file(&n).flush();
851        }
852        Ok(())
853    }
854
855    fn discard_segment(&mut self, segment: u64) -> io::Result<()> {
856        self.m
857            .borrow_mut()
858            .disk
859            .remove(&seg_name(self.shard, segment));
860        Ok(())
861    }
862
863    fn exclusive_log(&self) -> bool {
864        true
865    }
866
867    fn open_log(&mut self, segment: u64) -> io::Result<()> {
868        let mut m = self.m.borrow_mut();
869        let f = m.disk.file(&seg_name(self.shard, segment));
870        f.flush();
871        Ok(())
872    }
873}
874
875impl SimEnv {
876    fn load_newest_checkpoint(&mut self) -> io::Result<Option<Checkpoint>> {
877        let m = self.m.borrow();
878        let mut cps = m.disk.names_with_prefix(&format!("s{:03}/cp-", self.shard));
879        cps.sort();
880        for name in cps.iter().rev() {
881            if let Some(f) = m.disk.get(name)
882                && let Ok(cp) = Checkpoint::decode(&f.view())
883            {
884                return Ok(Some(cp));
885            }
886        }
887        Ok(None)
888    }
889}