use crate::config::{BatchConfig, WheelConfig};
use crate::task::{TaskCompletionReason, TaskId, TaskLocation, TimerTask};
use rustc_hash::FxHashMap;
use std::time::Duration;
use smallvec::SmallVec;
struct WheelLayer {
slots: Vec<Vec<TimerTask>>,
current_tick: u64,
slot_count: usize,
tick_duration: Duration,
tick_duration_ms: u64,
slot_mask: usize,
}
impl WheelLayer {
fn new(slot_count: usize, tick_duration: Duration) -> Self {
let mut slots = Vec::with_capacity(slot_count);
for _ in 0..slot_count {
slots.push(Vec::with_capacity(4));
}
let tick_duration_ms = tick_duration.as_millis() as u64;
let slot_mask = slot_count - 1;
Self {
slots,
current_tick: 0,
slot_count,
tick_duration,
tick_duration_ms,
slot_mask,
}
}
fn delay_to_ticks(&self, delay: Duration) -> u64 {
let ticks = delay.as_millis() as u64 / self.tick_duration.as_millis() as u64;
ticks.max(1) }
}
pub struct Wheel {
l0: WheelLayer,
l1: WheelLayer,
l1_tick_ratio: u64,
task_index: FxHashMap<TaskId, TaskLocation>,
batch_config: BatchConfig,
l0_capacity_ms: u64,
l1_capacity_ticks: u64,
}
impl Wheel {
pub fn new(config: WheelConfig, batch_config: BatchConfig) -> Self {
let h_config = config.hierarchical;
let l0 = WheelLayer::new(h_config.l0_slot_count, h_config.l0_tick_duration);
let l1 = WheelLayer::new(h_config.l1_slot_count, h_config.l1_tick_duration);
let l1_tick_ratio = l1.tick_duration_ms / l0.tick_duration_ms;
let l0_capacity_ms = (l0.slot_count as u64) * l0.tick_duration_ms;
let l1_capacity_ticks = l1.slot_count as u64;
Self {
l0,
l1,
l1_tick_ratio,
task_index: FxHashMap::default(),
batch_config,
l0_capacity_ms,
l1_capacity_ticks,
}
}
#[allow(dead_code)]
pub fn current_tick(&self) -> u64 {
self.l0.current_tick
}
#[allow(dead_code)]
pub fn tick_duration(&self) -> Duration {
self.l0.tick_duration
}
#[allow(dead_code)]
pub fn slot_count(&self) -> usize {
self.l0.slot_count
}
#[allow(dead_code)]
pub fn delay_to_ticks(&self, delay: Duration) -> u64 {
self.l0.delay_to_ticks(delay)
}
#[inline(always)]
fn determine_layer(&self, delay: Duration) -> (u8, u64, u32) {
let delay_ms = delay.as_millis() as u64;
if delay_ms < self.l0_capacity_ms {
let l0_ticks = (delay_ms / self.l0.tick_duration_ms).max(1);
return (0, l0_ticks, 0);
}
let l1_ticks = (delay_ms / self.l1.tick_duration_ms).max(1);
if l1_ticks < self.l1_capacity_ticks {
(1, l1_ticks, 0)
} else {
let rounds = (l1_ticks / self.l1_capacity_ticks) as u32;
(1, l1_ticks, rounds)
}
}
#[inline]
pub fn insert(&mut self, mut task: TimerTask, notifier: crate::task::CompletionNotifier) -> TaskId {
let (level, ticks, rounds) = self.determine_layer(task.delay);
let (current_tick, slot_mask, slots) = match level {
0 => (self.l0.current_tick, self.l0.slot_mask, &mut self.l0.slots),
_ => (self.l1.current_tick, self.l1.slot_mask, &mut self.l1.slots),
};
let total_ticks = current_tick + ticks;
let slot_index = (total_ticks as usize) & slot_mask;
task.prepare_for_registration(notifier, total_ticks, rounds);
let task_id = task.id;
let vec_index = slots[slot_index].len();
let location = TaskLocation::new(level, slot_index, vec_index);
slots[slot_index].push(task);
self.task_index.insert(task_id, location);
task_id
}
#[inline]
pub fn insert_batch(&mut self, tasks: Vec<(TimerTask, crate::task::CompletionNotifier)>) -> Vec<TaskId> {
let task_count = tasks.len();
self.task_index.reserve(task_count);
let mut task_ids = Vec::with_capacity(task_count);
for (mut task, notifier) in tasks {
let (level, ticks, rounds) = self.determine_layer(task.delay);
let (current_tick, slot_mask, slots) = match level {
0 => (self.l0.current_tick, self.l0.slot_mask, &mut self.l0.slots),
_ => (self.l1.current_tick, self.l1.slot_mask, &mut self.l1.slots),
};
let total_ticks = current_tick + ticks;
let slot_index = (total_ticks as usize) & slot_mask;
task.prepare_for_registration(notifier, total_ticks, rounds);
let task_id = task.id;
let vec_index = slots[slot_index].len();
let location = TaskLocation::new(level, slot_index, vec_index);
slots[slot_index].push(task);
self.task_index.insert(task_id, location);
task_ids.push(task_id);
}
task_ids
}
#[inline]
pub fn cancel(&mut self, task_id: TaskId) -> bool {
let location = match self.task_index.remove(&task_id) {
Some(loc) => loc,
None => return false,
};
let slot = match location.level {
0 => &mut self.l0.slots[location.slot_index],
_ => &mut self.l1.slots[location.slot_index],
};
if location.vec_index >= slot.len() || slot[location.vec_index].id != task_id {
self.task_index.insert(task_id, location);
return false;
}
if let Some(notifier) = slot[location.vec_index].completion_notifier.take() {
let _ = notifier.0.send(TaskCompletionReason::Cancelled);
}
let removed_task = slot.swap_remove(location.vec_index);
if location.vec_index < slot.len() {
let swapped_task_id = slot[location.vec_index].id;
if let Some(swapped_location) = self.task_index.get_mut(&swapped_task_id) {
swapped_location.vec_index = location.vec_index;
}
}
debug_assert_eq!(removed_task.id, task_id);
true
}
#[inline]
pub fn cancel_batch(&mut self, task_ids: &[TaskId]) -> usize {
let mut cancelled_count = 0;
if task_ids.len() <= self.batch_config.small_batch_threshold {
for &task_id in task_ids {
if self.cancel(task_id) {
cancelled_count += 1;
}
}
return cancelled_count;
}
let l0_slot_count = self.l0.slot_count;
let l1_slot_count = self.l1.slot_count;
let mut l0_tasks_by_slot: Vec<SmallVec<[(TaskId, usize); 4]>> =
vec![SmallVec::new(); l0_slot_count];
let mut l1_tasks_by_slot: Vec<SmallVec<[(TaskId, usize); 4]>> =
vec![SmallVec::new(); l1_slot_count];
for &task_id in task_ids {
if let Some(location) = self.task_index.get(&task_id) {
if location.level == 0 {
l0_tasks_by_slot[location.slot_index].push((task_id, location.vec_index));
} else {
l1_tasks_by_slot[location.slot_index].push((task_id, location.vec_index));
}
}
}
for (slot_index, tasks) in l0_tasks_by_slot.iter_mut().enumerate() {
if tasks.is_empty() {
continue;
}
tasks.sort_unstable_by(|a, b| b.1.cmp(&a.1));
let slot = &mut self.l0.slots[slot_index];
for &(task_id, vec_index) in tasks.iter() {
if vec_index < slot.len() && slot[vec_index].id == task_id {
if let Some(notifier) = slot[vec_index].completion_notifier.take() {
let _ = notifier.0.send(TaskCompletionReason::Cancelled);
}
slot.swap_remove(vec_index);
if vec_index < slot.len() {
let swapped_task_id = slot[vec_index].id;
if let Some(swapped_location) = self.task_index.get_mut(&swapped_task_id) {
swapped_location.vec_index = vec_index;
}
}
self.task_index.remove(&task_id);
cancelled_count += 1;
}
}
}
for (slot_index, tasks) in l1_tasks_by_slot.iter_mut().enumerate() {
if tasks.is_empty() {
continue;
}
tasks.sort_unstable_by(|a, b| b.1.cmp(&a.1));
let slot = &mut self.l1.slots[slot_index];
for &(task_id, vec_index) in tasks.iter() {
if vec_index < slot.len() && slot[vec_index].id == task_id {
if let Some(notifier) = slot[vec_index].completion_notifier.take() {
let _ = notifier.0.send(TaskCompletionReason::Cancelled);
}
slot.swap_remove(vec_index);
if vec_index < slot.len() {
let swapped_task_id = slot[vec_index].id;
if let Some(swapped_location) = self.task_index.get_mut(&swapped_task_id) {
swapped_location.vec_index = vec_index;
}
}
self.task_index.remove(&task_id);
cancelled_count += 1;
}
}
}
cancelled_count
}
pub fn advance(&mut self) -> Vec<TimerTask> {
self.l0.current_tick += 1;
let mut expired_tasks = Vec::new();
let l0_slot_index = (self.l0.current_tick as usize) & self.l0.slot_mask;
let l0_slot = &mut self.l0.slots[l0_slot_index];
let i = 0;
while i < l0_slot.len() {
let task = &l0_slot[i];
self.task_index.remove(&task.id);
let expired_task = l0_slot.swap_remove(i);
if i < l0_slot.len() {
let swapped_task_id = l0_slot[i].id;
if let Some(swapped_location) = self.task_index.get_mut(&swapped_task_id) {
swapped_location.vec_index = i;
}
}
expired_tasks.push(expired_task);
}
if self.l0.current_tick % self.l1_tick_ratio == 0 {
self.l1.current_tick += 1;
let l1_slot_index = (self.l1.current_tick as usize) & self.l1.slot_mask;
let l1_slot = &mut self.l1.slots[l1_slot_index];
let mut tasks_to_demote = Vec::new();
let mut i = 0;
while i < l1_slot.len() {
let task = &mut l1_slot[i];
if task.rounds > 0 {
task.rounds -= 1;
if let Some(location) = self.task_index.get_mut(&task.id) {
location.vec_index = i;
}
i += 1;
} else {
self.task_index.remove(&task.id);
let task_to_demote = l1_slot.swap_remove(i);
if i < l1_slot.len() {
let swapped_task_id = l1_slot[i].id;
if let Some(swapped_location) = self.task_index.get_mut(&swapped_task_id) {
swapped_location.vec_index = i;
}
}
tasks_to_demote.push(task_to_demote);
}
}
self.demote_tasks(tasks_to_demote);
}
expired_tasks
}
fn demote_tasks(&mut self, tasks: Vec<TimerTask>) {
for task in tasks {
let l1_tick_ratio = self.l1_tick_ratio;
let l1_deadline = task.deadline_tick;
let l0_deadline_tick = l1_deadline * l1_tick_ratio;
let l0_current_tick = self.l0.current_tick;
let remaining_l0_ticks = if l0_deadline_tick > l0_current_tick {
l0_deadline_tick - l0_current_tick
} else {
1 };
let target_l0_tick = l0_current_tick + remaining_l0_ticks;
let l0_slot_index = (target_l0_tick as usize) & self.l0.slot_mask;
let task_id = task.id;
let vec_index = self.l0.slots[l0_slot_index].len();
let location = TaskLocation::new(0, l0_slot_index, vec_index);
self.l0.slots[l0_slot_index].push(task);
self.task_index.insert(task_id, location);
}
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.task_index.is_empty()
}
#[inline]
pub fn postpone(
&mut self,
task_id: TaskId,
new_delay: Duration,
new_callback: Option<crate::task::CallbackWrapper>,
) -> bool {
let old_location = match self.task_index.remove(&task_id) {
Some(loc) => loc,
None => return false,
};
let slot = match old_location.level {
0 => &mut self.l0.slots[old_location.slot_index],
_ => &mut self.l1.slots[old_location.slot_index],
};
if old_location.vec_index >= slot.len() || slot[old_location.vec_index].id != task_id {
self.task_index.insert(task_id, old_location);
return false;
}
let mut task = slot.swap_remove(old_location.vec_index);
if old_location.vec_index < slot.len() {
let swapped_task_id = slot[old_location.vec_index].id;
if let Some(swapped_location) = self.task_index.get_mut(&swapped_task_id) {
swapped_location.vec_index = old_location.vec_index;
}
}
task.delay = new_delay;
if let Some(callback) = new_callback {
task.callback = Some(callback);
}
let (new_level, ticks, new_rounds) = self.determine_layer(new_delay);
let (current_tick, slot_mask, slots) = match new_level {
0 => (self.l0.current_tick, self.l0.slot_mask, &mut self.l0.slots),
_ => (self.l1.current_tick, self.l1.slot_mask, &mut self.l1.slots),
};
let total_ticks = current_tick + ticks;
let new_slot_index = (total_ticks as usize) & slot_mask;
task.deadline_tick = total_ticks;
task.rounds = new_rounds;
let new_vec_index = slots[new_slot_index].len();
let new_location = TaskLocation::new(new_level, new_slot_index, new_vec_index);
slots[new_slot_index].push(task);
self.task_index.insert(task_id, new_location);
true
}
#[inline]
pub fn postpone_batch(
&mut self,
updates: Vec<(TaskId, Duration)>,
) -> usize {
let mut postponed_count = 0;
for (task_id, new_delay) in updates {
if self.postpone(task_id, new_delay, None) {
postponed_count += 1;
}
}
postponed_count
}
pub fn postpone_batch_with_callbacks(
&mut self,
updates: Vec<(TaskId, Duration, Option<crate::task::CallbackWrapper>)>,
) -> usize {
let mut postponed_count = 0;
for (task_id, new_delay, new_callback) in updates {
if self.postpone(task_id, new_delay, new_callback) {
postponed_count += 1;
}
}
postponed_count
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::task::CallbackWrapper;
#[test]
fn test_wheel_creation() {
let wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
assert_eq!(wheel.slot_count(), 512);
assert_eq!(wheel.current_tick(), 0);
assert!(wheel.is_empty());
}
#[test]
fn test_hierarchical_wheel_creation() {
let config = WheelConfig::default();
let wheel = Wheel::new(config, BatchConfig::default());
assert_eq!(wheel.slot_count(), 512); assert_eq!(wheel.current_tick(), 0);
assert!(wheel.is_empty());
assert_eq!(wheel.l1.slot_count, 64);
assert_eq!(wheel.l1_tick_ratio, 100); }
#[test]
fn test_hierarchical_config_validation() {
let result = WheelConfig::builder()
.l0_tick_duration(Duration::from_millis(10))
.l0_slot_count(512)
.l1_tick_duration(Duration::from_millis(15)) .l1_slot_count(64)
.build();
assert!(result.is_err());
let result = WheelConfig::builder()
.l0_tick_duration(Duration::from_millis(10))
.l0_slot_count(512)
.l1_tick_duration(Duration::from_secs(1)) .l1_slot_count(64)
.build();
assert!(result.is_ok());
}
#[test]
fn test_layer_determination() {
let config = WheelConfig::default();
let wheel = Wheel::new(config, BatchConfig::default());
let (level, _, rounds) = wheel.determine_layer(Duration::from_millis(100));
assert_eq!(level, 0);
assert_eq!(rounds, 0);
let (level, _, rounds) = wheel.determine_layer(Duration::from_secs(10));
assert_eq!(level, 1);
assert_eq!(rounds, 0);
let (level, _, rounds) = wheel.determine_layer(Duration::from_secs(120));
assert_eq!(level, 1);
assert!(rounds > 0);
}
#[test]
fn test_hierarchical_insert_and_advance() {
use crate::task::{TimerTask, CompletionNotifier};
let config = WheelConfig::default();
let mut wheel = Wheel::new(config, BatchConfig::default());
let callback = CallbackWrapper::new(|| async {});
let (tx, _rx) = tokio::sync::oneshot::channel();
let task = TimerTask::new(Duration::from_millis(100), Some(callback));
let task_id = wheel.insert(task, CompletionNotifier(tx));
let location = wheel.task_index.get(&task_id).unwrap();
assert_eq!(location.level, 0);
for _ in 0..10 {
let expired = wheel.advance();
if !expired.is_empty() {
assert_eq!(expired.len(), 1);
assert_eq!(expired[0].id, task_id);
return;
}
}
panic!("Task should have expired");
}
#[test]
fn test_hierarchical_l1_to_l0_demotion() {
use crate::task::{TimerTask, CompletionNotifier};
let config = WheelConfig::builder()
.l0_tick_duration(Duration::from_millis(10))
.l0_slot_count(512)
.l1_tick_duration(Duration::from_millis(100)) .l1_slot_count(64)
.build()
.unwrap();
let mut wheel = Wheel::new(config, BatchConfig::default());
let l1_tick_ratio = wheel.l1_tick_ratio;
assert_eq!(l1_tick_ratio, 10);
let callback = CallbackWrapper::new(|| async {});
let (tx, _rx) = tokio::sync::oneshot::channel();
let task = TimerTask::new(Duration::from_millis(6000), Some(callback));
let task_id = wheel.insert(task, CompletionNotifier(tx));
let location = wheel.task_index.get(&task_id).unwrap();
assert_eq!(location.level, 1);
let mut demoted = false;
for i in 0..610 {
wheel.advance();
if let Some(location) = wheel.task_index.get(&task_id) {
if location.level == 0 && !demoted {
demoted = true;
println!("Task demoted to L0 at L0 tick {}", i);
}
}
}
assert!(demoted, "Task should have been demoted from L1 to L0");
}
#[test]
fn test_cross_layer_cancel() {
use crate::task::{TimerTask, CompletionNotifier};
let config = WheelConfig::default();
let mut wheel = Wheel::new(config, BatchConfig::default());
let callback1 = CallbackWrapper::new(|| async {});
let (tx1, _rx1) = tokio::sync::oneshot::channel();
let task1 = TimerTask::new(Duration::from_millis(100), Some(callback1));
let task_id1 = wheel.insert(task1, CompletionNotifier(tx1));
let callback2 = CallbackWrapper::new(|| async {});
let (tx2, _rx2) = tokio::sync::oneshot::channel();
let task2 = TimerTask::new(Duration::from_secs(10), Some(callback2));
let task_id2 = wheel.insert(task2, CompletionNotifier(tx2));
assert_eq!(wheel.task_index.get(&task_id1).unwrap().level, 0);
assert_eq!(wheel.task_index.get(&task_id2).unwrap().level, 1);
assert!(wheel.cancel(task_id1));
assert!(wheel.task_index.get(&task_id1).is_none());
assert!(wheel.cancel(task_id2));
assert!(wheel.task_index.get(&task_id2).is_none());
assert!(wheel.is_empty());
}
#[test]
fn test_cross_layer_postpone() {
use crate::task::{TimerTask, CompletionNotifier};
let config = WheelConfig::default();
let mut wheel = Wheel::new(config, BatchConfig::default());
let callback = CallbackWrapper::new(|| async {});
let (tx, _rx) = tokio::sync::oneshot::channel();
let task = TimerTask::new(Duration::from_millis(100), Some(callback));
let task_id = wheel.insert(task, CompletionNotifier(tx));
assert_eq!(wheel.task_index.get(&task_id).unwrap().level, 0);
assert!(wheel.postpone(task_id, Duration::from_secs(10), None));
assert_eq!(wheel.task_index.get(&task_id).unwrap().level, 1);
assert!(wheel.postpone(task_id, Duration::from_millis(200), None));
assert_eq!(wheel.task_index.get(&task_id).unwrap().level, 0);
}
#[test]
fn test_delay_to_ticks() {
let wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
assert_eq!(wheel.delay_to_ticks(Duration::from_millis(100)), 10);
assert_eq!(wheel.delay_to_ticks(Duration::from_millis(50)), 5);
assert_eq!(wheel.delay_to_ticks(Duration::from_millis(1)), 1); }
#[test]
fn test_wheel_invalid_slot_count() {
let result = WheelConfig::builder()
.l0_slot_count(100)
.build();
assert!(result.is_err());
if let Err(crate::error::TimerError::InvalidSlotCount { slot_count, reason }) = result {
assert_eq!(slot_count, 100);
assert_eq!(reason, "L0 层槽位数量必须是 2 的幂次方");
} else {
panic!("Expected InvalidSlotCount error");
}
}
#[test]
fn test_insert_batch() {
use crate::task::{TimerTask, CompletionNotifier};
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
let tasks: Vec<(TimerTask, CompletionNotifier)> = (0..10)
.map(|i| {
let callback = CallbackWrapper::new(|| async {});
let (completion_tx, _completion_rx) = tokio::sync::oneshot::channel();
let notifier = CompletionNotifier(completion_tx);
let task = TimerTask::new(Duration::from_millis(100 + i * 10), Some(callback));
(task, notifier)
})
.collect();
let task_ids = wheel.insert_batch(tasks);
assert_eq!(task_ids.len(), 10);
assert!(!wheel.is_empty());
}
#[test]
fn test_cancel_batch() {
use crate::task::{TimerTask, CompletionNotifier};
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
let mut task_ids = Vec::new();
for i in 0..10 {
let callback = CallbackWrapper::new(|| async {});
let (completion_tx, _completion_rx) = tokio::sync::oneshot::channel();
let notifier = CompletionNotifier(completion_tx);
let task = TimerTask::new(Duration::from_millis(100 + i * 10), Some(callback));
let task_id = wheel.insert(task, notifier);
task_ids.push(task_id);
}
assert_eq!(task_ids.len(), 10);
let to_cancel = &task_ids[0..5];
let cancelled_count = wheel.cancel_batch(to_cancel);
assert_eq!(cancelled_count, 5);
let cancelled_again = wheel.cancel_batch(to_cancel);
assert_eq!(cancelled_again, 0);
let remaining = &task_ids[5..10];
let cancelled_remaining = wheel.cancel_batch(remaining);
assert_eq!(cancelled_remaining, 5);
assert!(wheel.is_empty());
}
#[test]
fn test_batch_operations_same_slot() {
use crate::task::{TimerTask, CompletionNotifier};
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
let mut task_ids = Vec::new();
for _ in 0..20 {
let callback = CallbackWrapper::new(|| async {});
let (completion_tx, _completion_rx) = tokio::sync::oneshot::channel();
let notifier = CompletionNotifier(completion_tx);
let task = TimerTask::new(Duration::from_millis(100), Some(callback));
let task_id = wheel.insert(task, notifier);
task_ids.push(task_id);
}
let cancelled_count = wheel.cancel_batch(&task_ids);
assert_eq!(cancelled_count, 20);
assert!(wheel.is_empty());
}
#[test]
fn test_postpone_single_task() {
use crate::task::{TimerTask, CompletionNotifier};
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
let callback = CallbackWrapper::new(|| async {});
let (completion_tx, _completion_rx) = tokio::sync::oneshot::channel();
let notifier = CompletionNotifier(completion_tx);
let task = TimerTask::new(Duration::from_millis(100), Some(callback));
let task_id = wheel.insert(task, notifier);
let postponed = wheel.postpone(task_id, Duration::from_millis(200), None);
assert!(postponed);
assert!(!wheel.is_empty());
for _ in 0..10 {
let expired = wheel.advance();
assert!(expired.is_empty());
}
let mut triggered = false;
for _ in 0..10 {
let expired = wheel.advance();
if !expired.is_empty() {
assert_eq!(expired.len(), 1);
assert_eq!(expired[0].id, task_id);
triggered = true;
break;
}
}
assert!(triggered);
}
#[test]
fn test_postpone_with_new_callback() {
use crate::task::{TimerTask, CompletionNotifier};
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
let old_callback = CallbackWrapper::new(|| async {});
let (completion_tx, _completion_rx) = tokio::sync::oneshot::channel();
let notifier = CompletionNotifier(completion_tx);
let task = TimerTask::new(Duration::from_millis(100), Some(old_callback.clone()));
let task_id = wheel.insert(task, notifier);
let new_callback = CallbackWrapper::new(|| async {});
let postponed = wheel.postpone(task_id, Duration::from_millis(50), Some(new_callback));
assert!(postponed);
let mut triggered = false;
for i in 0..5 {
let expired = wheel.advance();
if !expired.is_empty() {
assert_eq!(expired.len(), 1, "第 {} 次推进时应该有 1 个任务触发", i + 1);
assert_eq!(expired[0].id, task_id);
triggered = true;
break;
}
}
assert!(triggered, "任务应该在 5 个 tick 内触发");
}
#[test]
fn test_postpone_nonexistent_task() {
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
let fake_task_id = TaskId::new();
let postponed = wheel.postpone(fake_task_id, Duration::from_millis(100), None);
assert!(!postponed);
}
#[test]
fn test_postpone_batch() {
use crate::task::{TimerTask, CompletionNotifier};
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
let mut task_ids = Vec::new();
for _ in 0..5 {
let callback = CallbackWrapper::new(|| async {});
let (completion_tx, _completion_rx) = tokio::sync::oneshot::channel();
let notifier = CompletionNotifier(completion_tx);
let task = TimerTask::new(Duration::from_millis(50), Some(callback));
let task_id = wheel.insert(task, notifier);
task_ids.push(task_id);
}
let updates: Vec<_> = task_ids
.iter()
.map(|&id| (id, Duration::from_millis(150)))
.collect();
let postponed_count = wheel.postpone_batch(updates);
assert_eq!(postponed_count, 5);
for _ in 0..5 {
let expired = wheel.advance();
assert!(expired.is_empty(), "前 5 个 tick 不应该有任务触发");
}
let mut total_triggered = 0;
for _ in 0..10 {
let expired = wheel.advance();
total_triggered += expired.len();
}
assert_eq!(total_triggered, 5, "应该有 5 个任务在推进到 tick 15 时触发");
}
#[test]
fn test_postpone_batch_partial() {
use crate::task::{TimerTask, CompletionNotifier};
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
let mut task_ids = Vec::new();
for _ in 0..10 {
let callback = CallbackWrapper::new(|| async {});
let (completion_tx, _completion_rx) = tokio::sync::oneshot::channel();
let notifier = CompletionNotifier(completion_tx);
let task = TimerTask::new(Duration::from_millis(50), Some(callback));
let task_id = wheel.insert(task, notifier);
task_ids.push(task_id);
}
let fake_task_id = TaskId::new();
let mut updates: Vec<_> = task_ids[0..5]
.iter()
.map(|&id| (id, Duration::from_millis(150)))
.collect();
updates.push((fake_task_id, Duration::from_millis(150)));
let postponed_count = wheel.postpone_batch(updates);
assert_eq!(postponed_count, 5, "应该有 5 个任务成功推迟(fake_task_id 失败)");
let mut triggered_at_50ms = 0;
for _ in 0..5 {
let expired = wheel.advance();
triggered_at_50ms += expired.len();
}
assert_eq!(triggered_at_50ms, 5, "应该有 5 个未推迟的任务在 tick 5 触发");
let mut triggered_at_150ms = 0;
for _ in 0..10 {
let expired = wheel.advance();
triggered_at_150ms += expired.len();
}
assert_eq!(triggered_at_150ms, 5, "应该有 5 个推迟的任务在 tick 15 触发");
}
#[test]
fn test_multi_round_tasks() {
use crate::task::{TimerTask, CompletionNotifier};
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
let callback = CallbackWrapper::new(|| async {});
let (completion_tx, _completion_rx) = tokio::sync::oneshot::channel();
let notifier = CompletionNotifier(completion_tx);
let task = TimerTask::new(Duration::from_secs(120), Some(callback));
let task_id = wheel.insert(task, notifier);
let location = wheel.task_index.get(&task_id).unwrap();
assert_eq!(location.level, 1);
for _ in 0..6400 {
let _expired = wheel.advance();
}
let location = wheel.task_index.get(&task_id);
if let Some(loc) = location {
assert_eq!(loc.level, 1);
}
let mut triggered = false;
for _ in 0..6000 {
let expired = wheel.advance();
if expired.iter().any(|t| t.id == task_id) {
triggered = true;
break;
}
}
assert!(triggered, "任务应该在 L1 第二轮触发");
}
#[test]
fn test_minimum_delay() {
use crate::task::{TimerTask, CompletionNotifier};
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
let callback = CallbackWrapper::new(|| async {});
let (completion_tx, _completion_rx) = tokio::sync::oneshot::channel();
let notifier = CompletionNotifier(completion_tx);
let task = TimerTask::new(Duration::from_millis(1), Some(callback));
let task_id: TaskId = wheel.insert(task, notifier);
let expired = wheel.advance();
assert_eq!(expired.len(), 1, "最小延迟任务应该在 1 tick 后触发");
assert_eq!(expired[0].id, task_id);
}
#[test]
fn test_empty_batch_operations() {
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
let task_ids = wheel.insert_batch(vec![]);
assert_eq!(task_ids.len(), 0);
let cancelled = wheel.cancel_batch(&[]);
assert_eq!(cancelled, 0);
let postponed = wheel.postpone_batch(vec![]);
assert_eq!(postponed, 0);
}
#[test]
fn test_postpone_same_task_multiple_times() {
use crate::task::{TimerTask, CompletionNotifier};
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
let callback = CallbackWrapper::new(|| async {});
let (completion_tx, _completion_rx) = tokio::sync::oneshot::channel();
let notifier = CompletionNotifier(completion_tx);
let task = TimerTask::new(Duration::from_millis(100), Some(callback));
let task_id = wheel.insert(task, notifier);
let postponed = wheel.postpone(task_id, Duration::from_millis(200), None);
assert!(postponed, "第一次推迟应该成功");
let postponed = wheel.postpone(task_id, Duration::from_millis(300), None);
assert!(postponed, "第二次推迟应该成功");
let postponed = wheel.postpone(task_id, Duration::from_millis(50), None);
assert!(postponed, "第三次推迟应该成功");
let mut triggered = false;
for _ in 0..5 {
let expired = wheel.advance();
if !expired.is_empty() {
assert_eq!(expired.len(), 1);
assert_eq!(expired[0].id, task_id);
triggered = true;
break;
}
}
assert!(triggered, "任务应该在最后一次推迟的时间触发");
}
#[test]
fn test_advance_empty_slots() {
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
for _ in 0..100 {
let expired = wheel.advance();
assert!(expired.is_empty(), "空槽位不应该返回任何任务");
}
assert_eq!(wheel.current_tick(), 100, "current_tick 应该正确递增");
}
#[test]
fn test_cancel_after_postpone() {
use crate::task::{TimerTask, CompletionNotifier};
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
let callback = CallbackWrapper::new(|| async {});
let (completion_tx, _completion_rx) = tokio::sync::oneshot::channel();
let notifier = CompletionNotifier(completion_tx);
let task = TimerTask::new(Duration::from_millis(100), Some(callback));
let task_id = wheel.insert(task, notifier);
let postponed = wheel.postpone(task_id, Duration::from_millis(200), None);
assert!(postponed, "推迟应该成功");
let cancelled = wheel.cancel(task_id);
assert!(cancelled, "取消应该成功");
for _ in 0..20 {
let expired = wheel.advance();
assert!(expired.is_empty(), "已取消的任务不应该触发");
}
assert!(wheel.is_empty(), "时间轮应该为空");
}
#[test]
fn test_slot_boundary() {
use crate::task::{TimerTask, CompletionNotifier};
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
let callback1 = CallbackWrapper::new(|| async {});
let (tx1, _rx1) = tokio::sync::oneshot::channel();
let task1 = TimerTask::new(Duration::from_millis(10), Some(callback1));
let task_id_1 = wheel.insert(task1, CompletionNotifier(tx1));
let callback2 = CallbackWrapper::new(|| async {});
let (tx2, _rx2) = tokio::sync::oneshot::channel();
let task2 = TimerTask::new(Duration::from_millis(5110), Some(callback2));
let task_id_2 = wheel.insert(task2, CompletionNotifier(tx2));
let expired = wheel.advance();
assert_eq!(expired.len(), 1, "第一个任务应该在 tick 1 触发");
assert_eq!(expired[0].id, task_id_1);
let mut triggered = false;
for i in 0..510 {
let expired = wheel.advance();
if !expired.is_empty() {
assert_eq!(expired.len(), 1, "第 {} 次推进应该触发第二个任务", i + 2);
assert_eq!(expired[0].id, task_id_2);
triggered = true;
break;
}
}
assert!(triggered, "第二个任务应该在 tick 511 触发");
assert!(wheel.is_empty(), "所有任务都应该已经触发");
}
#[test]
fn test_batch_cancel_small_threshold() {
use crate::task::{TimerTask, CompletionNotifier};
let batch_config = BatchConfig {
small_batch_threshold: 5,
};
let mut wheel = Wheel::new(WheelConfig::default(), batch_config);
let mut task_ids = Vec::new();
for _ in 0..10 {
let callback = CallbackWrapper::new(|| async {});
let (tx, _rx) = tokio::sync::oneshot::channel();
let task = TimerTask::new(Duration::from_millis(100), Some(callback));
let task_id = wheel.insert(task, CompletionNotifier(tx));
task_ids.push(task_id);
}
let cancelled = wheel.cancel_batch(&task_ids[0..3]);
assert_eq!(cancelled, 3);
let cancelled = wheel.cancel_batch(&task_ids[3..10]);
assert_eq!(cancelled, 7);
assert!(wheel.is_empty());
}
#[test]
fn test_task_id_uniqueness() {
use crate::task::{TimerTask, CompletionNotifier};
let mut wheel = Wheel::new(WheelConfig::default(), BatchConfig::default());
let mut task_ids = std::collections::HashSet::new();
for _ in 0..100 {
let callback = CallbackWrapper::new(|| async {});
let (tx, _rx) = tokio::sync::oneshot::channel();
let task = TimerTask::new(Duration::from_millis(100), Some(callback));
let task_id = wheel.insert(task, CompletionNotifier(tx));
assert!(task_ids.insert(task_id), "TaskId 应该是唯一的");
}
assert_eq!(task_ids.len(), 100);
}
}