From 254dff1e47cf320790c6f1d1f893a6c278be9616 Mon Sep 17 00:00:00 2001 From: pushkar-gr Date: Thu, 1 Jan 2026 11:11:38 +0530 Subject: [PATCH 1/3] fix: typos, bugs and code cleanup use config for LB address remove warnings --- src/config/config.rs | 14 ++++---- .../algorithm/static/round_robin.rs | 2 +- src/load_balancer/layer4.rs | 12 ++++--- src/load_balancer/layer7.rs | 11 ++++--- src/load_balancer/load_balancer.rs | 3 +- src/main.rs | 1 - src/server/server.rs | 32 +++++++++++++------ 7 files changed, 47 insertions(+), 28 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index 8609645..12948dd 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -26,7 +26,8 @@ pub enum Algorithm { pub struct Config { pub load_balancer_address: Uri, //address of load balancer pub servers: Arc>, //thread safe vector of servers - pub algorithm: Algorithm, //algorithm to pick server + #[allow(dead_code)] + pub algorithm: Algorithm, //algorithm to pick server pub last_picked_index: usize, //index of last picked server pub algorithm_object: Box, //algorithm object } @@ -36,7 +37,7 @@ impl Config { pub fn new(config_path: &Path) -> Self { //read contents of config file let contents = fs::read_to_string(config_path).unwrap(); - //parese config file contents + //parse config file contents let values = contents.parse::().unwrap(); //get host name, port and algorithm of load balancer @@ -159,13 +160,14 @@ impl Config { } } -//funciton to get Algorithm from string +//function to get Algorithm from string (case-insensitive) fn get_algorithm(algorithm: &String) -> Algorithm { - if algorithm == "RoundRobin" { + let algo_lower = algorithm.to_lowercase(); + if algo_lower == "roundrobin" || algo_lower == "round_robin" { Algorithm::RoundRobin - } else if algorithm == "WeightedRoundRobin" { + } else if algo_lower == "weightedroundrobin" || algo_lower == "weighted_round_robin" { Algorithm::WeightedRoundRobin - } else if algorithm == "IpHashing" { + } else if algo_lower == "iphashing" || algo_lower == "ip_hashing" { Algorithm::IpHashing } else { Algorithm::RoundRobin diff --git a/src/load_balancer/algorithm/static/round_robin.rs b/src/load_balancer/algorithm/static/round_robin.rs index b1e3b5a..4b9c4de 100644 --- a/src/load_balancer/algorithm/static/round_robin.rs +++ b/src/load_balancer/algorithm/static/round_robin.rs @@ -29,7 +29,7 @@ impl Algorithm for RoundRobin { //pick server let server = servers[self.index].clone(); let index = self.index; - //incriment index + //increment index self.index = (self.index + 1) % servers.len(); //return index and server Some((index, server)) diff --git a/src/load_balancer/layer4.rs b/src/load_balancer/layer4.rs index a5bb98a..fb04ec3 100644 --- a/src/load_balancer/layer4.rs +++ b/src/load_balancer/layer4.rs @@ -1,6 +1,5 @@ //defines the Layer4 load balancer that implements the LoadBalancer trait. It manages multiple backend servers and handles Layer 4 (transport layer) requests. The load balancer listens for incoming connections, picks an appropriate server, and transfers data between the client and the selected server -use hyper::Uri; use tokio::net::TcpListener; use crate::config::config::SyncConfig; @@ -22,15 +21,18 @@ impl load_balancer::LoadBalancer for Layer4 { //calls pick_server to pick a server when user sends a request //calls Server::transfer_data to transfer data between server and client async fn start(&self) -> Result<(), Box> { - //load balancer address - let lb_address = "http://127.0.0.1:8000".parse::().unwrap(); + //load balancer address from config + let lb_address = { + let config = self.config.lock().unwrap(); + config.load_balancer_address.clone() + }; let host = lb_address.host().unwrap(); let port = lb_address.port_u16().unwrap(); //create a TcpListener and binds it to load balancer address let listener = TcpListener::bind((host, port)).await?; - //loop to continuously accetp incoming connections + //loop to continuously accept incoming connections loop { //accept incoming connections let (stream, addr) = listener.accept().await?; @@ -46,7 +48,7 @@ impl load_balancer::LoadBalancer for Layer4 { .expect("No server"); //call Server::transfer_data to transfer data between server and client if let Err(err) = Server::transfer_data(server, stream).await { - eprintln!("Error transfering data {:?}", err); + eprintln!("Error transferring data {:?}", err); } }); } diff --git a/src/load_balancer/layer7.rs b/src/load_balancer/layer7.rs index 6f95e2c..61cf953 100644 --- a/src/load_balancer/layer7.rs +++ b/src/load_balancer/layer7.rs @@ -2,7 +2,6 @@ use hyper::server::conn::http1; use hyper::service::service_fn; -use hyper::Uri; use hyper_util::rt::TokioIo; use tokio::net::TcpListener; @@ -10,6 +9,7 @@ use crate::config::config::SyncConfig; use crate::load_balancer::load_balancer::LoadBalancer; use crate::server::server::Server; +#[allow(dead_code)] pub struct Layer7 { config: SyncConfig, } @@ -25,15 +25,18 @@ impl LoadBalancer for Layer7 { //calls pick_server to pick a server when user sends a request //calls Server::handle_request to forward request to the server async fn start(&self) -> Result<(), Box> { - //load balancer address - let lb_address = "http://127.0.0.1:8000".parse::().unwrap(); + //load balancer address from config + let lb_address = { + let config = self.config.lock().unwrap(); + config.load_balancer_address.clone() + }; let host = lb_address.host().unwrap(); let port = lb_address.port_u16().unwrap(); //create a TcpListener and binds it to load balancer address let listener = TcpListener::bind((host, port)).await?; - //loop to continuously accetp incoming connections + //loop to continuously accept incoming connections loop { //accept incoming connections let (stream, addr) = listener.accept().await?; diff --git a/src/load_balancer/load_balancer.rs b/src/load_balancer/load_balancer.rs index ee02cd0..8ec9e1f 100644 --- a/src/load_balancer/load_balancer.rs +++ b/src/load_balancer/load_balancer.rs @@ -20,7 +20,7 @@ pub trait LoadBalancer { let mut config = config.lock().unwrap(); //get servers let servers = config.servers.clone(); - //call AlgoRithm::pick_server and return the server + //call Algorithm::pick_server and return the server let (index, server) = config .algorithm_object .pick_server(servers, client_addr) @@ -32,5 +32,6 @@ pub trait LoadBalancer { } //stops the load balancer + #[allow(dead_code)] fn stop(&self); } diff --git a/src/main.rs b/src/main.rs index cb3c411..679d03c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,6 @@ mod server; use crate::load_balancer::load_balancer::LoadBalancer; use config::config::Config; use load_balancer::layer4::Layer4; -use load_balancer::layer7::Layer7; use std::path::Path; use std::sync::{Arc, Mutex}; diff --git a/src/server/server.rs b/src/server/server.rs index 79d6357..4c5feb9 100644 --- a/src/server/server.rs +++ b/src/server/server.rs @@ -18,22 +18,33 @@ pub type SyncServer = Arc>; #[derive(Clone)] pub struct Server { - uri: Uri, //uri of server + #[allow(dead_code)] + uri: Uri, //uri of server host: String, //hostname of server port: u16, //port at which server is running - max_connections: u32, //max connections server can handle - connections: u32, //number of alive connections - total_connections: u32, //total connections server has served - successful_connections: u32, //total successful connections server has sesrved - failed_connections: u32, //total failed connections - + #[allow(dead_code)] + max_connections: u32, //max connections server can handle + #[allow(dead_code)] + connections: u32, //number of alive connections + #[allow(dead_code)] + total_connections: u32, //total connections server has served + #[allow(dead_code)] + successful_connections: u32, //total successful connections server has served + #[allow(dead_code)] + failed_connections: u32, //total failed connections + + #[allow(dead_code)] last_request_time: SystemTime, //time of latest request + #[allow(dead_code)] last_health_check: SystemTime, //time of latest health check - response_time: f64, //last response time - avg_response_time: f64, //avegage response time + #[allow(dead_code)] + response_time: f64, //last response time + #[allow(dead_code)] + avg_response_time: f64, //average response time + #[allow(dead_code)] is_alive: bool, //is server alive? pub weight: usize, //for weighted algorithms @@ -95,7 +106,7 @@ impl Server { client_write.shutdown().await }); - //run both diretions concurrently + //run both directions concurrently let _ = try_join!(client_to_server, server_to_server)?; Ok(()) @@ -103,6 +114,7 @@ impl Server { //handle_request handles incoming request and forwards it to a server //returns the response from the server + #[allow(dead_code)] pub async fn handle_request( server: SyncServer, mut req: Request, From 9284744c2ff65ea6ed43370e517f152f62b6fc5e Mon Sep 17 00:00:00 2001 From: pushkar-gr Date: Thu, 1 Jan 2026 11:18:16 +0530 Subject: [PATCH 2/3] tets: add tests for modules --- Cargo.toml | 8 + src/config/config.rs | 11 +- src/lib.rs | 35 ++++ src/load_balancer/algorithm/algorithm.rs | 11 +- .../algorithm/static/ip_hashing.rs | 6 +- .../algorithm/static/round_robin.rs | 5 +- .../algorithm/static/weighted_round_robin.rs | 6 +- src/load_balancer/layer4.rs | 6 +- src/load_balancer/layer7.rs | 6 +- src/load_balancer/load_balancer.rs | 16 +- src/server/server.rs | 6 +- test/test.rs | 0 tests/algorithm_tests.rs | 150 +++++++++++++++++ tests/config_tests.rs | 153 ++++++++++++++++++ 14 files changed, 401 insertions(+), 18 deletions(-) create mode 100644 src/lib.rs delete mode 100644 test/test.rs create mode 100644 tests/algorithm_tests.rs create mode 100644 tests/config_tests.rs diff --git a/Cargo.toml b/Cargo.toml index b55145b..29b991b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,14 @@ name = "Deston" version = "0.1.0" edition = "2021" +[lib] +name = "Deston" +path = "src/lib.rs" + +[[bin]] +name = "Deston" +path = "src/main.rs" + [dependencies] hyper = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] } diff --git a/src/config/config.rs b/src/config/config.rs index 12948dd..851c4f9 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -1,4 +1,8 @@ -//defines Config struct that holds list of structures, selected load balancer algorithm, and an algorithm object. +//! Configuration module for the Deston load balancer. +//! +//! This module handles parsing and managing configuration from TOML files. +//! It defines the configuration structure including load balancer settings, +//! backend servers, and algorithm selection. use hyper::Uri; use std::fs; @@ -15,7 +19,7 @@ use crate::server::server::{Server, SyncServer}; //type alias for a thread-safe, synchronized Config using Arc and Mutex pub type SyncConfig = Arc>; -//enum for all the algorithm +/// Load balancing algorithm options #[derive(Clone)] pub enum Algorithm { RoundRobin, //round robin @@ -23,6 +27,7 @@ pub enum Algorithm { IpHashing, //ip hashing } +/// Configuration structure for the load balancer pub struct Config { pub load_balancer_address: Uri, //address of load balancer pub servers: Arc>, //thread safe vector of servers @@ -33,7 +38,7 @@ pub struct Config { } impl Config { - //creates and returns a new Config + /// Creates and returns a new Config from a TOML file pub fn new(config_path: &Path) -> Self { //read contents of config file let contents = fs::read_to_string(config_path).unwrap(); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f2cc657 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,35 @@ +// Library exports for Deston load balancer +// +//! # Deston Load Balancer +//! +//! A high-performance Layer 4 (L4) and Layer 7 (L7) load balancer implementation. +//! +//! ## Modules +//! +//! - `config`: Configuration parsing and management +//! - `load_balancer`: Load balancer trait and implementations (Layer 4 and Layer 7) +//! - `server`: Backend server management and request handling +//! +//! ## Example +//! +//! ```no_run +//! use Deston::config::config::Config; +//! use Deston::load_balancer::load_balancer::LoadBalancer; +//! use Deston::load_balancer::layer4::Layer4; +//! use std::path::Path; +//! use std::sync::{Arc, Mutex}; +//! +//! #[tokio::main] +//! async fn main() { +//! let config = Config::new(Path::new("config.toml")); +//! let lb = Layer4::new(Arc::new(Mutex::new(config))); +//! let _ = lb.start().await; +//! } +//! ``` + +pub mod config; +pub mod load_balancer; +pub mod server; + +// Re-export Arc for convenience +pub use std::sync::Arc; diff --git a/src/load_balancer/algorithm/algorithm.rs b/src/load_balancer/algorithm/algorithm.rs index 6e9c62a..5fe5a63 100644 --- a/src/load_balancer/algorithm/algorithm.rs +++ b/src/load_balancer/algorithm/algorithm.rs @@ -1,17 +1,22 @@ -//defines algorithm trait, it provies the blueprint for creating, and picking servers +//! Algorithm trait definition. +//! +//! This module defines the Algorithm trait that all load balancing algorithms must implement. use crate::Arc; use std::net::SocketAddr; use crate::server::server::SyncServer; +/// Algorithm trait for load balancing strategies pub trait Algorithm: Send { - //returns new Algorithm struct + /// Creates a new instance of the algorithm fn new() -> Self where Self: Sized; - //picks server based on algorithm and returns server index and server. returns None if no server available + /// Picks a server based on the algorithm's strategy + /// + /// Returns Some((index, server)) if a server is available, None otherwise fn pick_server( &mut self, servers: Arc>, diff --git a/src/load_balancer/algorithm/static/ip_hashing.rs b/src/load_balancer/algorithm/static/ip_hashing.rs index dede50f..4f20c0f 100644 --- a/src/load_balancer/algorithm/static/ip_hashing.rs +++ b/src/load_balancer/algorithm/static/ip_hashing.rs @@ -1,4 +1,7 @@ -//defines ip hashing, where servers are selected based on client ip address +//! IP Hashing load balancing algorithm. +//! +//! Uses consistent hashing based on client IP addresses to ensure the same client +//! is always routed to the same backend server, providing session affinity. use sha2::{Digest, Sha256}; use std::net::SocketAddr; @@ -7,6 +10,7 @@ use std::sync::Arc; use crate::load_balancer::algorithm::algorithm::Algorithm; use crate::server::server::SyncServer; +/// IP Hashing algorithm implementation pub struct IpHashing {} impl Algorithm for IpHashing { diff --git a/src/load_balancer/algorithm/static/round_robin.rs b/src/load_balancer/algorithm/static/round_robin.rs index 4b9c4de..0f23533 100644 --- a/src/load_balancer/algorithm/static/round_robin.rs +++ b/src/load_balancer/algorithm/static/round_robin.rs @@ -1,4 +1,6 @@ -//defines round robin algorithm, where servers are selected sequentially +//! Round Robin load balancing algorithm. +//! +//! Distributes requests evenly across all servers in a sequential, circular fashion. use crate::Arc; use std::net::SocketAddr; @@ -6,6 +8,7 @@ use std::net::SocketAddr; use crate::load_balancer::algorithm::algorithm::Algorithm; use crate::server::server::SyncServer; +/// Round Robin algorithm implementation pub struct RoundRobin { index: usize, } diff --git a/src/load_balancer/algorithm/static/weighted_round_robin.rs b/src/load_balancer/algorithm/static/weighted_round_robin.rs index dc9f7b1..4eb51f9 100644 --- a/src/load_balancer/algorithm/static/weighted_round_robin.rs +++ b/src/load_balancer/algorithm/static/weighted_round_robin.rs @@ -1,4 +1,7 @@ -//defines weighted round robin algorithm, where servers are selected sequentially, taking their assigned weights into account. Servers with higher weights are picked more frequently +//! Weighted Round Robin load balancing algorithm. +//! +//! Distributes requests based on server weights. Servers with higher weights +//! receive proportionally more requests. use std::net::SocketAddr; use std::sync::Arc; @@ -6,6 +9,7 @@ use std::sync::Arc; use crate::load_balancer::algorithm::algorithm::Algorithm; use crate::server::server::SyncServer; +/// Weighted Round Robin algorithm implementation pub struct WeightedRoundRobin { index: usize, curr_weight: usize, diff --git a/src/load_balancer/layer4.rs b/src/load_balancer/layer4.rs index fb04ec3..22af0f5 100644 --- a/src/load_balancer/layer4.rs +++ b/src/load_balancer/layer4.rs @@ -1,4 +1,7 @@ -//defines the Layer4 load balancer that implements the LoadBalancer trait. It manages multiple backend servers and handles Layer 4 (transport layer) requests. The load balancer listens for incoming connections, picks an appropriate server, and transfers data between the client and the selected server +//! Layer 4 (TCP) load balancer implementation. +//! +//! This module provides a Layer 4 load balancer that operates at the transport layer, +//! forwarding raw TCP connections between clients and backend servers. use tokio::net::TcpListener; @@ -6,6 +9,7 @@ use crate::config::config::SyncConfig; use crate::load_balancer::load_balancer; use crate::server::server::Server; +/// Layer 4 (TCP) Load Balancer pub struct Layer4 { config: SyncConfig, } diff --git a/src/load_balancer/layer7.rs b/src/load_balancer/layer7.rs index 61cf953..781bc2e 100644 --- a/src/load_balancer/layer7.rs +++ b/src/load_balancer/layer7.rs @@ -1,4 +1,7 @@ -//defines the Layer 7 Load Balancer that implements the LoadBalancer trait. It listens for incoming HTTP requests, selects an appropriate server, and forwards the requests to the chosen server +//! Layer 7 (HTTP) load balancer implementation. +//! +//! This module provides a Layer 7 load balancer that operates at the application layer, +//! forwarding HTTP requests with the ability to inspect and modify headers. use hyper::server::conn::http1; use hyper::service::service_fn; @@ -9,6 +12,7 @@ use crate::config::config::SyncConfig; use crate::load_balancer::load_balancer::LoadBalancer; use crate::server::server::Server; +/// Layer 7 (HTTP) Load Balancer #[allow(dead_code)] pub struct Layer7 { config: SyncConfig, diff --git a/src/load_balancer/load_balancer.rs b/src/load_balancer/load_balancer.rs index 8ec9e1f..2e41b35 100644 --- a/src/load_balancer/load_balancer.rs +++ b/src/load_balancer/load_balancer.rs @@ -1,20 +1,24 @@ -//defines load balancer trait, it provies the blueprint for creating, starting and stopping load balancer. With a method to pick the next server based on given algo +//! Load balancer trait and implementations. +//! +//! This module defines the core LoadBalancer trait and provides implementations +//! for Layer 4 (TCP) and Layer 7 (HTTP) load balancing. use std::net::SocketAddr; use crate::config::config::SyncConfig; use crate::server::server::SyncServer; +/// LoadBalancer trait defining the interface for load balancer implementations pub trait LoadBalancer { - //returns a LoadBalancer + /// Creates a new LoadBalancer with the given configuration fn new(config: SyncConfig) -> Self; - //starts the load balancer + /// Starts the load balancer and begins accepting connections async fn start(&self) -> Result<(), Box>; - //picks a server based on algo to handle incoming request - //returns an option of server if server available - //returns None if no servers are available + /// Picks a server based on the configured algorithm to handle an incoming request + /// + /// Returns Some(server) if a server is available, None otherwise async fn pick_server(config: SyncConfig, client_addr: SocketAddr) -> Option { //lock config let mut config = config.lock().unwrap(); diff --git a/src/server/server.rs b/src/server/server.rs index 4c5feb9..bb352df 100644 --- a/src/server/server.rs +++ b/src/server/server.rs @@ -1,4 +1,7 @@ -//defines the Server struct and methods for creating servers, handling data transfers between clients and servers, and forwarding HTTP requests +//! Backend server management and request handling. +//! +//! This module defines the Server struct and provides methods for handling +//! both Layer 4 (TCP) and Layer 7 (HTTP) connections. use http::header::{HeaderValue, FORWARDED}; use http_body_util::{combinators::BoxBody, BodyExt}; @@ -16,6 +19,7 @@ use tokio::try_join; //type alias for a thread-safe, synchronized Server using Arc and Mutex pub type SyncServer = Arc>; +/// Backend server representation with connection tracking and health metrics #[derive(Clone)] pub struct Server { #[allow(dead_code)] diff --git a/test/test.rs b/test/test.rs deleted file mode 100644 index e69de29..0000000 diff --git a/tests/algorithm_tests.rs b/tests/algorithm_tests.rs new file mode 100644 index 0000000..9e6761b --- /dev/null +++ b/tests/algorithm_tests.rs @@ -0,0 +1,150 @@ +use std::net::SocketAddr; +use std::sync::{Arc, Mutex}; + +// Import algorithm modules from main crate +use hyper::Uri; +use Deston::load_balancer::algorithm::algorithm::Algorithm; +use Deston::load_balancer::algorithm::r#static::{ + ip_hashing::IpHashing, round_robin::RoundRobin, weighted_round_robin::WeightedRoundRobin, +}; +use Deston::server::server::Server; + +// Helper function to create test servers +fn create_test_servers(count: usize, weights: Option>) -> Arc>>> { + let servers: Vec>> = (0..count) + .map(|i| { + let weight = weights.as_ref().map(|w| w[i]).unwrap_or(1); + let port = 3000 + i; + let uri = format!("http://127.0.0.1:{}", port).parse::().unwrap(); + Arc::new(Mutex::new(Server::new(uri, 1000, weight))) + }) + .collect(); + Arc::new(servers) +} + +// Helper to create a test socket address +fn test_addr() -> SocketAddr { + "127.0.0.1:5000".parse().unwrap() +} + +#[test] +fn test_round_robin_basic() { + let mut algorithm = RoundRobin::new(); + let servers = create_test_servers(3, None); + + // Pick servers in round-robin order + let (index1, _) = algorithm.pick_server(servers.clone(), test_addr()).unwrap(); + let (index2, _) = algorithm.pick_server(servers.clone(), test_addr()).unwrap(); + let (index3, _) = algorithm.pick_server(servers.clone(), test_addr()).unwrap(); + let (index4, _) = algorithm.pick_server(servers.clone(), test_addr()).unwrap(); + + assert_eq!(index1, 0); + assert_eq!(index2, 1); + assert_eq!(index3, 2); + assert_eq!(index4, 0); // Wraps around +} + +#[test] +fn test_round_robin_single_server() { + let mut algorithm = RoundRobin::new(); + let servers = create_test_servers(1, None); + + // Should always return the same server + let (index1, _) = algorithm.pick_server(servers.clone(), test_addr()).unwrap(); + let (index2, _) = algorithm.pick_server(servers.clone(), test_addr()).unwrap(); + + assert_eq!(index1, 0); + assert_eq!(index2, 0); +} + +#[test] +fn test_weighted_round_robin_equal_weights() { + let mut algorithm = WeightedRoundRobin::new(); + let servers = create_test_servers(2, Some(vec![1, 1])); + + // With equal weights, should alternate + let (index1, _) = algorithm.pick_server(servers.clone(), test_addr()).unwrap(); + let (index2, _) = algorithm.pick_server(servers.clone(), test_addr()).unwrap(); + let (index3, _) = algorithm.pick_server(servers.clone(), test_addr()).unwrap(); + + assert_eq!(index1, 0); + assert_eq!(index2, 1); + assert_eq!(index3, 0); +} + +#[test] +fn test_weighted_round_robin_different_weights() { + let mut algorithm = WeightedRoundRobin::new(); + let servers = create_test_servers(2, Some(vec![3, 1])); + + // Server 0 with weight 3 should be picked 3 times before server 1 + let mut picks = vec![]; + for _ in 0..8 { + let (index, _) = algorithm.pick_server(servers.clone(), test_addr()).unwrap(); + picks.push(index); + } + + // First 4 picks should follow pattern: 0, 0, 0, 1 + assert_eq!(picks[0], 0); + assert_eq!(picks[1], 0); + assert_eq!(picks[2], 0); + assert_eq!(picks[3], 1); + // Pattern repeats + assert_eq!(picks[4], 0); + assert_eq!(picks[5], 0); + assert_eq!(picks[6], 0); + assert_eq!(picks[7], 1); +} + +#[test] +fn test_ip_hashing_consistency() { + let mut algorithm = IpHashing::new(); + let servers = create_test_servers(3, None); + + let addr1: SocketAddr = "127.0.0.1:5000".parse().unwrap(); + let addr2: SocketAddr = "127.0.0.1:5001".parse().unwrap(); + + // Same IP should always map to same server + let (index1, _) = algorithm.pick_server(servers.clone(), addr1).unwrap(); + let (index2, _) = algorithm.pick_server(servers.clone(), addr1).unwrap(); + assert_eq!(index1, index2); + + // Different IPs might map to different servers + let (index3, _) = algorithm.pick_server(servers.clone(), addr2).unwrap(); + + // All indices should be valid + assert!(index1 < 3); + assert!(index3 < 3); +} + +#[test] +fn test_ip_hashing_distribution() { + let mut algorithm = IpHashing::new(); + let servers = create_test_servers(3, None); + + // Test multiple different IPs + let addrs: Vec = (5000..5010) + .map(|port| format!("127.0.0.1:{}", port).parse().unwrap()) + .collect(); + + let mut indices = vec![]; + for addr in addrs { + let (index, _) = algorithm.pick_server(servers.clone(), addr).unwrap(); + indices.push(index); + } + + // Should have distribution across servers (not all same index) + let unique_indices: std::collections::HashSet<_> = indices.iter().collect(); + assert!( + unique_indices.len() > 1, + "IP hashing should distribute across multiple servers" + ); +} + +#[test] +fn test_server_creation() { + let uri = "http://127.0.0.1:3000".parse::().unwrap(); + let server = Server::new(uri, 1000, 5); + + assert_eq!(server.weight, 5); +} diff --git a/tests/config_tests.rs b/tests/config_tests.rs new file mode 100644 index 0000000..9a0bf91 --- /dev/null +++ b/tests/config_tests.rs @@ -0,0 +1,153 @@ +use std::fs; +use std::path::Path; +use Deston::config::config::{Algorithm, Config}; + +#[test] +fn test_config_parsing_basic() { + // Create a temporary config file + let config_content = r#" +[load_balancer] +address = "127.0.0.1" +port = 8080 +algorithm = "round_robin" + +[[server]] +address = "127.0.0.1" +port = 3000 +max_connections = 1000 +weight = 1 + +[[server]] +address = "127.0.0.1" +port = 3001 +max_connections = 500 +weight = 2 +"#; + + let config_path = "/tmp/test_config.toml"; + fs::write(config_path, config_content).unwrap(); + + let config = Config::new(Path::new(config_path)); + + // Verify load balancer address + assert_eq!(config.load_balancer_address.to_string(), "127.0.0.1:8080"); + + // Verify servers + assert_eq!(config.servers.len(), 2); + + // Verify first server + let server1 = config.servers[0].lock().unwrap(); + assert_eq!(server1.weight, 1); + + // Verify second server + let server2 = config.servers[1].lock().unwrap(); + assert_eq!(server2.weight, 2); + + // Clean up + fs::remove_file(config_path).ok(); +} + +#[test] +fn test_config_algorithm_case_insensitive() { + let test_cases = vec![ + ("round_robin", Algorithm::RoundRobin), + ("RoundRobin", Algorithm::RoundRobin), + ("ROUND_ROBIN", Algorithm::RoundRobin), + ("weighted_round_robin", Algorithm::WeightedRoundRobin), + ("WeightedRoundRobin", Algorithm::WeightedRoundRobin), + ("ip_hashing", Algorithm::IpHashing), + ("IpHashing", Algorithm::IpHashing), + ]; + + for (algo_str, _expected) in test_cases { + let config_content = format!( + r#" +[load_balancer] +address = "127.0.0.1" +port = 8080 +algorithm = "{}" + +[[server]] +address = "127.0.0.1" +port = 3000 +max_connections = 1000 +weight = 1 +"#, + algo_str + ); + + let config_path = format!("/tmp/test_config_{}.toml", algo_str.replace("_", "")); + fs::write(&config_path, config_content).unwrap(); + + // Should not panic - algorithm should be parsed correctly + let _config = Config::new(Path::new(&config_path)); + + // Clean up + fs::remove_file(&config_path).ok(); + } +} + +#[test] +fn test_config_default_values() { + // Config with minimal settings + let config_content = r#" +[[server]] +address = "127.0.0.1" +port = 3000 +"#; + + let config_path = "/tmp/test_config_defaults.toml"; + fs::write(config_path, config_content).unwrap(); + + let config = Config::new(Path::new(config_path)); + + // Should use default load balancer address + assert_eq!(config.load_balancer_address.host().unwrap(), "localhost"); + + // Should have servers + assert!(!config.servers.is_empty()); + + // Clean up + fs::remove_file(config_path).ok(); +} + +#[test] +fn test_config_multiple_servers() { + let config_content = r#" +[load_balancer] +address = "0.0.0.0" +port = 9000 +algorithm = "round_robin" + +[[server]] +address = "192.168.1.1" +port = 3000 +max_connections = 100 +weight = 1 + +[[server]] +address = "192.168.1.2" +port = 3001 +max_connections = 200 +weight = 2 + +[[server]] +address = "192.168.1.3" +port = 3002 +max_connections = 300 +weight = 3 +"#; + + let config_path = "/tmp/test_config_multi.toml"; + fs::write(config_path, config_content).unwrap(); + + let config = Config::new(Path::new(config_path)); + + assert_eq!(config.servers.len(), 3); + assert_eq!(config.servers[0].lock().unwrap().weight, 1); + assert_eq!(config.servers[1].lock().unwrap().weight, 2); + assert_eq!(config.servers[2].lock().unwrap().weight, 3); + + // Clean up + fs::remove_file(config_path).ok(); +} From ec638129f6fccc9ed494807c3a299edf596fe40e Mon Sep 17 00:00:00 2001 From: pushkar-gr Date: Thu, 1 Jan 2026 19:52:46 +0530 Subject: [PATCH 3/3] cleanup: add L7/L4 in config, adde tests, imporve function parameters --- Cargo.lock | 26 +++---- Cargo.toml | 6 +- README.md | 35 ++++++++- config.toml | 2 +- src/config/config.rs | 38 +++++++-- src/config/mod.rs | 1 + src/lib.rs | 6 +- src/load_balancer/algorithm/mod.rs | 1 + src/load_balancer/load_balancer.rs | 1 + src/load_balancer/mod.rs | 1 + src/main.rs | 24 +++++- src/server/mod.rs | 1 + src/server/server.rs | 8 +- tests/algorithm_tests.rs | 8 +- tests/config_tests.rs | 120 ++++++++++++++++++++++++++++- tests/integration_tests.rs | 103 +++++++++++++++++++++++++ 16 files changed, 340 insertions(+), 41 deletions(-) create mode 100644 tests/integration_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 6cca590..7fb271e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,19 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "Deston" -version = "0.1.0" -dependencies = [ - "http", - "http-body-util", - "hyper", - "hyper-util", - "sha2", - "tokio", - "toml", -] - [[package]] name = "addr2line" version = "0.24.2" @@ -103,6 +90,19 @@ dependencies = [ "typenum", ] +[[package]] +name = "deston" +version = "0.1.0" +dependencies = [ + "http", + "http-body-util", + "hyper", + "hyper-util", + "sha2", + "tokio", + "toml", +] + [[package]] name = "digest" version = "0.10.7" diff --git a/Cargo.toml b/Cargo.toml index 29b991b..1bcb3b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,14 @@ [package] -name = "Deston" +name = "deston" version = "0.1.0" edition = "2021" [lib] -name = "Deston" +name = "deston" path = "src/lib.rs" [[bin]] -name = "Deston" +name = "deston" path = "src/main.rs" [dependencies] diff --git a/README.md b/README.md index 5b65d6e..1783a01 100644 --- a/README.md +++ b/README.md @@ -66,22 +66,49 @@ cargo run --release ``` ### Configuration -Modify `config.toml` to add servers, specify load-balancing algorithms, etc.: +Modify `config.toml` to configure the load balancer settings: + ```toml [load_balancer] address = "127.0.0.1" port = 8080 -algorithm = "RoundRobin" +algorithm = "round_robin" # Options: "round_robin", "weighted_round_robin", "ip_hashing" +layer = "L4" # Options: "L4" (TCP), "L7" (HTTP) [[server]] -address = "http://127.0.0.1:3000" +address = "127.0.0.1" port = 3000 +max_connections = 1000 +weight = 1 [[server]] -address = "http://127.0.0.1:3001" +address = "127.0.0.1" port = 3001 +max_connections = 1000 +weight = 1 ``` +#### Configuration Options + +- **layer**: Choose between Layer 4 (TCP) or Layer 7 (HTTP) load balancing + - `"L4"` (default): Transport layer (TCP) load balancing + - `"L7"`: Application layer (HTTP) load balancing + - Case-insensitive: accepts `"l4"`, `"L4"`, `"layer4"`, `"Layer_4"`, etc. + +- **algorithm**: Load balancing algorithm + - `"round_robin"`: Distributes requests equally across servers + - `"weighted_round_robin"`: Distributes based on server weights + - `"ip_hashing"`: Routes requests from the same IP to the same server + - Case-insensitive + +- **address** and **port**: The address where the load balancer listens + +- **server**: Backend server configuration + - `address`: Server IP address + - `port`: Server port + - `max_connections`: Maximum concurrent connections + - `weight`: Server weight (used with weighted_round_robin) + --- ## 📜 Code Snippets diff --git a/config.toml b/config.toml index 62e18a9..c5a6827 100644 --- a/config.toml +++ b/config.toml @@ -2,6 +2,7 @@ address = "127.0.0.1" port = 8080 algorithm = "round_robin" +layer = "L7" [[server]] address = "127.0.0.1" @@ -14,4 +15,3 @@ address = "127.0.0.1" port = 3001 max_connections = 1000 weight = 1 - diff --git a/src/config/config.rs b/src/config/config.rs index 851c4f9..fe161e4 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -27,6 +27,13 @@ pub enum Algorithm { IpHashing, //ip hashing } +/// Load balancer layer mode +#[derive(Clone, Debug, PartialEq)] +pub enum LayerMode { + L4, // Layer 4 (TCP) load balancer + L7, // Layer 7 (HTTP) load balancer +} + /// Configuration structure for the load balancer pub struct Config { pub load_balancer_address: Uri, //address of load balancer @@ -35,6 +42,7 @@ pub struct Config { pub algorithm: Algorithm, //algorithm to pick server pub last_picked_index: usize, //index of last picked server pub algorithm_object: Box, //algorithm object + pub layer_mode: LayerMode, //layer mode (L4 or L7) } impl Config { @@ -46,7 +54,7 @@ impl Config { let values = contents.parse::
().unwrap(); //get host name, port and algorithm of load balancer - let (load_balancer_host, load_balancer_port, algorithm) = { + let (load_balancer_host, load_balancer_port, algorithm, layer_mode) = { if let Some(table) = values.get("load_balancer") { let host = { if let Some(Value::String(address)) = table.get("address") { @@ -70,10 +78,17 @@ impl Config { Algorithm::RoundRobin } }; - (host, port, algorithm) + let layer = { + if let Some(Value::String(layer)) = table.get("layer") { + get_layer_mode(layer) + } else { + LayerMode::L4 // Default to L4 for backward compatibility + } + }; + (host, port, algorithm, layer) } else { //if host not found in config - ("localhost", &8080, Algorithm::RoundRobin) + ("localhost", &8080, Algorithm::RoundRobin, LayerMode::L4) } }; @@ -159,14 +174,15 @@ impl Config { Algorithm::IpHashing => Box::new(IpHashing::new()), } }, - algorithm: algorithm, + algorithm, last_picked_index: 0, + layer_mode, } } } //function to get Algorithm from string (case-insensitive) -fn get_algorithm(algorithm: &String) -> Algorithm { +fn get_algorithm(algorithm: &str) -> Algorithm { let algo_lower = algorithm.to_lowercase(); if algo_lower == "roundrobin" || algo_lower == "round_robin" { Algorithm::RoundRobin @@ -178,3 +194,15 @@ fn get_algorithm(algorithm: &String) -> Algorithm { Algorithm::RoundRobin } } + +//function to get LayerMode from string (case-insensitive) +fn get_layer_mode(layer: &str) -> LayerMode { + let layer_lower = layer.to_lowercase(); + if layer_lower == "l4" || layer_lower == "layer4" || layer_lower == "layer_4" { + LayerMode::L4 + } else if layer_lower == "l7" || layer_lower == "layer7" || layer_lower == "layer_7" { + LayerMode::L7 + } else { + LayerMode::L4 // Default to L4 + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index ef68c36..3c55160 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1 +1,2 @@ +#[allow(clippy::module_inception)] pub mod config; diff --git a/src/lib.rs b/src/lib.rs index f2cc657..92edc63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,9 +13,9 @@ //! ## Example //! //! ```no_run -//! use Deston::config::config::Config; -//! use Deston::load_balancer::load_balancer::LoadBalancer; -//! use Deston::load_balancer::layer4::Layer4; +//! use deston::config::config::Config; +//! use deston::load_balancer::load_balancer::LoadBalancer; +//! use deston::load_balancer::layer4::Layer4; //! use std::path::Path; //! use std::sync::{Arc, Mutex}; //! diff --git a/src/load_balancer/algorithm/mod.rs b/src/load_balancer/algorithm/mod.rs index 6359149..0da32fe 100644 --- a/src/load_balancer/algorithm/mod.rs +++ b/src/load_balancer/algorithm/mod.rs @@ -1,2 +1,3 @@ +#[allow(clippy::module_inception)] pub mod algorithm; pub mod r#static; diff --git a/src/load_balancer/load_balancer.rs b/src/load_balancer/load_balancer.rs index 2e41b35..f5dd4a8 100644 --- a/src/load_balancer/load_balancer.rs +++ b/src/load_balancer/load_balancer.rs @@ -9,6 +9,7 @@ use crate::config::config::SyncConfig; use crate::server::server::SyncServer; /// LoadBalancer trait defining the interface for load balancer implementations +#[allow(async_fn_in_trait)] pub trait LoadBalancer { /// Creates a new LoadBalancer with the given configuration fn new(config: SyncConfig) -> Self; diff --git a/src/load_balancer/mod.rs b/src/load_balancer/mod.rs index 07206c2..b9bdfc4 100644 --- a/src/load_balancer/mod.rs +++ b/src/load_balancer/mod.rs @@ -1,4 +1,5 @@ pub mod algorithm; pub mod layer4; pub mod layer7; +#[allow(clippy::module_inception)] pub mod load_balancer; diff --git a/src/main.rs b/src/main.rs index 679d03c..05fcf8a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,9 @@ mod config; mod load_balancer; mod server; use crate::load_balancer::load_balancer::LoadBalancer; -use config::config::Config; +use config::config::{Config, LayerMode}; use load_balancer::layer4::Layer4; +use load_balancer::layer7::Layer7; use std::path::Path; use std::sync::{Arc, Mutex}; @@ -11,6 +12,23 @@ use std::sync::{Arc, Mutex}; #[tokio::main] async fn main() { let config = Config::new(Path::new("config.toml")); - let lb = Layer4::new(Arc::new(Mutex::new(config))); - let _ = lb.start().await; + let layer_mode = config.layer_mode.clone(); + let config_arc = Arc::new(Mutex::new(config)); + + match layer_mode { + LayerMode::L4 => { + println!("Starting Layer 4 (TCP) Load Balancer..."); + let lb = Layer4::new(config_arc); + if let Err(e) = lb.start().await { + eprintln!("Error starting Layer 4 load balancer: {:?}", e); + } + } + LayerMode::L7 => { + println!("Starting Layer 7 (HTTP) Load Balancer..."); + let lb = Layer7::new(config_arc); + if let Err(e) = lb.start().await { + eprintln!("Error starting Layer 7 load balancer: {:?}", e); + } + } + } } diff --git a/src/server/mod.rs b/src/server/mod.rs index 74f47ad..f43c9a6 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1 +1,2 @@ +#[allow(clippy::module_inception)] pub mod server; diff --git a/src/server/server.rs b/src/server/server.rs index bb352df..9774281 100644 --- a/src/server/server.rs +++ b/src/server/server.rs @@ -60,9 +60,9 @@ impl Server { Self { host: uri.host().unwrap().to_string(), port: uri.port_u16().unwrap(), - uri: uri, + uri, - max_connections: max_connections, + max_connections, connections: 0, total_connections: 0, successful_connections: 0, @@ -76,7 +76,7 @@ impl Server { is_alive: true, - weight: weight, + weight, } } @@ -150,7 +150,7 @@ impl Server { //for: client address addr, //host: server address - uri.to_string(), + uri, //prototype: http1 "http1" ) diff --git a/tests/algorithm_tests.rs b/tests/algorithm_tests.rs index 9e6761b..e367f34 100644 --- a/tests/algorithm_tests.rs +++ b/tests/algorithm_tests.rs @@ -2,12 +2,12 @@ use std::net::SocketAddr; use std::sync::{Arc, Mutex}; // Import algorithm modules from main crate -use hyper::Uri; -use Deston::load_balancer::algorithm::algorithm::Algorithm; -use Deston::load_balancer::algorithm::r#static::{ +use deston::load_balancer::algorithm::algorithm::Algorithm; +use deston::load_balancer::algorithm::r#static::{ ip_hashing::IpHashing, round_robin::RoundRobin, weighted_round_robin::WeightedRoundRobin, }; -use Deston::server::server::Server; +use deston::server::server::Server; +use hyper::Uri; // Helper function to create test servers fn create_test_servers(count: usize, weights: Option>) -> Arc>>> { diff --git a/tests/config_tests.rs b/tests/config_tests.rs index 9a0bf91..3c55346 100644 --- a/tests/config_tests.rs +++ b/tests/config_tests.rs @@ -1,6 +1,6 @@ +use deston::config::config::{Algorithm, Config, LayerMode}; use std::fs; use std::path::Path; -use Deston::config::config::{Algorithm, Config}; #[test] fn test_config_parsing_basic() { @@ -151,3 +151,121 @@ weight = 3 // Clean up fs::remove_file(config_path).ok(); } + +#[test] +fn test_config_layer_mode_l4() { + let config_content = r#" +[load_balancer] +address = "127.0.0.1" +port = 8080 +algorithm = "round_robin" +layer = "L4" + +[[server]] +address = "127.0.0.1" +port = 3000 +"#; + + let config_path = "/tmp/test_config_layer_l4.toml"; + fs::write(config_path, config_content).unwrap(); + + let config = Config::new(Path::new(config_path)); + + assert_eq!(config.layer_mode, LayerMode::L4); + + // Clean up + fs::remove_file(config_path).ok(); +} + +#[test] +fn test_config_layer_mode_l7() { + let config_content = r#" +[load_balancer] +address = "127.0.0.1" +port = 8080 +algorithm = "round_robin" +layer = "L7" + +[[server]] +address = "127.0.0.1" +port = 3000 +"#; + + let config_path = "/tmp/test_config_layer_l7.toml"; + fs::write(config_path, config_content).unwrap(); + + let config = Config::new(Path::new(config_path)); + + assert_eq!(config.layer_mode, LayerMode::L7); + + // Clean up + fs::remove_file(config_path).ok(); +} + +#[test] +fn test_config_layer_mode_case_insensitive() { + let test_cases = vec![ + ("l4", LayerMode::L4), + ("L4", LayerMode::L4), + ("layer4", LayerMode::L4), + ("Layer4", LayerMode::L4), + ("layer_4", LayerMode::L4), + ("l7", LayerMode::L7), + ("L7", LayerMode::L7), + ("layer7", LayerMode::L7), + ("Layer7", LayerMode::L7), + ("layer_7", LayerMode::L7), + ]; + + for (layer_str, expected_mode) in test_cases { + let config_content = format!( + r#" +[load_balancer] +address = "127.0.0.1" +port = 8080 +algorithm = "round_robin" +layer = "{}" + +[[server]] +address = "127.0.0.1" +port = 3000 +"#, + layer_str + ); + + let config_path = format!("/tmp/test_config_layer_{}.toml", layer_str.replace("_", "")); + fs::write(&config_path, config_content).unwrap(); + + let config = Config::new(Path::new(&config_path)); + assert_eq!(config.layer_mode, expected_mode); + + // Clean up + fs::remove_file(&config_path).ok(); + } +} + +#[test] +fn test_config_layer_mode_default() { + // Config without layer specified should default to L4 + let config_content = r#" +[load_balancer] +address = "127.0.0.1" +port = 8080 +algorithm = "round_robin" + +[[server]] +address = "127.0.0.1" +port = 3000 +"#; + + let config_path = "/tmp/test_config_layer_default.toml"; + fs::write(config_path, config_content).unwrap(); + + let config = Config::new(Path::new(config_path)); + + // Should default to L4 + assert_eq!(config.layer_mode, LayerMode::L4); + + // Clean up + fs::remove_file(config_path).ok(); +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs new file mode 100644 index 0000000..f9b9d63 --- /dev/null +++ b/tests/integration_tests.rs @@ -0,0 +1,103 @@ +use deston::config::config::{Config, LayerMode}; +use std::fs; +use std::path::Path; + +#[test] +fn test_l4_config_file() { + // Test with the default config.toml that has L4 mode + let config_content = r#" +[load_balancer] +address = "127.0.0.1" +port = 8080 +algorithm = "round_robin" +layer = "L4" + +[[server]] +address = "127.0.0.1" +port = 3000 +max_connections = 1000 +weight = 1 + +[[server]] +address = "127.0.0.1" +port = 3001 +max_connections = 1000 +weight = 1 +"#; + + let config_path = "/tmp/test_integration_l4.toml"; + fs::write(config_path, config_content).unwrap(); + + let config = Config::new(Path::new(config_path)); + + // Verify L4 configuration + assert_eq!(config.layer_mode, LayerMode::L4); + assert_eq!(config.load_balancer_address.to_string(), "127.0.0.1:8080"); + assert_eq!(config.servers.len(), 2); + + // Clean up + fs::remove_file(config_path).ok(); +} + +#[test] +fn test_l7_config_file() { + // Test with L7 mode + let config_content = r#" +[load_balancer] +address = "127.0.0.1" +port = 8080 +algorithm = "round_robin" +layer = "L7" + +[[server]] +address = "127.0.0.1" +port = 3000 +max_connections = 1000 +weight = 1 + +[[server]] +address = "127.0.0.1" +port = 3001 +max_connections = 1000 +weight = 1 +"#; + + let config_path = "/tmp/test_integration_l7.toml"; + fs::write(config_path, config_content).unwrap(); + + let config = Config::new(Path::new(config_path)); + + // Verify L7 configuration + assert_eq!(config.layer_mode, LayerMode::L7); + assert_eq!(config.load_balancer_address.to_string(), "127.0.0.1:8080"); + assert_eq!(config.servers.len(), 2); + + // Clean up + fs::remove_file(config_path).ok(); +} + +#[test] +fn test_backward_compatibility_no_layer() { + // Test that missing layer defaults to L4 for backward compatibility + let config_content = r#" +[load_balancer] +address = "127.0.0.1" +port = 8080 +algorithm = "round_robin" + +[[server]] +address = "127.0.0.1" +port = 3000 +"#; + + let config_path = "/tmp/test_integration_backward_compat.toml"; + fs::write(config_path, config_content).unwrap(); + + let config = Config::new(Path::new(config_path)); + + // Should default to L4 + assert_eq!(config.layer_mode, LayerMode::L4); + + // Clean up + fs::remove_file(config_path).ok(); +}