Skip to content

arcboxlabs/loadwise

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

loadwise

A protocol-agnostic load balancing strategy engine for Rust.
Pick the right node — nothing more, nothing less.

crates.io docs.rs CI License


loadwise answers one question: "given these nodes, which one should handle the next request?"

It ships 7 strategies, a health state machine, composable filters, and a pluggable state store — all in a synchronous, zero-allocation hot path. No async runtime, no tracing crate, no metrics dependency.

use loadwise::{Strategy, SelectionContext};
use loadwise::strategy::RoundRobin;

let strategy = RoundRobin::new();
let backends = ["10.0.0.1:8080", "10.0.0.2:8080", "10.0.0.3:8080"];
let ctx = SelectionContext::default();

let idx = strategy.select(&backends, &ctx).unwrap();
println!("route to {}", backends[idx]);

Why loadwise?

Most load balancers live inside a proxy. loadwise gives you the algorithm without the infrastructure — embed it in any Rust application, from an API gateway to an LLM router to a game server matchmaker.

  • Zero-cost abstractionStrategy::select returns an Option<usize>, not a reference. No allocation on the hot path.
  • Protocol-agnostic — works with HTTP, gRPC, WebSocket, database connections, or anything you can put in a slice.
  • Three opt-in traitsNode (identity), Weighted (weight), LoadMetric (load score). Implement only what your strategy needs.
  • No opinion on I/O — no async runtime, no HTTP client, no health-check poller. You report events; loadwise manages state.

Strategies

Strategy Requires Algorithm
RoundRobin Atomic sequential cycling
WeightedRoundRobin Weighted + Node Nginx smooth weighted round-robin
Random Uniform random
WeightedRandom Weighted Proportional random by weight
LeastLoad LoadMetric Pick the node with lowest load_score()
PowerOfTwoChoices LoadMetric Randomly sample 2, pick the less loaded — O(1)
ConsistentHash Node Virtual-node hash ring with caching

Compose strategies with WithFallback (generic, zero-cost) or FallbackChain (dynamic dispatch):

use loadwise::strategy::{LeastLoad, RoundRobin, WithFallback};

// Try least-load first; if no candidates remain after filtering, fall back to round-robin.
let strategy = WithFallback::new(LeastLoad, RoundRobin::new());

Health Tracking

A four-state machine with configurable thresholds:

┌─────────┐  failures ≥ ⌈t/2⌉  ┌──────────┐   failures ≥ t   ┌───────────┐
│ Healthy │ ─────────────────→ │ Degraded │ ────────────────→ │ Unhealthy │
└─────────┘                    └──────────┘                   └───────────┘
     ↑                               │                              │
     │  recovery_successes met        │ 1 success                   │ 1 success
     │                               ↓                              ↓
     │                          ┌─────────┐                   ┌────────────┐
     └──────────────────────────│ Healthy │←── recoveries ────│ Recovering │
                                └─────────┘                   └────────────┘
use loadwise::{HealthTracker, HealthStatus, ConsecutiveFailurePolicy};

let tracker = HealthTracker::new(ConsecutiveFailurePolicy::default());

tracker.report_failure(&"node-1");
tracker.report_failure(&"node-1");
tracker.report_failure(&"node-1");
assert_eq!(tracker.status(&"node-1"), HealthStatus::Unhealthy);

tracker.report_success(&"node-1");
assert_eq!(tracker.status(&"node-1"), HealthStatus::Recovering);

Filtering

Narrow candidates before selection — zero-allocation bitmask filters:

use loadwise::filter::{HealthFilter, AllOf, FnFilter, Filter};

// Only route to healthy + recovering nodes
let filter = HealthFilter::available();

// Combine filters with AND logic
let combined = AllOf::new(vec![
    Box::new(HealthFilter::healthy_only()),
    Box::new(FnFilter(|node: &MyNode| node.region == "us-east")),
]);

Observability

Bring your own metrics stack — loadwise provides a hook, not a dependency:

use loadwise::metrics::MetricsCollector;

struct PrometheusCollector;
impl MetricsCollector for PrometheusCollector {
    fn on_selection(&self, node_id: &dyn std::fmt::Debug, strategy: &str) {
        // prometheus::counter!("lb_selections", "strategy" => strategy).inc();
    }
    fn on_health_change(&self, node_id: &dyn std::fmt::Debug,
                        from: loadwise::HealthStatus, to: loadwise::HealthStatus) {
        // record transition
    }
}

Crate Structure

loadwise/
├── loadwise-core          Core traits, 7 strategies, health, filters, state store
├── loadwise               Facade — re-exports core with optional feature flags
├── loadwise-store-redis   Redis-backed StateStore (HASH-based, JSON-serialised)
└── loadwise-tower         Tower Layer / Service adapter (planned)
Crate Description
loadwise Facade — re-exports core, optional tower / redis features
loadwise-core All logic: traits, strategies, health, filters, state store
loadwise-store-redis Redis-backed StateStore via single HASH
loadwise-tower Tower Layer / Service adapter (planned)

Installation

[dependencies]
loadwise = "0.1"

MSRV: 1.85 (Rust edition 2024)

Design Principles

  1. select() is synchronous — returns an index into the candidate slice. No futures, no allocation.
  2. Interior mutability — strategies use AtomicUsize or Mutex internally; callers hold &self.
  3. Fingerprint-based caching — stateful strategies (hash ring, WRR weights) detect candidate-set changes automatically via a hash of node IDs.
  4. Pluggable everythingStateStore for persistence, HealthPolicy for state transitions, MetricsCollector for observability. Implement the trait, bring your own backend.

Contributing

Contributions are welcome! Please make sure cargo test passes and new public items have /// doc comments with examples.

License

Licensed under either of Apache License, Version 2.0 or MIT License at your option.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages