1use core::fmt;
20use std::sync::Mutex;
21use std::time::{SystemTime, UNIX_EPOCH};
22
23use mongreldb_types::hlc::HlcTimestamp;
24use mongreldb_types::ids::TransactionId;
25
26use crate::commit_log::{
27 CommitLog, CommitReceipt, CommittedEntry, DurabilityLevel, ExecutionControl, LogError,
28 LogPosition, LogSnapshot,
29};
30use crate::envelope::CommandEnvelope;
31
32pub type TimestampSource = Box<dyn FnMut() -> HlcTimestamp + Send>;
37
38const SNAPSHOT_MAGIC: [u8; 8] = *b"MLOGSNAP";
39const SNAPSHOT_FORMAT_VERSION: u32 = 1;
40
41struct State {
42 entries: Vec<CommittedEntry>,
43 next_index: u64,
44 timestamp_source: TimestampSource,
45}
46
47pub struct InMemoryCommitLog {
49 state: Mutex<State>,
50}
51
52impl InMemoryCommitLog {
53 pub fn new() -> Self {
56 Self::with_timestamp_source(Box::new(system_time_micros))
57 }
58
59 pub fn with_timestamp_source(timestamp_source: TimestampSource) -> Self {
61 Self {
62 state: Mutex::new(State {
63 entries: Vec::new(),
64 next_index: 1,
65 timestamp_source,
66 }),
67 }
68 }
69}
70
71impl Default for InMemoryCommitLog {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77impl fmt::Debug for InMemoryCommitLog {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 let mut d = f.debug_struct("InMemoryCommitLog");
80 match self.state.lock() {
81 Ok(state) => d
82 .field("entries", &state.entries.len())
83 .field("next_index", &state.next_index)
84 .finish(),
85 Err(_) => d.finish_non_exhaustive(),
86 }
87 }
88}
89
90impl CommitLog for InMemoryCommitLog {
91 fn propose(
96 &self,
97 command: CommandEnvelope,
98 control: &ExecutionControl,
99 ) -> Result<CommitReceipt, LogError> {
100 control.check()?;
101 command.verify()?;
102 let mut state = self
103 .state
104 .lock()
105 .map_err(|_| LogError::Internal("in-memory commit log lock poisoned".to_owned()))?;
106 let position = LogPosition {
107 term: 0,
108 index: state.next_index,
109 };
110 state.next_index += 1;
111 let commit_ts = (state.timestamp_source)();
112 let command_id = command.command_id;
113 state.entries.push(CommittedEntry {
114 position,
115 commit_ts,
116 envelope: command,
117 });
118 Ok(CommitReceipt {
119 transaction_id: TransactionId::from_bytes(command_id),
120 commit_ts,
121 log_position: position,
122 durability: DurabilityLevel::GroupCommit,
123 })
124 }
125
126 fn read_committed(
127 &self,
128 after: LogPosition,
129 limit: usize,
130 ) -> Result<Vec<CommittedEntry>, LogError> {
131 let state = self
132 .state
133 .lock()
134 .map_err(|_| LogError::Internal("in-memory commit log lock poisoned".to_owned()))?;
135 Ok(state
136 .entries
137 .iter()
138 .filter(|entry| entry.position > after)
139 .take(limit)
140 .cloned()
141 .collect())
142 }
143
144 fn applied_position(&self) -> LogPosition {
145 self.state
146 .lock()
147 .expect("in-memory commit log lock poisoned")
148 .entries
149 .last()
150 .map_or(LogPosition::ZERO, |entry| entry.position)
151 }
152
153 fn create_snapshot(&self) -> Result<LogSnapshot, LogError> {
154 let state = self
155 .state
156 .lock()
157 .map_err(|_| LogError::Internal("in-memory commit log lock poisoned".to_owned()))?;
158 let (position, commit_ts) = state
159 .entries
160 .last()
161 .map_or((LogPosition::ZERO, HlcTimestamp::ZERO), |entry| {
162 (entry.position, entry.commit_ts)
163 });
164 Ok(LogSnapshot {
165 position,
166 commit_ts,
167 data: encode_snapshot(&state.entries),
168 })
169 }
170
171 fn install_snapshot(&self, snapshot: LogSnapshot) -> Result<(), LogError> {
172 let entries = decode_snapshot(&snapshot.data)?;
173 match entries.last() {
174 Some(last) if last.position != snapshot.position => {
175 return Err(LogError::Internal(
176 "in-memory snapshot position does not match its last entry".to_owned(),
177 ));
178 }
179 None if snapshot.position != LogPosition::ZERO => {
180 return Err(LogError::Internal(
181 "in-memory snapshot carries no entries but a nonzero position".to_owned(),
182 ));
183 }
184 _ => {}
185 }
186 let mut state = self
187 .state
188 .lock()
189 .map_err(|_| LogError::Internal("in-memory commit log lock poisoned".to_owned()))?;
190 state.entries = entries;
191 state.next_index = snapshot.position.index + 1;
192 Ok(())
193 }
194}
195
196fn system_time_micros() -> HlcTimestamp {
199 let physical_micros = SystemTime::now()
200 .duration_since(UNIX_EPOCH)
201 .map_or(0, |d| d.as_micros() as u64);
202 HlcTimestamp {
203 physical_micros,
204 logical: 0,
205 node_tiebreaker: 0,
206 }
207}
208
209fn encode_snapshot(entries: &[CommittedEntry]) -> Vec<u8> {
213 let mut out = Vec::new();
214 out.extend_from_slice(&SNAPSHOT_MAGIC);
215 out.extend_from_slice(&SNAPSHOT_FORMAT_VERSION.to_le_bytes());
216 out.extend_from_slice(&(entries.len() as u64).to_le_bytes());
217 for entry in entries {
218 out.extend_from_slice(&entry.position.term.to_le_bytes());
219 out.extend_from_slice(&entry.position.index.to_le_bytes());
220 out.extend_from_slice(&entry.commit_ts.physical_micros.to_le_bytes());
221 out.extend_from_slice(&entry.commit_ts.logical.to_le_bytes());
222 out.extend_from_slice(&entry.commit_ts.node_tiebreaker.to_le_bytes());
223 let envelope = entry.envelope.encode();
224 out.extend_from_slice(&(envelope.len() as u32).to_le_bytes());
225 out.extend_from_slice(&envelope);
226 }
227 out
228}
229
230fn malformed(reason: &str) -> LogError {
231 LogError::Internal(format!("malformed in-memory snapshot: {reason}"))
232}
233
234struct Cursor<'a> {
236 bytes: &'a [u8],
237 offset: usize,
238}
239
240impl<'a> Cursor<'a> {
241 fn take(&mut self, len: usize) -> Result<&'a [u8], LogError> {
242 let end = self
243 .offset
244 .checked_add(len)
245 .filter(|end| *end <= self.bytes.len())
246 .ok_or_else(|| malformed("truncated"))?;
247 let slice = &self.bytes[self.offset..end];
248 self.offset = end;
249 Ok(slice)
250 }
251
252 fn read_u32(&mut self) -> Result<u32, LogError> {
253 Ok(u32::from_le_bytes(
254 self.take(4)?.try_into().expect("slice len"),
255 ))
256 }
257
258 fn read_u64(&mut self) -> Result<u64, LogError> {
259 Ok(u64::from_le_bytes(
260 self.take(8)?.try_into().expect("slice len"),
261 ))
262 }
263
264 fn remaining(&self) -> usize {
265 self.bytes.len() - self.offset
266 }
267}
268
269fn decode_snapshot(data: &[u8]) -> Result<Vec<CommittedEntry>, LogError> {
272 let mut cursor = Cursor {
273 bytes: data,
274 offset: 0,
275 };
276 if cursor.take(SNAPSHOT_MAGIC.len())? != SNAPSHOT_MAGIC.as_slice() {
277 return Err(malformed("bad magic"));
278 }
279 let format_version = cursor.read_u32()?;
280 if format_version != SNAPSHOT_FORMAT_VERSION {
281 return Err(malformed("unsupported format version"));
282 }
283 let entry_count = cursor.read_u64()?;
284 let mut entries = Vec::with_capacity(entry_count.min(1024) as usize);
285 for _ in 0..entry_count {
286 let term = cursor.read_u64()?;
287 let index = cursor.read_u64()?;
288 let physical_micros = cursor.read_u64()?;
289 let logical = cursor.read_u32()?;
290 let node_tiebreaker = cursor.read_u32()?;
291 let envelope_len = cursor.read_u32()? as usize;
292 let envelope = CommandEnvelope::decode(cursor.take(envelope_len)?)?;
293 entries.push(CommittedEntry {
294 position: LogPosition { term, index },
295 commit_ts: HlcTimestamp {
296 physical_micros,
297 logical,
298 node_tiebreaker,
299 },
300 envelope,
301 });
302 }
303 if cursor.remaining() != 0 {
304 return Err(malformed("trailing bytes"));
305 }
306 Ok(entries)
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 fn envelope(id: u8) -> CommandEnvelope {
314 CommandEnvelope::new(1, [id; 16], vec![id; 8])
315 }
316
317 #[test]
318 fn empty_log_starts_at_zero() {
319 let log = InMemoryCommitLog::new();
320 assert_eq!(log.applied_position(), LogPosition::ZERO);
321 assert!(log
322 .read_committed(LogPosition::ZERO, 10)
323 .unwrap()
324 .is_empty());
325 let snapshot = log.create_snapshot().unwrap();
326 assert_eq!(snapshot.position, LogPosition::ZERO);
327 assert_eq!(snapshot.commit_ts, HlcTimestamp::ZERO);
328 }
329
330 #[test]
331 fn malformed_snapshot_data_is_rejected() {
332 let log = InMemoryCommitLog::new();
333 let snapshot = LogSnapshot {
334 position: LogPosition::ZERO,
335 commit_ts: HlcTimestamp::ZERO,
336 data: b"garbage".to_vec(),
337 };
338 assert!(matches!(
339 log.install_snapshot(snapshot),
340 Err(LogError::Internal(_))
341 ));
342 }
343
344 #[test]
345 fn snapshot_position_mismatch_is_rejected() {
346 let source = InMemoryCommitLog::new();
347 source
348 .propose(envelope(1), &ExecutionControl::default())
349 .unwrap();
350 let mut snapshot = source.create_snapshot().unwrap();
351 snapshot.position = LogPosition { term: 0, index: 99 };
352 let log = InMemoryCommitLog::new();
353 assert!(matches!(
354 log.install_snapshot(snapshot),
355 Err(LogError::Internal(_))
356 ));
357 assert_eq!(log.applied_position(), LogPosition::ZERO);
358 }
359}