load-balancer 0.5.0

Asynchronous load balancing utilities for Rust — round-robin, random, cooldown, window, fault-tolerant, proxy pool with health checks, and token-bucket rate limiting.
Documentation
# load-balancer

A set of asynchronous load balancers for Rust, built on Tokio.

| Module | Strategy |
|---|---|
| `round_robin` | Sequential round-robin allocation |
| `random` | Uniform random selection |
| `window` | Per-entry rate limit over a time window |
| `fault_tolerant` | Rate limit + error tracking with auto-disabling |
| `cooldown` | Per-entry cooldown interval between allocations |
| `proxy_pool` | HTTP proxy pool with health checking and latency sorting |
| `rate_limit` | Token-bucket rate limiter (single + per-key map) |

All balancers (except `RateLimiter`) implement the `LoadBalancer<T>` trait:

```rust
pub trait LoadBalancer<T>: Send + Sync + Clone + 'static {
    fn alloc(&self) -> impl Future<Output = T> + Send;
    fn try_alloc(&self) -> Option<T>;
}
```

## Examples

### RoundRobin

```rust
use load_balancer::round_robin::RoundRobin;
use load_balancer::LoadBalancer;

#[tokio::main]
async fn main() {
    let lb = RoundRobin::new(vec!["node-a", "node-b", "node-c"]);

    for _ in 0..6 {
        println!("{}", lb.alloc().await);
    }
}
```

```
node-a
node-b
node-c
node-a
node-b
node-c
```

---

### Random

```rust
use load_balancer::random::Random;
use load_balancer::LoadBalancer;

#[tokio::main]
async fn main() {
    let lb = Random::new(vec!["s1", "s2", "s3", "s4"]);

    for _ in 0..5 {
        println!("{}", lb.alloc().await);
    }
}
```

```
s3
s1
s4
s2
s3
```

---

### Window

Limits the number of allocations per entry within a time window.

```rust
use load_balancer::window::Window;
use load_balancer::LoadBalancer;
use std::time::Duration;

#[tokio::main]
async fn main() {
    // node-1: max 5/sec, node-2: max 3/sec
    let lb = Window::new(vec![(5, "node-1"), (3, "node-2")]);

    for _ in 0..10 {
        println!("{}", lb.alloc().await);
    }
}
```

```
node-1
node-1
node-1
node-1
node-1
node-2
node-2
node-2
node-1   ← window reset after 1s
node-1
```

Custom interval:

```rust
let lb = Window::new_interval(
    vec![(3, "db-1"), (1, "db-2")],
    Duration::from_secs(5),
);
```

---

### FaultTolerant

Extends `Window` with error tracking: entries that exceed their error
threshold are auto-disabled until the next reset cycle. Call `success()`
to gradually restore an entry's reputation.

```rust
use load_balancer::fault_tolerant::FaultTolerant;
use load_balancer::LoadBalancer;

#[tokio::main]
async fn main() {
    // (max_count, max_error_count, value)
    let lb = FaultTolerant::new(vec![
        (5, 2, "api-1"),
        (5, 2, "api-2"),
        (5, 2, "api-3"),
    ]);

    for _ in 0..10 {
        let (index, node) = lb.alloc_skip(usize::MAX).await;

        // Simulate: the first node always fails
        if index == 0 {
            lb.failure(index);
            println!("{node} → FAIL (errors will disable it)");
        } else {
            lb.success(index);
            println!("{node} → OK");
        }
    }
}
```

```
api-1 → FAIL
api-1 → FAIL
api-2 → OK    ← api-1 is now disabled
api-2 → OK
api-2 → OK
api-2 → OK
api-2 → OK
api-3 → OK
api-3 → OK
api-3 → OK
```

---

### Cooldown

Each entry enters a cooldown period after allocation and cannot be
reused until it expires. Use `tokio::time::sleep` to wait it out,
or let `alloc().await` block until an entry becomes available.

```rust
use load_balancer::cooldown::Cooldown;
use load_balancer::LoadBalancer;
use std::time::Duration;

#[tokio::main]
async fn main() {
    let lb = Cooldown::new(vec![
        (Duration::from_secs(2), "slow"),
        (Duration::ZERO, "fast"),
    ]);

    // slow gets picked first, then enters its 2 s cooldown
    println!("{}", lb.alloc().await); // slow
    println!("{}", lb.alloc().await); // fast (slow is cooling down)
    println!("{}", lb.alloc().await); // fast

    // wait for slow's cooldown to expire
    tokio::time::sleep(Duration::from_secs(2)).await;

    println!("{}", lb.alloc().await); // slow again
    println!("{}", lb.alloc().await); // fast
}
```

```
slow
fast
fast
slow
fast
```

---

### ProxyPool

HTTP proxy pool with health checking, latency-based sorting, and
automatic dead-proxy removal.

```rust
use load_balancer::proxy_pool::ProxyPool;
use load_balancer::LoadBalancer;
use std::time::Duration;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let pool = ProxyPool::new([
        "socks5://127.0.0.1:1080",
        "http://proxy1.example.com:8080",
        "http://proxy2.example.com:8080",
    ])
    .timeout(Duration::from_secs(10))
    .max_check_concurrency(500);

    // Health check: remove dead proxies, sort by latency ascending
    pool.check(2).await?;

    // Auto-check every 60 seconds
    pool.spawn_check(Duration::from_secs(60), 2).await?;

    // Round-robin allocation
    for _ in 0..3 {
        println!("{}", pool.alloc().await);
    }

    Ok(())
}
```

Adding proxies dynamically:

```rust
pool.extend(["http://new-proxy:3128"]).await;           // append only, no validation
pool.extend_check(["http://new-proxy:3128"], 2).await?; // append and validate immediately
```

---

### RateLimiter

A standalone global rate limiter. Does not implement `LoadBalancer`;
useful for protecting downstream APIs.

```rust
use load_balancer::rate_limit::RateLimiter;
use std::time::Duration;

#[tokio::main]
async fn main() {
    // Max 3 requests per second
    let limiter = RateLimiter::new_rate(3);

    for i in 0..5 {
        if limiter.check().await {
            println!("request {i} → allowed");
        } else {
            let wait = limiter.time_until_available().await;
            println!("request {i} → denied, retry in {wait:?}");
        }
    }
}
```

```
request 0 → allowed
request 1 → allowed
request 2 → allowed
request 3 → denied, retry in 1s
request 4 → denied, retry in 1s
```

### RateLimiterMap

Per-key rate limiting with independent limits per key. Keys that haven't been
inserted are rejected by default — call [`insert_rate`] or [`insert_rate_interval`] first.

```rust
use load_balancer::rate_limit::RateLimiterMap;
use std::time::Duration;

#[tokio::main]
async fn main() {
    let map = RateLimiterMap::new();

    // Each key gets its own limit
    map.insert_rate("alice", 3);                                // 3 req/s
    map.insert_rate_interval("bob", 10, Duration::from_secs(5)); // 10 req/5s

    for i in 0..4 {
        if map.check(&"alice").await {
            println!("alice {i} → allowed");
        } else {
            println!("alice {i} → denied");
        }
    }

    // Not inserted — rejected
    assert!(!map.check(&"charlie").await);

    // Remove a key to lift its limit (removed keys are also rejected)
    map.remove(&"alice");
}
```

```
alice 0 → allowed
alice 1 → allowed
alice 2 → allowed
alice 3 → denied
```

---

## Custom Timers

Every balancer (`RoundRobin`, `Random`, `Cooldown`, `Window`, `FaultTolerant`,
`ProxyPool`) exposes an `update_timer` method that spawns a background task to run
a custom handler at a fixed interval. This is useful for dynamic reconfiguration,
reloading entries from a file, or periodic health checks without blocking.

```rust
use load_balancer::round_robin::RoundRobin;
use std::time::Duration;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let lb = RoundRobin::new(vec!["node-a", "node-b"]);

    // Refresh the pool every 30 seconds
    let handle = lb.update_timer(
        |inner| async move {
            // e.g. fetch fresh node list, replace entries, reset cursor
            println!("reloading…");
            Ok(())
        },
        Duration::from_secs(30),
    ).await?;

    // … use the balancer …

    handle.abort(); // stop the timer when done
    Ok(())
}
```