use super::*;
use crate::metrics::{MetricEvent, MetricId, RecordOutcome};
use helix_core::tick::ReplyBytes;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex as StdMutex;
use std::time::{Duration, Instant};
#[derive(Default)]
struct RecordingMetricSink(StdMutex<Vec<MetricEvent>>);
impl AsyncMetricSink for RecordingMetricSink {
fn try_record(&self, event: MetricEvent) -> RecordOutcome {
self.0.lock().unwrap().push(event);
RecordOutcome::Accepted
}
}
#[tokio::test]
async fn test_bounded_spawner_drop_newest_no_block() {
let executed = Arc::new(AtomicU64::new(0));
let gate = Arc::new(tokio::sync::Notify::new());
let (reply_tx, _reply_rx) = mpsc::unbounded_channel::<Tick>();
let executed_c = Arc::clone(&executed);
let gate_c = Arc::clone(&gate);
let sp: BoundedSpawner<u64> = BoundedSpawner::new(
1,
1,
Overflow::DropNewest,
reply_tx,
move |_corr, _payload: u64| {
let executed = Arc::clone(&executed_c);
let gate = Arc::clone(&gate_c);
async move {
gate.notified().await; executed.fetch_add(1, Ordering::SeqCst);
PortOutcome::Ok(ReplyBytes::default())
}
},
);
let submit_all = async {
for i in 0..100u64 {
sp.submit(Job {
corr: None,
payload: i,
})
.await;
}
};
tokio::time::timeout(Duration::from_secs(2), submit_all)
.await
.expect("DropNewest submit 必须不阻塞泵(未超时)");
for _ in 0..200 {
gate.notify_one();
}
sp.shutdown().await;
let done = executed.load(Ordering::SeqCst);
assert!(
done < 100,
"DropNewest 必须丢弃溢出条目(最新来的),实际执行 {done}/100(应远小于 100)"
);
}
#[tokio::test]
async fn test_bounded_spawner_block_no_loss() {
let executed = Arc::new(AtomicU64::new(0));
let (reply_tx, _reply_rx) = mpsc::unbounded_channel::<Tick>();
let executed_c = Arc::clone(&executed);
let sp: BoundedSpawner<u64> = BoundedSpawner::new(
1,
1,
Overflow::Block,
reply_tx,
move |_corr, _payload: u64| {
let executed = Arc::clone(&executed_c);
async move {
tokio::time::sleep(Duration::from_millis(1)).await;
executed.fetch_add(1, Ordering::SeqCst);
PortOutcome::Ok(ReplyBytes::default())
}
},
);
const N: u64 = 50;
for i in 0..N {
sp.submit(Job {
corr: None,
payload: i,
})
.await;
}
sp.shutdown().await;
assert_eq!(
executed.load(Ordering::SeqCst),
N,
"Block 模式必达零丢失:{N} 条必须全部执行"
);
}
#[tokio::test]
async fn test_bounded_spawner_n1_preserves_order() {
let order = Arc::new(Mutex::new(Vec::<u64>::new()));
let (reply_tx, _reply_rx) = mpsc::unbounded_channel::<Tick>();
let order_c = Arc::clone(&order);
let sp: BoundedSpawner<u64> = BoundedSpawner::new(
1, 8,
Overflow::Block,
reply_tx,
move |_corr, payload: u64| {
let order = Arc::clone(&order_c);
async move {
tokio::time::sleep(Duration::from_millis(10 - (payload % 10))).await;
order.lock().await.push(payload);
PortOutcome::Ok(ReplyBytes::default())
}
},
);
for i in 0..20u64 {
sp.submit(Job {
corr: None,
payload: i,
})
.await;
}
sp.shutdown().await;
let recorded = order.lock().await.clone();
let expected: Vec<u64> = (0..20).collect();
assert_eq!(
recorded, expected,
"N=1 BoundedSpawner 必须保持入队序 == 执行序"
);
}
#[tokio::test]
async fn shutdown_with_timeout_caps_and_aborts_inflight() {
let entered = Arc::new(AtomicU64::new(0));
let (reply_tx, _reply_rx) = mpsc::unbounded_channel::<Tick>();
let entered_c = Arc::clone(&entered);
let sp: BoundedSpawner<u64> = BoundedSpawner::new(
1,
1,
Overflow::Block,
reply_tx,
move |_corr, _payload: u64| {
let entered = Arc::clone(&entered_c);
async move {
entered.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(Duration::from_secs(60)).await;
PortOutcome::Ok(ReplyBytes::default())
}
},
);
sp.submit(Job {
corr: Some(Correlation::from_raw(1)),
payload: 1,
})
.await;
for _ in 0..200 {
if entered.load(Ordering::SeqCst) == 1 {
break;
}
tokio::time::sleep(Duration::from_millis(5)).await;
}
assert_eq!(
entered.load(Ordering::SeqCst),
1,
"worker 应已进入在途 sleep"
);
let t = Instant::now();
let all_drained = sp.shutdown_with_timeout(Duration::from_millis(50)).await;
let elapsed = t.elapsed();
assert!(
!all_drained,
"在途 worker 卡 60s,50ms 上限必超时返回 false(abort 在途,不等满)"
);
assert!(
elapsed < Duration::from_secs(5),
"shutdown_with_timeout 必在远小于 worker 60s sleep 内返回(abort 在途),实际 {elapsed:?}"
);
}
#[tokio::test]
async fn shutdown_with_timeout_returns_true_when_drained_in_limit() {
let executed = Arc::new(AtomicU64::new(0));
let (reply_tx, _reply_rx) = mpsc::unbounded_channel::<Tick>();
let executed_c = Arc::clone(&executed);
let sp: BoundedSpawner<u64> = BoundedSpawner::new(
1,
8,
Overflow::Block,
reply_tx,
move |_corr, _payload: u64| {
let executed = Arc::clone(&executed_c);
async move {
executed.fetch_add(1, Ordering::SeqCst);
PortOutcome::Ok(ReplyBytes::default())
}
},
);
for i in 0..5u64 {
sp.submit(Job {
corr: None,
payload: i,
})
.await;
}
let all_drained = sp.shutdown_with_timeout(Duration::from_secs(5)).await;
assert!(all_drained, "秒级 job 在 5s 上限内必全部 drain 完成 → true");
assert_eq!(
executed.load(Ordering::SeqCst),
5,
"limit 内 drain:5 条必达 job 全部执行(不误 abort)"
);
}
#[tokio::test]
async fn test_bounded_spawner_reports_via_reply_tx() {
let (reply_tx, mut reply_rx) = mpsc::unbounded_channel::<Tick>();
let sp: BoundedSpawner<u64> = BoundedSpawner::new(
1,
8,
Overflow::Block,
reply_tx,
move |_corr, _payload: u64| async move { PortOutcome::Ok(ReplyBytes::default()) },
);
sp.submit(Job {
corr: Some(Correlation::from_raw(42)),
payload: 1,
})
.await;
sp.shutdown().await;
let reply = reply_rx.try_recv().expect("应收到 PortReply 回灌");
match reply {
Tick::PortReply { corr, .. } => assert_eq!(corr.raw(), 42),
_ => panic!("期望 Tick::PortReply"),
}
}
#[tokio::test]
async fn observed_spawner_exposes_queue_and_execution_boundaries() {
let metrics = Arc::new(RecordingMetricSink::default());
let (reply_tx, _reply_rx) = mpsc::unbounded_channel::<Tick>();
let sp = BoundedSpawner::new_observed(
1,
2,
Overflow::Block,
reply_tx,
metrics.clone(),
"persist",
move |_corr, _payload: u64| async move { PortOutcome::Ok(ReplyBytes::default()) },
);
sp.submit(Job {
corr: None,
payload: 7,
})
.await;
sp.shutdown().await;
let events = metrics.0.lock().unwrap();
for expected in [
MetricId::PoolQueueCapacity,
MetricId::PoolWorkers,
MetricId::PoolEnqueueBlockSeconds,
MetricId::PoolQueueResidencySeconds,
MetricId::PoolExecutionSeconds,
MetricId::PoolQueueDepth,
MetricId::PoolInflight,
] {
assert!(
events.iter().any(|event| event.id == expected),
"observed pool 缺少 {:?}",
expected
);
}
assert!(events
.iter()
.any(|event| { event.id == MetricId::PoolQueueDepth && event.value == 0.0 }));
assert!(events
.iter()
.any(|event| { event.id == MetricId::PoolInflight && event.value == 0.0 }));
}