Skip to main content

heliosdb_proxy/routing/
mod.rs

1//! Query Routing Hints - HeliosProxy Feature 03
2//!
3//! SQL comment-based routing hints give applications fine-grained control
4//! over query routing. Hints take precedence over default routing policies.
5//!
6//! # Supported Hints
7//!
8//! | Hint | Values | Description |
9//! |------|--------|-------------|
10//! | `route` | `primary`, `standby`, `sync`, `semisync`, `async`, `any`, `local` | Target node type |
11//! | `node` | Node name | Route to specific node |
12//! | `consistency` | `strong`, `bounded`, `eventual` | Consistency requirement |
13//! | `pool` | `session`, `transaction`, `statement` | Pooling mode |
14//! | `cache` | `skip`, `refresh`, `semantic` | Cache behavior |
15//! | `timeout` | Duration (e.g., `5s`, `100ms`) | Query timeout |
16//! | `priority` | `low`, `normal`, `high`, `critical` | Scheduling priority |
17//! | `lag` | Duration (e.g., `100ms`, `1s`) | Maximum acceptable lag |
18//!
19//! # Examples
20//!
21//! ```sql
22//! -- Route critical reads to primary
23//! /*helios:route=primary*/
24//! SELECT balance FROM accounts WHERE id = $1 FOR UPDATE;
25//!
26//! -- Route analytics to async replica
27//! /*helios:route=async,lag=10s,priority=low*/
28//! SELECT DATE(created_at), COUNT(*) FROM orders GROUP BY 1;
29//!
30//! -- Route to specific node
31//! /*helios:node=standby-sync-1*/
32//! SELECT * FROM debug_logs;
33//! ```
34
35pub mod hint_parser;
36pub mod query_router;
37pub mod node_filter;
38pub mod metrics;
39pub mod config;
40
41pub use hint_parser::{
42    HintParser, RoutingHint, RouteTarget, ConsistencyLevel,
43    QueryPriority, CacheBehavior, ParsedHints,
44};
45pub use query_router::{QueryRouter, RoutingDecision, RoutingReason};
46pub use node_filter::{NodeFilter, NodeCriteria, FilterResult, NodeInfo, NodeRole, SyncMode};
47pub use metrics::{RoutingMetrics, RoutingStats, HintUsageStats};
48pub use config::{RoutingConfig, HintConfig, ConsistencyConfig, AliasConfig};
49
50use thiserror::Error;
51use std::time::Duration;
52
53/// Routing errors
54#[derive(Debug, Error)]
55pub enum RoutingError {
56    #[error("No nodes match routing hints: {0}")]
57    NoMatchingNodes(String),
58
59    #[error("Invalid hint combination: {0}")]
60    InvalidHintCombination(String),
61
62    #[error("Node not found: {0}")]
63    NodeNotFound(String),
64
65    #[error("Hint not allowed: {0}")]
66    HintNotAllowed(String),
67
68    #[error("Parse error: {0}")]
69    ParseError(String),
70}
71
72pub type Result<T> = std::result::Result<T, RoutingError>;
73
74/// Parse duration from string (e.g., "5s", "100ms", "1m")
75pub fn parse_duration(s: &str) -> Option<Duration> {
76    let s = s.trim().to_lowercase();
77
78    if let Some(num) = s.strip_suffix("ms") {
79        num.parse::<u64>().ok().map(Duration::from_millis)
80    } else if let Some(num) = s.strip_suffix('s') {
81        num.parse::<u64>().ok().map(Duration::from_secs)
82    } else if let Some(num) = s.strip_suffix('m') {
83        num.parse::<u64>().ok().map(|m| Duration::from_secs(m * 60))
84    } else if let Some(num) = s.strip_suffix('h') {
85        num.parse::<u64>().ok().map(|h| Duration::from_secs(h * 3600))
86    } else {
87        // Try parsing as milliseconds by default
88        s.parse::<u64>().ok().map(Duration::from_millis)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_parse_duration() {
98        assert_eq!(parse_duration("100ms"), Some(Duration::from_millis(100)));
99        assert_eq!(parse_duration("5s"), Some(Duration::from_secs(5)));
100        assert_eq!(parse_duration("2m"), Some(Duration::from_secs(120)));
101        assert_eq!(parse_duration("1h"), Some(Duration::from_secs(3600)));
102        assert_eq!(parse_duration("500"), Some(Duration::from_millis(500)));
103        assert_eq!(parse_duration("invalid"), None);
104    }
105}