Skip to main content

zentinel_common/
types.rs

1//! Common type definitions for Zentinel proxy.
2//!
3//! This module provides shared type definitions used throughout the platform,
4//! with a focus on type safety and operational clarity.
5//!
6//! For identifier types (CorrelationId, RequestId, etc.), see the `ids` module.
7
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::str::FromStr;
11
12/// HTTP method wrapper with validation
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum HttpMethod {
15    GET,
16    POST,
17    PUT,
18    DELETE,
19    HEAD,
20    OPTIONS,
21    PATCH,
22    CONNECT,
23    TRACE,
24    #[serde(untagged)]
25    Custom(String),
26}
27
28impl FromStr for HttpMethod {
29    type Err = std::convert::Infallible;
30
31    fn from_str(s: &str) -> Result<Self, Self::Err> {
32        Ok(match s.to_uppercase().as_str() {
33            "GET" => Self::GET,
34            "POST" => Self::POST,
35            "PUT" => Self::PUT,
36            "DELETE" => Self::DELETE,
37            "HEAD" => Self::HEAD,
38            "OPTIONS" => Self::OPTIONS,
39            "PATCH" => Self::PATCH,
40            "CONNECT" => Self::CONNECT,
41            "TRACE" => Self::TRACE,
42            other => Self::Custom(other.to_string()),
43        })
44    }
45}
46
47impl fmt::Display for HttpMethod {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            Self::GET => write!(f, "GET"),
51            Self::POST => write!(f, "POST"),
52            Self::PUT => write!(f, "PUT"),
53            Self::DELETE => write!(f, "DELETE"),
54            Self::HEAD => write!(f, "HEAD"),
55            Self::OPTIONS => write!(f, "OPTIONS"),
56            Self::PATCH => write!(f, "PATCH"),
57            Self::CONNECT => write!(f, "CONNECT"),
58            Self::TRACE => write!(f, "TRACE"),
59            Self::Custom(method) => write!(f, "{}", method),
60        }
61    }
62}
63
64/// TLS version
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
66pub enum TlsVersion {
67    #[serde(rename = "TLS1.2")]
68    Tls12,
69    #[serde(rename = "TLS1.3")]
70    Tls13,
71}
72
73impl fmt::Display for TlsVersion {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            Self::Tls12 => write!(f, "TLS1.2"),
77            Self::Tls13 => write!(f, "TLS1.3"),
78        }
79    }
80}
81
82/// Trace ID format selection.
83///
84/// Controls how trace IDs are generated for request tracing.
85///
86/// # Formats
87///
88/// - **TinyFlake** (default): 11-character Base58 encoded ID with time prefix.
89///   Operator-friendly format designed for easy copying and log correlation.
90///   Example: `k7BxR3nVp2Ym`
91///
92/// - **UUID**: Standard 36-character UUID v4 format with dashes.
93///   Guaranteed unique, widely compatible.
94///   Example: `550e8400-e29b-41d4-a716-446655440000`
95///
96/// # Configuration
97///
98/// ```kdl
99/// server {
100///     trace-id-format "tinyflake"  // or "uuid"
101/// }
102/// ```
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
104#[serde(rename_all = "lowercase")]
105pub enum TraceIdFormat {
106    /// TinyFlake format: 11-char Base58, time-prefixed (default)
107    #[default]
108    TinyFlake,
109
110    /// UUID v4 format: 36-char with dashes
111    Uuid,
112}
113
114impl TraceIdFormat {
115    /// Parse format from string (case-insensitive)
116    pub fn from_str_loose(s: &str) -> Self {
117        match s.to_lowercase().as_str() {
118            "uuid" | "uuid4" | "uuidv4" => TraceIdFormat::Uuid,
119            _ => TraceIdFormat::TinyFlake, // Default to TinyFlake
120        }
121    }
122}
123
124impl fmt::Display for TraceIdFormat {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        match self {
127            TraceIdFormat::TinyFlake => write!(f, "tinyflake"),
128            TraceIdFormat::Uuid => write!(f, "uuid"),
129        }
130    }
131}
132
133/// Load balancing algorithm
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum LoadBalancingAlgorithm {
137    RoundRobin,
138    LeastConnections,
139    Random,
140    IpHash,
141    Weighted,
142    ConsistentHash,
143    PowerOfTwoChoices,
144    Adaptive,
145    /// Least tokens queued - for inference/LLM workloads
146    ///
147    /// Selects the upstream with the fewest estimated tokens currently
148    /// being processed. Useful for LLM inference backends where token
149    /// throughput varies significantly between requests.
150    LeastTokensQueued,
151    /// Maglev consistent hashing - Google's load balancing algorithm
152    ///
153    /// Provides minimal disruption when backend servers are added/removed,
154    /// with better load distribution than traditional consistent hashing.
155    /// Uses a permutation-based lookup table for O(1) selection.
156    Maglev,
157    /// Locality-aware load balancing
158    ///
159    /// Prefers targets in the same zone/region as the proxy, falling back
160    /// to other zones when local targets are unhealthy or overloaded.
161    /// Useful for multi-region deployments to minimize latency.
162    LocalityAware,
163    /// Peak EWMA (Exponentially Weighted Moving Average)
164    ///
165    /// Twitter Finagle's algorithm that tracks latency using EWMA and selects
166    /// the backend with the lowest predicted completion time. Reacts quickly
167    /// to latency spikes by using the peak of EWMA and recent latency.
168    PeakEwma,
169    /// Deterministic Subsetting
170    ///
171    /// For very large clusters (1000+ backends), limits each proxy instance
172    /// to a deterministic subset of backends. Reduces connection overhead
173    /// while ensuring even distribution across all proxies.
174    DeterministicSubset,
175    /// Weighted Least Connections
176    ///
177    /// Combines weight with connection counting. Selects the backend with
178    /// the lowest ratio of active connections to weight. Useful when backends
179    /// have different capacities.
180    WeightedLeastConnections,
181    /// Cookie-based sticky sessions
182    ///
183    /// Routes requests to the same backend based on an affinity cookie.
184    /// Falls back to a configurable algorithm when no cookie is present or
185    /// the target is unavailable. Useful for stateful applications that
186    /// require session affinity.
187    Sticky,
188}
189
190/// Health check type
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(rename_all = "snake_case")]
193pub enum HealthCheckType {
194    Http {
195        path: String,
196        expected_status: u16,
197        #[serde(skip_serializing_if = "Option::is_none")]
198        host: Option<String>,
199    },
200    Tcp,
201    Grpc {
202        service: String,
203    },
204    /// Inference health check for LLM/AI backends
205    ///
206    /// Probes the `/v1/models` endpoint (or custom endpoint) to verify
207    /// the inference server is running and expected models are available.
208    /// Optionally includes enhanced readiness checks for model availability.
209    Inference {
210        /// Endpoint to probe (default: "/v1/models")
211        endpoint: String,
212        /// Expected models that must be available (optional)
213        #[serde(default, skip_serializing_if = "Vec::is_empty")]
214        expected_models: Vec<String>,
215        /// Enhanced readiness checks (optional)
216        #[serde(default, skip_serializing_if = "Option::is_none")]
217        readiness: Option<Box<crate::inference::InferenceReadinessConfig>>,
218    },
219}
220
221/// Retry policy
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct RetryPolicy {
224    pub max_attempts: u32,
225    pub timeout_ms: u64,
226    pub backoff_base_ms: u64,
227    pub backoff_max_ms: u64,
228    pub retryable_status_codes: Vec<u16>,
229}
230
231impl Default for RetryPolicy {
232    fn default() -> Self {
233        Self {
234            max_attempts: 3,
235            timeout_ms: 30000,
236            backoff_base_ms: 100,
237            backoff_max_ms: 10000,
238            retryable_status_codes: vec![502, 503, 504],
239        }
240    }
241}
242
243/// Circuit breaker configuration
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct CircuitBreakerConfig {
246    pub failure_threshold: u32,
247    pub success_threshold: u32,
248    pub timeout_seconds: u64,
249    pub half_open_max_requests: u32,
250}
251
252impl Default for CircuitBreakerConfig {
253    fn default() -> Self {
254        Self {
255            failure_threshold: 5,
256            success_threshold: 2,
257            timeout_seconds: 30,
258            half_open_max_requests: 1,
259        }
260    }
261}
262
263/// Circuit breaker state
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(rename_all = "snake_case")]
266pub enum CircuitBreakerState {
267    Closed,
268    Open,
269    HalfOpen,
270}
271
272/// Route evaluation priority.
273///
274/// Routes are sorted in descending priority order — higher values are
275/// evaluated first. Any `i32` value is accepted; the named constants
276/// ([`LOW`](Self::LOW), [`NORMAL`](Self::NORMAL), [`HIGH`](Self::HIGH),
277/// [`CRITICAL`](Self::CRITICAL)) exist as conveniences for common cases, but
278/// gap-based values like `Priority(500)` are fully supported so routes can be
279/// finely ordered between named tiers.
280///
281/// KDL syntax accepts either an integer (`priority 100`) or one of the named
282/// string aliases (`priority "high"`), with the aliases resolving to the
283/// matching constant defined below.
284///
285/// # Examples
286///
287/// ```
288/// use zentinel_common::types::Priority;
289///
290/// assert!(Priority::HIGH > Priority::NORMAL);
291/// assert!(Priority(500) > Priority::HIGH);
292/// assert_eq!(Priority::default(), Priority::NORMAL);
293/// ```
294#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
295#[serde(transparent)]
296pub struct Priority(pub i32);
297
298impl Priority {
299    /// Low-priority routes (default weight: `10`). Evaluated after normal routes.
300    pub const LOW: Self = Self(10);
301    /// Normal-priority routes (default weight: `50`). The default for routes
302    /// that do not specify an explicit priority.
303    pub const NORMAL: Self = Self(50);
304    /// High-priority routes (default weight: `100`). Evaluated before normal routes.
305    pub const HIGH: Self = Self(100);
306    /// Critical-priority routes (default weight: `1000`). Evaluated first; intended
307    /// for health checks and other infrastructure-critical routes.
308    pub const CRITICAL: Self = Self(1000);
309
310    /// Returns the underlying integer weight.
311    #[inline]
312    pub const fn as_i32(self) -> i32 {
313        self.0
314    }
315}
316
317impl Default for Priority {
318    fn default() -> Self {
319        Self::NORMAL
320    }
321}
322
323impl std::fmt::Display for Priority {
324    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325        write!(f, "{}", self.0)
326    }
327}
328
329impl From<i32> for Priority {
330    fn from(value: i32) -> Self {
331        Self(value)
332    }
333}
334
335/// Time window for rate limiting and metrics
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
337pub struct TimeWindow {
338    pub seconds: u64,
339}
340
341impl TimeWindow {
342    pub fn new(seconds: u64) -> Self {
343        Self { seconds }
344    }
345
346    pub fn as_duration(&self) -> std::time::Duration {
347        std::time::Duration::from_secs(self.seconds)
348    }
349}
350
351/// Byte size with human-readable serialization
352#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
353pub struct ByteSize(pub usize);
354
355impl ByteSize {
356    pub const KB: usize = 1024;
357    pub const MB: usize = 1024 * 1024;
358    pub const GB: usize = 1024 * 1024 * 1024;
359
360    pub fn from_kb(kb: usize) -> Self {
361        Self(kb * Self::KB)
362    }
363
364    pub fn from_mb(mb: usize) -> Self {
365        Self(mb * Self::MB)
366    }
367
368    pub fn from_gb(gb: usize) -> Self {
369        Self(gb * Self::GB)
370    }
371
372    pub fn as_bytes(&self) -> usize {
373        self.0
374    }
375}
376
377impl fmt::Display for ByteSize {
378    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379        if self.0 >= Self::GB {
380            write!(f, "{:.2}GB", self.0 as f64 / Self::GB as f64)
381        } else if self.0 >= Self::MB {
382            write!(f, "{:.2}MB", self.0 as f64 / Self::MB as f64)
383        } else if self.0 >= Self::KB {
384            write!(f, "{:.2}KB", self.0 as f64 / Self::KB as f64)
385        } else {
386            write!(f, "{}B", self.0)
387        }
388    }
389}
390
391impl Serialize for ByteSize {
392    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
393    where
394        S: serde::Serializer,
395    {
396        serializer.serialize_str(&self.to_string())
397    }
398}
399
400impl<'de> Deserialize<'de> for ByteSize {
401    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
402    where
403        D: serde::Deserializer<'de>,
404    {
405        let s = String::deserialize(deserializer)?;
406        Self::from_str(&s).map_err(serde::de::Error::custom)
407    }
408}
409
410impl FromStr for ByteSize {
411    type Err = String;
412
413    fn from_str(s: &str) -> Result<Self, Self::Err> {
414        let s = s.trim();
415        if s.is_empty() {
416            return Err("Empty byte size string".to_string());
417        }
418
419        // Try to parse as plain number (bytes)
420        if let Ok(bytes) = s.parse::<usize>() {
421            return Ok(Self(bytes));
422        }
423
424        // Parse with unit suffix
425        let (num_part, unit_part) = s
426            .chars()
427            .position(|c| c.is_alphabetic())
428            .map(|i| s.split_at(i))
429            .ok_or_else(|| format!("Invalid byte size format: {}", s))?;
430
431        let value: f64 = num_part
432            .trim()
433            .parse()
434            .map_err(|_| format!("Invalid number: {}", num_part))?;
435
436        let multiplier = match unit_part.to_uppercase().as_str() {
437            "B" => 1,
438            "KB" | "K" => Self::KB,
439            "MB" | "M" => Self::MB,
440            "GB" | "G" => Self::GB,
441            _ => return Err(format!("Invalid unit: {}", unit_part)),
442        };
443
444        Ok(Self((value * multiplier as f64) as usize))
445    }
446}
447
448/// IP address wrapper with additional metadata
449#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
450pub struct ClientIp {
451    pub address: std::net::IpAddr,
452    #[serde(skip_serializing_if = "Option::is_none")]
453    pub forwarded_for: Option<Vec<std::net::IpAddr>>,
454}
455
456impl fmt::Display for ClientIp {
457    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
458        write!(f, "{}", self.address)
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn test_http_method_parsing() {
468        assert_eq!(HttpMethod::from_str("GET").unwrap(), HttpMethod::GET);
469        assert_eq!(HttpMethod::from_str("post").unwrap(), HttpMethod::POST);
470        assert_eq!(
471            HttpMethod::from_str("PROPFIND").unwrap(),
472            HttpMethod::Custom("PROPFIND".to_string())
473        );
474    }
475
476    #[test]
477    fn test_byte_size_parsing() {
478        assert_eq!(ByteSize::from_str("1024").unwrap().0, 1024);
479        assert_eq!(ByteSize::from_str("10KB").unwrap().0, 10 * 1024);
480        assert_eq!(
481            ByteSize::from_str("5.5MB").unwrap().0,
482            (5.5 * 1024.0 * 1024.0) as usize
483        );
484        assert_eq!(ByteSize::from_str("2GB").unwrap().0, 2 * 1024 * 1024 * 1024);
485        assert_eq!(ByteSize::from_str("100 B").unwrap().0, 100);
486    }
487
488    #[test]
489    fn test_byte_size_display() {
490        assert_eq!(ByteSize(512).to_string(), "512B");
491        assert_eq!(ByteSize(2048).to_string(), "2.00KB");
492        assert_eq!(ByteSize(1024 * 1024).to_string(), "1.00MB");
493        assert_eq!(ByteSize(1024 * 1024 * 1024).to_string(), "1.00GB");
494    }
495
496    #[test]
497    fn test_trace_id_format() {
498        assert_eq!(TraceIdFormat::from_str_loose("uuid"), TraceIdFormat::Uuid);
499        assert_eq!(
500            TraceIdFormat::from_str_loose("tinyflake"),
501            TraceIdFormat::TinyFlake
502        );
503        assert_eq!(
504            TraceIdFormat::from_str_loose("unknown"),
505            TraceIdFormat::TinyFlake
506        );
507    }
508}