use super::io::{FileSpill, SchedIo};
use super::lifecycle::{resume_swapped_seq, swap_out_sequence};
use super::test_support::{PreemptStubModel, active_seq, streaming_seq};
use super::types::ActiveSeq;
use metrale_cache::kv_spill::KvSpillManager;
use std::sync::atomic::Ordering;
fn spill(name: &str) -> FileSpill {
let dir = std::env::temp_dir().join(format!(
"metrale-swap-out-tests-{}-{name}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
FileSpill::new(KvSpillManager::new(dir, 1 << 20).expect("spill dir"))
}
fn three_actives() -> (ActiveSeq, ActiveSeq) {
let (a0, _rx0) = active_seq(0, 5);
let (a2, _rx2) = active_seq(2, 9);
(a0, a2)
}
#[test]
fn compaction_failure_reaches_the_blocking_client_instead_of_dropping_it() {
let model = std::sync::Arc::new(PreemptStubModel::failing_compact(
"compact_sequence: slot 1 still owned",
));
let (a0, a2) = three_actives();
let (victim, mut victim_rx) = active_seq(1, 2);
let mut active = vec![a0, victim, a2];
let r = swap_out_sequence(
&SchedIo::for_test_with(model.clone()),
&mut active,
1,
&spill("blocking"),
);
assert!(r.is_err(), "compaction failed, so the swap-out must fail");
let sent = victim_rx
.try_recv()
.expect("victim's sink was DROPPED — the client cannot tell this from its own abort");
let Err(err) = sent else {
panic!("a failed swap-out must not report success to the client");
};
let msg = format!("{err:#}");
assert!(
msg.contains("swap-out failed") && msg.contains("compact_sequence"),
"the client must be told what actually failed, got: {msg}"
);
assert!(
model.freed_slots.lock().unwrap().contains(&1),
"victim slot never freed: {:?}",
model.freed_slots.lock().unwrap()
);
}
#[test]
fn compaction_failure_sends_a_terminal_frame_to_a_streaming_client() {
let model = std::sync::Arc::new(PreemptStubModel::failing_compact(
"compact_sequence: slot 1 still owned",
));
let (a0, a2) = three_actives();
let (victim, mut victim_rx) = streaming_seq(1, 2);
let mut active = vec![a0, victim, a2];
assert!(
swap_out_sequence(
&SchedIo::for_test_with(model.clone()),
&mut active,
1,
&spill("streaming")
)
.is_err()
);
match victim_rx.try_recv() {
Ok(crate::api::StreamEvent::Error(msg)) => assert!(
msg.contains("swap-out failed"),
"terminal frame must name the failure, got: {msg}"
),
Ok(_) => panic!("expected an Error frame, got a different StreamEvent"),
Err(e) => panic!(
"no terminal frame on the victim's stream ({e:?}) — the body is \
truncated under an already-committed HTTP 200"
),
}
}
#[test]
fn a_successful_swap_out_still_hands_back_the_sink_untouched() {
let model = std::sync::Arc::new(PreemptStubModel::default());
let (a0, a2) = three_actives();
let (victim, mut victim_rx) = active_seq(1, 2);
let mut active = vec![a0, victim, a2];
let s = swap_out_sequence(
&SchedIo::for_test_with(model.clone()),
&mut active,
1,
&spill("ok"),
)
.expect("swap-out");
assert_eq!(s.output_tokens.len(), 2);
assert_eq!(
model.compact_calls.load(Ordering::SeqCst),
1,
"the migrated survivor must still be compacted"
);
assert!(
matches!(
victim_rx.try_recv(),
Err(tokio::sync::oneshot::error::TryRecvError::Empty)
),
"a parked request's client must hear nothing yet"
);
}
fn park(
model: &std::sync::Arc<PreemptStubModel>,
victim: ActiveSeq,
name: &str,
) -> (super::types::SwappedSeq, FileSpill) {
let sp = spill(name);
let (a0, a2) = three_actives();
let mut active = vec![a0, victim, a2];
let s = swap_out_sequence(&SchedIo::for_test_with(model.clone()), &mut active, 1, &sp)
.expect("park");
(s, sp)
}
#[test]
fn swap_in_failure_reaches_the_blocking_client_instead_of_dropping_it() {
let model = std::sync::Arc::new(PreemptStubModel::default());
let (victim, mut victim_rx) = active_seq(1, 2);
let (s, sp) = park(&model, victim, "swapin-blocking");
let r = resume_swapped_seq(
None,
None,
&*model,
&SchedIo::for_test_with(model.clone()),
s,
&sp,
);
assert!(r.is_err(), "restore failed, so the resume must fail");
let sent = victim_rx.try_recv().expect(
"the parked request's sink was DROPPED — its client reads a server-side \
swap-in failure as its own abort",
);
let Err(err) = sent else {
panic!("a failed swap-in must not report success to the client");
};
assert!(
format!("{err:#}").contains("swap-in failed"),
"got: {err:#}"
);
}
#[test]
fn swap_in_failure_sends_a_terminal_frame_to_a_streaming_client() {
let model = std::sync::Arc::new(PreemptStubModel::default());
let (victim, mut victim_rx) = streaming_seq(1, 2);
let (s, sp) = park(&model, victim, "swapin-streaming");
assert!(
resume_swapped_seq(
None,
None,
&*model,
&SchedIo::for_test_with(model.clone()),
s,
&sp
)
.is_err()
);
match victim_rx.try_recv() {
Ok(crate::api::StreamEvent::Error(msg)) => assert!(
msg.contains("swap-in failed"),
"terminal frame must name the failure, got: {msg}"
),
Ok(_) => panic!("expected an Error frame, got a different StreamEvent"),
Err(e) => panic!(
"no terminal frame on the parked request's stream ({e:?}) — the body \
is truncated under an already-committed HTTP 200"
),
}
}