Skip to content

Commit 65864f2

Browse files
committed
feat(storage): implement WalBackend for durable mutation logging
This commit: - Integrates verisim-wal into the verisim-storage abstraction - Adds a generic WalBackend wrapper for any StorageBackend - Ensures all mutations (put, delete, batch) are logged before execution - Synchronizes WAL on flush calls to guarantee crash safety
1 parent b3170bb commit 65864f2

4 files changed

Lines changed: 148 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust-core/verisim-storage/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ redb-backend = ["dep:redb"]
1616
[dependencies]
1717
serde = { workspace = true }
1818
serde_json = { workspace = true }
19+
chrono = { workspace = true }
1920
thiserror = { workspace = true }
2021
tracing = { workspace = true }
2122
async-trait = { workspace = true }
2223
tokio = { workspace = true }
24+
verisim-wal = { path = "../verisim-wal" }
2325

2426
# Optional: redb for persistent on-disk storage (pure Rust, no C/C++)
2527
redb = { workspace = true, optional = true }

rust-core/verisim-storage/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ pub mod error;
4545
pub mod memory;
4646
pub mod metrics;
4747
pub mod typed;
48+
pub mod wal_backend;
4849

4950
// Optional persistent backends — feature-gated to keep the default build lean.
5051
#[cfg(feature = "redb-backend")]
@@ -56,6 +57,7 @@ pub use error::StorageError;
5657
pub use memory::InMemoryBackend;
5758
pub use metrics::{BackendStats, MetricsBackend};
5859
pub use typed::TypedStore;
60+
pub use wal_backend::WalBackend;
5961

6062
#[cfg(feature = "redb-backend")]
6163
pub use redb_backend::RedbBackend;
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
//
4+
// Write-Ahead Log (WAL) wrapper for storage backends.
5+
//
6+
// The `WalBackend` provides durability for any `StorageBackend` by recording
7+
// all mutations to a persistent log before they are applied to the inner
8+
// backend.
9+
10+
use async_trait::async_trait;
11+
use chrono::Utc;
12+
use std::sync::Arc;
13+
use tokio::sync::Mutex;
14+
15+
use crate::backend::StorageBackend;
16+
use crate::error::StorageError;
17+
use verisim_wal::{WalEntry, WalModality, WalOperation, WalWriter};
18+
19+
/// A storage backend wrapper that adds write-ahead logging.
20+
///
21+
/// Every mutation (`put`, `delete`, `batch_put`) is first serialized into a
22+
/// [`WalEntry`] and appended to the log. Only after the log append succeeds
23+
/// (and potentially syncs to disk, depending on `SyncMode`) is the operation
24+
/// applied to the inner backend.
25+
pub struct WalBackend<B: StorageBackend> {
26+
inner: B,
27+
wal: Arc<Mutex<WalWriter>>,
28+
modality: WalModality,
29+
}
30+
31+
impl<B: StorageBackend> WalBackend<B> {
32+
/// Create a new WAL-protected backend.
33+
///
34+
/// # Arguments
35+
///
36+
/// * `inner` - The storage backend to protect.
37+
/// * `wal` - A shared, mutex-protected [`WalWriter`].
38+
/// * `modality` - The modality identifier to use for log entries.
39+
pub fn new(inner: B, wal: Arc<Mutex<WalWriter>>, modality: WalModality) -> Self {
40+
Self {
41+
inner,
42+
wal,
43+
modality,
44+
}
45+
}
46+
47+
/// Access the inner backend.
48+
pub fn inner(&self) -> &B {
49+
&self.inner
50+
}
51+
52+
/// Append an operation to the WAL.
53+
async fn log_op(
54+
&self,
55+
operation: WalOperation,
56+
key: &[u8],
57+
value: &[u8],
58+
) -> Result<(), StorageError> {
59+
let entry = WalEntry {
60+
sequence: 0, // Assigned by writer
61+
timestamp: Utc::now(),
62+
operation,
63+
modality: self.modality,
64+
entity_id: String::from_utf8_lossy(key).to_string(),
65+
payload: value.to_vec(),
66+
};
67+
68+
let mut writer = self.wal.lock().await;
69+
writer.append(entry).map_err(|e| {
70+
StorageError::BackendUnavailable(format!("WAL append failed: {e}"))
71+
})?;
72+
73+
Ok(())
74+
}
75+
}
76+
77+
#[async_trait]
78+
impl<B: StorageBackend> StorageBackend for WalBackend<B> {
79+
async fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, StorageError> {
80+
self.inner.get(key).await
81+
}
82+
83+
async fn put(&self, key: &[u8], value: &[u8]) -> Result<(), StorageError> {
84+
// 1. Log intent to WAL
85+
self.log_op(WalOperation::Update, key, value).await?;
86+
87+
// 2. Apply to inner storage
88+
self.inner.put(key, value).await
89+
}
90+
91+
async fn delete(&self, key: &[u8]) -> Result<bool, StorageError> {
92+
// 1. Log intent to WAL
93+
self.log_op(WalOperation::Delete, key, b"").await?;
94+
95+
// 2. Apply to inner storage
96+
self.inner.delete(key).await
97+
}
98+
99+
async fn exists(&self, key: &[u8]) -> Result<bool, StorageError> {
100+
self.inner.exists(key).await
101+
}
102+
103+
async fn scan_prefix(
104+
&self,
105+
prefix: &[u8],
106+
limit: usize,
107+
) -> Result<Vec<(Vec<u8>, Vec<u8>)>, StorageError> {
108+
self.inner.scan_prefix(prefix, limit).await
109+
}
110+
111+
async fn multi_get(&self, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>, StorageError> {
112+
self.inner.multi_get(keys).await
113+
}
114+
115+
async fn batch_put(&self, entries: &[(&[u8], &[u8])]) -> Result<(), StorageError> {
116+
// For batch puts, we log multiple entries to the WAL.
117+
// A future optimization could be a single WalOperation::Batch.
118+
for (key, value) in entries {
119+
self.log_op(WalOperation::Update, key, value).await?;
120+
}
121+
self.inner.batch_put(entries).await
122+
}
123+
124+
async fn flush(&self) -> Result<(), StorageError> {
125+
// Sync the WAL first, then the inner store.
126+
let mut writer = self.wal.lock().await;
127+
writer.sync().map_err(|e| {
128+
StorageError::BackendUnavailable(format!("WAL sync failed: {e}"))
129+
})?;
130+
drop(writer);
131+
132+
self.inner.flush().await
133+
}
134+
135+
fn name(&self) -> &str {
136+
self.inner.name()
137+
}
138+
139+
async fn approximate_size(&self) -> Result<Option<u64>, StorageError> {
140+
self.inner.approximate_size().await
141+
}
142+
}

0 commit comments

Comments
 (0)