Skip to content

Commit 35f498d

Browse files
danielgerlagCopilot
andcommitted
Address review feedback: WAL tests, docs, and new-code cleanups
- tests/wal_provider_test.rs: add smoke tests for the redb WAL provider (creation + builder wiring/lifecycle), mirroring state_store_test.rs. - instance_paths.rs: add a doc comment explaining the hex-encoding rationale and two edge-case tests (empty ID, happy-path ASCII). - server.rs / instance_handlers.rs: hoist the duplicated instance_storage_key computation so the index and WAL paths share one safe_id. - builder.rs: document that with_index_provider registers under the name "rocksdb". - README.md / CLAUDE.md: document the always-on, redb-backed WAL, its ./data/<instance-key>/wal location, and the Docker volume implication. The WAL remains always-on by design (durability of source events as a baseline guarantee), not gated behind a config field. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3c04cbe commit 35f498d

7 files changed

Lines changed: 128 additions & 5 deletions

File tree

CLAUDE.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,23 @@ The REST API is exposed under `/api/v1/instances/{instanceId}/...` for multi-ins
209209

210210
**Important**: Sources and reactions are plugins that must be provided programmatically or via the configuration file's tagged enum format. Queries can also be defined via configuration files.
211211

212+
### Write-Ahead Log (WAL)
213+
214+
Every DrasiLib instance is unconditionally wired with a durable, redb-backed
215+
write-ahead log for source events (`drasi-wal-redb::RedbWalProvider`, attached
216+
via `builder.with_wal_provider(...)`). The WAL is created under
217+
`./data/<instance-key>/wal/`, where `<instance-key>` is `instance_storage_key()`
218+
applied to the instance ID (a hex-encoded, path-traversal-safe form). The
219+
per-source redb files inside that directory are created lazily on first append.
220+
221+
Unlike `persistIndex` (RocksDB query indexes) and `stateStore` (plugin state),
222+
the WAL is **always on** and there is currently no config field to disable it.
223+
This is an intentional design choice: durability of source events is treated as
224+
a baseline guarantee rather than an opt-in. The wiring lives in `server.rs`
225+
(config/startup path) and `instance_handlers.rs` (REST API create path).
226+
Operators should account for this directory in disk-usage planning and mount a
227+
volume for `./data/` in containerized deployments.
228+
212229
### Configuration Persistence
213230

214231
Persistence uses a snapshot-based approach: when saving, `ConfigPersistence::save()` calls `snapshot_configuration()` on each DrasiLib instance via the ComponentGraph. The ComponentGraph is the single source of truth — there are no shadow caches or separate registration steps. Mutations flow through the ComponentGraph, and the persisted YAML is reconstructed from the current graph state at save time.

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,16 @@ queries: []
822822
reactions: []
823823
```
824824
825+
> **Write-ahead log (always on):** Every instance maintains a durable
826+
> write-ahead log (WAL) for source events, backed by redb. It is written to
827+
> `./data/<instance-key>/wal/` (where `<instance-key>` is a hex-encoded form of
828+
> the instance ID), relative to the server's working directory. The WAL is
829+
> always enabled and there is currently no configuration option to disable it,
830+
> so account for this directory when planning disk usage and, in containerized
831+
> deployments, mount a volume for `./data/` to persist it across restarts. This
832+
> is separate from the optional `persistIndex` (query indexes) and `stateStore`
833+
> (plugin state) storage.
834+
825835
### Plugins Configuration
826836

827837
The `plugins` section declares plugin dependencies that can be installed with `drasi-server plugin install --from-config`. Each entry specifies a plugin reference that supports three URI formats:

src/api/shared/handlers/instance_handlers.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,11 @@ pub async fn create_instance(
8989
builder = builder.with_dispatch_buffer_capacity(capacity);
9090
}
9191

92+
// Filesystem-safe key shared by the persistent index and WAL paths.
93+
let safe_id = instance_storage_key(&instance_id);
94+
9295
// Set up RocksDB persistent indexing if requested
9396
if persist_index {
94-
let safe_id = instance_storage_key(&instance_id);
9597
let index_path = PathBuf::from(format!("./data/{safe_id}/index"));
9698
log::info!(
9799
"Enabling persistent indexing for instance '{}' with RocksDB at: {}",
@@ -107,7 +109,6 @@ pub async fn create_instance(
107109

108110
// WAL provider for durable source event persistence
109111
{
110-
let safe_id = instance_storage_key(&instance_id);
111112
let wal_path = PathBuf::from(format!("./data/{safe_id}/wal"));
112113
log::info!(
113114
"Enabling WAL provider for instance '{}' at: {}",

src/builder.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ impl DrasiServerBuilder {
7878
/// Add an index provider for persistent storage
7979
///
8080
/// By default, DrasiLib uses in-memory indexes. Use this method to inject
81-
/// a persistent index provider like RocksDB.
81+
/// a persistent index provider like RocksDB. The provider is registered as
82+
/// the default index backend under the name `"rocksdb"`.
8283
pub fn with_index_provider(mut self, provider: Arc<dyn IndexBackendPlugin>) -> Self {
8384
let builder = self.primary_builder_mut();
8485
*builder = std::mem::take(builder).with_default_index_provider("rocksdb", provider);

src/instance_paths.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
/// Converts an arbitrary instance ID into a filesystem-safe storage key.
16+
///
17+
/// Each byte of the ID is hex-encoded as two lowercase digits and prefixed with
18+
/// `id-`. Hex encoding is used (rather than naive character substitution) because
19+
/// it is injective: it prevents path traversal (e.g. `../tenant` →
20+
/// `id-2e2e2f74656e616e74`), eliminates separator collisions (e.g. `a/b` and
21+
/// `a_b` map to distinct keys), and always yields a valid single-segment
22+
/// directory name on every platform. Used to derive the per-instance index and
23+
/// WAL directories under `./data/`.
1524
pub(crate) fn instance_storage_key(instance_id: &str) -> String {
1625
let mut key = String::with_capacity(3 + instance_id.len() * 2);
1726
key.push_str("id-");
@@ -36,6 +45,16 @@ mod tests {
3645
assert_ne!(instance_storage_key("a\\b"), instance_storage_key("a_b"));
3746
}
3847

48+
#[test]
49+
fn instance_storage_key_encodes_empty_id() {
50+
assert_eq!(instance_storage_key(""), "id-");
51+
}
52+
53+
#[test]
54+
fn instance_storage_key_encodes_ascii_id() {
55+
assert_eq!(instance_storage_key("default"), "id-64656661756c74");
56+
}
57+
3958
#[test]
4059
fn instance_storage_key_encodes_path_traversal_characters() {
4160
let key = instance_storage_key("../tenant");

src/server.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,9 +454,11 @@ impl DrasiServer {
454454
builder = builder.with_dispatch_buffer_capacity(capacity);
455455
}
456456

457+
// Filesystem-safe key shared by the persistent index and WAL paths.
458+
let safe_id = instance_storage_key(&instance.id);
459+
457460
// Create and add RocksDB index provider if persist_index is enabled
458461
if instance.persist_index {
459-
let safe_id = instance_storage_key(&instance.id);
460462
let index_path = PathBuf::from(format!("./data/{safe_id}/index"));
461463
info!(
462464
"Enabling persistent indexing for instance '{}' with RocksDB at: {}",
@@ -484,7 +486,6 @@ impl DrasiServer {
484486

485487
// Create WAL provider for durable source event persistence
486488
{
487-
let safe_id = instance_storage_key(&instance.id);
488489
let wal_path = PathBuf::from(format!("./data/{safe_id}/wal"));
489490
info!(
490491
"Enabling WAL provider for instance '{}' at: {}",

tests/wal_provider_test.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2025 The Drasi Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! Integration tests for the redb-backed WAL provider.
16+
//!
17+
//! The WAL provider is the primary feature added in this PR and is wired into
18+
//! every instance, so these tests verify:
19+
//! - `RedbWalProvider` can be created from a directory path without panicking
20+
//! - `DrasiLib::builder().with_wal_provider(...)` accepts the provider and the
21+
//! instance reaches Running and stops cleanly
22+
//!
23+
//! Per-source WAL file/directory creation is exercised by the `drasi-wal-redb`
24+
//! crate's own tests, since it only happens once a source appends events.
25+
26+
use anyhow::Result;
27+
use drasi_lib::DrasiLib;
28+
use drasi_wal_redb::RedbWalProvider;
29+
use std::sync::Arc;
30+
use tempfile::TempDir;
31+
32+
/// Test that RedbWalProvider can be created with a valid directory path.
33+
#[test]
34+
fn test_redb_wal_provider_creation() {
35+
let temp_dir = TempDir::new().expect("Failed to create temp directory");
36+
let wal_path = temp_dir.path().join("wal");
37+
38+
let provider = RedbWalProvider::new(&wal_path);
39+
40+
// Provider should be created successfully.
41+
drop(provider);
42+
}
43+
44+
/// Test that DrasiLib builder accepts a redb WAL provider and the instance
45+
/// starts and stops cleanly.
46+
#[tokio::test]
47+
async fn test_drasi_lib_builder_with_redb_wal_provider() -> Result<()> {
48+
let temp_dir = TempDir::new()?;
49+
let wal_path = temp_dir.path().join("wal");
50+
51+
let provider = RedbWalProvider::new(&wal_path);
52+
53+
let core = DrasiLib::builder()
54+
.with_id("test-wal-provider")
55+
.with_wal_provider(Arc::new(provider))
56+
.build()
57+
.await?;
58+
59+
core.start().await?;
60+
assert!(core.is_running().await);
61+
drasi_lib::wait_for_status(
62+
&core.component_graph(),
63+
"__component_graph__",
64+
&[drasi_lib::channels::ComponentStatus::Running],
65+
std::time::Duration::from_secs(5),
66+
)
67+
.await
68+
.expect("component graph should reach Running");
69+
70+
core.stop().await?;
71+
assert!(!core.is_running().await);
72+
73+
Ok(())
74+
}

0 commit comments

Comments
 (0)