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 config;
36pub mod hint_parser;
37pub mod metrics;
38pub mod node_filter;
39pub mod query_router;
40
41pub use config::{AliasConfig, ConsistencyConfig, HintConfig, RoutingConfig};
42pub use hint_parser::{
43    CacheBehavior, ConsistencyLevel, HintParser, ParsedHints, QueryPriority, RouteTarget,
44    RoutingHint,
45};
46pub use metrics::{HintUsageStats, RoutingMetrics, RoutingStats};
47pub use node_filter::{FilterResult, NodeCriteria, NodeFilter, NodeInfo, NodeRole, SyncMode};
48pub use query_router::{QueryRouter, RoutingDecision, RoutingReason};
49
50use std::time::Duration;
51use thiserror::Error;
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>()
86            .ok()
87            .map(|h| Duration::from_secs(h * 3600))
88    } else {
89        // Try parsing as milliseconds by default
90        s.parse::<u64>().ok().map(Duration::from_millis)
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_parse_duration() {
100        assert_eq!(parse_duration("100ms"), Some(Duration::from_millis(100)));
101        assert_eq!(parse_duration("5s"), Some(Duration::from_secs(5)));
102        assert_eq!(parse_duration("2m"), Some(Duration::from_secs(120)));
103        assert_eq!(parse_duration("1h"), Some(Duration::from_secs(3600)));
104        assert_eq!(parse_duration("500"), Some(Duration::from_millis(500)));
105        assert_eq!(parse_duration("invalid"), None);
106    }
107}