Merged 10 upstream commits (MemTable, read-your-writes tests, feed endpoint, security hardening, signed assertions, source registry, dashboard enhancements) and fixed all test failures across the full workspace (2656/2656 passing). Key fixes: - fix(cluster): DashMap deadlock in swim.rs suspect_node/fail_node/alive_node - DashMap::get_mut RefMut + iter() on same map = non-reentrant write lock deadlock - Fix: extract clone in scoped block to drop RefMut before calling update_node_gauges() - 6 previously-hanging SWIM tests now pass in <2s - fix(sim): replace background-task+polling ingestion with synchronous process_pending() - smoke_high_volume_simulation was CPU-starved under 2656 parallel tests - Removed ingestor.start() + wait_until_ingested() pattern throughout sim - All arena functions now call ingestor.process_pending() directly (deterministic) - fix(test): v2 signature helper used wrong hash (rkyv vs canonical compute_content_hash_v2) - fix(test): quota test signed "test" but v1 requires "subject:predicate" format - fix(test): http_validation now accepts 400 for valid-format-but-invalid-crypto hex - fix(test): scale_adaptive micro tier assertions updated (auto_promote upstream change) - config: add nextest.toml with slow-timeout for background-task-tests group Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
436 lines
14 KiB
Rust
436 lines
14 KiB
Rust
//! Arena 3 tests: Materialized Views integration, fast-path verification, and freshness under load.
|
|
|
|
use std::sync::Arc;
|
|
use stemedb_core::types::{LifecycleStage, ObjectValue};
|
|
use stemedb_lens::VoteAwareConsensusLens;
|
|
use stemedb_query::{Materializer, Query, QueryEngine};
|
|
use stemedb_storage::{GenericVoteStore, KVStore};
|
|
use stemedb_wal::Journal;
|
|
use tokio::sync::Mutex;
|
|
use tracing::debug;
|
|
|
|
use crate::agent::Agent;
|
|
use crate::helpers::{verify_assertion_text, write_assertion_to_wal};
|
|
use crate::types::{ErrorKind, SimulationError, SimulationResult};
|
|
|
|
// ============================================================================
|
|
// Arena 3.1: MV Integration Test
|
|
// ============================================================================
|
|
|
|
/// Test that Materializer creates MV keys after ingestion.
|
|
///
|
|
/// Steps:
|
|
/// 1. Write assertion to WAL
|
|
/// 2. Wait for ingestion
|
|
/// 3. Run Materializer step()
|
|
/// 4. Verify MV:{subject}:{predicate} key exists
|
|
/// 5. Verify MaterializedView contains correct winner
|
|
pub(crate) async fn run_mv_integration_test<S: KVStore + 'static>(
|
|
journal: &Arc<Mutex<Journal>>,
|
|
store: &Arc<S>,
|
|
ingestor: &stemedb_ingest::Ingestor<S>,
|
|
agents: &[Agent],
|
|
result: &mut SimulationResult,
|
|
) -> bool {
|
|
let agent = &agents[0];
|
|
let subject = "MV_Test_Entity";
|
|
let predicate = "test_property";
|
|
|
|
// Write assertion to WAL
|
|
let assertion = agent.sign_assertion_with_options(
|
|
subject,
|
|
predicate,
|
|
ObjectValue::Text("mv_test_value".to_string()),
|
|
LifecycleStage::Proposed,
|
|
Some(3000),
|
|
);
|
|
|
|
if let Err(e) = write_assertion_to_wal(journal, &assertion).await {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::WriteFailure,
|
|
message: format!("MV integration test: failed to write assertion: {}", e),
|
|
});
|
|
return false;
|
|
}
|
|
result.assertions_written += 1;
|
|
|
|
// Synchronously drain WAL entries
|
|
if let Err(e) = ingestor.process_pending().await {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::WriteFailure,
|
|
message: format!("MV integration test: ingestion failed: {}", e),
|
|
});
|
|
return false;
|
|
}
|
|
|
|
// Verify assertion was ingested by querying it
|
|
let engine = QueryEngine::new(store.clone());
|
|
let query = Query::builder().subject(subject).predicate(predicate).build();
|
|
let query_result = match engine.execute(&query).await {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::QueryFailure,
|
|
message: format!(
|
|
"MV integration test: query failed (assertion not ingested?): {}",
|
|
e
|
|
),
|
|
});
|
|
return false;
|
|
}
|
|
};
|
|
|
|
if query_result.assertions.is_empty() {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::QueryFailure,
|
|
message: format!(
|
|
"MV integration test: assertion {}:{} not found after ingestion",
|
|
subject, predicate
|
|
),
|
|
});
|
|
return false;
|
|
}
|
|
|
|
debug!(
|
|
" MV integration test: assertion found via QueryEngine ({} results)",
|
|
query_result.assertions.len()
|
|
);
|
|
|
|
// Create Materializer with VoteAwareConsensusLens
|
|
let vote_store = Arc::new(GenericVoteStore::new(store.clone()));
|
|
let lens = VoteAwareConsensusLens::new(vote_store);
|
|
let materializer = Materializer::new(store.clone(), Box::new(lens));
|
|
|
|
// Directly materialize the specific subject+predicate pair
|
|
// This is more targeted than step() which materializes ALL pairs
|
|
let mv = match materializer.materialize_pair(subject, predicate).await {
|
|
Ok(Some(view)) => {
|
|
result.views_materialized += 1;
|
|
view
|
|
}
|
|
Ok(None) => {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::MaterializerFailure,
|
|
message: format!(
|
|
"MV integration test: materialize_pair returned None for {}:{}",
|
|
subject, predicate
|
|
),
|
|
});
|
|
return false;
|
|
}
|
|
Err(e) => {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::MaterializerFailure,
|
|
message: format!("MV integration test: materialize_pair failed: {}", e),
|
|
});
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Verify the MV was written to the store
|
|
let stored_mv = match materializer.get_materialized_view(subject, predicate).await {
|
|
Ok(Some(view)) => view,
|
|
Ok(None) => {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::MaterializerFailure,
|
|
message: format!(
|
|
"MV integration test: MV:{}:{} key does not exist after materialization",
|
|
subject, predicate
|
|
),
|
|
});
|
|
return false;
|
|
}
|
|
Err(e) => {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::MaterializerFailure,
|
|
message: format!("MV integration test: failed to read MV: {}", e),
|
|
});
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Sanity check: stored MV should match what materialize_pair returned
|
|
let _ = stored_mv;
|
|
|
|
// Verify winner matches our assertion
|
|
if let Err(e) = verify_assertion_text(
|
|
&mv.winner,
|
|
subject,
|
|
predicate,
|
|
"mv_test_value",
|
|
"MV integration test",
|
|
) {
|
|
result.errors.push(e);
|
|
return false;
|
|
}
|
|
|
|
debug!(" MV integration test passed: MV:{}:{} exists with correct winner", subject, predicate);
|
|
true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Arena 3.2: Fast-Path Verification Test
|
|
// ============================================================================
|
|
|
|
/// Test that QueryEngine uses fast-path for MV reads.
|
|
///
|
|
/// Steps:
|
|
/// 1. Write assertion + materialize (reuse state from 3.1)
|
|
/// 2. Query via QueryEngine with subject+predicate
|
|
/// 3. Verify result matches MV winner
|
|
pub(crate) async fn run_fast_path_test<S: KVStore + 'static>(
|
|
journal: &Arc<Mutex<Journal>>,
|
|
store: &Arc<S>,
|
|
ingestor: &stemedb_ingest::Ingestor<S>,
|
|
agents: &[Agent],
|
|
result: &mut SimulationResult,
|
|
) -> bool {
|
|
let agent = &agents[0];
|
|
let subject = "FastPath_Entity";
|
|
let predicate = "fast_property";
|
|
|
|
// Write assertion
|
|
let assertion = agent.sign_assertion_with_options(
|
|
subject,
|
|
predicate,
|
|
ObjectValue::Text("fast_path_value".to_string()),
|
|
LifecycleStage::Proposed,
|
|
Some(3100),
|
|
);
|
|
|
|
if let Err(e) = write_assertion_to_wal(journal, &assertion).await {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::WriteFailure,
|
|
message: format!("Fast-path test: failed to write assertion: {}", e),
|
|
});
|
|
return false;
|
|
}
|
|
result.assertions_written += 1;
|
|
|
|
// Synchronously drain WAL entries
|
|
if let Err(e) = ingestor.process_pending().await {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::WriteFailure,
|
|
message: format!("Fast-path test: ingestion failed: {}", e),
|
|
});
|
|
return false;
|
|
}
|
|
|
|
// Materialize
|
|
let vote_store = Arc::new(GenericVoteStore::new(store.clone()));
|
|
let lens = VoteAwareConsensusLens::new(vote_store);
|
|
let materializer = Materializer::new(store.clone(), Box::new(lens));
|
|
|
|
if let Err(e) = materializer.step().await {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::MaterializerFailure,
|
|
message: format!("Fast-path test: materializer step failed: {}", e),
|
|
});
|
|
return false;
|
|
}
|
|
|
|
// Query via QueryEngine - this should use fast-path (MV lookup)
|
|
let engine = QueryEngine::new(store.clone());
|
|
let query = Query::builder().subject(subject).predicate(predicate).build();
|
|
|
|
let query_result = match engine.execute(&query).await {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::QueryFailure,
|
|
message: format!("Fast-path test: query failed: {}", e),
|
|
});
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Verify result
|
|
if query_result.assertions.is_empty() {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::QueryFailure,
|
|
message: "Fast-path test: query returned no results".to_string(),
|
|
});
|
|
return false;
|
|
}
|
|
|
|
// When using fast-path (MV), QueryEngine returns exactly 1 result (the winner)
|
|
if query_result.assertions.len() != 1 {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::MaterializerFailure,
|
|
message: format!(
|
|
"Fast-path test: expected 1 result (MV winner), got {}",
|
|
query_result.assertions.len()
|
|
),
|
|
});
|
|
return false;
|
|
}
|
|
|
|
let winner = &query_result.assertions[0];
|
|
if let Err(e) =
|
|
verify_assertion_text(winner, subject, predicate, "fast_path_value", "Fast-path test")
|
|
{
|
|
result.errors.push(e);
|
|
return false;
|
|
}
|
|
|
|
result.queries_executed += 1;
|
|
debug!(" Fast-path test passed: QueryEngine returned MV winner");
|
|
true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Arena 3.3: MV Freshness Under Load Test
|
|
// ============================================================================
|
|
|
|
/// Test that MV reflects latest state under rapid writes.
|
|
///
|
|
/// Steps:
|
|
/// 1. Write 10 assertions in rapid succession for same subject+predicate
|
|
/// 2. Each with incrementing timestamp
|
|
/// 3. Wait for ingestion
|
|
/// 4. Run Materializer step()
|
|
/// 5. Verify MV winner is the NEWEST assertion (highest timestamp)
|
|
pub(crate) async fn run_mv_freshness_test<S: KVStore + 'static>(
|
|
journal: &Arc<Mutex<Journal>>,
|
|
store: &Arc<S>,
|
|
ingestor: &stemedb_ingest::Ingestor<S>,
|
|
agents: &[Agent],
|
|
result: &mut SimulationResult,
|
|
) -> bool {
|
|
let agent = &agents[0];
|
|
let subject = "Freshness_Entity";
|
|
let predicate = "rapid_update";
|
|
let num_assertions: usize = 10;
|
|
let base_timestamp = 4000u64;
|
|
|
|
// Write 10 assertions with incrementing timestamps
|
|
for i in 0..num_assertions {
|
|
let assertion = agent.sign_assertion_with_options(
|
|
subject,
|
|
predicate,
|
|
ObjectValue::Text(format!("value_{}", i)),
|
|
LifecycleStage::Proposed,
|
|
Some(base_timestamp + i as u64),
|
|
);
|
|
|
|
if let Err(e) = write_assertion_to_wal(journal, &assertion).await {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::WriteFailure,
|
|
message: format!("MV freshness test: failed to write assertion {}: {}", i, e),
|
|
});
|
|
return false;
|
|
}
|
|
result.assertions_written += 1;
|
|
}
|
|
|
|
// Synchronously drain all pending WAL entries
|
|
if let Err(e) = ingestor.process_pending().await {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::WriteFailure,
|
|
message: format!("MV freshness test: ingestion failed: {}", e),
|
|
});
|
|
return false;
|
|
}
|
|
|
|
// Materialize
|
|
let vote_store = Arc::new(GenericVoteStore::new(store.clone()));
|
|
let lens = VoteAwareConsensusLens::new(vote_store);
|
|
let materializer = Materializer::new(store.clone(), Box::new(lens));
|
|
|
|
let report = match materializer.step().await {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::MaterializerFailure,
|
|
message: format!("MV freshness test: materializer step failed: {}", e),
|
|
});
|
|
return false;
|
|
}
|
|
};
|
|
|
|
result.views_materialized += report.views_updated as u64;
|
|
|
|
// Verify MV winner has the highest timestamp
|
|
let mv = match materializer.get_materialized_view(subject, predicate).await {
|
|
Ok(Some(view)) => view,
|
|
Ok(None) => {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::MaterializerFailure,
|
|
message: format!(
|
|
"MV freshness test: MV:{}:{} key does not exist after materialization",
|
|
subject, predicate
|
|
),
|
|
});
|
|
return false;
|
|
}
|
|
Err(e) => {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::MaterializerFailure,
|
|
message: format!("MV freshness test: failed to read MV: {}", e),
|
|
});
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// The winner should have the highest timestamp (base_timestamp + 9 = 4009)
|
|
let expected_timestamp = base_timestamp + (num_assertions - 1) as u64;
|
|
if mv.winner.timestamp != expected_timestamp {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::MaterializerFailure,
|
|
message: format!(
|
|
"MV freshness test: winner has wrong timestamp. Expected {}, got {}",
|
|
expected_timestamp, mv.winner.timestamp
|
|
),
|
|
});
|
|
return false;
|
|
}
|
|
|
|
// Verify the correct value (value_9 for the last assertion)
|
|
let expected_value = format!("value_{}", num_assertions - 1);
|
|
if let Err(e) =
|
|
verify_assertion_text(&mv.winner, subject, predicate, &expected_value, "MV freshness test")
|
|
{
|
|
result.errors.push(e);
|
|
return false;
|
|
}
|
|
|
|
// Verify candidates_count reflects all 10 assertions
|
|
if mv.candidates_count != num_assertions {
|
|
result.errors.push(SimulationError {
|
|
tick: 0,
|
|
kind: ErrorKind::MaterializerFailure,
|
|
message: format!(
|
|
"MV freshness test: expected {} candidates, got {}",
|
|
num_assertions, mv.candidates_count
|
|
),
|
|
});
|
|
return false;
|
|
}
|
|
|
|
debug!(
|
|
" MV freshness test passed: winner has timestamp {} with {} candidates",
|
|
mv.winner.timestamp, mv.candidates_count
|
|
);
|
|
true
|
|
}
|