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}