use super::super::*;
use super::*;
#[test]
fn resource_lock_shared_acquires() {
let _tempfile_keep_alive = tempfile::Builder::new()
.prefix("ktstr-test-flock-shared-acquires-")
.suffix(".lock")
.tempfile()
.unwrap();
let path = _tempfile_keep_alive.path().to_str().unwrap();
let fd = try_flock(path, FlockMode::Shared).expect("open should succeed");
assert!(fd.is_some(), "shared lock on fresh file should succeed");
}
#[test]
fn resource_lock_exclusive_contention() {
let _tempfile_keep_alive = tempfile::Builder::new()
.prefix("ktstr-test-flock-excl-contention-")
.suffix(".lock")
.tempfile()
.unwrap();
let path = _tempfile_keep_alive.path().to_str().unwrap();
let holder = try_flock(path, FlockMode::Exclusive)
.expect("open should succeed")
.expect("first lock should succeed");
let second = try_flock(path, FlockMode::Exclusive).expect("open should succeed");
assert!(
second.is_none(),
"second exclusive lock while held should return None",
);
drop(holder);
}
#[test]
fn resource_lock_shared_coexist() {
let _tempfile_keep_alive = tempfile::Builder::new()
.prefix("ktstr-test-flock-shared-coexist-")
.suffix(".lock")
.tempfile()
.unwrap();
let path = _tempfile_keep_alive.path().to_str().unwrap();
let h1 = try_flock(path, FlockMode::Shared)
.expect("open should succeed")
.expect("first shared lock should succeed");
let h2 = try_flock(path, FlockMode::Shared)
.expect("open should succeed")
.expect("second shared lock should succeed");
drop(h1);
drop(h2);
}
#[test]
fn resource_lock_exclusive_blocks_shared() {
let _tempfile_keep_alive = tempfile::Builder::new()
.prefix("ktstr-test-flock-excl-blocks-sh-")
.suffix(".lock")
.tempfile()
.unwrap();
let path = _tempfile_keep_alive.path().to_str().unwrap();
let holder = try_flock(path, FlockMode::Exclusive)
.expect("open should succeed")
.expect("exclusive lock should succeed");
let shared = try_flock(path, FlockMode::Shared).expect("open should succeed");
assert!(
shared.is_none(),
"shared lock should fail while exclusive is held",
);
drop(holder);
}
#[test]
fn resource_lock_shared_blocks_exclusive() {
let _tempfile_keep_alive = tempfile::Builder::new()
.prefix("ktstr-test-flock-sh-blocks-excl-")
.suffix(".lock")
.tempfile()
.unwrap();
let path = _tempfile_keep_alive.path().to_str().unwrap();
let holder = try_flock(path, FlockMode::Shared)
.expect("open should succeed")
.expect("shared lock should succeed");
let excl = try_flock(path, FlockMode::Exclusive).expect("open should succeed");
assert!(
excl.is_none(),
"exclusive lock should fail while shared is held",
);
drop(holder);
}
#[test]
fn resource_lock_release_on_drop() {
let _tempfile_keep_alive = tempfile::Builder::new()
.prefix("ktstr-test-flock-release-drop-")
.suffix(".lock")
.tempfile()
.unwrap();
let path = _tempfile_keep_alive.path().to_str().unwrap();
{
let _holder = try_flock(path, FlockMode::Exclusive)
.expect("open should succeed")
.expect("lock should succeed");
}
let fd = try_flock(path, FlockMode::Exclusive)
.expect("open should succeed")
.expect("lock should be available after drop");
drop(fd);
}
#[test]
fn resource_lock_exclusive_success() {
let _prefixes = LockPrefixesGuard::new();
let plan = PinningPlan {
assignments: vec![(0, 90100), (1, 90101)],
service_cpu: None,
llc_indices: vec![90100],
locks: Vec::new(),
};
let llc_indices = &[90100usize];
let outcome = acquire_resource_locks(&plan, llc_indices, LlcLockMode::Exclusive).unwrap();
let (llc_offset, locks) = unwrap_acquired(outcome, None);
assert_eq!(llc_offset, 90100);
assert_eq!(locks.len(), 1);
}
#[test]
fn resource_lock_shared_includes_cpu_locks() {
let _prefixes = LockPrefixesGuard::new();
let plan = PinningPlan {
assignments: vec![(0, 90200), (1, 90201)],
service_cpu: None,
llc_indices: vec![90200],
locks: Vec::new(),
};
let llc_indices = &[90200usize];
let outcome = acquire_resource_locks(&plan, llc_indices, LlcLockMode::Shared).unwrap();
let (_, locks) = unwrap_acquired(outcome, None);
assert_eq!(locks.len(), 3);
}
#[test]
fn resource_lock_shared_with_service_cpu() {
let _prefixes = LockPrefixesGuard::new();
let plan = PinningPlan {
assignments: vec![(0, 90300)],
service_cpu: Some(90301),
llc_indices: vec![90300],
locks: Vec::new(),
};
let llc_indices = &[90300usize];
let outcome = acquire_resource_locks(&plan, llc_indices, LlcLockMode::Shared).unwrap();
let (_, locks) = unwrap_acquired(outcome, None);
assert_eq!(locks.len(), 3);
}
#[test]
fn resource_lock_exclusive_skips_cpu_locks() {
let _prefixes = LockPrefixesGuard::new();
let plan = PinningPlan {
assignments: vec![(0, 90400), (1, 90401)],
service_cpu: Some(90402),
llc_indices: vec![90400],
locks: Vec::new(),
};
let llc_indices = &[90400usize];
let outcome = acquire_resource_locks(&plan, llc_indices, LlcLockMode::Exclusive).unwrap();
let (_, locks) = unwrap_acquired(outcome, None);
assert_eq!(locks.len(), 1);
}
#[test]
fn resource_lock_contention_returns_unavailable() {
let _prefixes = LockPrefixesGuard::new();
let plan = PinningPlan {
assignments: vec![(0, 90500)],
service_cpu: None,
llc_indices: vec![90500],
locks: Vec::new(),
};
let llc_indices = &[90500usize];
let lock_path = llc_lock_path(90500);
let holder = try_flock(&lock_path, FlockMode::Exclusive)
.unwrap()
.unwrap();
let outcome = acquire_resource_locks(&plan, llc_indices, LlcLockMode::Exclusive).unwrap();
let reason = expect_unavailable(outcome, Some("while lock is held"));
assert!(
reason.contains("90500"),
"reason should identify the busy LLC: {reason}",
);
drop(holder);
}
#[test]
fn resource_lock_all_or_nothing() {
let _prefixes = LockPrefixesGuard::new();
let plan = PinningPlan {
assignments: vec![(0, 90600), (1, 90601)],
service_cpu: None,
llc_indices: vec![90600, 90601],
locks: Vec::new(),
};
let llc_indices = &[90600usize, 90601];
let llc_600 = llc_lock_path(90600);
let llc_601 = llc_lock_path(90601);
let holder = try_flock(&llc_601, FlockMode::Exclusive).unwrap().unwrap();
let outcome = acquire_resource_locks(&plan, llc_indices, LlcLockMode::Exclusive).unwrap();
assert!(
matches!(outcome, LockOutcome::Unavailable(_)),
"should fail when second LLC is busy",
);
let reacquire = try_flock(&llc_600, FlockMode::Exclusive)
.unwrap()
.expect("LLC 90600 should be released after all-or-nothing failure");
drop(reacquire);
drop(holder);
}
#[test]
fn resource_lock_shared_cpu_contention() {
let _prefixes = LockPrefixesGuard::new();
let plan = PinningPlan {
assignments: vec![(0, 90700)],
service_cpu: None,
llc_indices: vec![90700],
locks: Vec::new(),
};
let llc_indices = &[90700usize];
let llc_path = llc_lock_path(90700);
let cpu_path = cpu_lock_path(90700);
let holder = try_flock(&cpu_path, FlockMode::Exclusive).unwrap().unwrap();
let outcome = acquire_resource_locks(&plan, llc_indices, LlcLockMode::Shared).unwrap();
assert!(
matches!(outcome, LockOutcome::Unavailable(_)),
"should fail when CPU lock is held",
);
let reacquire = try_flock(&llc_path, FlockMode::Shared)
.unwrap()
.expect("LLC 90700 should be released after CPU contention");
drop(reacquire);
drop(holder);
}
#[test]
fn resource_lock_empty_llc_indices() {
let plan = PinningPlan {
assignments: vec![(0, 90800)],
service_cpu: None,
llc_indices: vec![],
locks: Vec::new(),
};
let outcome = acquire_resource_locks(&plan, &[], LlcLockMode::Exclusive).unwrap();
let (llc_offset, locks) = unwrap_acquired(outcome, None);
assert_eq!(llc_offset, 0);
assert!(locks.is_empty());
}
#[test]
fn resource_lock_service_cpu_contention() {
let _prefixes = LockPrefixesGuard::new();
let plan = PinningPlan {
assignments: vec![(0, 90900)],
service_cpu: Some(90901),
llc_indices: vec![90850],
locks: Vec::new(),
};
let llc_indices = &[90850usize];
let llc_path = llc_lock_path(90850);
let cpu_900 = cpu_lock_path(90900);
let cpu_901 = cpu_lock_path(90901);
let holder = try_flock(&cpu_901, FlockMode::Exclusive).unwrap().unwrap();
let outcome = acquire_resource_locks(&plan, llc_indices, LlcLockMode::Shared).unwrap();
let reason = expect_unavailable(outcome, Some("when service CPU is held"));
assert!(
reason.contains("service CPU") && reason.contains("90901"),
"reason should mention service CPU 90901: {reason}",
);
let reacquire_llc = try_flock(&llc_path, FlockMode::Shared)
.unwrap()
.expect("LLC 90850 should be released after service CPU contention");
let reacquire_cpu = try_flock(&cpu_900, FlockMode::Exclusive)
.unwrap()
.expect("CPU 90900 should be released after service CPU contention");
drop(reacquire_llc);
drop(reacquire_cpu);
drop(holder);
}
#[test]
fn cpu_lock_window_success() {
let _cpu_prefix = CpuLockPrefixGuard::new();
let locks = try_acquire_cpu_window(91300, 3).unwrap();
assert_eq!(locks.len(), 3);
}
#[test]
fn cpu_lock_window_contention_all_or_nothing() {
let _cpu_prefix = CpuLockPrefixGuard::new();
let cpu_400 = cpu_lock_path(91400);
let cpu_401 = cpu_lock_path(91401);
let holder = try_flock(&cpu_400, FlockMode::Exclusive).unwrap().unwrap();
let result = try_acquire_cpu_window(91400, 2);
assert!(result.is_err(), "should fail when first CPU is held");
drop(holder);
let holder2 = try_flock(&cpu_401, FlockMode::Exclusive).unwrap().unwrap();
let result2 = try_acquire_cpu_window(91400, 2);
assert!(result2.is_err(), "should fail when second CPU is held");
let reacquire = try_flock(&cpu_400, FlockMode::Exclusive)
.unwrap()
.expect("CPU 91400 should be released after all-or-nothing");
drop(reacquire);
drop(holder2);
}
#[test]
fn cpu_lock_zero_count() {
let result = acquire_cpu_locks(0, 4, None).unwrap();
assert!(result.locks.is_empty());
assert!(result.cpus.is_empty());
}
#[test]
fn cpu_lock_contention_slides_window() {
let _cpu_prefix = CpuLockPrefixGuard::new();
let holder = try_flock(cpu_lock_path(91500), FlockMode::Exclusive)
.unwrap()
.unwrap();
let result = try_acquire_cpu_window(91500, 2);
assert!(result.is_err(), "window starting at held CPU should fail");
let locks = try_acquire_cpu_window(91501, 2).unwrap();
assert_eq!(locks.len(), 2);
drop(locks);
drop(holder);
}
#[test]
fn cpu_lock_acquire_success() {
let result = match acquire_cpu_locks(3, 100, None) {
Ok(r) => r,
Err(e) if e.downcast_ref::<ResourceContention>().is_some() => {
panic!("{e}");
}
Err(e) => panic!("{e:#}"),
};
assert_eq!(result.locks.len(), 3);
assert_eq!(result.cpus.len(), 3);
}
#[test]
fn pid_window_offset_spreads_adjacent_pids() {
let max_start = 33usize;
let pids: Vec<u32> = (100_000..100_100).collect();
let offsets: Vec<usize> = pids
.iter()
.map(|&p| pid_window_offset(p, max_start))
.collect();
for (pid, off) in pids.iter().zip(offsets.iter()) {
assert!(
*off < max_start,
"pid_window_offset({pid}, {max_start}) = {off}, exceeds max_start",
);
}
let unique: std::collections::HashSet<_> = offsets.iter().copied().collect();
assert!(
unique.len() >= 25,
"100 adjacent pids spread to only {} unique offsets (max_start={max_start}); \
hash mixer is losing entropy. offsets: {offsets:?}",
unique.len(),
);
let adjacent_step_count = offsets
.windows(2)
.filter(|w| {
let d = (w[0] as i64 - w[1] as i64).unsigned_abs() as usize;
d == 1 || d == max_start - 1
})
.count();
assert!(
adjacent_step_count <= 30,
"{adjacent_step_count} of {} adjacent pid pairs landed at +/-1 offset \
(max_start={max_start}); the bare `pid % {max_start}` baseline produces \
99/99 such pairs. SipHash13 avalanche should give ~6. offsets: {offsets:?}",
offsets.len() - 1,
);
let gaps: Vec<usize> = offsets
.windows(2)
.map(|w| {
let a = w[0] as i64;
let b = w[1] as i64;
(a - b).unsigned_abs() as usize
})
.collect();
let mean_gap: f64 = gaps.iter().sum::<usize>() as f64 / gaps.len() as f64;
assert!(
mean_gap > 5.0,
"mean offset gap between adjacent pids = {mean_gap:.2}, expected > 5 \
(the bare `pid % {max_start}` baseline produces gap = 1; SipHash13 \
avalanche should produce >> 5). offsets: {offsets:?}",
);
}
#[test]
fn pid_window_offset_deterministic() {
for &pid in &[1u32, 100, 12345, 999_999, u32::MAX] {
for &max_start in &[1usize, 3, 33, 1024, usize::MAX] {
assert_eq!(
pid_window_offset(pid, max_start),
pid_window_offset(pid, max_start),
"non-deterministic offset for pid={pid}, max_start={max_start}",
);
}
}
}
#[test]
fn pid_window_offset_max_start_one() {
for &pid in &[0u32, 1, 100, u32::MAX] {
assert_eq!(pid_window_offset(pid, 1), 0);
}
}
#[test]
fn cpu_lock_acquire_slides_past_held() {
let _cpu_prefix = CpuLockPrefixGuard::new();
let cpu0 = cpu_lock_path(0);
let holder = try_flock(&cpu0, FlockMode::Exclusive).unwrap().unwrap();
let result = match acquire_cpu_locks(2, 100, None) {
Ok(r) => r,
Err(e) if e.downcast_ref::<ResourceContention>().is_some() => {
drop(holder);
panic!("{e}");
}
Err(e) => panic!("{e:#}"),
};
assert_eq!(result.locks.len(), 2);
assert_eq!(result.cpus.len(), 2);
drop(result);
drop(holder);
}
#[test]
fn cpu_lock_acquire_no_windows_fit() {
let err = acquire_cpu_locks(2, 0, None).unwrap_err();
assert!(
err.downcast_ref::<ResourceContention>().is_some(),
"error should be ResourceContention: {err}",
);
}
#[test]
fn cpu_lock_acquire_with_llc_shared() {
let _prefixes = LockPrefixesGuard::new();
let topo = HostTopology::new_for_tests(&[((0..100).collect(), 0)]);
let result = match acquire_cpu_locks(2, 100, Some(&topo)) {
Ok(r) => r,
Err(e) if e.downcast_ref::<ResourceContention>().is_some() => {
panic!("{e}");
}
Err(e) => panic!("{e:#}"),
};
assert_eq!(result.locks.len(), 3);
assert_eq!(result.cpus.len(), 2);
let llc_path = llc_lock_path(0);
let shared2 = try_flock(&llc_path, FlockMode::Shared)
.unwrap()
.expect("second shared LLC should coexist");
let excl = try_flock(&llc_path, FlockMode::Exclusive).unwrap();
assert!(
excl.is_none(),
"exclusive LLC should fail while shared is held",
);
drop(shared2);
drop(result);
}
#[test]
fn cpu_lock_llc_shared_protection() {
let _llc_prefix = LlcLockPrefixGuard::new();
let topo = HostTopology::new_for_tests(&[(vec![91200, 91201], 0)]);
let cpus = vec![91200usize, 91201];
let llc_locks = acquire_llc_shared_locks(&topo, &cpus).unwrap();
assert_eq!(llc_locks.len(), 1);
let llc_path = llc_lock_path(0);
let shared2 = try_flock(&llc_path, FlockMode::Shared)
.unwrap()
.expect("second shared LLC should coexist");
let excl = try_flock(&llc_path, FlockMode::Exclusive).unwrap();
assert!(
excl.is_none(),
"exclusive LLC should fail while shared is held",
);
drop(shared2);
drop(llc_locks);
}
#[test]
fn acquire_llc_plan_none_cap_reserves_thirty_percent_cpus() {
let _llc_prefix = LlcLockPrefixGuard::new();
let _allowed = AllowedCpusGuard::new(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
let topo = HostTopology::new_for_tests(&[
(vec![0, 1], 0),
(vec![2, 3], 0),
(vec![4, 5], 0),
(vec![6, 7], 0),
(vec![8, 9], 0),
]);
let test_topo = crate::topology::TestTopology::synthetic(4, 1);
let plan = acquire_llc_plan(&topo, &test_topo, None)
.expect("clean pool must allow SH on every selected LLC");
assert_eq!(
plan.locked_llcs.len(),
2,
"budget of 3 CPUs flocks 2 LLCs (2 CPUs + 1 partial): {:?}",
plan.locked_llcs,
);
assert_eq!(
plan.cpus.len(),
3,
"plan.cpus is truncated to exactly the budget: {:?}",
plan.cpus,
);
assert_eq!(plan.locks.len(), 2, "one fd per selected LLC");
}
#[test]
fn acquire_llc_plan_bails_on_exclusive_peer() {
let _llc_prefix = LlcLockPrefixGuard::new();
let _allowed = AllowedCpusGuard::new(vec![0]);
let topo = HostTopology::new_for_tests(&[(vec![0], 0)]);
let busy_path = llc_lock_path(0);
let _peer_ex = try_flock(&busy_path, FlockMode::Exclusive)
.unwrap()
.expect("peer EX must acquire on clean pool");
let test_topo = crate::topology::TestTopology::synthetic(4, 1);
let err = acquire_llc_plan(&topo, &test_topo, None)
.expect_err("EX peer must block SH acquisition of the only LLC");
let rendered = format!("{err:#}");
assert!(
rendered.contains("LLC 0"),
"error must name the busy LLC index so fuser can trace: {rendered}",
);
assert!(
err.downcast_ref::<ResourceContention>().is_some(),
"error must downcast to ResourceContention for retry routing: {rendered}",
);
drop(_peer_ex);
}
#[test]
fn acquire_llc_plan_coexists_with_shared_peer() {
let _llc_prefix = LlcLockPrefixGuard::new();
let _allowed = AllowedCpusGuard::new(vec![0]);
let topo = HostTopology::new_for_tests(&[(vec![0], 0)]);
let shared_path = llc_lock_path(0);
let _peer_sh = try_flock(&shared_path, FlockMode::Shared)
.unwrap()
.expect("peer SH must acquire on clean pool");
let test_topo = crate::topology::TestTopology::synthetic(4, 1);
let plan = acquire_llc_plan(&topo, &test_topo, None)
.expect("second SH caller must coexist with the first");
assert_eq!(
plan.locks.len(),
topo.llc_groups.len(),
"second SH caller must acquire one fd per LLC group",
);
}
#[test]
fn cpu_cap_new_rejects_zero() {
let err = CpuCap::new(0).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("≥ 1"), "msg={msg}");
assert!(msg.contains("got 0"), "msg={msg}");
}