Skip to main content

krabka_log/log/
sync.rs

1//! Explicit flush and `fsync` of the segment files and of the log
2//! directory.
3//!
4//! A newly created segment name is durable only once the parent directory
5//! is synced, so the log tracks that debt and pays it here rather than at
6//! every site that creates a segment.
7
8#[cfg(unix)]
9use crate::io::LogIo;
10// `active.size().bytes_u64()` below is only read where `pace_writeback` exists.
11#[cfg(not(target_os = "wasi"))]
12#[cfg(unix)]
13use std::path::Path;
14use {
15    super::Log,
16    crate::{error::LogError, segment::Segment},
17    krabka_ids::Offset,
18    std::time::Instant,
19};
20
21#[cfg(test)]
22pub mod sync_observer {
23    #[cfg(unix)]
24    use std::path::PathBuf;
25    use {krabka_ids::Offset, std::cell::RefCell};
26
27    #[cfg(unix)]
28    thread_local! {
29        static DIR_SYNCS: RefCell<Vec<PathBuf>> = const { RefCell::new(Vec::new()) };
30    }
31
32    thread_local! {
33        static SEGMENT_FLUSHES: RefCell<Vec<Offset>> = const { RefCell::new(Vec::new()) };
34    }
35
36    #[cfg(unix)]
37    pub(super) fn take_dir_syncs() -> Vec<PathBuf> { DIR_SYNCS.take() }
38
39    pub fn take_segment_flushes() -> Vec<Offset> { SEGMENT_FLUSHES.take() }
40
41    #[cfg(unix)]
42    pub(super) fn record_dir_sync(dir: PathBuf) {
43        DIR_SYNCS.with_borrow_mut(|synced| synced.push(dir));
44    }
45
46    pub(super) fn record_segment_flush(base: Offset) {
47        SEGMENT_FLUSHES.with_borrow_mut(|flushed| flushed.push(base));
48    }
49}
50
51impl Log {
52    /// Drain rollover work and `fsync` every segment to stable storage, independent of
53    /// [`crate::LogConfig::flush_on_append`].
54    ///
55    /// This method also fsyncs the log directory after it creates a new
56    /// segment file. The segment therefore stays reachable after a crash on
57    /// filesystems that require a parent-directory fsync. A log start that
58    /// retention moved and has not yet checkpointed is written too.
59    ///
60    /// # Errors
61    /// Returns a [`LogError`] if the underlying segment or directory flush fails.
62    pub fn sync(&mut self) -> Result<(), LogError> {
63        self.rollover_flusher.finish()?;
64        // Everything is synced below, so the next window starts from zero.
65        // The whole pages written since the last hand-over go to the ledger
66        // first: a sync between windows must leave no page outside the
67        // budget's accounting, and the frontier must stay on a page boundary.
68        #[cfg(not(target_os = "wasi"))]
69        {
70            self.writeback_unsynced = 0;
71            self.writeback_jobs.clear();
72            if self.active.is_some() {
73                let budget = self.page_cache_budget();
74                self.hand_over_pages(budget)?;
75            }
76        }
77        for segment in &mut self.segments {
78            Self::segment_flush(segment)?;
79        }
80        self.active_segment_flush()?;
81        if self.dir_sync_needed {
82            // Rust's standard directory-open path is supported on Unix, where
83            // syncing the parent makes newly-created segment names durable. On
84            // Windows the platform provides no equivalent through `std`; the
85            // segment, offset-index, and time-index handles above have still
86            // been flushed with `sync_data`.
87            #[cfg(unix)]
88            Self::sync_log_dir(&*self.io, &self.dir)?;
89            self.dir_sync_needed = false;
90        }
91        // The segments the start follows are durable now, so the start they
92        // record is too.
93        self.flush_log_start_checkpoint()?;
94        self.unflushed_messages = 0;
95        self.last_flush = Instant::now();
96        Ok(())
97    }
98
99    #[cfg(unix)]
100    fn sync_log_dir(io: &dyn LogIo, dir: &Path) -> Result<(), LogError> {
101        io.sync_dir(dir)?;
102        #[cfg(test)]
103        sync_observer::record_dir_sync(dir.to_path_buf());
104        Ok(())
105    }
106
107    pub(super) fn active_segment_flush(&mut self) -> Result<(), LogError> {
108        let active = self.active.as_mut().ok_or_else(super::no_active_segment)?;
109        Self::segment_flush(active)
110    }
111
112    pub(super) fn rollback_failed_append(&mut self, base_offset: Offset) -> Result<(), LogError> {
113        // Use the full log truncation path so rollback restores bytes, the
114        // file cursor, every sidecar, producer/transaction state, cached
115        // visibility frontiers, and the leader-epoch checkpoint together.
116        self.truncate_to(base_offset)?;
117        self.active_segment_flush()
118    }
119
120    fn segment_flush(segment: &mut Segment) -> Result<(), LogError> {
121        #[cfg(test)]
122        sync_observer::record_segment_flush(segment.base_offset());
123        segment.flush()
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use {
130        super::*,
131        crate::{
132            log::test_support::{rolling_test_log, sample_batch},
133            test_support::open_log,
134        },
135        krabka_ids::Offset,
136    };
137
138    #[test]
139    fn sync_persists_appended_records() {
140        let dir = tempfile::tempdir().unwrap();
141        {
142            let mut log = open_log(dir.path());
143            log.append(&mut sample_batch(3)).unwrap();
144            log.sync().unwrap(); // fsync without relying on flush_on_append
145        }
146        // Reopen from disk: the synced records are present.
147        let log = open_log(dir.path());
148        assert2::assert!(log.log_end_offset() == Offset(3));
149    }
150
151    #[cfg(unix)]
152    #[yare::parameterized(
153        initial_creation = { "initial creation" },
154        reopen_before_prior_sync = { "reopen before prior sync" },
155        rollover = { "rollover" },
156    )]
157    fn sync_fsyncs_parent_dir_after_segment_lifecycle_events(case: &str) {
158        let dir = tempfile::tempdir().unwrap();
159        let mut log = match case {
160            "initial creation" => open_log(dir.path()),
161            "reopen before prior sync" => {
162                drop(open_log(dir.path()));
163                open_log(dir.path())
164            }
165            "rollover" => {
166                let mut log = rolling_test_log(dir.path());
167                log.append(&mut sample_batch(1)).unwrap();
168                log.sync().unwrap();
169                log.append(&mut sample_batch(1)).unwrap();
170                log
171            }
172            _ => unreachable!("known yare case: {case}"),
173        };
174        sync_observer::take_dir_syncs();
175
176        log.sync().unwrap();
177
178        assert2::assert!(sync_observer::take_dir_syncs() == vec![dir.path().to_path_buf()]);
179    }
180
181    #[test]
182    fn sync_flushes_sealed_and_active_segments_after_rollover() {
183        let dir = tempfile::tempdir().unwrap();
184        let mut log = rolling_test_log(dir.path());
185        log.append(&mut sample_batch(1)).unwrap();
186        log.sync().unwrap();
187        sync_observer::take_segment_flushes();
188
189        log.append(&mut sample_batch(1)).unwrap();
190        log.sync().unwrap();
191
192        // The rollover worker flushed offset 0 before its snapshot. This
193        // thread's explicit sync then flushes sealed and active segments.
194        assert2::assert!(sync_observer::take_segment_flushes() == vec![Offset(0), Offset(1)]);
195    }
196}