kestrel-timer

Crates.iokestrel-timer
lib.rskestrel-timer
version0.3.6
created_at2025-10-31 16:18:18.02212+00
updated_at2025-12-27 09:38:56.876831+00
descriptionHigh-performance async timer library based on Hierarchical Timing Wheel algorithm
homepagehttps://github.com/ShaoG-R/kestrel-timer
repositoryhttps://github.com/ShaoG-R/kestrel-timer
max_upload_size
id1910274
size505,465
Shao G. (shaogme)

documentation

https://docs.rs/kestrel-timer

README

Kestrel Timer

High-performance async timer library based on Hierarchical Timing Wheel algorithm

Rust Tokio Crates.io Documentation License

δΈ­ζ–‡ζ–‡ζ‘£

πŸ“š Table of Contents

Overview

kestrel-timer is a high-performance async timer library based on the Hierarchical Timing Wheel algorithm, designed for Rust and Tokio runtime. It provides O(1) time complexity for timer operations and easily handles 10,000+ concurrent timers.

Core Advantages:

  • Hierarchical timing wheel architecture that automatically separates short-delay and long-delay tasks
  • 2-12x performance improvement over traditional heap-based implementations
  • Support for timer postponement, batch operations, and completion notifications
  • Production-ready with comprehensive testing

Key Features

πŸ—οΈ Hierarchical Timing Wheel

  • Dual-layer design: L0 layer (high-precision short-delay) + L1 layer (long-delay) with automatic layering
  • Smart demotion: L1 tasks automatically demote to L0 layer when due
  • No round checks: L0 layer eliminates rounds checking for 90% of tasks

⚑ High Performance

  • O(1) time complexity: Insert, delete, and trigger operations are all O(1)
  • Optimized data structures:
    • DeferredMap (generational arena) for task indexing with O(1) operations
    • parking_lot::Mutex for efficient locking
  • Bitwise optimization: Slot count is power of 2 for fast modulo operations
  • Large-scale support: Easily handles 10,000+ concurrent timers

πŸ”„ Complete Features

  • βœ… Async callback support (based on Tokio)
  • βœ… Timer postponement (keep or replace callback)
  • βœ… Batch operations (schedule, cancel, postpone)
  • βœ… Completion notification mechanism
  • βœ… TimerService (Actor-based management)
  • βœ… Thread-safe

Quick Start

Installation

Add to your Cargo.toml:

[dependencies]
kestrel-timer = "0.2.0"
tokio = { version = "1.48", features = ["full"] }

Basic Usage

use kestrel_timer::{TimerWheel, CallbackWrapper, TimerTask};
use std::time::Duration;

#[tokio::main]
async fn main() {
    // Create timer with default config
    let timer = TimerWheel::with_defaults();
    
    // Step 1: Allocate handle
    let handle = timer.allocate_handle();
    let task_id = handle.task_id();
    
    // Step 2: Create task
    let callback = Some(CallbackWrapper::new(|| async {
        println!("Timer fired!");
    }));
    let task = TimerTask::new_oneshot(Duration::from_secs(1), callback);
    
    // Step 3: Register task
    let timer_handle = timer.register(handle, task).unwrap();
    
    // Wait for completion or cancel
    // timer_handle.cancel();
}

Batch Operations

use kestrel_timer::{TimerWheel, CallbackWrapper, TimerTask};
use std::time::Duration;

let timer = TimerWheel::with_defaults();

// Step 1: Batch allocate handles
let handles = timer.allocate_handles(100);
let task_ids: Vec<_> = handles.iter().map(|h| h.task_id()).collect();

// Step 2: Create tasks
let tasks: Vec<_> = (0..100)
    .map(|i| {
        let delay = Duration::from_millis(100 + i * 10);
        let callback = Some(CallbackWrapper::new(move || async move {
            println!("Timer {} fired", i);
        }));
        TimerTask::new_oneshot(delay, callback)
    })
    .collect();

// Step 3: Batch register
let batch_handle = timer.register_batch(handles, tasks).unwrap();

// Batch cancel
batch_handle.cancel_all();

Postpone Timer

use kestrel_timer::{TimerWheel, CallbackWrapper, TimerTask};
use std::time::Duration;

let timer = TimerWheel::with_defaults();

// Step 1: Allocate handle and get task_id
let handle = timer.allocate_handle();
let task_id = handle.task_id();

// Step 2: Create and register task
let callback = Some(CallbackWrapper::new(|| async {
    println!("Original callback");
}));
let task = TimerTask::new_oneshot(Duration::from_millis(50), callback);
let timer_handle = timer.register(handle, task).unwrap();

// Postpone and keep original callback
timer.postpone(task_id, Duration::from_millis(150), None);

// Postpone and replace callback
let new_callback = Some(CallbackWrapper::new(|| async {
    println!("New callback");
}));
timer.postpone(task_id, Duration::from_millis(200), new_callback);

TimerService Usage

use kestrel_timer::{TimerWheel, TimerService, TimerTask, CallbackWrapper, TaskNotification};
use kestrel_timer::config::ServiceConfig;
use std::time::Duration;

let timer = TimerWheel::with_defaults();
let mut service = timer.create_service(ServiceConfig::default());

// Step 1: Allocate handles
let handles = service.allocate_handles(2);

// Step 2: Create tasks
let tasks: Vec<_> = vec![
    TimerTask::new_oneshot(Duration::from_millis(100), Some(CallbackWrapper::new(|| async {}))),
    TimerTask::new_oneshot(Duration::from_millis(200), Some(CallbackWrapper::new(|| async {}))),
];

// Step 3: Batch register
service.register_batch(handles, tasks).unwrap();

// Receive timeout notifications
let mut timeout_rx = service.take_receiver().unwrap();
while let Some(notification) = timeout_rx.recv().await {
    match notification {
        TaskNotification::OneShot(task_id) => {
            println!("One-shot task {:?} completed", task_id);
        },
        TaskNotification::Periodic(task_id) => {
            println!("Periodic task {:?} called", task_id);
        },
    }
}

service.shutdown().await;

Architecture

Hierarchical Timing Wheel Design

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              L1 Layer (Upper)               β”‚
β”‚  Slots: 64 | Tick: 1s | Range: 64s          β”‚
β”‚              ↓ Demote to L0                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              L0 Layer (Lower)               β”‚
β”‚  Slots: 512 | Tick: 10ms | Range: 5.12s     β”‚
β”‚              β–² Current Pointer              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

L0 Layer (Lower - High Precision):

  • Slots: 512 (default), Tick: 10ms
  • Coverage: 5.12 seconds
  • Handles 80-90% of short-delay tasks

L1 Layer (Upper - Long Duration):

  • Slots: 64 (default), Tick: 1000ms
  • Coverage: 64 seconds
  • Handles long-delay tasks with rounds mechanism

Workflow:

  1. Short delay (< 5.12s) β†’ Insert directly into L0 layer
  2. Long delay (β‰₯ 5.12s) β†’ Insert into L1 layer
  3. L1 task due β†’ Automatically demote to L0 layer
  4. L0 task due β†’ Trigger immediately

Task Indexing with DeferredMap

Uses DeferredMap (a generational arena) for efficient task management:

  • Two-Step Registration:

    • Allocate handle to get task ID (cheap, no value needed)
    • Insert task using the handle (with completion notifiers)
  • Generational Safety: Each task ID includes:

    • Lower 32 bits: Slot index
    • Upper 32 bits: Generation counter
    • Prevents use-after-free and ABA problems
  • Memory Efficiency: Union-based slot storage

    • Occupied slots: Store task data
    • Vacant slots: Store free-list pointer

Performance Optimizations

  • Hierarchical architecture: Avoids single-layer round checks, L0 layer requires no rounds checking
  • DeferredMap: O(1) task lookup, insertion, and removal with generational safety
  • Efficient locking: parking_lot::Mutex is faster than standard Mutex
  • Bitwise optimization: Slot count is power of 2, uses & (n-1) for fast modulo
  • Cache optimization: Pre-compute slot masks, tick durations, and other frequently used values
  • Batch optimization: Reduce lock contention with smart small-batch handling

Usage Examples

Full API documentation at docs.rs/kestrel-timer

Main APIs

TimerTask:

  • TimerTask::new_oneshot(delay, callback) - Create one-shot task
  • TimerTask::new_periodic(initial_delay, interval, callback, buffer_size) - Create periodic task
  • get_task_type() - Get task type
  • get_interval() - Get interval for periodic tasks

TaskHandle (Pre-allocated handle):

  • task_id() - Get task ID from handle

TimerWheel:

  • TimerWheel::with_defaults() - Create with default config
  • TimerWheel::new(config) - Create with custom config
  • allocate_handle() - Allocate single handle
  • allocate_handles(count) - Batch allocate handles
  • register(handle, task) - Register task with handle
  • register_batch(handles, tasks) - Batch register tasks
  • cancel(task_id) - Cancel task
  • cancel_batch(task_ids) - Batch cancel
  • postpone(task_id, delay, callback) - Postpone task
  • postpone_batch(updates) - Batch postpone

TimerHandle (Returned after registration):

  • cancel() - Cancel timer
  • task_id() - Get task ID

TimerService:

  • allocate_handle() - Allocate single handle
  • allocate_handles(count) - Batch allocate handles
  • register(handle, task) - Register task with handle
  • register_batch(handles, tasks) - Batch register tasks
  • take_receiver() - Get timeout notification receiver
  • cancel_task(task_id) - Cancel task
  • cancel_batch(task_ids) - Batch cancel
  • postpone(task_id, delay, callback) - Postpone task
  • postpone_batch(updates) - Batch postpone
  • shutdown() - Shutdown service

Configuration

Default Configuration

let timer = TimerWheel::with_defaults();
// L0: 512 slots Γ— 10ms = 5.12 seconds
// L1: 64 slots Γ— 1s = 64 seconds

Custom Configuration

use kestrel_timer::WheelConfig;

let config = WheelConfig::builder()
    .l0_tick_duration(Duration::from_millis(10))  // L0 tick
    .l0_slot_count(512)                            // L0 slots (must be power of 2)
    .l1_tick_duration(Duration::from_secs(1))      // L1 tick
    .l1_slot_count(64)                             // L1 slots (must be power of 2)
    .build()?;
let timer = TimerWheel::new(config);

Recommended Configurations

High Precision (Network Timeouts):

let config = WheelConfig::builder()
    .l0_tick_duration(Duration::from_millis(5))
    .l0_slot_count(1024)
    .l1_tick_duration(Duration::from_millis(500))
    .l1_slot_count(64)
    .build()?;

Low Precision (Heartbeat Detection):

let config = WheelConfig::builder()
    .l0_tick_duration(Duration::from_millis(100))
    .l0_slot_count(512)
    .l1_tick_duration(Duration::from_secs(10))
    .l1_slot_count(128)
    .build()?;

Benchmarks

Run Benchmarks

cargo bench

Performance Comparison

Compared to traditional heap-based (BinaryHeap) timer implementations:

Operation Hierarchical Wheel Heap Implementation Advantage
Insert Single O(1) ~5ΞΌs O(log n) ~10-20ΞΌs 2-4x faster
Batch Insert 1000 ~2ms ~15-25ms 7-12x faster
Cancel Task O(1) ~2ΞΌs O(n) ~50-100ΞΌs 25-50x faster
Postpone Task O(1) ~4ΞΌs O(log n) ~15-30ΞΌs 4-7x faster

Large Scale Testing

cargo test --test integration_test test_large_scale_timers
  • βœ… 10,000 concurrent timers
  • βœ… Creation time < 100ms
  • βœ… All timers fire correctly

Use Cases

1. Network Timeout Management

use kestrel_timer::{TimerWheel, CallbackWrapper, TimerTask};
use std::time::Duration;

async fn handle_connection(timer: &TimerWheel, conn_id: u64) {
    // Allocate handle first
    let handle = timer.allocate_handle();
    
    // Create task
    let callback = Some(CallbackWrapper::new(move || async move {
        println!("Connection {} timed out", conn_id);
        close_connection(conn_id).await;
    }));
    let task = TimerTask::new_oneshot(Duration::from_secs(30), callback);
    
    // Register task
    let timer_handle = timer.register(handle, task).unwrap();
    
    // Cancel timeout when connection completes
    // timer_handle.cancel();
}

2. Heartbeat Detection

use kestrel_timer::{TimerWheel, TimerTask, CallbackWrapper};
use kestrel_timer::config::ServiceConfig;
use std::time::Duration;

let timer = TimerWheel::with_defaults();
let mut service = timer.create_service(ServiceConfig::default());

for client_id in client_ids {
    // Allocate handle
    let handle = service.allocate_handle();
    
    // Create and register task
    let callback = Some(CallbackWrapper::new(move || async move {
        println!("Client {} heartbeat timeout", client_id);
    }));
    let task = TimerTask::new_oneshot(Duration::from_secs(30), callback);
    service.register(handle, task).unwrap();
}

3. Cache Expiration

use kestrel_timer::{TimerTask, CallbackWrapper};
use std::sync::Arc;
use std::time::Duration;

async fn set_cache(&self, key: String, value: String, ttl: Duration) {
    self.cache.lock().insert(key.clone(), value);
    
    // Allocate handle
    let handle = self.timer.allocate_handle();
    
    // Create task with callback
    let cache = Arc::clone(&self.cache);
    let callback = Some(CallbackWrapper::new(move || {
        let cache = Arc::clone(&cache);
        let key = key.clone();
        async move {
            cache.lock().remove(&key);
        }
    }));
    let task = TimerTask::new_oneshot(ttl, callback);
    
    // Register task
    self.timer.register(handle, task).unwrap();
}

4. Game Buff System

use kestrel_timer::{TimerWheel, TimerTask, CallbackWrapper, TaskId};
use std::time::Duration;

async fn apply_buff(
    timer: &TimerWheel,
    player_id: u64,
    buff_type: BuffType,
    duration: Duration
) -> TaskId {
    // Allocate handle and get task_id
    let handle = timer.allocate_handle();
    let task_id = handle.task_id();
    
    // Create and register task
    let callback = Some(CallbackWrapper::new(move || async move {
        remove_buff(player_id, buff_type).await;
    }));
    let task = TimerTask::new_oneshot(duration, callback);
    timer.register(handle, task).unwrap();
    
    task_id
}

// Extend buff duration
timer.postpone(task_id, new_duration, None);

5. Retry Mechanism

use kestrel_timer::{TimerWheel, TimerTask, CallbackWrapper};
use std::time::Duration;

async fn retry_with_backoff(timer: &TimerWheel, operation: impl Fn()) {
    for retry in 1..=5 {
        let delay = Duration::from_secs(2_u64.pow(retry - 1));
        
        // Allocate handle
        let handle = timer.allocate_handle();
        
        // Create and register task
        let callback = Some(CallbackWrapper::new(move || async move {
            operation().await;
        }));
        let task = TimerTask::new_oneshot(delay, callback);
        timer.register(handle, task).unwrap();
    }
}

License

This project is licensed under either of:

Acknowledgments

The timing wheel algorithm was first proposed by George Varghese and Tony Lauck in the paper "Hashed and Hierarchical Timing Wheels" (SOSP '87).


Full Documentation: docs.rs/kestrel-timer

Commit count: 0

cargo fmt