Files
dirigent/crates/dirigent_archivist/tests/multi_backend_health_test.rs
T
2026-05-08 01:59:04 +02:00

130 lines
4.1 KiB
Rust

#![cfg(feature = "test-utils")]
use std::sync::Arc;
use dirigent_archivist::backend::mock::MockBackend;
use dirigent_archivist::backend::{ArchiveBackend, HealthStatus};
use dirigent_archivist::coordinator::Archivist;
use dirigent_archivist::registry::{ArchiveRegistration, FailureMode, WritePolicy};
use dirigent_archivist::types::SessionMetadata;
use uuid::Uuid;
fn reg(
name: &str,
backend: Arc<MockBackend>,
priority: u32,
failure: FailureMode,
) -> Arc<ArchiveRegistration> {
Arc::new(ArchiveRegistration::new(
name.into(),
"mock",
backend as Arc<dyn ArchiveBackend>,
true,
failure,
priority,
true,
WritePolicy::Inline,
None,
HealthStatus::Healthy,
))
}
#[tokio::test]
async fn five_consecutive_failures_drifts_to_unavailable() {
let primary = Arc::new(MockBackend::new());
let secondary = Arc::new(MockBackend::new());
secondary.inject_write_failures(10);
let archivist = Archivist::from_registrations(vec![
reg("primary", primary.clone(), 0, FailureMode::Required),
reg("secondary", secondary.clone(), 10, FailureMode::BestEffort),
]);
for _ in 0..5 {
archivist
.append_messages(Uuid::new_v4(), vec![], None)
.await
.ok();
}
let snapshot = archivist.list_archives_with_health().await;
let secondary_status = snapshot.iter().find(|s| s.name == "secondary").unwrap();
assert!(
matches!(secondary_status.health, HealthStatus::Unavailable { .. }),
"secondary should be Unavailable after 5 failures; got {:?}",
secondary_status.health
);
}
#[tokio::test]
async fn success_after_failure_recovers_to_healthy() {
let backend = Arc::new(MockBackend::new());
backend.inject_write_failures(1);
let archivist = Archivist::from_registrations(vec![reg(
"only",
backend.clone(),
0,
FailureMode::Required,
)]);
// First call fails.
let _ = archivist
.append_messages(Uuid::new_v4(), vec![], None)
.await;
let snapshot = archivist.list_archives_with_health().await;
assert!(
matches!(snapshot[0].health, HealthStatus::Degraded { .. }),
"expected Degraded after first failure; got {:?}",
snapshot[0].health
);
// Second call succeeds — health returns to Healthy.
archivist
.append_messages(Uuid::new_v4(), vec![], None)
.await
.unwrap();
let snapshot = archivist.list_archives_with_health().await;
assert!(
matches!(snapshot[0].health, HealthStatus::Healthy),
"expected Healthy after recovery; got {:?}",
snapshot[0].health
);
}
#[tokio::test]
async fn unavailable_backend_skipped_during_read_walk() {
let primary = Arc::new(MockBackend::new());
let secondary = Arc::new(MockBackend::new());
let scroll = Uuid::new_v4();
secondary
.put_session(SessionMetadata::stub(scroll))
.await
.unwrap();
secondary.break_permanently("kaput");
let primary_reg = reg("primary", primary.clone(), 0, FailureMode::Required);
let secondary_reg = reg("secondary", secondary.clone(), 10, FailureMode::Required);
// Force secondary's cached health to Unavailable BEFORE the walk,
// so the routing layer skips it entirely rather than attempting + failing.
*secondary_reg.last_health.write().await = HealthStatus::Unavailable {
reason: "test".into(),
};
let archivist = Archivist::from_registrations(vec![primary_reg, secondary_reg]);
// Primary doesn't have the session; secondary has it but is marked Unavailable.
// Read walk should skip secondary → Ok(None)-style ergonomics, bubbling up
// as `SessionUnknown` per `get_session_metadata`'s contract.
let result = archivist.get_session_metadata(scroll, None).await;
assert!(
result.is_err(),
"expected SessionUnknown error when Unavailable backend is skipped"
);
assert!(matches!(
result.unwrap_err(),
dirigent_archivist::error::ArchivistError::SessionUnknown(_)
));
}