Skip to main content

rushdb_storage/
log_export.rs

1//! Durable-log export: crash-log dumps and the streaming-replication
2//! follower read path. Both derive the committed
3//! command-batch sequence from the WAL's embedded-command envelope
4//! rather than a separate journal.
5
6use super::{prefix_range, wal, ReadLimits, ScanProjection, StorageError, Store, LOG_PREFIX};
7
8impl Store {
9    /// Durable-log export for tests/tools/backups.
10    pub fn log_bytes(&self) -> Vec<Vec<u8>> {
11        self.take_log_for_crash()
12    }
13
14    /// Export the decoded commit-batch payloads of the committed command log,
15    /// ordered by LSN.
16    ///
17    /// Lock poisoning fences the store until reopen.
18    pub fn take_log_for_crash(&self) -> Vec<Vec<u8>> {
19        self.try_take_log_for_crash().unwrap_or_default()
20    }
21
22    /// Fallible crash-log export for recovery and archive callers. Unlike
23    /// [`Store::take_log_for_crash`], this path never converts a scan,
24    /// framing, or WAL decode failure into an empty/partial log.
25    ///
26    /// A durable store derives this from the WAL's embedded-command envelope
27    ///; an in-memory store has no WAL file and keeps the row-based
28    /// journal ([`log_key`], test mode only).
29    ///
30    /// # Errors
31    /// Returns [`StorageError::CorruptLog`] for malformed WAL rows and the
32    /// underlying storage error for failed backend scans.
33    ///
34    /// Lock poisoning fences the store until reopen.
35    pub fn try_take_log_for_crash(&self) -> Result<Vec<Vec<u8>>, StorageError> {
36        self.check_health()?;
37        let _guard = self.health.guard(self.publish_lock.lock())?;
38        if self.is_durable() {
39            let records = {
40                let guard = self.health.guard(self.log.lock())?;
41                match guard.as_ref() {
42                    Some(log) => log.read_from(1).map_err(|error| {
43                        StorageError::Durability(format!("read WAL for crash log: {error}"))
44                    })?,
45                    None => Vec::new(),
46                }
47            };
48            let mut out = Vec::with_capacity(records.len());
49            // A chained commit (large-transaction support) spans
50            // several physical records sharing one LSN; reassemble before
51            // extracting its command, the same way WAL replay does.
52            let mut assembler = wal::ChainAssembler::default();
53            for (lsn, raw) in records {
54                let (header, body) = match assembler
55                    .feed(&raw, 0)
56                    .map_err(|_| StorageError::CorruptLog)?
57                {
58                    wal::ChainFeed::Pending => continue,
59                    wal::ChainFeed::Complete(header, body) => (header, body),
60                };
61                if header.lsn != lsn {
62                    return Err(StorageError::CorruptLog);
63                }
64                let (_, _ops, command) = wal::decode_logical_body(header, &body)
65                    .map_err(|_| StorageError::CorruptLog)?;
66                if let Some(batch) = command {
67                    out.push(rushdb_types::command_codec::encode(&batch));
68                }
69            }
70            return Ok(out);
71        }
72        // Every store has at least one backend, so read the log rows
73        // natively from each shard. Multi-shard: shards are scanned in shard
74        // order, so the union must be re-sorted by the LSN carried in the WAL
75        // frame's header.
76        let mut out: Vec<(u64, Vec<u8>)> = Vec::new();
77        for backend in self.all_backends() {
78            let view = backend.read_view()?;
79            let (ls, le) = prefix_range(LOG_PREFIX);
80            let rows =
81                view.current_range(&ls, &le, ReadLimits::default(), ScanProjection::KeyValue)?;
82            for row in rows {
83                let (_, value) = row?;
84                let (header, payload) =
85                    wal::decode_wal_record(&value).map_err(|_| StorageError::CorruptLog)?;
86                out.push((header.lsn, payload.to_vec()));
87            }
88        }
89        out.sort_by_key(|(lsn, _)| *lsn);
90        Ok(out.into_iter().map(|(_, payload)| payload).collect())
91    }
92
93    /// Durable-log export of commit-batch payloads from `from_lsn` onward
94    /// (each paired with its LSN), for a streaming-replication follower
95    /// to replay via [`Store::apply_replicated_batch_with_payload`].
96    /// Empty for a non-durable store. Takes the same `publish_lock` as
97    /// [`Store::try_take_log_for_crash`] (briefly contends with commits —
98    /// poll at a bounded interval, not a tight loop).
99    ///
100    /// # Errors
101    /// Returns [`StorageError::CorruptLog`] for a malformed WAL record and
102    /// the underlying storage error for a failed WAL read.
103    pub fn try_log_commands_from(
104        &self,
105        from_lsn: u64,
106    ) -> Result<Vec<(u64, Vec<u8>)>, StorageError> {
107        self.check_health()?;
108        if !self.is_durable() {
109            return Ok(Vec::new());
110        }
111        let _guard = self.health.guard(self.publish_lock.lock())?;
112        let records = {
113            let guard = self.health.guard(self.log.lock())?;
114            match guard.as_ref() {
115                Some(log) => log.read_from(from_lsn).map_err(|error| {
116                    StorageError::Durability(format!("read WAL for replication: {error}"))
117                })?,
118                None => Vec::new(),
119            }
120        };
121        let mut out = Vec::with_capacity(records.len());
122        // Same chain reassembly as `try_take_log_for_crash`: a replicated
123        // commit too big for one WAL record arrives here as several
124        // physical records sharing one LSN, and must be reconstructed
125        // before its command is extracted and shipped to the follower —
126        // which otherwise receives nothing for that LSN at all, or (worse)
127        // a `CHAIN_CONTINUE` fragment's partial bytes misread as a complete
128        // command.
129        let mut assembler = wal::ChainAssembler::default();
130        for (lsn, raw) in records {
131            let (header, body) = match assembler
132                .feed(&raw, 0)
133                .map_err(|_| StorageError::CorruptLog)?
134            {
135                wal::ChainFeed::Pending => continue,
136                wal::ChainFeed::Complete(header, body) => (header, body),
137            };
138            if header.lsn != lsn {
139                return Err(StorageError::CorruptLog);
140            }
141            let (_, _ops, command) =
142                wal::decode_logical_body(header, &body).map_err(|_| StorageError::CorruptLog)?;
143            if let Some(batch) = command {
144                out.push((lsn, rushdb_types::command_codec::encode(&batch)));
145            }
146        }
147        Ok(out)
148    }
149}