#![allow(clippy::unwrap_used)]
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::Duration;
use drasi_lib::channels::ChangeReceiver;
use drasi_lib::config::SourceSubscriptionSettings;
use drasi_lib::context::SourceRuntimeContext;
use drasi_lib::wal::{CapacityPolicy, WalProvider};
use drasi_lib::Source;
use drasi_source_application::{ApplicationSource, ApplicationSourceConfig, PropertyMapBuilder};
use drasi_wal_redb::RedbWalProvider;
use tempfile::TempDir;
fn durability_config(
enabled: bool,
max_events: u64,
policy: CapacityPolicy,
) -> drasi_lib::DurabilityConfig {
drasi_lib::DurabilityConfig {
enabled,
max_events,
capacity_policy: policy,
}
}
fn app_config(durability: Option<drasi_lib::DurabilityConfig>) -> ApplicationSourceConfig {
ApplicationSourceConfig {
properties: HashMap::new(),
durability,
}
}
async fn init_source_with_wal(
source: &ApplicationSource,
wal: Arc<dyn WalProvider>,
source_id: &str,
) {
let (update_tx, _update_rx) = tokio::sync::mpsc::channel(16);
source
.initialize(SourceRuntimeContext {
instance_id: "test-instance".to_string(),
source_id: source_id.to_string(),
update_tx,
state_store: None,
identity_provider: None,
wal_provider: Some(wal),
})
.await;
}
fn fresh_settings(source_id: &str, query_id: &str) -> SourceSubscriptionSettings {
SourceSubscriptionSettings {
source_id: source_id.to_string(),
query_id: query_id.to_string(),
enable_bootstrap: false,
nodes: HashSet::new(),
relations: HashSet::new(),
request_position_handle: true,
resume_from: None,
last_sequence: None,
}
}
fn resume_settings(source_id: &str, query_id: &str, resume_seq: u64) -> SourceSubscriptionSettings {
SourceSubscriptionSettings {
source_id: source_id.to_string(),
query_id: query_id.to_string(),
enable_bootstrap: false,
nodes: HashSet::new(),
relations: HashSet::new(),
request_position_handle: true,
resume_from: Some(bytes::Bytes::from(resume_seq.to_be_bytes().to_vec())),
last_sequence: Some(resume_seq),
}
}
async fn subscribe_fresh(
source: &ApplicationSource,
source_id: &str,
) -> Box<dyn ChangeReceiver<drasi_lib::channels::events::SourceEventWrapper>> {
let resp = source
.subscribe(fresh_settings(source_id, "test-query"))
.await
.unwrap();
resp.receiver
}
async fn subscribe_with_resume(
source: &ApplicationSource,
source_id: &str,
resume_seq: u64,
) -> Box<dyn ChangeReceiver<drasi_lib::channels::events::SourceEventWrapper>> {
let resp = source
.subscribe(resume_settings(source_id, "test-query-resume", resume_seq))
.await
.unwrap();
resp.receiver
}
#[tokio::test]
#[ignore]
async fn test_wal_disabled_no_persistence() {
let config = app_config(None);
let (source, _handle) = ApplicationSource::new("disabled-src", config).unwrap();
let tmp = TempDir::new().unwrap();
let wal = Arc::new(RedbWalProvider::new(tmp.path()));
init_source_with_wal(&source, wal.clone(), "disabled-src").await;
source.start().await.unwrap();
assert!(!source.supports_replay());
let count = wal.event_count("disabled-src").await;
assert!(
count.is_err(),
"WAL should not be registered for disabled source"
);
source.stop().await.unwrap();
}
#[tokio::test]
#[ignore]
async fn test_wal_enabled_events_persisted() {
let config = app_config(Some(durability_config(
true,
10_000,
CapacityPolicy::RejectIncoming,
)));
let (source, handle) = ApplicationSource::new("persist-src", config).unwrap();
let tmp = TempDir::new().unwrap();
let wal = Arc::new(RedbWalProvider::new(tmp.path()));
init_source_with_wal(&source, wal.clone(), "persist-src").await;
source.start().await.unwrap();
assert!(source.supports_replay());
let mut rx = subscribe_fresh(&source, "persist-src").await;
let props = PropertyMapBuilder::new()
.with_string("name", "Alice")
.build();
handle
.send_node_insert("node-1", vec!["Person"], props)
.await
.unwrap();
let event = tokio::time::timeout(Duration::from_secs(2), rx.recv())
.await
.unwrap()
.unwrap();
assert!(event.sequence.is_some());
assert_eq!(event.sequence.unwrap(), 1);
assert!(event.source_position.is_some());
let count = wal.event_count("persist-src").await.unwrap();
assert_eq!(count, 1);
let props2 = PropertyMapBuilder::new().with_string("name", "Bob").build();
handle
.send_node_insert("node-2", vec!["Person"], props2)
.await
.unwrap();
let event2 = tokio::time::timeout(Duration::from_secs(2), rx.recv())
.await
.unwrap()
.unwrap();
assert_eq!(event2.sequence.unwrap(), 2);
let count = wal.event_count("persist-src").await.unwrap();
assert_eq!(count, 2);
source.stop().await.unwrap();
}
#[tokio::test]
#[ignore]
async fn test_capacity_exhaustion_reject_incoming() {
let config = app_config(Some(durability_config(
true,
16,
CapacityPolicy::RejectIncoming,
)));
let (source, handle) = ApplicationSource::new("reject-src", config).unwrap();
let tmp = TempDir::new().unwrap();
let wal = Arc::new(RedbWalProvider::new(tmp.path()));
init_source_with_wal(&source, wal.clone(), "reject-src").await;
source.start().await.unwrap();
let mut rx = subscribe_fresh(&source, "reject-src").await;
for i in 1..=16 {
let props = PropertyMapBuilder::new().with_integer("x", i).build();
handle
.send_node_insert(format!("n{i}"), vec!["T"], props)
.await
.unwrap();
let _ = tokio::time::timeout(Duration::from_secs(2), rx.recv())
.await
.unwrap();
}
let props = PropertyMapBuilder::new().with_integer("x", 17).build();
let result = handle.send_node_insert("n17", vec!["T"], props).await;
assert!(result.is_err(), "Expected capacity exhaustion error");
assert!(
result
.unwrap_err()
.to_string()
.contains("capacity exhausted"),
"Error should mention capacity exhaustion"
);
let count = wal.event_count("reject-src").await.unwrap();
assert_eq!(count, 16);
source.stop().await.unwrap();
}
#[tokio::test]
#[ignore]
async fn test_deprovision_removes_wal() {
let config = app_config(Some(durability_config(
true,
10_000,
CapacityPolicy::RejectIncoming,
)));
let (source, handle) = ApplicationSource::new("deprov-src", config).unwrap();
let tmp = TempDir::new().unwrap();
let wal = Arc::new(RedbWalProvider::new(tmp.path()));
init_source_with_wal(&source, wal.clone(), "deprov-src").await;
source.start().await.unwrap();
let mut rx = subscribe_fresh(&source, "deprov-src").await;
let props = PropertyMapBuilder::new().with_string("x", "1").build();
handle
.send_node_insert("n1", vec!["T"], props)
.await
.unwrap();
let _ = tokio::time::timeout(Duration::from_secs(2), rx.recv())
.await
.unwrap();
let count = wal.event_count("deprov-src").await.unwrap();
assert!(count > 0);
source.stop().await.unwrap();
source.deprovision().await.unwrap();
let count = wal.event_count("deprov-src").await;
assert!(count.is_err(), "WAL should be deleted after deprovision");
}
#[tokio::test]
#[ignore]
async fn test_crash_recovery_resumes_sequence() {
let tmp = TempDir::new().unwrap();
let wal = Arc::new(RedbWalProvider::new(tmp.path()));
{
let config = app_config(Some(durability_config(
true,
10_000,
CapacityPolicy::RejectIncoming,
)));
let (source, handle) = ApplicationSource::new("crash-src", config).unwrap();
init_source_with_wal(&source, wal.clone(), "crash-src").await;
source.start().await.unwrap();
let mut rx = subscribe_fresh(&source, "crash-src").await;
for i in 1..=5 {
let props = PropertyMapBuilder::new().with_integer("i", i).build();
handle
.send_node_insert(format!("n{i}"), vec!["T"], props)
.await
.unwrap();
let _ = tokio::time::timeout(Duration::from_secs(2), rx.recv())
.await
.unwrap();
}
let head = wal.head_sequence("crash-src").await.unwrap();
assert_eq!(head, 5);
source.stop().await.unwrap();
}
{
let config = app_config(Some(durability_config(
true,
10_000,
CapacityPolicy::RejectIncoming,
)));
let (source, handle) = ApplicationSource::new("crash-src", config).unwrap();
init_source_with_wal(&source, wal.clone(), "crash-src").await;
source.start().await.unwrap();
let mut rx = subscribe_fresh(&source, "crash-src").await;
let props = PropertyMapBuilder::new().with_integer("i", 6).build();
handle
.send_node_insert("n6", vec!["T"], props)
.await
.unwrap();
let event = tokio::time::timeout(Duration::from_secs(2), rx.recv())
.await
.unwrap()
.unwrap();
assert_eq!(event.sequence.unwrap(), 6);
source.stop().await.unwrap();
}
}
#[tokio::test]
#[ignore]
async fn test_replay_via_subscribe() {
let tmp = TempDir::new().unwrap();
let wal = Arc::new(RedbWalProvider::new(tmp.path()));
let config = app_config(Some(durability_config(
true,
10_000,
CapacityPolicy::RejectIncoming,
)));
let (source, handle) = ApplicationSource::new("replay-src", config).unwrap();
init_source_with_wal(&source, wal.clone(), "replay-src").await;
source.start().await.unwrap();
let mut rx1 = subscribe_fresh(&source, "replay-src").await;
for i in 1..=5 {
let props = PropertyMapBuilder::new().with_integer("i", i).build();
handle
.send_node_insert(format!("n{i}"), vec!["T"], props)
.await
.unwrap();
let _ = tokio::time::timeout(Duration::from_secs(2), rx1.recv())
.await
.unwrap();
}
let mut rx2 = subscribe_with_resume(&source, "replay-src", 2).await;
for expected_seq in 3..=5 {
let event = tokio::time::timeout(Duration::from_secs(2), rx2.recv())
.await
.unwrap()
.unwrap();
assert_eq!(
event.sequence.unwrap(),
expected_seq,
"Expected replay seq {expected_seq}"
);
}
let props = PropertyMapBuilder::new().with_integer("i", 6).build();
handle
.send_node_insert("n6", vec!["T"], props)
.await
.unwrap();
let event1 = tokio::time::timeout(Duration::from_secs(2), rx1.recv())
.await
.unwrap()
.unwrap();
assert_eq!(event1.sequence.unwrap(), 6);
let event2 = tokio::time::timeout(Duration::from_secs(2), rx2.recv())
.await
.unwrap()
.unwrap();
assert_eq!(event2.sequence.unwrap(), 6);
source.stop().await.unwrap();
}
#[tokio::test]
#[ignore]
async fn test_concurrent_writes_monotonic_sequences() {
let tmp = TempDir::new().unwrap();
let wal = Arc::new(RedbWalProvider::new(tmp.path()));
let config = app_config(Some(durability_config(
true,
10_000,
CapacityPolicy::RejectIncoming,
)));
let (source, handle) = ApplicationSource::new("conc-src", config).unwrap();
init_source_with_wal(&source, wal.clone(), "conc-src").await;
source.start().await.unwrap();
let mut rx = subscribe_fresh(&source, "conc-src").await;
let total_events: usize = 100;
let tasks_count = 4;
let events_per_task = total_events / tasks_count;
let mut tasks = vec![];
for task_id in 0..tasks_count {
let h = handle.clone();
tasks.push(tokio::spawn(async move {
for i in 0..events_per_task {
let props = PropertyMapBuilder::new()
.with_integer("task", task_id as i64)
.with_integer("i", i as i64)
.build();
h.send_node_insert(format!("t{task_id}-n{i}"), vec!["T"], props)
.await
.unwrap();
}
}));
}
for t in tasks {
t.await.unwrap();
}
let mut sequences = vec![];
for _ in 0..total_events {
let event = tokio::time::timeout(Duration::from_secs(5), rx.recv())
.await
.unwrap()
.unwrap();
sequences.push(event.sequence.unwrap());
}
sequences.sort();
let expected: Vec<u64> = (1..=total_events as u64).collect();
assert_eq!(
sequences, expected,
"Sequences should be 1..={total_events} with no gaps"
);
let count = wal.event_count("conc-src").await.unwrap();
assert_eq!(count, total_events as u64);
source.stop().await.unwrap();
}
#[tokio::test]
#[ignore]
async fn test_resume_from_position_end_to_end() {
let tmp = TempDir::new().unwrap();
let wal: Arc<dyn WalProvider> = Arc::new(RedbWalProvider::new(tmp.path()));
let config = app_config(Some(durability_config(
true,
10_000,
CapacityPolicy::RejectIncoming,
)));
let (source, handle) = ApplicationSource::new("e2e-src", config).unwrap();
init_source_with_wal(&source, wal.clone(), "e2e-src").await;
source.start().await.unwrap();
let settings = SourceSubscriptionSettings {
source_id: "e2e-src".to_string(),
query_id: "e2e-query".to_string(),
enable_bootstrap: false,
nodes: HashSet::new(),
relations: HashSet::new(),
request_position_handle: true,
resume_from: None,
last_sequence: None,
};
let resp = source.subscribe(settings).await.unwrap();
let mut rx = resp.receiver;
let position_handle = resp.position_handle.expect("should have position handle");
for i in 1..=5 {
let props = PropertyMapBuilder::new().with_integer("val", i).build();
handle
.send_node_insert(format!("n{i}"), vec!["Item"], props)
.await
.unwrap();
}
let mut last_confirmed_seq = 0u64;
for _ in 0..5 {
let event = tokio::time::timeout(Duration::from_secs(2), rx.recv())
.await
.expect("timed out waiting for event")
.unwrap();
let seq = event.sequence.expect("event should have WAL sequence");
position_handle.store(seq, std::sync::atomic::Ordering::Release);
last_confirmed_seq = seq;
}
assert_eq!(last_confirmed_seq, 5, "Should have confirmed through seq 5");
for i in 6..=8 {
let props = PropertyMapBuilder::new().with_integer("val", i).build();
handle
.send_node_insert(format!("n{i}"), vec!["Item"], props)
.await
.unwrap();
}
for _ in 0..3 {
let _ = tokio::time::timeout(Duration::from_secs(2), rx.recv())
.await
.expect("timed out")
.unwrap();
}
source.stop().await.unwrap();
drop(rx);
drop(handle);
drop(source);
let wal_count = wal.event_count("e2e-src").await.unwrap();
assert!(
wal_count >= 3,
"WAL should have at least the unconfirmed events, got {wal_count}"
);
let config2 = app_config(Some(durability_config(
true,
10_000,
CapacityPolicy::RejectIncoming,
)));
let (source2, handle2) = ApplicationSource::new("e2e-src", config2).unwrap();
init_source_with_wal(&source2, wal.clone(), "e2e-src").await;
source2.start().await.unwrap();
let mut rx2 = subscribe_with_resume(&source2, "e2e-src", last_confirmed_seq).await;
let mut replayed_seqs = vec![];
for _ in 0..3 {
let event = tokio::time::timeout(Duration::from_secs(2), rx2.recv())
.await
.expect("timed out waiting for replay event")
.unwrap();
replayed_seqs.push(event.sequence.expect("replay event should have sequence"));
}
assert_eq!(
replayed_seqs,
vec![6, 7, 8],
"Should replay events 6, 7, 8 after checkpoint"
);
let props = PropertyMapBuilder::new().with_integer("val", 9).build();
handle2
.send_node_insert("n9", vec!["Item"], props)
.await
.unwrap();
let live_event = tokio::time::timeout(Duration::from_secs(2), rx2.recv())
.await
.expect("timed out waiting for live event")
.unwrap();
assert_eq!(
live_event
.sequence
.expect("live event should have sequence"),
9,
"Live event after restart should continue sequence"
);
let pos = live_event
.source_position
.as_ref()
.expect("should have source_position");
let pos_seq = u64::from_be_bytes(pos[..8].try_into().unwrap());
assert_eq!(
pos_seq, 9,
"source_position should encode the sequence number"
);
source2.stop().await.unwrap();
}