nexus-timer 1.4.3

High-performance timer wheel with O(1) insert and cancel
Documentation
# Bounded vs unbounded

The timer wheel is generic over its storage backend. Two types are
pre-built:

- **`Wheel<T>`** — backed by `nexus_slab::unbounded::Slab`, grows by adding
  new chunks.
- **`BoundedWheel<T>`** — backed by `nexus_slab::bounded::Slab`, fixed
  capacity, returns `Full<T>` on exhaustion.

Both share the exact same `schedule`/`cancel`/`poll` API for the common
case — bounded just adds `try_schedule` / `try_schedule_forget` for
graceful exhaustion handling.

## When to use bounded

- You know the peak timer count at design time (you sized the order
  router for 100k orders, each has one TTL timer, so the wheel needs
  100k slots).
- You want exhaustion to be an *explicit* failure mode rather than an
  implicit allocation.
- You want stable, predictable memory use — no growth events under load.

```rust
use std::time::Instant;
use nexus_timer::BoundedWheel;

// Capacity planning: peak orders × safety factor
const PEAK_ORDERS: usize = 100_000;
const SAFETY: usize = 2;

let wheel: BoundedWheel<OrderId> =
    BoundedWheel::bounded(PEAK_ORDERS * SAFETY, Instant::now());
# #[derive(Clone, Copy)] struct OrderId(u64);
```

## When to use unbounded

- You genuinely don't know how many timers you'll have (e.g. you're
  building a general-purpose event loop).
- You want zero allocation on the hot path after warm-up, but are OK with
  occasional growth events early in the process lifetime.
- Amortized O(1) is good enough.

The unbounded slab grows by allocating *new chunks*, not by reallocating
the existing storage. Once a chunk is allocated it stays put — the
addresses of existing entries never move. This is why it's safe for a
wheel holding raw pointers to entries.

```rust
use std::time::Instant;
use nexus_timer::Wheel;

// chunk_capacity = how many entries per chunk
// Pick big enough that you rarely grow under normal load
let wheel: Wheel<u64> = Wheel::unbounded(4096, Instant::now());
```

## Growth behavior

An unbounded slab with `chunk_capacity = 4096` will:

1. Allocate its first chunk on first `alloc`.
2. Allocate a second chunk when the first fills.
3. Continue adding chunks as needed.

No chunk is ever reallocated or moved. Growth is a one-time cost amortized
over 4096 entries — negligible in steady state.

## Fallible scheduling on bounded

```rust
use std::time::{Duration, Instant};
use nexus_timer::{BoundedWheel, Full};

let now = Instant::now();
let mut wheel: BoundedWheel<u64> = BoundedWheel::bounded(8, now);

// Fill it up
for i in 0..8 {
    wheel.try_schedule_forget(now + Duration::from_millis(10), i).unwrap();
}

// 9th insert fails — you get your value back
match wheel.try_schedule_forget(now + Duration::from_millis(10), 42) {
    Ok(()) => unreachable!(),
    Err(Full(value)) => assert_eq!(value, 42),
}
```

The `Full<T>` error carries the value you tried to insert, so you don't
lose it on exhaustion.

## Memory footprint

Each wheel entry is:

- One `WheelEntry<T>` in the slab — header (~32 bytes of metadata: deadline,
  refcount, DLL pointers) plus your `T`.
- No separate allocation per timer — everything is inside the slab chunk.

For `T = u64`, each timer is ~40 bytes. A wheel holding 100k timers uses
~4 MB of slab storage plus the level slot arrays (which are tiny — 64
slots × 7 levels × pointer pair ≈ 7 KB).