1use async_trait::async_trait;
4use corium_core::{
5 Datom, EntityId,
6 encoding::{decode_value, encode_value},
7};
8use std::{
9 collections::HashMap,
10 fs::{self, File, OpenOptions},
11 io::{self, Read, Write},
12 path::{Path, PathBuf},
13 sync::{Arc, Mutex, RwLock},
14};
15use thiserror::Error;
16
17#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct TxRecord {
20 pub t: u64,
22 pub tx_instant: i64,
24 pub datoms: Vec<Datom>,
26}
27
28#[derive(Debug, Error)]
30pub enum LogError {
31 #[error("log I/O failed: {0}")]
33 Io(#[from] io::Error),
34 #[error("corrupt transaction log")]
36 Corrupt,
37 #[error("native transaction log store failed: {0}")]
39 Native(String),
40 #[error("this transaction log requires asynchronous access")]
42 AsyncOnly,
43}
44
45#[async_trait]
47pub trait TransactionLog: Send + Sync {
48 fn append(&self, record: &TxRecord) -> Result<(), LogError>;
53 async fn append_async(&self, record: &TxRecord) -> Result<(), LogError> {
60 self.append(record)
61 }
62 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError>;
67 async fn tx_range_async(
72 &self,
73 start: u64,
74 end: Option<u64>,
75 ) -> Result<Vec<TxRecord>, LogError> {
76 self.tx_range(start, end)
77 }
78 fn replay(&self) -> Result<Vec<TxRecord>, LogError> {
83 self.tx_range(0, None)
84 }
85 async fn replay_async(&self) -> Result<Vec<TxRecord>, LogError> {
90 self.tx_range_async(0, None).await
91 }
92}
93
94#[derive(Clone, Default)]
96pub struct MemoryLog(Arc<RwLock<Vec<TxRecord>>>);
97impl TransactionLog for MemoryLog {
98 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
99 let mut records = self.0.write().expect("poisoned log lock");
100 if records.last().map_or(1, |r| r.t + 1) != record.t {
101 return Err(LogError::Corrupt);
102 }
103 records.push(record.clone());
104 Ok(())
105 }
106 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
107 Ok(self
108 .0
109 .read()
110 .expect("poisoned log lock")
111 .iter()
112 .filter(|r| r.t >= start && end.is_none_or(|e| r.t < e))
113 .cloned()
114 .collect())
115 }
116}
117
118pub struct FileLog {
124 path: PathBuf,
125 next_t: RwLock<u64>,
126}
127impl FileLog {
128 pub fn open(path: impl AsRef<Path>) -> Result<Self, LogError> {
134 let path = path.as_ref().to_path_buf();
135 if let Some(parent) = path.parent() {
136 fs::create_dir_all(parent)?;
137 }
138 OpenOptions::new().create(true).append(true).open(&path)?;
139 let (records, durable_len) = read_records(&path)?;
140 if fs::metadata(&path)?.len() > durable_len {
141 let file = OpenOptions::new().write(true).open(&path)?;
142 file.set_len(durable_len)?;
143 file.sync_all()?;
144 }
145 Ok(Self {
146 path,
147 next_t: RwLock::new(records.last().map_or(1, |r| r.t + 1)),
148 })
149 }
150}
151impl TransactionLog for FileLog {
152 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
153 let mut next_t = self.next_t.write().expect("poisoned log lock");
154 if *next_t != record.t {
155 return Err(LogError::Corrupt);
156 }
157 let payload = encode_record(record);
158 let mut file = OpenOptions::new().append(true).open(&self.path)?;
159 file.write_all(
160 &u64::try_from(payload.len())
161 .map_err(|_| LogError::Corrupt)?
162 .to_be_bytes(),
163 )?;
164 file.write_all(&payload)?;
165 file.sync_all()?;
166 *next_t += 1;
167 Ok(())
168 }
169 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
170 let _guard = self.next_t.read().expect("poisoned log lock");
171 Ok(read_records(&self.path)?
172 .0
173 .into_iter()
174 .filter(|r| r.t >= start && end.is_none_or(|e| r.t < e))
175 .collect())
176 }
177}
178
179pub struct VersionedLog {
191 dir: PathBuf,
192 name: String,
193 write_path: PathBuf,
194 next_t: RwLock<u64>,
195}
196
197impl VersionedLog {
198 pub fn open(dir: impl AsRef<Path>, name: &str, write_version: u64) -> Result<Self, LogError> {
206 let dir = dir.as_ref().to_path_buf();
207 fs::create_dir_all(&dir)?;
208 let write_path = version_path(&dir, name, write_version);
209 OpenOptions::new()
210 .create(true)
211 .append(true)
212 .open(&write_path)?;
213 let (_, durable_len) = read_records(&write_path)?;
214 if fs::metadata(&write_path)?.len() > durable_len {
215 let file = OpenOptions::new().write(true).open(&write_path)?;
216 file.set_len(durable_len)?;
217 file.sync_all()?;
218 }
219 let records = read_merged(&dir, name)?;
220 Ok(Self {
221 dir,
222 name: name.to_owned(),
223 write_path,
224 next_t: RwLock::new(records.last().map_or(1, |r| r.t + 1)),
225 })
226 }
227
228 pub fn open_read_only(dir: impl AsRef<Path>, name: &str) -> Result<Self, LogError> {
234 let dir = dir.as_ref().to_path_buf();
235 Ok(Self {
236 write_path: PathBuf::new(),
237 name: name.to_owned(),
238 next_t: RwLock::new(u64::MAX),
239 dir,
240 })
241 }
242
243 #[must_use]
245 pub fn exists(dir: impl AsRef<Path>, name: &str) -> bool {
246 !version_files(dir.as_ref(), name).is_empty()
247 }
248
249 pub fn delete_all(dir: impl AsRef<Path>, name: &str) -> Result<(), LogError> {
254 for (_, path) in version_files(dir.as_ref(), name) {
255 match fs::remove_file(&path) {
256 Ok(()) => {}
257 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
258 Err(error) => return Err(error.into()),
259 }
260 }
261 Ok(())
262 }
263}
264
265impl TransactionLog for VersionedLog {
266 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
267 let mut next_t = self.next_t.write().expect("poisoned log lock");
268 if *next_t != record.t {
269 return Err(LogError::Corrupt);
270 }
271 let payload = encode_record(record);
272 let mut file = OpenOptions::new().append(true).open(&self.write_path)?;
273 file.write_all(
274 &u64::try_from(payload.len())
275 .map_err(|_| LogError::Corrupt)?
276 .to_be_bytes(),
277 )?;
278 file.write_all(&payload)?;
279 file.sync_all()?;
280 *next_t += 1;
281 Ok(())
282 }
283
284 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
285 let _guard = self.next_t.read().expect("poisoned log lock");
286 Ok(read_merged(&self.dir, &self.name)?
287 .into_iter()
288 .filter(|r| r.t >= start && end.is_none_or(|e| r.t < e))
289 .collect())
290 }
291}
292
293fn merge_versions(mut per_version: Vec<Vec<TxRecord>>) -> Vec<TxRecord> {
298 let mut cutoff = u64::MAX;
299 for records in per_version.iter_mut().rev() {
300 let first = records.first().map(|r| r.t);
301 records.retain(|r| r.t < cutoff);
302 if let Some(first) = first {
303 cutoff = cutoff.min(first);
304 }
305 }
306 per_version.into_iter().flatten().collect()
307}
308
309pub(crate) const LOG_CHUNK_MAX_BYTES: usize = 256 * 1024;
314
315#[async_trait]
324pub trait NativeLogStorage: Send + Sync {
325 async fn read_chunk(
331 &self,
332 name: &str,
333 version: u64,
334 chunk: u64,
335 ) -> Result<Option<Vec<u8>>, LogError>;
336 async fn cas_chunk(
343 &self,
344 name: &str,
345 version: u64,
346 chunk: u64,
347 expected: Option<&[u8]>,
348 new: &[u8],
349 ) -> Result<(), LogError>;
350 async fn list_chunks(&self, name: &str) -> Result<Vec<(u64, u64)>, LogError>;
356 async fn delete_all(&self, name: &str) -> Result<(), LogError>;
361}
362
363struct WriteState {
377 next_t: u64,
379 chunk: u64,
381 bytes: Vec<u8>,
384 exists: bool,
388}
389
390pub struct NativeVersionedLog<S: ?Sized> {
392 storage: Arc<S>,
393 name: String,
394 write_version: u64,
395 write: tokio::sync::Mutex<WriteState>,
396}
397
398impl<S: NativeLogStorage + ?Sized + 'static> NativeVersionedLog<S> {
399 pub async fn open(storage: Arc<S>, name: &str, write_version: u64) -> Result<Self, LogError> {
404 let records = read_native_merged(storage.as_ref(), name).await?;
407 let next_t = records.last().map_or(1, |r| r.t + 1);
408 let chunk = storage
412 .list_chunks(name)
413 .await?
414 .into_iter()
415 .filter_map(|(version, chunk)| (version == write_version).then_some(chunk))
416 .max()
417 .unwrap_or(0);
418 let current = storage.read_chunk(name, write_version, chunk).await?;
419 let exists = current.is_some();
420 let bytes = current.unwrap_or_default();
421 Ok(Self {
422 storage,
423 name: name.to_owned(),
424 write_version,
425 write: tokio::sync::Mutex::new(WriteState {
426 next_t,
427 chunk,
428 bytes,
429 exists,
430 }),
431 })
432 }
433}
434
435#[async_trait]
436impl<S: NativeLogStorage + ?Sized + 'static> TransactionLog for NativeVersionedLog<S> {
437 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
438 let _ = record;
439 Err(LogError::AsyncOnly)
440 }
441
442 async fn append_async(&self, record: &TxRecord) -> Result<(), LogError> {
443 let mut write = self.write.lock().await;
444 if write.next_t != record.t {
445 return Err(LogError::Corrupt);
446 }
447 let roll = write.exists && write.bytes.len() >= LOG_CHUNK_MAX_BYTES;
451 let chunk = write.chunk + u64::from(roll);
452 let exists = write.exists && !roll;
453 let mut candidate = if roll {
454 Vec::new()
455 } else {
456 write.bytes.clone()
457 };
458 let old_len = candidate.len();
459 append_framed_record(&mut candidate, record)?;
460 let expected = exists.then_some(&candidate[..old_len]);
461 match self
462 .storage
463 .cas_chunk(&self.name, self.write_version, chunk, expected, &candidate)
464 .await
465 {
466 Ok(()) => {
467 write.chunk = chunk;
468 write.exists = true;
469 write.bytes = candidate;
470 write.next_t += 1;
471 Ok(())
472 }
473 Err(error) => Err(error),
474 }
475 }
476
477 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
478 let _ = (start, end);
479 Err(LogError::AsyncOnly)
480 }
481
482 async fn tx_range_async(
483 &self,
484 start: u64,
485 end: Option<u64>,
486 ) -> Result<Vec<TxRecord>, LogError> {
487 let _guard = self.write.lock().await;
490 Ok(read_native_merged(self.storage.as_ref(), &self.name)
491 .await?
492 .into_iter()
493 .filter(|r| r.t >= start && end.is_none_or(|e| r.t < e))
494 .collect())
495 }
496}
497
498async fn read_native_merged<S: NativeLogStorage + ?Sized>(
499 storage: &S,
500 name: &str,
501) -> Result<Vec<TxRecord>, LogError> {
502 let mut chunks = storage.list_chunks(name).await?;
505 chunks.sort_unstable();
506 let mut per_version: Vec<Vec<TxRecord>> = Vec::new();
507 let mut current_version: Option<u64> = None;
508 for (version, chunk) in chunks {
509 if current_version != Some(version) {
510 per_version.push(Vec::new());
511 current_version = Some(version);
512 }
513 let bytes = storage
514 .read_chunk(name, version, chunk)
515 .await?
516 .unwrap_or_default();
517 per_version
518 .last_mut()
519 .expect("a version group was pushed")
520 .extend(decode_framed_records(&bytes)?);
521 }
522 let merged = merge_versions(per_version);
523 for pair in merged.windows(2) {
524 if pair[1].t != pair[0].t + 1 {
525 return Err(LogError::Corrupt);
526 }
527 }
528 Ok(merged)
529}
530
531type VersionedRecords = Arc<Mutex<Vec<(u64, TxRecord)>>>;
534
535#[derive(Clone, Default)]
541pub struct MemLogRegistry {
542 logs: Arc<Mutex<HashMap<String, VersionedRecords>>>,
543}
544
545impl MemLogRegistry {
546 #[must_use]
548 pub fn new() -> Self {
549 Self::default()
550 }
551
552 fn entry(&self, name: &str) -> VersionedRecords {
553 Arc::clone(
554 self.logs
555 .lock()
556 .unwrap_or_else(std::sync::PoisonError::into_inner)
557 .entry(name.to_owned())
558 .or_default(),
559 )
560 }
561
562 #[must_use]
565 pub fn open(&self, name: &str, write_version: u64) -> MemVersionedLog {
566 let records = self.entry(name);
567 let next_t = {
568 let guard = records
569 .lock()
570 .unwrap_or_else(std::sync::PoisonError::into_inner);
571 MemVersionedLog::merged(&guard)
572 .last()
573 .map_or(1, |r| r.t + 1)
574 };
575 MemVersionedLog {
576 records,
577 write_version,
578 next_t: Mutex::new(next_t),
579 }
580 }
581
582 #[must_use]
584 pub fn exists(&self, name: &str) -> bool {
585 self.logs
586 .lock()
587 .unwrap_or_else(std::sync::PoisonError::into_inner)
588 .get(name)
589 .is_some_and(|entry| {
590 !entry
591 .lock()
592 .unwrap_or_else(std::sync::PoisonError::into_inner)
593 .is_empty()
594 })
595 }
596
597 pub fn delete_all(&self, name: &str) {
599 self.logs
600 .lock()
601 .unwrap_or_else(std::sync::PoisonError::into_inner)
602 .remove(name);
603 }
604}
605
606pub struct MemVersionedLog {
610 records: VersionedRecords,
611 write_version: u64,
612 next_t: Mutex<u64>,
616}
617
618impl MemVersionedLog {
619 fn merged(records: &[(u64, TxRecord)]) -> Vec<TxRecord> {
620 let mut versions: Vec<u64> = records.iter().map(|(version, _)| *version).collect();
621 versions.sort_unstable();
622 versions.dedup();
623 let per_version = versions
624 .into_iter()
625 .map(|version| {
626 records
627 .iter()
628 .filter(|(record_version, _)| *record_version == version)
629 .map(|(_, record)| record.clone())
630 .collect::<Vec<_>>()
631 })
632 .collect();
633 merge_versions(per_version)
634 }
635}
636
637impl TransactionLog for MemVersionedLog {
638 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
639 let mut next_t = self
640 .next_t
641 .lock()
642 .unwrap_or_else(std::sync::PoisonError::into_inner);
643 if *next_t != record.t {
644 return Err(LogError::Corrupt);
645 }
646 self.records
647 .lock()
648 .unwrap_or_else(std::sync::PoisonError::into_inner)
649 .push((self.write_version, record.clone()));
650 *next_t += 1;
651 Ok(())
652 }
653
654 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
655 let records = self
656 .records
657 .lock()
658 .unwrap_or_else(std::sync::PoisonError::into_inner);
659 Ok(Self::merged(&records)
660 .into_iter()
661 .filter(|r| r.t >= start && end.is_none_or(|e| r.t < e))
662 .collect())
663 }
664}
665
666fn version_path(dir: &Path, name: &str, version: u64) -> PathBuf {
667 if version == 0 {
668 dir.join(format!("{name}.log"))
669 } else {
670 dir.join(format!("{name}.v{version}.log"))
671 }
672}
673
674fn version_files(dir: &Path, name: &str) -> Vec<(u64, PathBuf)> {
676 let mut files = Vec::new();
677 let legacy = version_path(dir, name, 0);
678 if legacy.is_file() {
679 files.push((0, legacy));
680 }
681 let prefix = format!("{name}.v");
682 if let Ok(entries) = fs::read_dir(dir) {
683 for entry in entries.flatten() {
684 let file_name = entry.file_name();
685 let Some(text) = file_name.to_str() else {
686 continue;
687 };
688 if let Some(version) = text
689 .strip_prefix(&prefix)
690 .and_then(|rest| rest.strip_suffix(".log"))
691 .and_then(|v| v.parse::<u64>().ok())
692 && version > 0
693 {
694 files.push((version, entry.path()));
695 }
696 }
697 }
698 files.sort_by_key(|(version, _)| *version);
699 files
700}
701
702fn read_merged(dir: &Path, name: &str) -> Result<Vec<TxRecord>, LogError> {
705 let files = version_files(dir, name);
706 let mut per_file: Vec<Vec<TxRecord>> = Vec::with_capacity(files.len());
707 for (_, path) in &files {
708 per_file.push(read_records(path)?.0);
709 }
710 let merged = merge_versions(per_file);
715 for pair in merged.windows(2) {
716 if pair[1].t != pair[0].t + 1 {
717 return Err(LogError::Corrupt);
718 }
719 }
720 Ok(merged)
721}
722
723fn encode_record(record: &TxRecord) -> Vec<u8> {
724 let mut out = Vec::new();
725 out.extend_from_slice(&record.t.to_be_bytes());
726 out.extend_from_slice(&record.tx_instant.to_be_bytes());
727 out.extend_from_slice(&(record.datoms.len() as u64).to_be_bytes());
728 for d in &record.datoms {
729 out.extend_from_slice(&d.e.raw().to_be_bytes());
730 out.extend_from_slice(&d.a.raw().to_be_bytes());
731 out.extend_from_slice(&d.tx.raw().to_be_bytes());
732 out.push(u8::from(d.added));
733 let v = encode_value(&d.v);
734 out.extend_from_slice(&(v.len() as u64).to_be_bytes());
735 out.extend_from_slice(&v);
736 }
737 out
738}
739fn decode_record(mut bytes: &[u8]) -> Result<TxRecord, LogError> {
740 fn take<'a>(bytes: &mut &'a [u8], n: usize) -> Result<&'a [u8], LogError> {
741 let value = bytes.get(..n).ok_or(LogError::Corrupt)?;
742 *bytes = &bytes[n..];
743 Ok(value)
744 }
745 fn u64_be(bytes: &mut &[u8]) -> Result<u64, LogError> {
746 Ok(u64::from_be_bytes(
747 take(bytes, 8)?.try_into().map_err(|_| LogError::Corrupt)?,
748 ))
749 }
750 let t = u64_be(&mut bytes)?;
751 let tx_instant = i64::from_be_bytes(
752 take(&mut bytes, 8)?
753 .try_into()
754 .map_err(|_| LogError::Corrupt)?,
755 );
756 let count = u64_be(&mut bytes)?;
757 let mut datoms = Vec::new();
758 for _ in 0..count {
759 let e = EntityId::from_raw(u64_be(&mut bytes)?);
760 let a = EntityId::from_raw(u64_be(&mut bytes)?);
761 let tx = EntityId::from_raw(u64_be(&mut bytes)?);
762 let added = take(&mut bytes, 1)?[0] != 0;
763 let len = usize::try_from(u64_be(&mut bytes)?).map_err(|_| LogError::Corrupt)?;
764 let raw = take(&mut bytes, len)?;
765 let (v, used) = decode_value(raw).map_err(|_| LogError::Corrupt)?;
766 if used != len {
767 return Err(LogError::Corrupt);
768 }
769 datoms.push(Datom { e, a, v, tx, added });
770 }
771 if !bytes.is_empty() {
772 return Err(LogError::Corrupt);
773 }
774 Ok(TxRecord {
775 t,
776 tx_instant,
777 datoms,
778 })
779}
780fn read_records(path: &Path) -> Result<(Vec<TxRecord>, u64), LogError> {
786 let mut file = File::open(path)?;
787 let mut records = Vec::new();
788 let mut durable_len = 0_u64;
789 loop {
790 let mut len = [0; 8];
791 match file.read_exact(&mut len) {
792 Ok(()) => {}
793 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
794 Err(e) => return Err(e.into()),
795 }
796 let len = usize::try_from(u64::from_be_bytes(len)).map_err(|_| LogError::Corrupt)?;
797 let mut payload = vec![0; len];
798 match file.read_exact(&mut payload) {
799 Ok(()) => {}
800 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
801 Err(e) => return Err(e.into()),
802 }
803 records.push(decode_record(&payload)?);
804 durable_len += 8 + len as u64;
805 }
806 Ok((records, durable_len))
807}
808
809pub fn append_framed_record(out: &mut Vec<u8>, record: &TxRecord) -> Result<(), LogError> {
814 let payload = encode_record(record);
815 out.extend_from_slice(
816 &u64::try_from(payload.len())
817 .map_err(|_| LogError::Corrupt)?
818 .to_be_bytes(),
819 );
820 out.extend_from_slice(&payload);
821 Ok(())
822}
823
824pub fn decode_framed_records(mut bytes: &[u8]) -> Result<Vec<TxRecord>, LogError> {
833 let mut records = Vec::new();
834 while !bytes.is_empty() {
835 if bytes.len() < 8 {
836 return Err(LogError::Corrupt);
837 }
838 let len = usize::try_from(u64::from_be_bytes(
839 bytes[..8].try_into().map_err(|_| LogError::Corrupt)?,
840 ))
841 .map_err(|_| LogError::Corrupt)?;
842 bytes = &bytes[8..];
843 let payload = bytes.get(..len).ok_or(LogError::Corrupt)?;
844 records.push(decode_record(payload)?);
845 bytes = &bytes[len..];
846 }
847 Ok(records)
848}