router-radix

Crates.iorouter-radix
lib.rsrouter-radix
version0.4.0
created_at2025-10-08 12:57:34.210163+00
updated_at2025-10-23 12:26:46.458139+00
descriptionA high-performance radix tree based HTTP router for Rust
homepage
repositoryhttps://github.com/cj2a7t/routerix
max_upload_size
id1873971
size273,005
cj2a7t (cj2a7t)

documentation

README

router_radix

A high-performance, thread-safe radix tree based HTTP router for Rust

License Rust

Based on Redis's radix tree implementation


๐Ÿ“– Table of Contents


๐ŸŽฏ About

router_radix is a Rust port of lua-resty-radixtree, providing fast and flexible HTTP routing. The underlying radix tree (rax) is the same battle-tested data structure used in Redis for Redis Streams and internal routing.

Why router_radix?

  • โšก High performance with lock-free queries
  • ๐Ÿ”’ Thread-safe with zero contention
  • ๐ŸŽฏ Rich matching capabilities
  • ๐Ÿฆบ Type-safe with proper error handling
  • ๐Ÿš€ Production-ready

โœจ Features

  • Path Matching: Exact paths, parameters (:id), wildcards (*path)
  • HTTP Methods: Match specific methods (GET, POST, etc.)
  • Host Matching: Match hosts with wildcard support (*.example.com)
  • Priority Routing: Higher priority routes match first
  • Custom Filters: Add custom logic with filter functions
  • Variable Expressions: Match based on request variables with regex support
  • Thread-Safe: Lock-free queries, safe for concurrent access
  • Async Compatible: Works with Tokio, async-std, etc.
  • Type-Safe: Full Rust type safety with anyhow error handling

๐Ÿš€ Quick Start

Installation

Add to your Cargo.toml:

[dependencies]
router_radix = "0.4.0"

Hello Router

use router_radix::{RadixRouter, RadixNode, RadixHttpMethod, RadixMatchOpts};

fn main() -> anyhow::Result<()> {
    // Create routes
    let routes = vec![
        RadixNode {
            id: "get_users".to_string(),
            paths: vec!["/api/users".to_string()],
            methods: Some(RadixHttpMethod::GET),
            hosts: None,
            remote_addrs: None,
            vars: None,
            filter_fn: None,
            priority: 0,
            metadata: serde_json::json!({"handler": "list_users"}),
        },
    ];

    // Initialize router
    let mut router = RadixRouter::new()?;
    router.add_routes(routes)?;

    // Match a request
    let opts = RadixMatchOpts {
        method: Some("GET".to_string()),
        ..Default::default()
    };

    if let Some(result) = router.match_route("/api/users", &opts)? {
        println!("โœ“ Matched! Route ID: {}", result.id);
        println!("  Handler: {}", result.metadata["handler"]);
        println!("  Params: {:?}", result.matched);
    }

    Ok(())
}

๐Ÿ“š Usage Guide

Basic Routing

Match exact paths:

let routes = vec![
    RadixNode {
        id: "home".to_string(),
        paths: vec!["/".to_string()],
        methods: None,
        hosts: None,
        remote_addrs: None,
        vars: None,
        filter_fn: None,
        priority: 0,
        metadata: serde_json::json!({"page": "home"}),
    },
];

let mut router = RadixRouter::new()?;
router.add_routes(routes)?;
let result = router.match_route("/", &RadixMatchOpts::default())?;

Path Parameters

Extract dynamic segments from paths:

let routes = vec![
    RadixNode {
        id: "user_detail".to_string(),
        paths: vec!["/user/:id/post/:pid".to_string()],
        // ... other fields
        metadata: serde_json::json!({"handler": "get_user_post"}),
    },
];

let mut router = RadixRouter::new()?;
router.add_routes(routes)?;
let result = router.match_route("/user/123/post/456", &RadixMatchOpts::default())?
    .expect("should match");

// Access route ID directly
assert_eq!(result.id, "user_detail");
// Extract path parameters
assert_eq!(result.matched.get("id").unwrap(), "123");
assert_eq!(result.matched.get("pid").unwrap(), "456");

Wildcards

Match remaining path segments:

let routes = vec![
    RadixNode {
        id: "static_files".to_string(),
        paths: vec!["/files/*path".to_string()],
        // ... other fields
        metadata: serde_json::json!({"handler": "serve_file"}),
    },
];

let mut router = RadixRouter::new()?;
router.add_routes(routes)?;
let result = router.match_route("/files/css/main.css", &RadixMatchOpts::default())?
    .expect("should match");

assert_eq!(result.matched.get("path").unwrap(), "css/main.css");

HTTP Methods

Match specific HTTP methods:

let routes = vec![
    RadixNode {
        id: "users_api".to_string(),
        paths: vec!["/api/users".to_string()],
        methods: Some(RadixHttpMethod::GET | RadixHttpMethod::POST), // Multiple methods
        // ... other fields
        metadata: serde_json::json!({"handler": "users"}),
    },
];

let mut router = RadixRouter::new()?;
router.add_routes(routes)?;

// GET - matches
let opts = RadixMatchOpts {
    method: Some("GET".to_string()),
    ..Default::default()
};
assert!(router.match_route("/api/users", &opts)?.is_some());

// DELETE - doesn't match
let opts = RadixMatchOpts {
    method: Some("DELETE".to_string()),
    ..Default::default()
};
assert!(router.match_route("/api/users", &opts)?.is_none());

Host Matching

Route based on hostname with wildcard support:

let routes = vec![
    RadixNode {
        id: "api_subdomain".to_string(),
        paths: vec!["/api".to_string()],
        methods: None,
        hosts: Some(vec!["*.example.com".to_string()]), // Wildcard
        // ... other fields
        metadata: serde_json::json!({"handler": "api"}),
    },
];

let mut router = RadixRouter::new()?;
router.add_routes(routes)?;

let opts = RadixMatchOpts {
    host: Some("api.example.com".to_string()),
    ..Default::default()
};
assert!(router.match_route("/api", &opts)?.is_some());

Priority Routing

Higher priority routes are matched first:

let routes = vec![
    RadixNode {
        id: "catch_all".to_string(),
        paths: vec!["/api/*".to_string()],
        priority: 0, // Lower priority
        metadata: serde_json::json!({"handler": "fallback"}),
        // ... other fields
    },
    RadixNode {
        id: "specific".to_string(),
        paths: vec!["/api/users".to_string()],
        priority: 10, // Higher priority - matches first
        metadata: serde_json::json!({"handler": "users"}),
        // ... other fields
    },
];

let mut router = RadixRouter::new()?;
router.add_routes(routes)?;
let result = router.match_route("/api/users", &RadixMatchOpts::default())?
    .expect("should match");

assert_eq!(result.id, "specific"); // Higher priority route ID
assert_eq!(result.metadata["handler"], "users"); // Higher priority wins

Advanced Features

Custom Filter Functions

Add custom matching logic:

use std::sync::Arc;
use std::collections::HashMap;

let routes = vec![
    RadixNode {
        id: "v2_api".to_string(),
        paths: vec!["/api/data".to_string()],
        filter_fn: Some(Arc::new(|vars, _opts| {
            // Custom logic: check API version
            vars.get("version").map(|v| v == "v2").unwrap_or(false)
        })),
        // ... other fields
        metadata: serde_json::json!({"handler": "api_v2"}),
    },
];

let mut router = RadixRouter::new()?;
router.add_routes(routes)?;

// With version variable - matches
let mut vars = HashMap::new();
vars.insert("version".to_string(), "v2".to_string());
let opts = RadixMatchOpts {
    vars: Some(vars),
    ..Default::default()
};
assert!(router.match_route("/api/data", &opts)?.is_some());

// Without version - doesn't match
assert!(router.match_route("/api/data", &RadixMatchOpts::default())?.is_none());

Variable Expressions

Match based on request variables:

use router_radix::Expr;
use regex::Regex;

let routes = vec![
    RadixNode {
        id: "prod_api".to_string(),
        paths: vec!["/api/users".to_string()],
        vars: Some(vec![
            Expr::Eq("env".to_string(), "production".to_string()),
            Expr::Regex("user_agent".to_string(), Regex::new("Chrome")?),
        ]),
        // ... other fields
        metadata: serde_json::json!({"handler": "prod_users"}),
    },
];

let mut router = RadixRouter::new()?;
router.add_routes(routes)?;

let mut vars = HashMap::new();
vars.insert("env".to_string(), "production".to_string());
vars.insert("user_agent".to_string(), "Chrome/120.0".to_string());

let opts = RadixMatchOpts {
    vars: Some(vars),
    ..Default::default()
};
assert!(router.match_route("/api/users", &opts)?.is_some());

๐Ÿ“‹ MatchResult Structure

The MatchResult struct contains all information about a matched route:

pub struct MatchResult {
    pub id: String,                    // Route ID - NEW in v0.4.0!
    pub metadata: serde_json::Value,   // Route metadata
    pub matched: HashMap<String, String>, // Extracted parameters
}

Accessing Route Information

if let Some(result) = router.match_route("/api/user/123", &opts)? {
    // Direct access to route ID (no need to extract from metadata)
    println!("Matched route ID: {}", result.id);
    
    // Access route metadata
    println!("Handler: {}", result.metadata["handler"]);
    println!("Version: {}", result.metadata["version"]);
    
    // Access extracted path parameters
    println!("User ID: {}", result.matched.get("id").unwrap());
    
    // Access system-provided information
    println!("Full path: {}", result.matched.get("_path").unwrap());
    println!("HTTP method: {}", result.matched.get("_method").unwrap());
}

Key Benefits of Direct ID Access

  • Performance: No need to parse metadata JSON
  • Type Safety: Direct string access instead of JSON value handling
  • Simplicity: Cleaner API for route identification
  • Consistency: ID is always available as a string

๐Ÿ›ก๏ธ Error Handling

The router uses anyhow::Result for proper error handling:

use router_radix::{RadixRouter, RadixMatchOpts};
use anyhow::Context;

fn handle_request(router: &RadixRouter, path: &str) -> anyhow::Result<String> {
    let opts = RadixMatchOpts::default();
    
    match router.match_route(path, &opts)? {
        Some(result) => {
            Ok(format!("Route ID: {}, Handler: {}", result.id, result.metadata["handler"]))
        }
        None => {
            Ok("404 Not Found".to_string())
        }
    }
    // System errors (e.g., lock errors) propagate via ?
}

Return Value Semantics:

  • Ok(Some(MatchResult)) โ†’ Route found and matched
  • Ok(None) โ†’ No matching route (normal case, not an error)
  • Err(anyhow::Error) โ†’ System error (e.g., internal lock failure)

๐Ÿ”’ Concurrency & Thread Safety

The router is fully thread-safe and optimized for concurrent access:

Architecture

  • Lock-Free Queries: Each query creates its own iterator
  • Immutable Routes: Route data is immutable after initialization
  • Pre-compiled Patterns: Regex compiled once at startup
  • Zero Contention: Multiple threads query without blocking

Usage with Multiple Threads

use std::sync::Arc;
use std::thread;

fn main() -> anyhow::Result<()> {
    let routes = vec![/* your routes */];
    let mut router = RadixRouter::new()?;
    router.add_routes(routes)?;
    let router = Arc::new(router);

    // Share across threads
    let mut handles = vec![];
    for i in 0..8 {
        let router = Arc::clone(&router);
        handles.push(thread::spawn(move || {
            let opts = RadixMatchOpts {
                method: Some("GET".to_string()),
                ..Default::default()
            };
            // Lock-free concurrent access
            router.match_route("/api/users", &opts)
        }));
    }

    for handle in handles {
        handle.join().unwrap()?;
    }

    Ok(())
}

Dynamic Routes (Optional)

For dynamic route updates, wrap in an additional RwLock:

use std::sync::{Arc, RwLock};

let router = Arc::new(RwLock::new(RadixRouter::new()?));

// Write (exclusive)
router.write().unwrap().add_route(new_route)?;

// Read (shared, many concurrent readers)
router.read().unwrap().match_route("/path", &opts)?;

โš ๏ธ Best Practice: Initialize routes at startup for best performance.


โšก Performance

Benchmark Results (Release Mode)

Single Thread:

  • Exact match: 15M+ ops/sec (hash-based, O(1))
  • Parameter extraction: 5M+ ops/sec
  • Wildcard matching: 4M+ ops/sec

Multi-threaded (8 threads):

  • Near-linear scaling
  • Zero contention on query path
  • Suitable for high-concurrency servers

Run Benchmarks

# Performance benchmark
cargo run --example benchmark --release

# Concurrency test
cargo run --example concurrency_test --release

# Stress test (10,000 routes, 16 threads)
cargo run --example stress_test --release

๐Ÿงช Examples & Tests

Built-in Examples

The project includes comprehensive examples:

Example Description Lines
basic.rs Basic usage and core features 235
edge_cases.rs Boundary conditions and edge cases 460
integration.rs Real-world API gateway scenarios 630
vars_filter_test.rs Advanced filters and expressions 506
benchmark.rs Performance benchmarks 413
concurrency_test.rs Multi-threaded performance 174
stress_test.rs Large-scale stress testing 319

Run Examples

# Basic examples
cargo run --example basic
cargo run --example edge_cases
cargo run --example integration
cargo run --example vars_filter_test

# Performance tests (use --release)
cargo run --example benchmark --release
cargo run --example concurrency_test --release
cargo run --example stress_test --release

# Run all tests
./run_all_tests.sh --release

Run Unit Tests

cargo test

๐Ÿ“– For detailed documentation, see examples/README.md


๐Ÿ—๏ธ Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚             Rust API Layer                      โ”‚
โ”‚  โ€ข Route matching & parameter extraction        โ”‚
โ”‚  โ€ข Filter evaluation & priority sorting         โ”‚
โ”‚  โ€ข Error handling (anyhow)                      โ”‚
โ”‚  โ€ข Thread-safe querying (RwLock + iterators)    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                      โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚             Rust FFI Layer                      โ”‚
โ”‚  โ€ข Safe wrappers around C functions             โ”‚
โ”‚  โ€ข RAII for resource management                 โ”‚
โ”‚  โ€ข Memory safety guarantees                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                      โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚         C Layer (Redis rax)                     โ”‚
โ”‚  โ€ข Radix tree implementation                    โ”‚
โ”‚  โ€ข Battle-tested in Redis production            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Key Components

  • RadixRouter: Main router struct with thread-safe API
  • RadixNode: Route node definition with matching rules
  • RadixMatchOpts: Request matching options
  • MatchResult: Matched route with extracted parameters
  • Expr: Variable expression for conditional matching
  • FilterFn: Custom filter function type

๐Ÿ“„ License

Apache-2.0


๐Ÿ™ Credits

  • Based on lua-resty-radixtree by Apache APISIX
  • Radix tree implementation from Redis by Salvatore Sanfilippo
  • Inspired by high-performance routing needs in API gateways

Built with โค๏ธ for high-performance routing

Report Bug ยท Request Feature

Commit count: 0

cargo fmt