stemedb/crates/stemedb-sim/src/arenas/arena3.rs
jordan 02ecac9a07 fix: merge upstream 10 commits, fix DashMap deadlock, deterministic sim ingestion
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>
2026-02-20 20:27:32 -07:00

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
}