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 async fn append_batch_async(&self, records: &[TxRecord]) -> Result<(), LogError> {
72 for record in records {
73 self.append_async(record).await?;
74 }
75 Ok(())
76 }
77 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError>;
82 async fn tx_range_async(
87 &self,
88 start: u64,
89 end: Option<u64>,
90 ) -> Result<Vec<TxRecord>, LogError> {
91 self.tx_range(start, end)
92 }
93 fn replay(&self) -> Result<Vec<TxRecord>, LogError> {
98 self.tx_range(0, None)
99 }
100 async fn replay_async(&self) -> Result<Vec<TxRecord>, LogError> {
105 self.tx_range_async(0, None).await
106 }
107}
108
109#[derive(Clone, Default)]
111pub struct MemoryLog(Arc<RwLock<Vec<TxRecord>>>);
112impl TransactionLog for MemoryLog {
113 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
114 let mut records = self.0.write().expect("poisoned log lock");
115 if records.last().map_or(1, |r| r.t + 1) != record.t {
116 return Err(LogError::Corrupt);
117 }
118 records.push(record.clone());
119 Ok(())
120 }
121 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
122 Ok(self
123 .0
124 .read()
125 .expect("poisoned log lock")
126 .iter()
127 .filter(|r| r.t >= start && end.is_none_or(|e| r.t < e))
128 .cloned()
129 .collect())
130 }
131}
132
133pub struct FileLog {
139 path: PathBuf,
140 next_t: RwLock<u64>,
141}
142impl FileLog {
143 pub fn open(path: impl AsRef<Path>) -> Result<Self, LogError> {
149 let path = path.as_ref().to_path_buf();
150 if let Some(parent) = path.parent() {
151 fs::create_dir_all(parent)?;
152 }
153 OpenOptions::new().create(true).append(true).open(&path)?;
154 let (records, durable_len) = read_records(&path)?;
155 if fs::metadata(&path)?.len() > durable_len {
156 let file = OpenOptions::new().write(true).open(&path)?;
157 file.set_len(durable_len)?;
158 file.sync_all()?;
159 }
160 Ok(Self {
161 path,
162 next_t: RwLock::new(records.last().map_or(1, |r| r.t + 1)),
163 })
164 }
165}
166impl TransactionLog for FileLog {
167 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
168 let mut next_t = self.next_t.write().expect("poisoned log lock");
169 if *next_t != record.t {
170 return Err(LogError::Corrupt);
171 }
172 let payload = encode_record(record);
173 let mut file = OpenOptions::new().append(true).open(&self.path)?;
174 file.write_all(
175 &u64::try_from(payload.len())
176 .map_err(|_| LogError::Corrupt)?
177 .to_be_bytes(),
178 )?;
179 file.write_all(&payload)?;
180 file.sync_all()?;
181 *next_t += 1;
182 Ok(())
183 }
184 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
185 let _guard = self.next_t.read().expect("poisoned log lock");
186 Ok(read_records(&self.path)?
187 .0
188 .into_iter()
189 .filter(|r| r.t >= start && end.is_none_or(|e| r.t < e))
190 .collect())
191 }
192}
193
194pub struct VersionedLog {
206 dir: PathBuf,
207 name: String,
208 write_path: PathBuf,
209 next_t: RwLock<u64>,
210}
211
212impl VersionedLog {
213 pub fn open(dir: impl AsRef<Path>, name: &str, write_version: u64) -> Result<Self, LogError> {
221 let dir = dir.as_ref().to_path_buf();
222 fs::create_dir_all(&dir)?;
223 let write_path = version_path(&dir, name, write_version);
224 OpenOptions::new()
225 .create(true)
226 .append(true)
227 .open(&write_path)?;
228 let (_, durable_len) = read_records(&write_path)?;
229 if fs::metadata(&write_path)?.len() > durable_len {
230 let file = OpenOptions::new().write(true).open(&write_path)?;
231 file.set_len(durable_len)?;
232 file.sync_all()?;
233 }
234 let records = read_merged(&dir, name)?;
235 Ok(Self {
236 dir,
237 name: name.to_owned(),
238 write_path,
239 next_t: RwLock::new(records.last().map_or(1, |r| r.t + 1)),
240 })
241 }
242
243 pub fn open_read_only(dir: impl AsRef<Path>, name: &str) -> Result<Self, LogError> {
249 let dir = dir.as_ref().to_path_buf();
250 Ok(Self {
251 write_path: PathBuf::new(),
252 name: name.to_owned(),
253 next_t: RwLock::new(u64::MAX),
254 dir,
255 })
256 }
257
258 #[must_use]
260 pub fn exists(dir: impl AsRef<Path>, name: &str) -> bool {
261 !version_files(dir.as_ref(), name).is_empty()
262 }
263
264 pub fn delete_all(dir: impl AsRef<Path>, name: &str) -> Result<(), LogError> {
269 for (_, path) in version_files(dir.as_ref(), name) {
270 match fs::remove_file(&path) {
271 Ok(()) => {}
272 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
273 Err(error) => return Err(error.into()),
274 }
275 }
276 Ok(())
277 }
278}
279
280impl TransactionLog for VersionedLog {
281 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
282 let mut next_t = self.next_t.write().expect("poisoned log lock");
283 if *next_t != record.t {
284 return Err(LogError::Corrupt);
285 }
286 let payload = encode_record(record);
287 let mut file = OpenOptions::new().append(true).open(&self.write_path)?;
288 file.write_all(
289 &u64::try_from(payload.len())
290 .map_err(|_| LogError::Corrupt)?
291 .to_be_bytes(),
292 )?;
293 file.write_all(&payload)?;
294 file.sync_all()?;
295 *next_t += 1;
296 Ok(())
297 }
298
299 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
300 let _guard = self.next_t.read().expect("poisoned log lock");
301 Ok(read_merged(&self.dir, &self.name)?
302 .into_iter()
303 .filter(|r| r.t >= start && end.is_none_or(|e| r.t < e))
304 .collect())
305 }
306}
307
308fn merge_versions(mut per_version: Vec<Vec<TxRecord>>) -> Vec<TxRecord> {
313 let mut cutoff = u64::MAX;
314 for records in per_version.iter_mut().rev() {
315 let first = records.first().map(|r| r.t);
316 records.retain(|r| r.t < cutoff);
317 if let Some(first) = first {
318 cutoff = cutoff.min(first);
319 }
320 }
321 per_version.into_iter().flatten().collect()
322}
323
324#[async_trait]
340pub trait NativeLogStorage: Send + Sync {
341 async fn put_batch(
354 &self,
355 name: &str,
356 version: u64,
357 records: &[(u64, Vec<u8>)],
358 ) -> Result<bool, LogError>;
359 async fn read_record(
365 &self,
366 name: &str,
367 version: u64,
368 t: u64,
369 ) -> Result<Option<Vec<u8>>, LogError>;
370 async fn list_records(&self, name: &str) -> Result<Vec<(u64, u64)>, LogError>;
376 async fn read_legacy_chunk(
382 &self,
383 name: &str,
384 version: u64,
385 chunk: u64,
386 ) -> Result<Option<Vec<u8>>, LogError>;
387 async fn list_legacy_chunks(&self, name: &str) -> Result<Vec<(u64, u64)>, LogError>;
395 async fn delete_all(&self, name: &str) -> Result<(), LogError>;
400}
401
402pub struct NativeVersionedLog<S: ?Sized> {
411 storage: Arc<S>,
412 name: String,
413 write_version: u64,
414 next_t: tokio::sync::Mutex<u64>,
416}
417
418impl<S: NativeLogStorage + ?Sized + 'static> NativeVersionedLog<S> {
419 pub async fn open(storage: Arc<S>, name: &str, write_version: u64) -> Result<Self, LogError> {
424 let records = read_native_merged(storage.as_ref(), name).await?;
428 let next_t = records.last().map_or(1, |r| r.t + 1);
429 Ok(Self {
430 storage,
431 name: name.to_owned(),
432 write_version,
433 next_t: tokio::sync::Mutex::new(next_t),
434 })
435 }
436}
437
438#[async_trait]
439impl<S: NativeLogStorage + ?Sized + 'static> TransactionLog for NativeVersionedLog<S> {
440 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
441 let _ = record;
442 Err(LogError::AsyncOnly)
443 }
444
445 async fn append_async(&self, record: &TxRecord) -> Result<(), LogError> {
446 self.append_batch_async(std::slice::from_ref(record)).await
447 }
448
449 async fn append_batch_async(&self, records: &[TxRecord]) -> Result<(), LogError> {
450 if records.is_empty() {
451 return Ok(());
452 }
453 let mut next_t = self.next_t.lock().await;
454 for (offset, record) in records.iter().enumerate() {
456 if record.t != *next_t + offset as u64 {
457 return Err(LogError::Corrupt);
458 }
459 }
460 let framed = records
461 .iter()
462 .map(|record| {
463 let mut bytes = Vec::new();
464 append_framed_record(&mut bytes, record)?;
465 Ok((record.t, bytes))
466 })
467 .collect::<Result<Vec<_>, LogError>>()?;
468 if !self
473 .storage
474 .put_batch(&self.name, self.write_version, &framed)
475 .await?
476 {
477 return Err(LogError::Corrupt);
478 }
479 *next_t += records.len() as u64;
480 Ok(())
481 }
482
483 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
484 let _ = (start, end);
485 Err(LogError::AsyncOnly)
486 }
487
488 async fn tx_range_async(
489 &self,
490 start: u64,
491 end: Option<u64>,
492 ) -> Result<Vec<TxRecord>, LogError> {
493 let _guard = self.next_t.lock().await;
496 Ok(read_native_merged(self.storage.as_ref(), &self.name)
497 .await?
498 .into_iter()
499 .filter(|r| r.t >= start && end.is_none_or(|e| r.t < e))
500 .collect())
501 }
502}
503
504async fn read_native_merged<S: NativeLogStorage + ?Sized>(
505 storage: &S,
506 name: &str,
507) -> Result<Vec<TxRecord>, LogError> {
508 use std::collections::BTreeMap;
509
510 let mut per_version: BTreeMap<u64, Vec<TxRecord>> = BTreeMap::new();
513
514 let mut chunks = storage.list_legacy_chunks(name).await?;
518 chunks.sort_unstable();
519 for (version, chunk) in chunks {
520 let bytes = storage
521 .read_legacy_chunk(name, version, chunk)
522 .await?
523 .unwrap_or_default();
524 per_version
525 .entry(version)
526 .or_default()
527 .extend(decode_framed_records(&bytes)?);
528 }
529
530 let mut records = storage.list_records(name).await?;
532 records.sort_unstable();
533 for (version, t) in records {
534 let bytes = storage
535 .read_record(name, version, t)
536 .await?
537 .unwrap_or_default();
538 per_version
539 .entry(version)
540 .or_default()
541 .extend(decode_framed_records(&bytes)?);
542 }
543
544 let per_version: Vec<Vec<TxRecord>> = per_version
549 .into_values()
550 .map(|mut records| {
551 records.sort_by_key(|record| record.t);
552 records
553 })
554 .collect();
555 let merged = merge_versions(per_version);
556 for pair in merged.windows(2) {
557 if pair[1].t != pair[0].t + 1 {
558 return Err(LogError::Corrupt);
559 }
560 }
561 Ok(merged)
562}
563
564type VersionedRecords = Arc<Mutex<Vec<(u64, TxRecord)>>>;
567
568#[derive(Clone, Default)]
574pub struct MemLogRegistry {
575 logs: Arc<Mutex<HashMap<String, VersionedRecords>>>,
576}
577
578impl MemLogRegistry {
579 #[must_use]
581 pub fn new() -> Self {
582 Self::default()
583 }
584
585 fn entry(&self, name: &str) -> VersionedRecords {
586 Arc::clone(
587 self.logs
588 .lock()
589 .unwrap_or_else(std::sync::PoisonError::into_inner)
590 .entry(name.to_owned())
591 .or_default(),
592 )
593 }
594
595 #[must_use]
598 pub fn open(&self, name: &str, write_version: u64) -> MemVersionedLog {
599 let records = self.entry(name);
600 let next_t = {
601 let guard = records
602 .lock()
603 .unwrap_or_else(std::sync::PoisonError::into_inner);
604 MemVersionedLog::merged(&guard)
605 .last()
606 .map_or(1, |r| r.t + 1)
607 };
608 MemVersionedLog {
609 records,
610 write_version,
611 next_t: Mutex::new(next_t),
612 }
613 }
614
615 #[must_use]
617 pub fn exists(&self, name: &str) -> bool {
618 self.logs
619 .lock()
620 .unwrap_or_else(std::sync::PoisonError::into_inner)
621 .get(name)
622 .is_some_and(|entry| {
623 !entry
624 .lock()
625 .unwrap_or_else(std::sync::PoisonError::into_inner)
626 .is_empty()
627 })
628 }
629
630 pub fn delete_all(&self, name: &str) {
632 self.logs
633 .lock()
634 .unwrap_or_else(std::sync::PoisonError::into_inner)
635 .remove(name);
636 }
637}
638
639pub struct MemVersionedLog {
643 records: VersionedRecords,
644 write_version: u64,
645 next_t: Mutex<u64>,
649}
650
651impl MemVersionedLog {
652 fn merged(records: &[(u64, TxRecord)]) -> Vec<TxRecord> {
653 let mut versions: Vec<u64> = records.iter().map(|(version, _)| *version).collect();
654 versions.sort_unstable();
655 versions.dedup();
656 let per_version = versions
657 .into_iter()
658 .map(|version| {
659 records
660 .iter()
661 .filter(|(record_version, _)| *record_version == version)
662 .map(|(_, record)| record.clone())
663 .collect::<Vec<_>>()
664 })
665 .collect();
666 merge_versions(per_version)
667 }
668}
669
670impl TransactionLog for MemVersionedLog {
671 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
672 let mut next_t = self
673 .next_t
674 .lock()
675 .unwrap_or_else(std::sync::PoisonError::into_inner);
676 if *next_t != record.t {
677 return Err(LogError::Corrupt);
678 }
679 self.records
680 .lock()
681 .unwrap_or_else(std::sync::PoisonError::into_inner)
682 .push((self.write_version, record.clone()));
683 *next_t += 1;
684 Ok(())
685 }
686
687 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
688 let records = self
689 .records
690 .lock()
691 .unwrap_or_else(std::sync::PoisonError::into_inner);
692 Ok(Self::merged(&records)
693 .into_iter()
694 .filter(|r| r.t >= start && end.is_none_or(|e| r.t < e))
695 .collect())
696 }
697}
698
699fn version_path(dir: &Path, name: &str, version: u64) -> PathBuf {
700 if version == 0 {
701 dir.join(format!("{name}.log"))
702 } else {
703 dir.join(format!("{name}.v{version}.log"))
704 }
705}
706
707fn version_files(dir: &Path, name: &str) -> Vec<(u64, PathBuf)> {
709 let mut files = Vec::new();
710 let legacy = version_path(dir, name, 0);
711 if legacy.is_file() {
712 files.push((0, legacy));
713 }
714 let prefix = format!("{name}.v");
715 if let Ok(entries) = fs::read_dir(dir) {
716 for entry in entries.flatten() {
717 let file_name = entry.file_name();
718 let Some(text) = file_name.to_str() else {
719 continue;
720 };
721 if let Some(version) = text
722 .strip_prefix(&prefix)
723 .and_then(|rest| rest.strip_suffix(".log"))
724 .and_then(|v| v.parse::<u64>().ok())
725 && version > 0
726 {
727 files.push((version, entry.path()));
728 }
729 }
730 }
731 files.sort_by_key(|(version, _)| *version);
732 files
733}
734
735fn read_merged(dir: &Path, name: &str) -> Result<Vec<TxRecord>, LogError> {
738 let files = version_files(dir, name);
739 let mut per_file: Vec<Vec<TxRecord>> = Vec::with_capacity(files.len());
740 for (_, path) in &files {
741 per_file.push(read_records(path)?.0);
742 }
743 let merged = merge_versions(per_file);
748 for pair in merged.windows(2) {
749 if pair[1].t != pair[0].t + 1 {
750 return Err(LogError::Corrupt);
751 }
752 }
753 Ok(merged)
754}
755
756fn encode_record(record: &TxRecord) -> Vec<u8> {
757 let mut out = Vec::new();
758 out.extend_from_slice(&record.t.to_be_bytes());
759 out.extend_from_slice(&record.tx_instant.to_be_bytes());
760 out.extend_from_slice(&(record.datoms.len() as u64).to_be_bytes());
761 for d in &record.datoms {
762 out.extend_from_slice(&d.e.raw().to_be_bytes());
763 out.extend_from_slice(&d.a.raw().to_be_bytes());
764 out.extend_from_slice(&d.tx.raw().to_be_bytes());
765 out.push(u8::from(d.added));
766 let v = encode_value(&d.v);
767 out.extend_from_slice(&(v.len() as u64).to_be_bytes());
768 out.extend_from_slice(&v);
769 }
770 out
771}
772fn decode_record(mut bytes: &[u8]) -> Result<TxRecord, LogError> {
773 fn take<'a>(bytes: &mut &'a [u8], n: usize) -> Result<&'a [u8], LogError> {
774 let value = bytes.get(..n).ok_or(LogError::Corrupt)?;
775 *bytes = &bytes[n..];
776 Ok(value)
777 }
778 fn u64_be(bytes: &mut &[u8]) -> Result<u64, LogError> {
779 Ok(u64::from_be_bytes(
780 take(bytes, 8)?.try_into().map_err(|_| LogError::Corrupt)?,
781 ))
782 }
783 let t = u64_be(&mut bytes)?;
784 let tx_instant = i64::from_be_bytes(
785 take(&mut bytes, 8)?
786 .try_into()
787 .map_err(|_| LogError::Corrupt)?,
788 );
789 let count = u64_be(&mut bytes)?;
790 let mut datoms = Vec::new();
791 for _ in 0..count {
792 let e = EntityId::from_raw(u64_be(&mut bytes)?);
793 let a = EntityId::from_raw(u64_be(&mut bytes)?);
794 let tx = EntityId::from_raw(u64_be(&mut bytes)?);
795 let added = take(&mut bytes, 1)?[0] != 0;
796 let len = usize::try_from(u64_be(&mut bytes)?).map_err(|_| LogError::Corrupt)?;
797 let raw = take(&mut bytes, len)?;
798 let (v, used) = decode_value(raw).map_err(|_| LogError::Corrupt)?;
799 if used != len {
800 return Err(LogError::Corrupt);
801 }
802 datoms.push(Datom { e, a, v, tx, added });
803 }
804 if !bytes.is_empty() {
805 return Err(LogError::Corrupt);
806 }
807 Ok(TxRecord {
808 t,
809 tx_instant,
810 datoms,
811 })
812}
813fn read_records(path: &Path) -> Result<(Vec<TxRecord>, u64), LogError> {
819 let mut file = File::open(path)?;
820 let mut records = Vec::new();
821 let mut durable_len = 0_u64;
822 loop {
823 let mut len = [0; 8];
824 match file.read_exact(&mut len) {
825 Ok(()) => {}
826 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
827 Err(e) => return Err(e.into()),
828 }
829 let len = usize::try_from(u64::from_be_bytes(len)).map_err(|_| LogError::Corrupt)?;
830 let mut payload = vec![0; len];
831 match file.read_exact(&mut payload) {
832 Ok(()) => {}
833 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
834 Err(e) => return Err(e.into()),
835 }
836 records.push(decode_record(&payload)?);
837 durable_len += 8 + len as u64;
838 }
839 Ok((records, durable_len))
840}
841
842pub fn append_framed_record(out: &mut Vec<u8>, record: &TxRecord) -> Result<(), LogError> {
847 let payload = encode_record(record);
848 out.extend_from_slice(
849 &u64::try_from(payload.len())
850 .map_err(|_| LogError::Corrupt)?
851 .to_be_bytes(),
852 );
853 out.extend_from_slice(&payload);
854 Ok(())
855}
856
857pub fn decode_framed_records(mut bytes: &[u8]) -> Result<Vec<TxRecord>, LogError> {
866 let mut records = Vec::new();
867 while !bytes.is_empty() {
868 if bytes.len() < 8 {
869 return Err(LogError::Corrupt);
870 }
871 let len = usize::try_from(u64::from_be_bytes(
872 bytes[..8].try_into().map_err(|_| LogError::Corrupt)?,
873 ))
874 .map_err(|_| LogError::Corrupt)?;
875 bytes = &bytes[8..];
876 let payload = bytes.get(..len).ok_or(LogError::Corrupt)?;
877 records.push(decode_record(payload)?);
878 bytes = &bytes[len..];
879 }
880 Ok(records)
881}