use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use crate::control::cluster::calvin::executor::ollp::config::OllpConfig;
use nodedb_cluster::calvin::sequencer::error::SequencerError;
fn zero_backoff_orchestrator() -> OllpOrchestrator {
OllpOrchestrator::new(OllpConfig {
backoff_initial: std::time::Duration::ZERO,
backoff_max: std::time::Duration::ZERO,
..OllpConfig::default()
})
}
fn spawn_fake_scheduler(
registry: Arc<CalvinCompletionRegistry>,
mut rx: tokio::sync::mpsc::Receiver<TxnId>,
mismatch_count: u32,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let mut seen: u32 = 0;
while let Some(txn) = rx.recv().await {
if seen < mismatch_count {
registry.note_ollp_mismatch(txn);
} else {
registry.note_completion_ack(txn, 1);
}
seen += 1;
}
})
}
fn fake_assignment(inbox_seq: u64) -> RoutedAssignment {
RoutedAssignment {
inbox_seq,
epoch: inbox_seq,
position: 0,
participants: 1,
}
}
#[test]
fn converges_after_two_mismatches() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("build current-thread runtime");
rt.block_on(async {
let registry = CalvinCompletionRegistry::new_detached();
let orchestrator = zero_backoff_orchestrator();
let (tx, rx) = tokio::sync::mpsc::channel::<TxnId>(16);
let fake = spawn_fake_scheduler(Arc::clone(®istry), rx, 2);
let seq = Arc::new(AtomicU64::new(1));
let submit_calls = Arc::new(AtomicU64::new(0));
let rescan_calls = Arc::new(AtomicU32::new(0));
let result = {
let seq = Arc::clone(&seq);
let submit_calls = Arc::clone(&submit_calls);
let rescan_calls = Arc::clone(&rescan_calls);
let tx = tx.clone();
run_dependent_with_retry(DependentRetryArgs {
registry: ®istry,
orchestrator: &orchestrator,
predicate_class_hash: 0xABCD,
timeout: std::time::Duration::from_secs(5),
ollp_max_retries: 5,
initial_predicted: vec![1, 2, 3],
submit: move |_predicted: &Vec<u32>| {
let seq = Arc::clone(&seq);
let submit_calls = Arc::clone(&submit_calls);
let tx = tx.clone();
async move {
submit_calls.fetch_add(1, Ordering::SeqCst);
let inbox_seq = seq.fetch_add(1, Ordering::SeqCst);
let assignment = fake_assignment(inbox_seq);
let txn = TxnId::new(assignment.epoch, assignment.position);
tx.send(txn).await.expect("fake recv alive");
Ok::<RoutedAssignment, OllpError>(assignment)
}
},
rescan: move || {
let rescan_calls = Arc::clone(&rescan_calls);
async move {
let n = rescan_calls.fetch_add(1, Ordering::SeqCst);
Ok(vec![100 + n])
}
},
})
.await
};
assert!(result.is_ok(), "expected Ok(txn_id), got {result:?}");
assert_eq!(
submit_calls.load(Ordering::SeqCst),
3,
"two mismatches + one success → three submits"
);
assert_eq!(
rescan_calls.load(Ordering::SeqCst),
2,
"fresh re-scan runs once per mismatch"
);
drop(tx);
let _ = fake.await;
});
}
#[test]
fn exhausts_on_persistent_mismatch() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("build current-thread runtime");
rt.block_on(async {
let registry = CalvinCompletionRegistry::new_detached();
let orchestrator = zero_backoff_orchestrator();
let (tx, rx) = tokio::sync::mpsc::channel::<TxnId>(16);
let fake = spawn_fake_scheduler(Arc::clone(®istry), rx, u32::MAX);
let seq = Arc::new(AtomicU64::new(1));
let submit_calls = Arc::new(AtomicU64::new(0));
let result = {
let seq = Arc::clone(&seq);
let submit_calls = Arc::clone(&submit_calls);
let tx = tx.clone();
run_dependent_with_retry(DependentRetryArgs {
registry: ®istry,
orchestrator: &orchestrator,
predicate_class_hash: 0xABCD,
timeout: std::time::Duration::from_secs(5),
ollp_max_retries: 3,
initial_predicted: vec![1],
submit: move |_predicted: &Vec<u32>| {
let seq = Arc::clone(&seq);
let submit_calls = Arc::clone(&submit_calls);
let tx = tx.clone();
async move {
submit_calls.fetch_add(1, Ordering::SeqCst);
let inbox_seq = seq.fetch_add(1, Ordering::SeqCst);
let assignment = fake_assignment(inbox_seq);
let txn = TxnId::new(assignment.epoch, assignment.position);
tx.send(txn).await.expect("fake recv alive");
Ok::<RoutedAssignment, OllpError>(assignment)
}
},
rescan: move || async move { Ok(vec![1]) },
})
.await
};
assert!(
matches!(result, Err(Error::OllpExhausted { retries: 3 })),
"expected OllpExhausted {{ retries: 3 }}, got {result:?}"
);
assert_eq!(
submit_calls.load(Ordering::SeqCst),
4,
"max_retries (3) + 1 → four submits before exhaustion"
);
drop(tx);
let _ = fake.await;
});
}
#[test]
fn pre_admission_retry_does_not_rescan() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("build current-thread runtime");
rt.block_on(async {
let registry = CalvinCompletionRegistry::new_detached();
let orchestrator = zero_backoff_orchestrator();
let (tx, rx) = tokio::sync::mpsc::channel::<TxnId>(16);
let fake = spawn_fake_scheduler(Arc::clone(®istry), rx, 0);
let seq = Arc::new(AtomicU64::new(1));
let submit_calls = Arc::new(AtomicU64::new(0));
let rescan_calls = Arc::new(AtomicU32::new(0));
let result = {
let seq = Arc::clone(&seq);
let submit_calls = Arc::clone(&submit_calls);
let rescan_calls = Arc::clone(&rescan_calls);
let tx = tx.clone();
run_dependent_with_retry(DependentRetryArgs {
registry: ®istry,
orchestrator: &orchestrator,
predicate_class_hash: 0xABCD,
timeout: std::time::Duration::from_secs(5),
ollp_max_retries: 5,
initial_predicted: vec![1],
submit: move |_predicted: &Vec<u32>| {
let seq = Arc::clone(&seq);
let submit_calls = Arc::clone(&submit_calls);
let tx = tx.clone();
async move {
let n = submit_calls.fetch_add(1, Ordering::SeqCst);
if n < 2 {
return Err(OllpError::Sequencer(SequencerError::Unavailable));
}
let inbox_seq = seq.fetch_add(1, Ordering::SeqCst);
let assignment = fake_assignment(inbox_seq);
let txn = TxnId::new(assignment.epoch, assignment.position);
tx.send(txn).await.expect("fake recv alive");
Ok::<RoutedAssignment, OllpError>(assignment)
}
},
rescan: move || {
let rescan_calls = Arc::clone(&rescan_calls);
async move {
rescan_calls.fetch_add(1, Ordering::SeqCst);
Ok(vec![1])
}
},
})
.await
};
assert!(result.is_ok(), "expected Ok(txn_id), got {result:?}");
assert_eq!(
submit_calls.load(Ordering::SeqCst),
3,
"two pre-admission failures + one success → three submits"
);
assert_eq!(
rescan_calls.load(Ordering::SeqCst),
0,
"pre-admission failure resubmits the SAME prediction — no re-scan"
);
drop(tx);
let _ = fake.await;
});
}