1use corium_core::{
4 Datom, EntityId,
5 encoding::{decode_value, encode_value},
6};
7use std::{
8 collections::HashMap,
9 fs::{self, File, OpenOptions},
10 io::{self, Read, Write},
11 path::{Path, PathBuf},
12 sync::{Arc, Mutex, RwLock},
13};
14use thiserror::Error;
15
16#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct TxRecord {
19 pub t: u64,
21 pub tx_instant: i64,
23 pub datoms: Vec<Datom>,
25}
26
27#[derive(Debug, Error)]
29pub enum LogError {
30 #[error("log I/O failed: {0}")]
32 Io(#[from] io::Error),
33 #[error("corrupt transaction log")]
35 Corrupt,
36 #[error("native transaction log store failed: {0}")]
38 Native(String),
39}
40
41pub trait TransactionLog: Send + Sync {
43 fn append(&self, record: &TxRecord) -> Result<(), LogError>;
48 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError>;
53 fn replay(&self) -> Result<Vec<TxRecord>, LogError> {
58 self.tx_range(0, None)
59 }
60}
61
62#[derive(Clone, Default)]
64pub struct MemoryLog(Arc<RwLock<Vec<TxRecord>>>);
65impl TransactionLog for MemoryLog {
66 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
67 let mut records = self.0.write().expect("poisoned log lock");
68 if records.last().map_or(1, |r| r.t + 1) != record.t {
69 return Err(LogError::Corrupt);
70 }
71 records.push(record.clone());
72 Ok(())
73 }
74 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
75 Ok(self
76 .0
77 .read()
78 .expect("poisoned log lock")
79 .iter()
80 .filter(|r| r.t >= start && end.is_none_or(|e| r.t < e))
81 .cloned()
82 .collect())
83 }
84}
85
86pub struct FileLog {
92 path: PathBuf,
93 next_t: RwLock<u64>,
94}
95impl FileLog {
96 pub fn open(path: impl AsRef<Path>) -> Result<Self, LogError> {
102 let path = path.as_ref().to_path_buf();
103 if let Some(parent) = path.parent() {
104 fs::create_dir_all(parent)?;
105 }
106 OpenOptions::new().create(true).append(true).open(&path)?;
107 let (records, durable_len) = read_records(&path)?;
108 if fs::metadata(&path)?.len() > durable_len {
109 let file = OpenOptions::new().write(true).open(&path)?;
110 file.set_len(durable_len)?;
111 file.sync_all()?;
112 }
113 Ok(Self {
114 path,
115 next_t: RwLock::new(records.last().map_or(1, |r| r.t + 1)),
116 })
117 }
118}
119impl TransactionLog for FileLog {
120 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
121 let mut next_t = self.next_t.write().expect("poisoned log lock");
122 if *next_t != record.t {
123 return Err(LogError::Corrupt);
124 }
125 let payload = encode_record(record);
126 let mut file = OpenOptions::new().append(true).open(&self.path)?;
127 file.write_all(
128 &u64::try_from(payload.len())
129 .map_err(|_| LogError::Corrupt)?
130 .to_be_bytes(),
131 )?;
132 file.write_all(&payload)?;
133 file.sync_all()?;
134 *next_t += 1;
135 Ok(())
136 }
137 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
138 let _guard = self.next_t.read().expect("poisoned log lock");
139 Ok(read_records(&self.path)?
140 .0
141 .into_iter()
142 .filter(|r| r.t >= start && end.is_none_or(|e| r.t < e))
143 .collect())
144 }
145}
146
147pub struct VersionedLog {
159 dir: PathBuf,
160 name: String,
161 write_path: PathBuf,
162 next_t: RwLock<u64>,
163}
164
165impl VersionedLog {
166 pub fn open(dir: impl AsRef<Path>, name: &str, write_version: u64) -> Result<Self, LogError> {
174 let dir = dir.as_ref().to_path_buf();
175 fs::create_dir_all(&dir)?;
176 let write_path = version_path(&dir, name, write_version);
177 OpenOptions::new()
178 .create(true)
179 .append(true)
180 .open(&write_path)?;
181 let (_, durable_len) = read_records(&write_path)?;
182 if fs::metadata(&write_path)?.len() > durable_len {
183 let file = OpenOptions::new().write(true).open(&write_path)?;
184 file.set_len(durable_len)?;
185 file.sync_all()?;
186 }
187 let records = read_merged(&dir, name)?;
188 Ok(Self {
189 dir,
190 name: name.to_owned(),
191 write_path,
192 next_t: RwLock::new(records.last().map_or(1, |r| r.t + 1)),
193 })
194 }
195
196 pub fn open_read_only(dir: impl AsRef<Path>, name: &str) -> Result<Self, LogError> {
202 let dir = dir.as_ref().to_path_buf();
203 Ok(Self {
204 write_path: PathBuf::new(),
205 name: name.to_owned(),
206 next_t: RwLock::new(u64::MAX),
207 dir,
208 })
209 }
210
211 #[must_use]
213 pub fn exists(dir: impl AsRef<Path>, name: &str) -> bool {
214 !version_files(dir.as_ref(), name).is_empty()
215 }
216
217 pub fn delete_all(dir: impl AsRef<Path>, name: &str) -> Result<(), LogError> {
222 for (_, path) in version_files(dir.as_ref(), name) {
223 match fs::remove_file(&path) {
224 Ok(()) => {}
225 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
226 Err(error) => return Err(error.into()),
227 }
228 }
229 Ok(())
230 }
231}
232
233impl TransactionLog for VersionedLog {
234 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
235 let mut next_t = self.next_t.write().expect("poisoned log lock");
236 if *next_t != record.t {
237 return Err(LogError::Corrupt);
238 }
239 let payload = encode_record(record);
240 let mut file = OpenOptions::new().append(true).open(&self.write_path)?;
241 file.write_all(
242 &u64::try_from(payload.len())
243 .map_err(|_| LogError::Corrupt)?
244 .to_be_bytes(),
245 )?;
246 file.write_all(&payload)?;
247 file.sync_all()?;
248 *next_t += 1;
249 Ok(())
250 }
251
252 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
253 let _guard = self.next_t.read().expect("poisoned log lock");
254 Ok(read_merged(&self.dir, &self.name)?
255 .into_iter()
256 .filter(|r| r.t >= start && end.is_none_or(|e| r.t < e))
257 .collect())
258 }
259}
260
261fn merge_versions(mut per_version: Vec<Vec<TxRecord>>) -> Vec<TxRecord> {
266 let mut cutoff = u64::MAX;
267 for records in per_version.iter_mut().rev() {
268 let first = records.first().map(|r| r.t);
269 records.retain(|r| r.t < cutoff);
270 if let Some(first) = first {
271 cutoff = cutoff.min(first);
272 }
273 }
274 per_version.into_iter().flatten().collect()
275}
276
277pub trait NativeLogStorage: Send + Sync {
282 fn read_version(&self, name: &str, version: u64) -> Result<Option<Vec<u8>>, LogError>;
288 fn cas_version(
294 &self,
295 name: &str,
296 version: u64,
297 expected: Option<&[u8]>,
298 new: &[u8],
299 ) -> Result<(), LogError>;
300 fn versions(&self, name: &str) -> Result<Vec<u64>, LogError>;
306 fn delete_versions(&self, name: &str) -> Result<(), LogError>;
311}
312
313pub struct NativeVersionedLog<S: ?Sized> {
315 storage: Arc<S>,
316 name: String,
317 write_version: u64,
318 next_t: RwLock<u64>,
319}
320
321impl<S: NativeLogStorage + ?Sized + 'static> NativeVersionedLog<S> {
322 pub fn open(storage: Arc<S>, name: &str, write_version: u64) -> Result<Self, LogError> {
327 let records = read_native_merged(storage.as_ref(), name)?;
328 Ok(Self {
329 storage,
330 name: name.to_owned(),
331 write_version,
332 next_t: RwLock::new(records.last().map_or(1, |r| r.t + 1)),
333 })
334 }
335}
336
337impl<S: NativeLogStorage + ?Sized + 'static> TransactionLog for NativeVersionedLog<S> {
338 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
339 let mut next_t = self.next_t.write().expect("poisoned log lock");
340 if *next_t != record.t {
341 return Err(LogError::Corrupt);
342 }
343 let current = self.storage.read_version(&self.name, self.write_version)?;
344 let current_bytes = current.as_deref().unwrap_or_default();
345 let existing = decode_framed_records(current_bytes)?;
346 if existing.last().map_or(*next_t, |r| r.t + 1) != record.t {
347 return Err(LogError::Corrupt);
348 }
349 let mut new = current_bytes.to_vec();
350 append_framed_record(&mut new, record)?;
351 self.storage
352 .cas_version(&self.name, self.write_version, current.as_deref(), &new)?;
353 *next_t += 1;
354 Ok(())
355 }
356
357 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
358 let _guard = self.next_t.read().expect("poisoned log lock");
359 Ok(read_native_merged(self.storage.as_ref(), &self.name)?
360 .into_iter()
361 .filter(|r| r.t >= start && end.is_none_or(|e| r.t < e))
362 .collect())
363 }
364}
365
366fn read_native_merged<S: NativeLogStorage + ?Sized>(
367 storage: &S,
368 name: &str,
369) -> Result<Vec<TxRecord>, LogError> {
370 let mut per_version = Vec::new();
371 for version in storage.versions(name)? {
372 let bytes = storage.read_version(name, version)?.unwrap_or_default();
373 per_version.push(decode_framed_records(&bytes)?);
374 }
375 let merged = merge_versions(per_version);
376 for pair in merged.windows(2) {
377 if pair[1].t != pair[0].t + 1 {
378 return Err(LogError::Corrupt);
379 }
380 }
381 Ok(merged)
382}
383
384type VersionedRecords = Arc<Mutex<Vec<(u64, TxRecord)>>>;
387
388#[derive(Clone, Default)]
394pub struct MemLogRegistry {
395 logs: Arc<Mutex<HashMap<String, VersionedRecords>>>,
396}
397
398impl MemLogRegistry {
399 #[must_use]
401 pub fn new() -> Self {
402 Self::default()
403 }
404
405 fn entry(&self, name: &str) -> VersionedRecords {
406 Arc::clone(
407 self.logs
408 .lock()
409 .unwrap_or_else(std::sync::PoisonError::into_inner)
410 .entry(name.to_owned())
411 .or_default(),
412 )
413 }
414
415 #[must_use]
418 pub fn open(&self, name: &str, write_version: u64) -> MemVersionedLog {
419 let records = self.entry(name);
420 let next_t = {
421 let guard = records
422 .lock()
423 .unwrap_or_else(std::sync::PoisonError::into_inner);
424 MemVersionedLog::merged(&guard)
425 .last()
426 .map_or(1, |r| r.t + 1)
427 };
428 MemVersionedLog {
429 records,
430 write_version,
431 next_t: Mutex::new(next_t),
432 }
433 }
434
435 #[must_use]
437 pub fn exists(&self, name: &str) -> bool {
438 self.logs
439 .lock()
440 .unwrap_or_else(std::sync::PoisonError::into_inner)
441 .get(name)
442 .is_some_and(|entry| {
443 !entry
444 .lock()
445 .unwrap_or_else(std::sync::PoisonError::into_inner)
446 .is_empty()
447 })
448 }
449
450 pub fn delete_all(&self, name: &str) {
452 self.logs
453 .lock()
454 .unwrap_or_else(std::sync::PoisonError::into_inner)
455 .remove(name);
456 }
457}
458
459pub struct MemVersionedLog {
463 records: VersionedRecords,
464 write_version: u64,
465 next_t: Mutex<u64>,
469}
470
471impl MemVersionedLog {
472 fn merged(records: &[(u64, TxRecord)]) -> Vec<TxRecord> {
473 let mut versions: Vec<u64> = records.iter().map(|(version, _)| *version).collect();
474 versions.sort_unstable();
475 versions.dedup();
476 let per_version = versions
477 .into_iter()
478 .map(|version| {
479 records
480 .iter()
481 .filter(|(record_version, _)| *record_version == version)
482 .map(|(_, record)| record.clone())
483 .collect::<Vec<_>>()
484 })
485 .collect();
486 merge_versions(per_version)
487 }
488}
489
490impl TransactionLog for MemVersionedLog {
491 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
492 let mut next_t = self
493 .next_t
494 .lock()
495 .unwrap_or_else(std::sync::PoisonError::into_inner);
496 if *next_t != record.t {
497 return Err(LogError::Corrupt);
498 }
499 self.records
500 .lock()
501 .unwrap_or_else(std::sync::PoisonError::into_inner)
502 .push((self.write_version, record.clone()));
503 *next_t += 1;
504 Ok(())
505 }
506
507 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
508 let records = self
509 .records
510 .lock()
511 .unwrap_or_else(std::sync::PoisonError::into_inner);
512 Ok(Self::merged(&records)
513 .into_iter()
514 .filter(|r| r.t >= start && end.is_none_or(|e| r.t < e))
515 .collect())
516 }
517}
518
519fn version_path(dir: &Path, name: &str, version: u64) -> PathBuf {
520 if version == 0 {
521 dir.join(format!("{name}.log"))
522 } else {
523 dir.join(format!("{name}.v{version}.log"))
524 }
525}
526
527fn version_files(dir: &Path, name: &str) -> Vec<(u64, PathBuf)> {
529 let mut files = Vec::new();
530 let legacy = version_path(dir, name, 0);
531 if legacy.is_file() {
532 files.push((0, legacy));
533 }
534 let prefix = format!("{name}.v");
535 if let Ok(entries) = fs::read_dir(dir) {
536 for entry in entries.flatten() {
537 let file_name = entry.file_name();
538 let Some(text) = file_name.to_str() else {
539 continue;
540 };
541 if let Some(version) = text
542 .strip_prefix(&prefix)
543 .and_then(|rest| rest.strip_suffix(".log"))
544 .and_then(|v| v.parse::<u64>().ok())
545 && version > 0
546 {
547 files.push((version, entry.path()));
548 }
549 }
550 }
551 files.sort_by_key(|(version, _)| *version);
552 files
553}
554
555fn read_merged(dir: &Path, name: &str) -> Result<Vec<TxRecord>, LogError> {
558 let files = version_files(dir, name);
559 let mut per_file: Vec<Vec<TxRecord>> = Vec::with_capacity(files.len());
560 for (_, path) in &files {
561 per_file.push(read_records(path)?.0);
562 }
563 let merged = merge_versions(per_file);
568 for pair in merged.windows(2) {
569 if pair[1].t != pair[0].t + 1 {
570 return Err(LogError::Corrupt);
571 }
572 }
573 Ok(merged)
574}
575
576fn encode_record(record: &TxRecord) -> Vec<u8> {
577 let mut out = Vec::new();
578 out.extend_from_slice(&record.t.to_be_bytes());
579 out.extend_from_slice(&record.tx_instant.to_be_bytes());
580 out.extend_from_slice(&(record.datoms.len() as u64).to_be_bytes());
581 for d in &record.datoms {
582 out.extend_from_slice(&d.e.raw().to_be_bytes());
583 out.extend_from_slice(&d.a.raw().to_be_bytes());
584 out.extend_from_slice(&d.tx.raw().to_be_bytes());
585 out.push(u8::from(d.added));
586 let v = encode_value(&d.v);
587 out.extend_from_slice(&(v.len() as u64).to_be_bytes());
588 out.extend_from_slice(&v);
589 }
590 out
591}
592fn decode_record(mut bytes: &[u8]) -> Result<TxRecord, LogError> {
593 fn take<'a>(bytes: &mut &'a [u8], n: usize) -> Result<&'a [u8], LogError> {
594 let value = bytes.get(..n).ok_or(LogError::Corrupt)?;
595 *bytes = &bytes[n..];
596 Ok(value)
597 }
598 fn u64_be(bytes: &mut &[u8]) -> Result<u64, LogError> {
599 Ok(u64::from_be_bytes(
600 take(bytes, 8)?.try_into().map_err(|_| LogError::Corrupt)?,
601 ))
602 }
603 let t = u64_be(&mut bytes)?;
604 let tx_instant = i64::from_be_bytes(
605 take(&mut bytes, 8)?
606 .try_into()
607 .map_err(|_| LogError::Corrupt)?,
608 );
609 let count = u64_be(&mut bytes)?;
610 let mut datoms = Vec::new();
611 for _ in 0..count {
612 let e = EntityId::from_raw(u64_be(&mut bytes)?);
613 let a = EntityId::from_raw(u64_be(&mut bytes)?);
614 let tx = EntityId::from_raw(u64_be(&mut bytes)?);
615 let added = take(&mut bytes, 1)?[0] != 0;
616 let len = usize::try_from(u64_be(&mut bytes)?).map_err(|_| LogError::Corrupt)?;
617 let raw = take(&mut bytes, len)?;
618 let (v, used) = decode_value(raw).map_err(|_| LogError::Corrupt)?;
619 if used != len {
620 return Err(LogError::Corrupt);
621 }
622 datoms.push(Datom { e, a, v, tx, added });
623 }
624 if !bytes.is_empty() {
625 return Err(LogError::Corrupt);
626 }
627 Ok(TxRecord {
628 t,
629 tx_instant,
630 datoms,
631 })
632}
633fn read_records(path: &Path) -> Result<(Vec<TxRecord>, u64), LogError> {
639 let mut file = File::open(path)?;
640 let mut records = Vec::new();
641 let mut durable_len = 0_u64;
642 loop {
643 let mut len = [0; 8];
644 match file.read_exact(&mut len) {
645 Ok(()) => {}
646 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
647 Err(e) => return Err(e.into()),
648 }
649 let len = usize::try_from(u64::from_be_bytes(len)).map_err(|_| LogError::Corrupt)?;
650 let mut payload = vec![0; len];
651 match file.read_exact(&mut payload) {
652 Ok(()) => {}
653 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
654 Err(e) => return Err(e.into()),
655 }
656 records.push(decode_record(&payload)?);
657 durable_len += 8 + len as u64;
658 }
659 Ok((records, durable_len))
660}
661
662pub fn append_framed_record(out: &mut Vec<u8>, record: &TxRecord) -> Result<(), LogError> {
667 let payload = encode_record(record);
668 out.extend_from_slice(
669 &u64::try_from(payload.len())
670 .map_err(|_| LogError::Corrupt)?
671 .to_be_bytes(),
672 );
673 out.extend_from_slice(&payload);
674 Ok(())
675}
676
677pub fn decode_framed_records(mut bytes: &[u8]) -> Result<Vec<TxRecord>, LogError> {
686 let mut records = Vec::new();
687 while !bytes.is_empty() {
688 if bytes.len() < 8 {
689 return Err(LogError::Corrupt);
690 }
691 let len = usize::try_from(u64::from_be_bytes(
692 bytes[..8].try_into().map_err(|_| LogError::Corrupt)?,
693 ))
694 .map_err(|_| LogError::Corrupt)?;
695 bytes = &bytes[8..];
696 let payload = bytes.get(..len).ok_or(LogError::Corrupt)?;
697 records.push(decode_record(payload)?);
698 bytes = &bytes[len..];
699 }
700 Ok(records)
701}