1use crate::admin::{AdminServer, AdminState, ConfigSnapshot, NodeSnapshot};
7#[cfg(feature = "ha-tr")]
8use crate::backend::{tls::default_client_config, BackendConfig, TlsMode};
9use crate::client_tls::{build_tls_acceptor, ClientStream};
10use crate::config::{HbaAction, HbaRule, NodeConfig, NodeRole, ProxyConfig, TrMode};
11#[cfg(feature = "wasm-plugins")]
12use crate::protocol::QueryMessage;
13use crate::protocol::{
14 ErrorResponse, Message, MessageType, ProtocolCodec, StartupMessage, TransactionStatus,
15};
16use crate::{ProxyError, Result};
17use arc_swap::ArcSwap;
18use bytes::{BufMut, BytesMut};
19use dashmap::DashMap;
20use std::collections::{HashMap, HashSet};
21use std::net::SocketAddr;
22use std::sync::atomic::{AtomicU64, Ordering};
23use std::sync::Arc;
24use std::time::Duration;
25use tokio::io::{AsyncReadExt, AsyncWriteExt};
26use tokio::net::{TcpListener, TcpStream};
27use tokio::sync::{broadcast, RwLock};
28use uuid::Uuid;
29
30#[cfg(feature = "pool-modes")]
32use crate::pool::lease::ClientId;
33#[cfg(feature = "pool-modes")]
34use crate::pool::{ConnectionPoolManager, PoolModeConfig, PoolingMode};
35#[cfg(feature = "pool-modes")]
36use crate::NodeEndpoint;
37
38#[cfg(feature = "wasm-plugins")]
40use crate::plugins::{
41 AuthRequest as PluginAuthRequest, AuthResult, HookContext, HookType, Identity, PluginManager,
42 PostQueryOutcome, PreQueryResult, QueryContext, RouteResult,
43};
44
45pub struct ProxyServer {
47 config: ProxyConfig,
48 state: Arc<ServerState>,
49 shutdown_tx: broadcast::Sender<()>,
50 config_path: Option<String>,
54}
55
56#[cfg(not(unix))]
59struct HangupNever;
60#[cfg(not(unix))]
61impl HangupNever {
62 async fn recv(&mut self) -> Option<()> {
63 std::future::pending().await
64 }
65}
66
67#[cfg(feature = "ha-tr")]
81fn build_replay_backend_template(_config: &ProxyConfig) -> BackendConfig {
82 BackendConfig {
83 host: "placeholder".to_string(),
84 port: 0,
85 user: "postgres".to_string(),
86 password: None,
87 database: None,
88 application_name: Some("heliosdb-proxy-replay".to_string()),
89 tls_mode: TlsMode::Disable,
90 connect_timeout: Duration::from_secs(5),
91 query_timeout: Duration::from_secs(30),
92 tls_config: default_client_config(),
93 }
94}
95
96#[cfg(feature = "anomaly-detection")]
106fn anomaly_fingerprint(sql: &str) -> String {
107 let mut out = String::with_capacity(sql.len());
108 let mut in_single = false;
109 let mut prev_space = false;
110 let mut chars = sql.chars().peekable();
111 while let Some(c) = chars.next() {
112 if c == '\'' {
113 in_single = !in_single;
114 if in_single {
117 out.push('?');
118 while let Some(&n) = chars.peek() {
119 chars.next();
120 if n == '\'' {
121 in_single = false;
122 break;
123 }
124 }
125 prev_space = false;
126 continue;
127 }
128 }
129 if c.is_ascii_digit() {
130 if !out.ends_with('?') {
131 out.push('?');
132 }
133 while matches!(chars.peek(), Some(c) if c.is_ascii_digit() || *c == '.') {
135 chars.next();
136 }
137 prev_space = false;
138 continue;
139 }
140 if c.is_ascii_whitespace() {
141 if !prev_space && !out.is_empty() {
142 out.push(' ');
143 prev_space = true;
144 }
145 continue;
146 }
147 out.push(c.to_ascii_lowercase());
148 prev_space = false;
149 }
150 out.trim_end().to_string()
151}
152
153struct ServerState {
155 sessions: DashMap<Uuid, Arc<ClientSession>>,
161 health: ArcSwap<HashMap<String, NodeHealth>>,
167 health_write: parking_lot::Mutex<()>,
172 live_config: ArcSwap<ProxyConfig>,
179 metrics: ServerMetrics,
181 cancel_map: Arc<DashMap<(u32, u32), String>>,
187 cancel_order: Arc<parking_lot::Mutex<std::collections::VecDeque<(u32, u32)>>>,
191 tls_acceptor: Option<tokio_rustls::TlsAcceptor>,
194 auth_file: Option<Arc<crate::auth_scram::AuthFile>>,
198 mirror: Option<crate::mirror::MirrorHandle>,
201 cutover: Arc<ArcSwap<Option<Arc<crate::mirror::CutoverTarget>>>>,
205 lb_state: LoadBalancerState,
207 #[cfg(feature = "routing-hints")]
212 hint_parser: Option<crate::routing::HintParser>,
213 #[cfg(feature = "rate-limiting")]
216 rate_limiter: Option<Arc<crate::rate_limit::RateLimiter>>,
217 #[cfg(feature = "circuit-breaker")]
221 circuit_breaker: Option<Arc<crate::circuit_breaker::CircuitBreakerManager>>,
222 #[cfg(feature = "query-analytics")]
225 analytics: Option<Arc<crate::analytics::QueryAnalytics>>,
226 #[cfg(feature = "query-cache")]
229 query_cache: Option<Arc<crate::cache::QueryCache>>,
230 #[cfg(feature = "query-rewriting")]
233 rewriter: Option<Arc<crate::rewriter::QueryRewriter>>,
234 #[cfg(feature = "multi-tenancy")]
237 tenant_manager: Option<Arc<crate::multi_tenancy::TenantManager>>,
238 #[cfg(feature = "schema-routing")]
241 schema_analyzer: Option<Arc<crate::schema_routing::QueryAnalyzer>>,
242 #[cfg(feature = "pool-modes")]
244 pool_manager: Option<Arc<ConnectionPoolManager>>,
245 #[cfg(feature = "pool-modes")]
250 backend_pool: Option<Arc<crate::pool::BackendIdlePool>>,
251 #[cfg(feature = "wasm-plugins")]
255 plugin_manager: Option<Arc<PluginManager>>,
256 #[cfg(feature = "ha-tr")]
261 transaction_journal: Arc<crate::transaction_journal::TransactionJournal>,
262 #[cfg(feature = "anomaly-detection")]
265 anomaly_detector: Arc<crate::anomaly::AnomalyDetector>,
266 #[cfg(feature = "edge-proxy")]
270 edge_cache: Arc<crate::edge::EdgeCache>,
271 #[cfg(feature = "edge-proxy")]
272 edge_registry: Arc<crate::edge::EdgeRegistry>,
273}
274
275#[derive(Debug, Clone)]
277pub struct NodeHealth {
278 pub address: String,
280 pub healthy: bool,
282 pub last_check: chrono::DateTime<chrono::Utc>,
284 pub failure_count: u32,
286 pub last_error: Option<String>,
288 pub latency_ms: f64,
290 pub replication_lag_bytes: Option<u64>,
292}
293
294#[derive(Default)]
296struct ServerMetrics {
297 connections_accepted: AtomicU64,
299 connections_closed: AtomicU64,
301 queries_processed: AtomicU64,
303 bytes_received: AtomicU64,
305 bytes_sent: AtomicU64,
307 failovers: AtomicU64,
309}
310
311struct LoadBalancerState {
313 rr_counter: AtomicU64,
316}
317
318pub struct ClientSession {
320 pub id: Uuid,
322 pub client_addr: SocketAddr,
324 pub current_node: RwLock<Option<String>>,
326 pub in_transaction: std::sync::atomic::AtomicBool,
334 pub copy_in_progress: std::sync::atomic::AtomicBool,
342 pub tx_state: RwLock<TransactionState>,
346 pub variables: RwLock<HashMap<String, String>>,
348 pub created_at: chrono::DateTime<chrono::Utc>,
350 pub tr_mode: TrMode,
352 #[cfg(feature = "lag-routing")]
357 pub last_write_at: RwLock<Option<std::time::Instant>>,
358 #[cfg(feature = "pool-modes")]
360 pub pool_client_id: ClientId,
361 #[cfg(feature = "wasm-plugins")]
366 pub plugin_identity: RwLock<Option<Identity>>,
367 #[cfg(feature = "edge-proxy")]
374 pub edge_ineligible: std::sync::atomic::AtomicBool,
375 #[cfg(feature = "edge-proxy")]
379 pub pending_edge_copy_tables: std::sync::Mutex<Option<Vec<String>>>,
380}
381
382#[derive(Debug, Clone, Default)]
384pub struct TransactionState {
385 pub in_transaction: bool,
387 pub tx_id: Option<Uuid>,
389 pub statements: Vec<StatementLog>,
391 pub read_only: bool,
393 pub savepoints: Vec<String>,
395}
396
397#[derive(Debug, Clone)]
399pub struct StatementLog {
400 pub sql: String,
402 pub params: Vec<String>,
404 pub result_checksum: Option<u64>,
406 pub executed_at: chrono::DateTime<chrono::Utc>,
408}
409
410struct BackendConn {
423 stream: TcpStream,
424 prepared: HashSet<String>,
425 unnamed_sig: Option<bytes::Bytes>,
431 dirty: bool,
441}
442
443impl BackendConn {
444 fn new(stream: TcpStream) -> Self {
445 Self {
446 stream,
447 prepared: HashSet::new(),
448 unnamed_sig: None,
449 dirty: false,
450 }
451 }
452}
453
454pub(crate) fn bind_reuseport(addr: &str) -> Result<TcpListener> {
460 use socket2::{Domain, Protocol, Socket, Type};
461 let sockaddr: SocketAddr = addr
462 .parse()
463 .map_err(|e| ProxyError::Config(format!("invalid listen address '{}': {}", addr, e)))?;
464 let domain = if sockaddr.is_ipv6() {
465 Domain::IPV6
466 } else {
467 Domain::IPV4
468 };
469 let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))
470 .map_err(|e| ProxyError::Network(format!("socket(): {}", e)))?;
471 socket
472 .set_reuse_address(true)
473 .map_err(|e| ProxyError::Network(format!("SO_REUSEADDR: {}", e)))?;
474 #[cfg(all(unix, not(target_os = "solaris")))]
475 socket
476 .set_reuse_port(true)
477 .map_err(|e| ProxyError::Network(format!("SO_REUSEPORT: {}", e)))?;
478 socket
479 .set_nonblocking(true)
480 .map_err(|e| ProxyError::Network(format!("set_nonblocking: {}", e)))?;
481 socket
482 .bind(&sockaddr.into())
483 .map_err(|e| ProxyError::Network(format!("Failed to bind {}: {}", addr, e)))?;
484 socket
485 .listen(1024)
486 .map_err(|e| ProxyError::Network(format!("listen(): {}", e)))?;
487 let std_listener: std::net::TcpListener = socket.into();
488 TcpListener::from_std(std_listener)
489 .map_err(|e| ProxyError::Network(format!("from_std listener: {}", e)))
490}
491
492#[derive(Debug)]
498#[allow(dead_code)] enum PreQueryAction {
500 Forward,
502 Block(String),
505 Cached(Vec<u8>),
511}
512
513#[derive(Debug)]
519#[allow(dead_code)] enum RouteOverride {
521 None,
523 Primary,
525 Standby,
527 Node(String),
531 Block(String),
536}
537
538struct SessionGuard {
545 state: Arc<ServerState>,
546 session_id: Uuid,
547}
548
549impl Drop for SessionGuard {
550 fn drop(&mut self) {
551 self.state.sessions.remove(&self.session_id);
552 self.state
553 .metrics
554 .connections_closed
555 .fetch_add(1, Ordering::Relaxed);
556 #[cfg(feature = "query-cache")]
559 if let Some(ref qc) = self.state.query_cache {
560 qc.remove_l1_cache(self.session_id.as_u64_pair().0);
561 }
562 }
563}
564
565impl ProxyServer {
566 #[cfg(feature = "wasm-plugins")]
574 fn init_plugin_manager(
575 toml_cfg: &crate::config::PluginToml,
576 ) -> Option<Arc<crate::plugins::PluginManager>> {
577 if !toml_cfg.enabled {
578 return None;
579 }
580
581 let runtime_cfg = crate::plugins::PluginRuntimeConfig::from(toml_cfg);
582 let plugin_dir = runtime_cfg.plugin_dir.clone();
583
584 let pm = match crate::plugins::PluginManager::new(runtime_cfg) {
585 Ok(pm) => Arc::new(pm),
586 Err(e) => {
587 tracing::error!(error = %e, "Failed to create plugin manager; plugins disabled");
588 return None;
589 }
590 };
591
592 match std::fs::read_dir(&plugin_dir) {
593 Ok(entries) => {
594 let mut loaded = 0usize;
595 let mut failed = 0usize;
596 for entry in entries.flatten() {
597 let path = entry.path();
598 if path.extension().and_then(|s| s.to_str()) != Some("wasm") {
599 continue;
600 }
601 match pm.load_plugin(&path) {
602 Ok(()) => loaded += 1,
603 Err(e) => {
604 failed += 1;
605 tracing::warn!(
606 path = %path.display(),
607 error = %e,
608 "Failed to load plugin"
609 );
610 }
611 }
612 }
613 tracing::info!(
614 dir = %plugin_dir.display(),
615 loaded = loaded,
616 failed = failed,
617 "Plugin loading complete"
618 );
619 }
620 Err(e) => {
621 tracing::warn!(
622 dir = %plugin_dir.display(),
623 error = %e,
624 "Plugin directory not readable; no plugins loaded"
625 );
626 }
627 }
628
629 Some(pm)
630 }
631
632 pub fn new(config: ProxyConfig) -> Result<Self> {
634 let (shutdown_tx, _) = broadcast::channel(1);
635
636 let mut health = HashMap::new();
638 for node in &config.nodes {
639 health.insert(
640 node.address(),
641 NodeHealth {
642 address: node.address(),
643 healthy: true, last_check: chrono::Utc::now(),
645 failure_count: 0,
646 last_error: None,
647 latency_ms: 0.0,
648 replication_lag_bytes: None,
649 },
650 );
651 }
652
653 #[cfg(feature = "pool-modes")]
655 let pool_manager = {
656 use crate::pool::PreparedStatementMode as PoolPreparedStatementMode;
657
658 let pool_config = PoolModeConfig {
659 default_mode: match config.pool_mode.mode {
660 crate::config::PoolingMode::Session => PoolingMode::Session,
661 crate::config::PoolingMode::Transaction => PoolingMode::Transaction,
662 crate::config::PoolingMode::Statement => PoolingMode::Statement,
663 },
664 max_pool_size: config.pool_mode.max_pool_size,
665 min_idle: config.pool_mode.min_idle,
666 idle_timeout_secs: config.pool_mode.idle_timeout_secs,
667 max_lifetime_secs: config.pool_mode.max_lifetime_secs,
668 acquire_timeout_secs: config.pool_mode.acquire_timeout_secs,
669 reset_query: config.pool_mode.reset_query.clone(),
670 prepared_statement_mode: match config.pool_mode.prepared_statement_mode {
671 crate::config::PreparedStatementMode::Disable => {
672 PoolPreparedStatementMode::Disable
673 }
674 crate::config::PreparedStatementMode::Track => PoolPreparedStatementMode::Track,
675 crate::config::PreparedStatementMode::Named => PoolPreparedStatementMode::Named,
676 },
677 test_on_acquire: config.pool.test_on_acquire,
678 validation_query: "SELECT 1".to_string(),
679 queue_timeout_secs: 30,
680 max_queue_size: 0,
681 };
682 Some(Arc::new(ConnectionPoolManager::new(pool_config)))
683 };
684
685 #[cfg(feature = "pool-modes")]
689 let backend_pool = match config.pool_mode.mode {
690 crate::config::PoolingMode::Transaction | crate::config::PoolingMode::Statement => {
691 tracing::info!(
692 mode = ?config.pool_mode.mode,
693 max_idle_per_identity = config.pool_mode.max_pool_size,
694 "pool-modes: data-path connection pooling enabled"
695 );
696 Some(Arc::new(crate::pool::BackendIdlePool::new(
697 config.pool_mode.max_pool_size as usize,
698 Self::MAX_TOTAL_IDLE_BACKEND_CONNS,
699 )))
700 }
701 crate::config::PoolingMode::Session => None,
702 };
703
704 #[cfg(feature = "wasm-plugins")]
709 let plugin_manager = Self::init_plugin_manager(&config.plugins);
710
711 let tls_acceptor = match config.tls.as_ref() {
715 Some(tls) if tls.enabled => match build_tls_acceptor(tls) {
716 Ok(acc) => {
717 tracing::info!(
718 mtls = tls.require_client_cert,
719 "client TLS termination enabled"
720 );
721 Some(acc)
722 }
723 Err(e) => {
724 return Err(ProxyError::Config(format!("TLS init failed: {}", e)));
725 }
726 },
727 _ => None,
728 };
729
730 let auth_file = if config.auth.mode == crate::config::AuthMode::Scram {
733 let path = config.auth.auth_file.as_ref().ok_or_else(|| {
734 ProxyError::Config("auth mode 'scram' requires auth_file".to_string())
735 })?;
736 let af = crate::auth_scram::AuthFile::load(path)
737 .map_err(|e| ProxyError::Config(format!("auth_file: {}", e)))?;
738 tracing::info!(users = %(!af.is_empty()), "proxy SCRAM auth enabled");
739 Some(Arc::new(af))
740 } else {
741 None
742 };
743
744 let mirror = if config.mirror.enabled {
747 tracing::info!(target = %format!("{}:{}", config.mirror.backend_host, config.mirror.backend_port),
748 writes_only = config.mirror.writes_only, "traffic mirroring enabled");
749 Some(crate::mirror::spawn(config.mirror.clone()))
750 } else {
751 None
752 };
753
754 #[cfg(feature = "rate-limiting")]
756 let rate_limiter = if config.rate_limit.enabled {
757 let rl = &config.rate_limit;
758 tracing::info!(
759 qps = rl.default_qps,
760 burst = rl.default_burst,
761 key_by = ?rl.key_by,
762 "rate limiting enabled"
763 );
764 let rlc = crate::rate_limit::RateLimitConfig {
765 enabled: true,
766 default_qps: rl.default_qps,
767 default_burst: rl.default_burst,
768 default_concurrency: if rl.max_concurrent > 0 {
769 rl.max_concurrent
770 } else {
771 crate::rate_limit::RateLimitConfig::default().default_concurrency
772 },
773 ..Default::default()
774 };
775 Some(Arc::new(crate::rate_limit::RateLimiter::new(rlc)))
776 } else {
777 None
778 };
779
780 #[cfg(feature = "circuit-breaker")]
782 let circuit_breaker = if config.circuit_breaker.enabled {
783 let cb = &config.circuit_breaker;
784 tracing::info!(
785 failure_threshold = cb.failure_threshold,
786 open_secs = cb.open_secs,
787 "circuit breaker enabled"
788 );
789 let cbc = crate::circuit_breaker::CircuitBreakerConfig {
790 failure_threshold: cb.failure_threshold,
791 cooldown: Duration::from_secs(cb.open_secs),
792 half_open_success_threshold: cb.success_threshold,
793 ..Default::default()
794 };
795 let mgr = crate::circuit_breaker::CircuitBreakerManager::new(
796 crate::circuit_breaker::ManagerConfig::new(cbc),
797 );
798 Some(Arc::new(mgr))
799 } else {
800 None
801 };
802
803 #[cfg(feature = "query-analytics")]
805 let analytics = if config.analytics.enabled {
806 let a = &config.analytics;
807 tracing::info!(
808 slow_query_ms = a.slow_query_ms,
809 max_fingerprints = a.max_fingerprints,
810 "query analytics enabled"
811 );
812 let ac = crate::analytics::AnalyticsConfig {
813 enabled: true,
814 max_fingerprints: a.max_fingerprints as usize,
815 slow_query: crate::analytics::SlowQueryConfig {
816 threshold: Duration::from_millis(a.slow_query_ms),
817 ..Default::default()
818 },
819 ..Default::default()
820 };
821 Some(Arc::new(crate::analytics::QueryAnalytics::new(ac)))
822 } else {
823 None
824 };
825
826 #[cfg(feature = "query-cache")]
828 let query_cache = if config.cache.enabled {
829 let c = &config.cache;
830 tracing::info!(
831 ttl_secs = c.ttl_secs,
832 max_result_bytes = c.max_result_bytes,
833 "query cache enabled (L1 hot + L2 warm)"
834 );
835 let ttl = Duration::from_secs(c.ttl_secs);
836 let cc = crate::cache::CacheConfig {
837 enabled: true,
838 default_ttl: ttl,
839 max_result_size: c.max_result_bytes,
840 l1: crate::cache::L1Config {
841 ttl,
842 ..Default::default()
843 },
844 l2: crate::cache::L2Config {
845 ttl,
846 ..Default::default()
847 },
848 ..Default::default()
849 };
850 Some(Arc::new(crate::cache::QueryCache::new(cc)))
851 } else {
852 None
853 };
854
855 #[cfg(feature = "query-rewriting")]
857 let rewriter = if config.query_rewrite.enabled && !config.query_rewrite.rules.is_empty() {
858 use crate::rewriter::{
859 QueryPattern, QueryRewriter, RewriteRule, RewriterConfig, Transformation,
860 };
861 let rw = QueryRewriter::new(RewriterConfig {
862 enabled: true,
863 ..Default::default()
864 });
865 let mut n = 0usize;
866 for (i, r) in config.query_rewrite.rules.iter().enumerate() {
867 let transformation =
868 if let (Some(from), Some(to)) = (&r.match_table, &r.replace_table_with) {
869 Transformation::ReplaceTable {
870 from: from.clone(),
871 to: to.clone(),
872 }
873 } else if let Some(w) = &r.append_where {
874 Transformation::AppendWhereAnd(w.clone())
875 } else if let Some(limit) = r.add_limit {
876 Transformation::AddLimit(limit)
877 } else {
878 continue; };
880 let pattern = if let Some(t) = &r.match_table {
881 QueryPattern::Table(t.clone())
882 } else if let Some(re) = &r.match_regex {
883 QueryPattern::regex(re.clone())
884 } else {
885 QueryPattern::All
886 };
887 rw.add_rule(
888 RewriteRule::build(format!("rule-{i}"))
889 .pattern(pattern)
890 .transform(transformation)
891 .build(),
892 );
893 n += 1;
894 }
895 tracing::info!(rules = n, "query rewriting enabled");
896 Some(Arc::new(rw))
897 } else {
898 None
899 };
900
901 #[cfg(feature = "multi-tenancy")]
903 let tenant_manager =
904 if config.multi_tenancy.enabled && !config.multi_tenancy.tenants.is_empty() {
905 use crate::multi_tenancy::{
906 IdentificationMethod, IsolationStrategy, MultiTenancyConfig, TenantConfig,
907 TenantId, TenantManagerBuilder, TenantQueryTransformer,
908 };
909 let mt = &config.multi_tenancy;
910 let identification = match mt.identify_by.as_str() {
911 "database" => IdentificationMethod::DatabaseName,
912 param => IdentificationMethod::Header {
913 header_name: param.to_string(),
914 },
915 };
916 let mtc = MultiTenancyConfig {
917 enabled: true,
918 identification,
919 ..Default::default()
920 };
921 let table_refs: Vec<&str> = mt.tenant_tables.iter().map(|s| s.as_str()).collect();
923 let transformer = TenantQueryTransformer::new()
924 .register_tables(&table_refs, mt.tenant_column.clone());
925 let tm = TenantManagerBuilder::new()
926 .config(mtc)
927 .query_transformer(transformer)
928 .build();
929 for id in &mt.tenants {
930 tm.register_tenant(TenantConfig::new(
931 TenantId::new(id.clone()),
932 IsolationStrategy::row("public", mt.tenant_column.clone()),
933 ));
934 }
935 tracing::info!(
936 tenants = mt.tenants.len(),
937 identify_by = %mt.identify_by,
938 "multi-tenancy enabled"
939 );
940 Some(Arc::new(tm))
941 } else {
942 None
943 };
944
945 #[cfg(feature = "schema-routing")]
947 let schema_analyzer =
948 if config.schema_routing.enabled && !config.schema_routing.analytics_node.is_empty() {
949 tracing::info!(
950 analytics_node = %config.schema_routing.analytics_node,
951 "schema/workload routing enabled (OLAP -> analytics node)"
952 );
953 let registry = Arc::new(crate::schema_routing::SchemaRegistry::new());
954 Some(Arc::new(crate::schema_routing::QueryAnalyzer::new(
955 registry,
956 )))
957 } else {
958 None
959 };
960
961 let state = Arc::new(ServerState {
962 sessions: DashMap::new(),
963 health: ArcSwap::from_pointee(health),
964 health_write: parking_lot::Mutex::new(()),
965 live_config: ArcSwap::from_pointee(config.clone()),
966 metrics: ServerMetrics::default(),
967 cancel_map: Arc::new(DashMap::new()),
968 cancel_order: Arc::new(parking_lot::Mutex::new(std::collections::VecDeque::new())),
969 tls_acceptor,
970 auth_file,
971 mirror,
972 cutover: Arc::new(ArcSwap::from_pointee(None)),
973 lb_state: LoadBalancerState {
974 rr_counter: AtomicU64::new(0),
975 },
976 #[cfg(feature = "routing-hints")]
977 hint_parser: if config.routing_hints.enabled {
978 tracing::info!(
979 strip = config.routing_hints.strip_hints,
980 "SQL-comment routing hints enabled"
981 );
982 Some(if config.routing_hints.strip_hints {
983 crate::routing::HintParser::new()
984 } else {
985 crate::routing::HintParser::without_stripping()
986 })
987 } else {
988 None
989 },
990 #[cfg(feature = "rate-limiting")]
991 rate_limiter,
992 #[cfg(feature = "circuit-breaker")]
993 circuit_breaker,
994 #[cfg(feature = "query-analytics")]
995 analytics,
996 #[cfg(feature = "query-cache")]
997 query_cache,
998 #[cfg(feature = "query-rewriting")]
999 rewriter,
1000 #[cfg(feature = "multi-tenancy")]
1001 tenant_manager,
1002 #[cfg(feature = "schema-routing")]
1003 schema_analyzer,
1004 #[cfg(feature = "pool-modes")]
1005 pool_manager,
1006 #[cfg(feature = "pool-modes")]
1007 backend_pool,
1008 #[cfg(feature = "wasm-plugins")]
1009 plugin_manager,
1010 #[cfg(feature = "ha-tr")]
1011 transaction_journal: Arc::new(crate::transaction_journal::TransactionJournal::new()),
1012 #[cfg(feature = "anomaly-detection")]
1013 anomaly_detector: Arc::new(crate::anomaly::AnomalyDetector::new(
1014 crate::anomaly::AnomalyConfig::default(),
1015 )),
1016 #[cfg(feature = "edge-proxy")]
1017 edge_cache: Arc::new(crate::edge::EdgeCache::new(config.edge.max_entries.max(1))),
1018 #[cfg(feature = "edge-proxy")]
1019 edge_registry: Arc::new(crate::edge::EdgeRegistry::new(
1020 config.edge.max_edges,
1021 std::time::Duration::from_secs(config.edge.liveness_window_secs),
1022 )),
1023 });
1024
1025 Ok(Self {
1026 config,
1027 state,
1028 shutdown_tx,
1029 config_path: None,
1030 })
1031 }
1032
1033 pub fn with_config_path(mut self, path: Option<String>) -> Self {
1037 self.config_path = path;
1038 self
1039 }
1040
1041 #[cfg(unix)]
1044 fn hangup_stream() -> tokio::signal::unix::Signal {
1045 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup())
1046 .expect("failed to install SIGHUP handler")
1047 }
1048 #[cfg(not(unix))]
1049 fn hangup_stream() -> HangupNever {
1050 HangupNever
1051 }
1052
1053 #[cfg(unix)]
1056 fn usr2_stream() -> tokio::signal::unix::Signal {
1057 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::user_defined2())
1058 .expect("failed to install SIGUSR2 handler")
1059 }
1060 #[cfg(not(unix))]
1061 fn usr2_stream() -> HangupNever {
1062 HangupNever
1063 }
1064
1065 async fn drain_connections(state: &Arc<ServerState>, timeout: Duration) {
1069 let deadline = tokio::time::Instant::now() + timeout;
1070 loop {
1071 let active = state.sessions.len();
1072 if active == 0 {
1073 tracing::info!("drain complete — all in-flight connections finished");
1074 return;
1075 }
1076 if tokio::time::Instant::now() >= deadline {
1077 tracing::warn!(
1078 active,
1079 "drain timeout reached — exiting with connections still open"
1080 );
1081 return;
1082 }
1083 tokio::time::sleep(Duration::from_millis(200)).await;
1084 }
1085 }
1086
1087 fn drain_timeout(config_secs: u64) -> Duration {
1092 let secs = std::env::var("HELIOS_DRAIN_TIMEOUT_SECS")
1093 .ok()
1094 .and_then(|s| s.parse::<u64>().ok())
1095 .unwrap_or(config_secs);
1096 Duration::from_secs(secs)
1097 }
1098
1099 async fn reload_config(&self) {
1108 let Some(path) = self.config_path.as_deref() else {
1109 tracing::warn!(
1110 "SIGHUP received but config was not loaded from a file — nothing to reload"
1111 );
1112 return;
1113 };
1114 tracing::info!(path, "SIGHUP: reloading configuration");
1115 let mut new_config = match ProxyConfig::from_file(path) {
1116 Ok(c) => c,
1117 Err(e) => {
1118 tracing::error!(path, error = %e, "SIGHUP reload failed to parse — keeping current config");
1119 return;
1120 }
1121 };
1122 let old = self.state.live_config.load_full();
1123 if new_config.edge != old.edge {
1131 tracing::warn!(
1132 "[edge] configuration changed on SIGHUP but is applied at startup only — \
1133 keeping the running edge settings (restart to apply)"
1134 );
1135 new_config.edge = old.edge.clone();
1136 }
1137 if new_config.listen_address != old.listen_address {
1138 tracing::warn!(old = %old.listen_address, new = %new_config.listen_address,
1139 "listen_address change needs a restart/handoff; the bound socket is kept");
1140 }
1141 if new_config.admin_address != old.admin_address {
1142 tracing::warn!(old = %old.admin_address, new = %new_config.admin_address,
1143 "admin_address change needs a restart; the bound socket is kept");
1144 }
1145 Self::reconcile_health(&self.state, &new_config);
1148 let nodes = new_config.nodes.len();
1149 let hba_rules = new_config.hba.len();
1150 let pool_max = new_config.pool.max_connections;
1151 self.state.live_config.store(Arc::new(new_config));
1152 tracing::info!(
1153 nodes,
1154 hba_rules,
1155 pool_max,
1156 "SIGHUP: configuration reloaded — applies to new connections"
1157 );
1158 }
1159
1160 fn reconcile_health(state: &Arc<ServerState>, config: &ProxyConfig) {
1164 let _writers = state.health_write.lock();
1167 let current = state.health.load_full();
1168 let mut next: HashMap<String, NodeHealth> = HashMap::new();
1169 for node in &config.nodes {
1170 let addr = node.address();
1171 match current.get(&addr) {
1172 Some(existing) => {
1173 next.insert(addr, existing.clone());
1174 }
1175 None => {
1176 tracing::info!(node = %addr, "SIGHUP: new node added — seeding healthy");
1177 next.insert(
1178 addr.clone(),
1179 NodeHealth {
1180 address: addr,
1181 healthy: true,
1182 last_check: chrono::Utc::now(),
1183 failure_count: 0,
1184 last_error: None,
1185 latency_ms: 0.0,
1186 replication_lag_bytes: None,
1187 },
1188 );
1189 }
1190 }
1191 }
1192 for gone in current.keys().filter(|k| !next.contains_key(*k)) {
1193 tracing::info!(node = %gone, "SIGHUP: node removed from config");
1194 }
1195 state.health.store(Arc::new(next));
1196 }
1197
1198 pub async fn run(&self) -> Result<()> {
1200 let listener = bind_reuseport(&self.config.listen_address)?;
1206
1207 tracing::info!(
1208 "Proxy listening on {} (SO_REUSEPORT)",
1209 self.config.listen_address
1210 );
1211
1212 let health_task = self.spawn_health_checker();
1214 let pool_task = self.spawn_pool_manager();
1215
1216 #[cfg(feature = "edge-proxy")]
1218 let _edge_maintenance_task = if self.config.edge.enabled {
1219 Some(self.spawn_edge_maintenance())
1220 } else {
1221 None
1222 };
1223
1224 let admin_task = self.spawn_admin_server();
1226
1227 let mcp_task = if self.config.mcp.enabled {
1229 let mcp_cfg = self.config.mcp.clone();
1230 let contract = mcp_cfg.contract.as_ref().and_then(|id| {
1232 let found = self.config.agent_contracts.iter().find(|c| &c.id == id).cloned();
1233 if found.is_none() {
1234 tracing::warn!(%id, "mcp.contract names an unknown agent_contract; gateway runs with only the read-only guardrail");
1235 }
1236 found
1237 });
1238 Some(tokio::spawn(async move {
1239 if let Err(e) = crate::mcp::McpServer::new(mcp_cfg, contract).run().await {
1240 tracing::error!("MCP gateway error: {}", e);
1241 }
1242 }))
1243 } else {
1244 None
1245 };
1246
1247 let http_gw_task = if self.config.http_gateway.enabled {
1249 let gw_cfg = self.config.http_gateway.clone();
1250 Some(tokio::spawn(async move {
1251 if let Err(e) = crate::http_gateway::HttpGateway::new(gw_cfg).run().await {
1252 tracing::error!("HTTP gateway error: {}", e);
1253 }
1254 }))
1255 } else {
1256 None
1257 };
1258
1259 #[cfg(feature = "graphql-gateway")]
1261 let _graphql_gw_task = if self.config.graphql_gateway.enabled {
1262 let gw_cfg = self.config.graphql_gateway.clone();
1263 Some(tokio::spawn(async move {
1264 if let Err(e) = crate::graphql_gateway::GraphqlGateway::new(gw_cfg)
1265 .run()
1266 .await
1267 {
1268 tracing::error!("GraphQL gateway error: {}", e);
1269 }
1270 }))
1271 } else {
1272 None
1273 };
1274
1275 #[cfg(feature = "edge-proxy")]
1278 let _edge_client_task =
1279 if self.config.edge.enabled && self.config.edge.role == crate::edge::EdgeRole::Edge {
1280 tracing::info!(
1281 home_url = %self.config.edge.home_url,
1282 region = %self.config.edge.region,
1283 "edge role: subscribing to home invalidation stream"
1284 );
1285 Some(crate::edge::client::EdgeClient::spawn(
1286 self.config.edge.clone(),
1287 self.state.edge_cache.clone(),
1288 ))
1289 } else {
1290 None
1291 };
1292
1293 let mut shutdown_rx = self.shutdown_tx.subscribe();
1294
1295 let mut sighup = Self::hangup_stream();
1299 let mut sigusr2 = Self::usr2_stream();
1300 let mut graceful = false;
1301
1302 loop {
1303 tokio::select! {
1304 _ = sighup.recv() => {
1305 self.reload_config().await;
1306 }
1307 _ = sigusr2.recv() => {
1308 tracing::info!(
1309 "SIGUSR2: graceful binary-handoff drain — closing the listener so new \
1310 connections route to the sibling process; finishing in-flight connections"
1311 );
1312 graceful = true;
1313 break;
1314 }
1315 accept_result = listener.accept() => {
1316 match accept_result {
1317 Ok((stream, addr)) => {
1318 let _ = stream.set_nodelay(true);
1322 self.state.metrics.connections_accepted.fetch_add(1, Ordering::Relaxed);
1323 let state = self.state.clone();
1324 let config = (*self.state.live_config.load_full()).clone();
1328 let shutdown_tx = self.shutdown_tx.clone();
1329
1330 tokio::spawn(async move {
1331 if let Err(e) = Self::handle_client(stream, addr, state, config, shutdown_tx).await {
1332 tracing::error!("Client handler error: {}", e);
1333 }
1334 });
1335 }
1336 Err(e) => {
1337 tracing::error!("Accept error: {}", e);
1338 }
1339 }
1340 }
1341 _ = shutdown_rx.recv() => {
1342 tracing::info!("Shutdown signal received");
1343 break;
1344 }
1345 }
1346 }
1347
1348 drop(listener);
1352
1353 if graceful {
1356 let timeout =
1357 Self::drain_timeout(self.state.live_config.load().shutdown_drain_timeout_secs);
1358 tracing::info!(
1359 timeout_secs = timeout.as_secs(),
1360 "draining in-flight connections"
1361 );
1362 Self::drain_connections(&self.state, timeout).await;
1363 }
1364
1365 health_task.abort();
1367 pool_task.abort();
1368 admin_task.abort();
1369 if let Some(t) = mcp_task {
1370 t.abort();
1371 }
1372 if let Some(t) = http_gw_task {
1373 t.abort();
1374 }
1375
1376 Ok(())
1377 }
1378
1379 fn spawn_admin_server(&self) -> tokio::task::JoinHandle<()> {
1381 let config = self.config.clone();
1382 let state = self.state.clone();
1383 let mut shutdown_rx = self.shutdown_tx.subscribe();
1384
1385 tokio::spawn(async move {
1386 let admin_state = Arc::new(AdminState::new());
1388
1389 {
1391 let mut snapshot = admin_state.config_snapshot.write().await;
1392 *snapshot = ConfigSnapshot {
1393 listen_address: config.listen_address.clone(),
1394 admin_address: config.admin_address.clone(),
1395 tr_enabled: config.tr_enabled,
1396 tr_mode: format!("{:?}", config.tr_mode),
1397 pool_min_connections: config.pool.min_connections,
1398 pool_max_connections: config.pool.max_connections,
1399 nodes: config
1400 .nodes
1401 .iter()
1402 .map(|n| NodeSnapshot {
1403 address: n.address(),
1404 role: format!("{:?}", n.role),
1405 weight: n.weight,
1406 enabled: n.enabled,
1407 })
1408 .collect(),
1409 };
1410 }
1411
1412 admin_state.set_proxy_config(config.clone()).await;
1414
1415 admin_state
1417 .with_auth_token(config.admin_token.clone())
1418 .await;
1419
1420 if config.branch.enabled {
1422 admin_state.with_branch(config.branch.clone()).await;
1423 }
1424
1425 if let Some(ref mirror) = state.mirror {
1427 admin_state
1428 .with_migration(crate::admin::MigrationInfo {
1429 target: mirror.target().to_string(),
1430 writes_only: mirror.writes_only(),
1431 metrics: mirror.metrics.clone(),
1432 config: config.mirror.clone(),
1433 cutover: state.cutover.clone(),
1434 cutover_target: crate::mirror::CutoverTarget {
1435 addr: format!(
1436 "{}:{}",
1437 config.mirror.backend_host, config.mirror.backend_port
1438 ),
1439 user: config.mirror.backend_user.clone(),
1440 password: config.mirror.backend_password.clone(),
1441 database: config.mirror.backend_database.clone(),
1442 },
1443 })
1444 .await;
1445 }
1446
1447 #[cfg(feature = "wasm-plugins")]
1452 if let Some(ref pm) = state.plugin_manager {
1453 admin_state.with_plugin_manager(pm.clone()).await;
1454 }
1455
1456 #[cfg(feature = "pool-modes")]
1459 if let Some(ref pm) = state.pool_manager {
1460 admin_state.with_pool_manager(pm.clone()).await;
1461 }
1462
1463 #[cfg(feature = "circuit-breaker")]
1466 if let Some(ref cb) = state.circuit_breaker {
1467 admin_state.with_circuit_breaker(cb.clone()).await;
1468 }
1469
1470 #[cfg(feature = "ha-tr")]
1477 {
1478 let template = build_replay_backend_template(&config);
1479 let engine = Arc::new(crate::replay::ReplayEngine::new(
1480 state.transaction_journal.clone(),
1481 template,
1482 ));
1483 admin_state.with_replay_engine(engine).await;
1484 }
1485
1486 #[cfg(feature = "anomaly-detection")]
1490 admin_state
1491 .with_anomaly_detector(state.anomaly_detector.clone())
1492 .await;
1493
1494 #[cfg(feature = "query-analytics")]
1496 if let Some(a) = state.analytics.as_ref() {
1497 admin_state.with_analytics(a.clone()).await;
1498 }
1499
1500 #[cfg(feature = "edge-proxy")]
1503 admin_state
1504 .with_edge(state.edge_cache.clone(), state.edge_registry.clone())
1505 .await;
1506
1507 let admin_server = AdminServer::new(config.admin_address.clone(), admin_state.clone());
1509
1510 let admin_state_sync = admin_state.clone();
1512 let server_state = state.clone();
1513 let sync_task = tokio::spawn(async move {
1514 let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
1515 loop {
1516 interval.tick().await;
1517
1518 {
1520 let health = server_state.health.load_full();
1521 let mut admin_health = admin_state_sync.node_health.write().await;
1522 *admin_health = (*health).clone();
1523 }
1524
1525 {
1527 let metrics = ServerMetricsSnapshot {
1528 connections_accepted: server_state
1529 .metrics
1530 .connections_accepted
1531 .load(Ordering::Relaxed),
1532 connections_closed: server_state
1533 .metrics
1534 .connections_closed
1535 .load(Ordering::Relaxed),
1536 queries_processed: server_state
1537 .metrics
1538 .queries_processed
1539 .load(Ordering::Relaxed),
1540 bytes_received: server_state
1541 .metrics
1542 .bytes_received
1543 .load(Ordering::Relaxed),
1544 bytes_sent: server_state.metrics.bytes_sent.load(Ordering::Relaxed),
1545 failovers: server_state.metrics.failovers.load(Ordering::Relaxed),
1546 };
1547 let mut admin_metrics = admin_state_sync.metrics.write().await;
1548 *admin_metrics = metrics;
1549 }
1550
1551 {
1553 let mut admin_sessions = admin_state_sync.active_sessions.write().await;
1554 *admin_sessions = server_state.sessions.len() as u64;
1555 }
1556 }
1557 });
1558
1559 tokio::select! {
1561 result = admin_server.run() => {
1562 if let Err(e) = result {
1563 tracing::error!("Admin server error: {}", e);
1564 }
1565 }
1566 _ = shutdown_rx.recv() => {
1567 tracing::info!("Admin server shutting down");
1568 }
1569 }
1570
1571 sync_task.abort();
1572 })
1573 }
1574
1575 async fn handle_client(
1577 stream: TcpStream,
1578 addr: SocketAddr,
1579 state: Arc<ServerState>,
1580 config: ProxyConfig,
1581 _shutdown_tx: broadcast::Sender<()>,
1582 ) -> Result<()> {
1583 tracing::debug!("New client connection from {}", addr);
1584
1585 let session = Arc::new(ClientSession {
1587 id: Uuid::new_v4(),
1588 client_addr: addr,
1589 current_node: RwLock::new(None),
1590 in_transaction: std::sync::atomic::AtomicBool::new(false),
1591 copy_in_progress: std::sync::atomic::AtomicBool::new(false),
1592 tx_state: RwLock::new(TransactionState::default()),
1593 variables: RwLock::new(HashMap::new()),
1594 created_at: chrono::Utc::now(),
1595 tr_mode: config.tr_mode,
1596 #[cfg(feature = "lag-routing")]
1597 last_write_at: RwLock::new(None),
1598 #[cfg(feature = "pool-modes")]
1599 pool_client_id: ClientId::new(),
1600 #[cfg(feature = "wasm-plugins")]
1601 plugin_identity: RwLock::new(None),
1602 #[cfg(feature = "edge-proxy")]
1603 edge_ineligible: std::sync::atomic::AtomicBool::new(false),
1604 #[cfg(feature = "edge-proxy")]
1605 pending_edge_copy_tables: std::sync::Mutex::new(None),
1606 });
1607
1608 state.sessions.insert(session.id, session.clone());
1616 let _session_guard = SessionGuard {
1617 state: state.clone(),
1618 session_id: session.id,
1619 };
1620
1621 let negotiated = match tokio::time::timeout(
1632 Self::STARTUP_TIMEOUT,
1633 Self::negotiate_client_tls(stream, &state),
1634 )
1635 .await
1636 {
1637 Ok(r) => r,
1638 Err(_) => {
1639 tracing::debug!(client = %addr, "pre-auth negotiation timed out; closing");
1640 Err(ProxyError::Connection(
1641 "startup negotiation timeout".to_string(),
1642 ))
1643 }
1644 };
1645 let result = match negotiated {
1646 Ok((mut client_stream, pre)) => {
1647 Self::client_loop(&mut client_stream, pre, &session, &state, &config).await
1648 }
1649 Err(e) => Err(e),
1650 };
1651
1652 result
1656 }
1657
1658 async fn client_loop(
1660 stream: &mut ClientStream,
1661 pre: Option<StartupMessage>,
1662 session: &Arc<ClientSession>,
1663 state: &Arc<ServerState>,
1664 config: &ProxyConfig,
1665 ) -> Result<()> {
1666 let codec = ProtocolCodec::new();
1667 let mut buffer = BytesMut::with_capacity(8192);
1668
1669 let mut conns: HashMap<String, BackendConn> = HashMap::new();
1686 let startup_result = match tokio::time::timeout(
1691 Self::STARTUP_TIMEOUT,
1692 Self::handle_startup(stream, &mut buffer, &codec, pre, session, state, config),
1693 )
1694 .await
1695 {
1696 Ok(r) => r,
1697 Err(_) => Err(ProxyError::Connection("startup timeout".to_string())),
1698 };
1699 let mut current_node: Option<String> = match startup_result {
1700 Ok((Some(stream_conn), node_addr)) => {
1701 conns.insert(node_addr.clone(), BackendConn::new(stream_conn));
1702 Some(node_addr)
1703 }
1704 Ok((None, _)) => {
1705 return Ok(());
1707 }
1708 Err(e) => {
1709 tracing::error!("Startup failed: {}", e);
1710 let err_msg =
1712 Self::create_error_response("08006", &format!("Startup failed: {}", e));
1713 let _ = stream.write_all(&err_msg).await;
1714 return Err(e);
1715 }
1716 };
1717
1718 let mut pending = BytesMut::new();
1733 let mut pending_route_sql: Option<String> = None;
1734 let mut stmt_registry: HashMap<String, bytes::Bytes> = HashMap::new();
1742 let mut stmt_registry_bytes: usize = 0;
1745 let mut batch_defines: Vec<String> = Vec::new();
1746 let mut batch_refs: Vec<String> = Vec::new();
1747 let mut batch_closes: Vec<String> = Vec::new();
1748 #[cfg(feature = "edge-proxy")]
1756 let mut edge_stmt_meta: HashMap<String, Option<Vec<String>>> = HashMap::new();
1757 #[cfg(feature = "edge-proxy")]
1758 let mut edge_unnamed_meta: Option<Vec<String>> = None;
1759 #[cfg(feature = "edge-proxy")]
1760 let mut edge_batch_bound_unnamed = false;
1761 let promote_unnamed = config.optimize_unnamed_parse;
1767 let mut held_unnamed: Option<(bytes::Bytes, bytes::Bytes)> = None;
1768 loop {
1769 buffer.reserve(16384);
1784 let watch_node: Option<String> = if session
1785 .copy_in_progress
1786 .load(std::sync::atomic::Ordering::Relaxed)
1787 {
1788 None
1789 } else {
1790 current_node.clone().filter(|node| conns.contains_key(node))
1791 };
1792 let n: usize = 'client_read: {
1793 let Some(node) = watch_node.as_deref() else {
1794 break 'client_read stream
1795 .read_buf(&mut buffer)
1796 .await
1797 .map_err(|e| ProxyError::Network(format!("Read error: {}", e)))?;
1798 };
1799 loop {
1800 let mut backend_gone = false;
1801 let mut client_bytes: Option<usize> = None;
1802 {
1803 let bc = conns.get_mut(node).expect("watch_node is in conns");
1804 let mut abuf = [0u8; 16384];
1805 tokio::select! {
1806 r = stream.read_buf(&mut buffer) => {
1807 client_bytes = Some(r.map_err(|e| {
1808 ProxyError::Network(format!("Read error: {}", e))
1809 })?);
1810 }
1811 r = bc.stream.read(&mut abuf) => match r {
1812 Ok(0) => backend_gone = true,
1813 Ok(bn) => {
1814 stream.write_all(&abuf[..bn]).await.map_err(|e| {
1815 ProxyError::Network(format!("Client write error: {}", e))
1816 })?;
1817 state.metrics.bytes_sent.fetch_add(bn as u64, Ordering::Relaxed);
1818 }
1819 Err(e) => {
1820 tracing::debug!(node = %node, error = %e, "backend read error while idle; dropping cached connection");
1821 backend_gone = true;
1822 }
1823 },
1824 }
1825 }
1826 if backend_gone {
1827 conns.remove(node);
1831 if current_node.as_deref() == Some(node) {
1832 current_node = None;
1833 }
1834 break 'client_read stream
1835 .read_buf(&mut buffer)
1836 .await
1837 .map_err(|e| ProxyError::Network(format!("Read error: {}", e)))?;
1838 }
1839 if let Some(cn) = client_bytes {
1840 break 'client_read cn;
1841 }
1842 }
1844 };
1845
1846 if n == 0 {
1847 break;
1849 }
1850
1851 state
1852 .metrics
1853 .bytes_received
1854 .fetch_add(n as u64, Ordering::Relaxed);
1855
1856 if buffer.len() > Self::MAX_PENDING_BYTES {
1860 let emsg =
1861 Self::create_error_response("53400", "message exceeds per-session size limit");
1862 let _ = stream.write_all(&emsg).await;
1863 let _ = stream.write_all(&Self::create_ready_for_query(b'I')).await;
1864 tracing::warn!(
1865 client = %session.client_addr,
1866 bytes = buffer.len(),
1867 "inbound message exceeds size cap; closing connection"
1868 );
1869 return Ok(());
1870 }
1871
1872 while let Some(msg) = codec.decode_message(&mut buffer)? {
1874 match msg.msg_type {
1875 MessageType::Terminate => return Ok(()),
1876
1877 MessageType::Query => {
1879 #[cfg(feature = "anomaly-detection")]
1883 Self::record_anomaly_observation(&msg, state, session);
1884
1885 let (msg, action) = Self::apply_pre_query_hook(msg, state, session);
1888
1889 if let PreQueryAction::Block(reason) = &action {
1890 tracing::info!(reason = %reason, "pre-query plugin blocked query");
1891 Self::send_block_response(stream, reason, state).await?;
1892 state
1893 .metrics
1894 .queries_processed
1895 .fetch_add(1, Ordering::Relaxed);
1896 continue;
1897 }
1898
1899 #[cfg(feature = "wasm-plugins")]
1900 if let PreQueryAction::Cached(bytes) = &action {
1901 match Self::synthesise_cached_response(bytes) {
1902 Ok(reply) => {
1903 stream.write_all(&reply).await.map_err(|e| {
1904 ProxyError::Network(format!("Write error: {}", e))
1905 })?;
1906 state
1907 .metrics
1908 .bytes_sent
1909 .fetch_add(reply.len() as u64, Ordering::Relaxed);
1910 state
1911 .metrics
1912 .queries_processed
1913 .fetch_add(1, Ordering::Relaxed);
1914 continue;
1915 }
1916 Err(e) => {
1917 tracing::warn!(error = %e, "failed to synthesise cached response; falling back to backend");
1918 }
1919 }
1920 }
1921
1922 if let Some(ref mirror) = state.mirror {
1926 if let Some(sql) = crate::protocol::query_text(&msg.payload) {
1927 mirror.offer(sql, Self::is_write_query(sql));
1928 }
1929 }
1930
1931 #[cfg(feature = "wasm-plugins")]
1932 let forward_start = std::time::Instant::now();
1933 let fr = Self::forward_simple_query(
1934 stream,
1935 &msg,
1936 &mut conns,
1937 current_node.as_deref(),
1938 session,
1939 state,
1940 config,
1941 )
1942 .await;
1943 #[cfg(feature = "wasm-plugins")]
1944 Self::fire_post_query_hook(
1945 &msg,
1946 session,
1947 state,
1948 &fr,
1949 forward_start.elapsed(),
1950 );
1951 let (used_node, sent) = fr?;
1952 if let Some(n) = used_node {
1953 current_node = Some(n);
1954 }
1955 #[cfg(feature = "pool-modes")]
1958 Self::release_to_pool_if_idle(
1959 &mut conns,
1960 current_node.as_deref(),
1961 session,
1962 state,
1963 config,
1964 )
1965 .await;
1966 state.metrics.bytes_sent.fetch_add(sent, Ordering::Relaxed);
1967 state
1968 .metrics
1969 .queries_processed
1970 .fetch_add(1, Ordering::Relaxed);
1971 }
1972
1973 MessageType::Parse
1975 | MessageType::Bind
1976 | MessageType::Describe
1977 | MessageType::Execute
1978 | MessageType::Close => {
1979 let mut add_to_pending = true;
1983 match msg.msg_type {
1984 MessageType::Parse => {
1985 let name = Self::parse_stmt_name(&msg.payload);
1989 let unnamed = name.is_empty();
1990 if !unnamed {
1991 let name = name.to_string();
1992 let existed = stmt_registry.contains_key(&name);
1993 if !existed
1997 && stmt_registry.len() >= Self::MAX_PREPARED_STATEMENTS
1998 {
1999 let emsg = Self::create_error_response(
2000 "54000",
2001 "too many prepared statements for this session",
2002 );
2003 let _ = stream.write_all(&emsg).await;
2004 let _ = stream
2005 .write_all(&Self::create_ready_for_query(b'I'))
2006 .await;
2007 tracing::warn!(
2008 client = %session.client_addr,
2009 limit = Self::MAX_PREPARED_STATEMENTS,
2010 "prepared-statement cap exceeded; closing connection"
2011 );
2012 return Ok(());
2013 }
2014 let encoded = msg.encode().freeze();
2015 let old_len =
2019 stmt_registry.get(&name).map(|b| b.len()).unwrap_or(0);
2020 let projected =
2021 stmt_registry_bytes.saturating_sub(old_len) + encoded.len();
2022 if projected > Self::MAX_PREPARED_BYTES {
2023 let emsg = Self::create_error_response(
2024 "54000",
2025 "prepared-statement memory limit exceeded for this session",
2026 );
2027 let _ = stream.write_all(&emsg).await;
2028 let _ = stream
2029 .write_all(&Self::create_ready_for_query(b'I'))
2030 .await;
2031 tracing::warn!(
2032 client = %session.client_addr,
2033 limit = Self::MAX_PREPARED_BYTES,
2034 "prepared-statement byte cap exceeded; closing connection"
2035 );
2036 return Ok(());
2037 }
2038 stmt_registry.insert(name.clone(), encoded);
2039 stmt_registry_bytes = projected;
2040 batch_defines.push(name);
2041 }
2042 if pending_route_sql.is_none() {
2043 if let Some(end) = msg.payload.iter().position(|&b| b == 0) {
2044 if let Some(q) =
2045 crate::protocol::query_text(&msg.payload[end + 1..])
2046 {
2047 if !q.is_empty() {
2048 pending_route_sql = Some(q.to_string());
2049 #[cfg(feature = "anomaly-detection")]
2050 Self::record_anomaly_sql(q, state, session);
2051 }
2052 }
2053 }
2054 }
2055 #[cfg(feature = "edge-proxy")]
2064 if config.edge.enabled {
2065 if let Some(end) = msg.payload.iter().position(|&b| b == 0) {
2066 if let Some(q) =
2067 crate::protocol::query_text(&msg.payload[end + 1..])
2068 {
2069 if !session
2070 .edge_ineligible
2071 .load(std::sync::atomic::Ordering::Relaxed)
2072 && Self::stmt_leaves_session_state(q)
2073 {
2074 session.edge_ineligible.store(
2075 true,
2076 std::sync::atomic::Ordering::Relaxed,
2077 );
2078 }
2079 let meta = if Self::is_edge_dml_sql(q) {
2080 Some(crate::edge::fingerprint::tables_only(q))
2081 } else if Self::is_edge_procedural_sql(q)
2082 || Self::is_edge_txn_end_sql(q)
2083 {
2084 Some(Vec::new())
2090 } else {
2091 None
2092 };
2093 if unnamed {
2094 edge_unnamed_meta = meta;
2095 } else {
2096 edge_stmt_meta.insert(
2097 Self::parse_stmt_name(&msg.payload).to_string(),
2098 meta,
2099 );
2100 }
2101 }
2102 }
2103 }
2104 if promote_unnamed
2111 && unnamed
2112 && pending.is_empty()
2113 && held_unnamed.is_none()
2114 {
2115 let sig = bytes::Bytes::copy_from_slice(&msg.payload[1..]);
2116 held_unnamed = Some((msg.encode().freeze(), sig));
2117 add_to_pending = false;
2118 } else if let Some((held_msg, _)) = held_unnamed.take() {
2119 let mut combined =
2120 BytesMut::with_capacity(held_msg.len() + pending.len());
2121 combined.extend_from_slice(&held_msg);
2122 combined.extend_from_slice(&pending);
2123 pending = combined;
2124 }
2125 }
2126 MessageType::Bind => {
2127 let stmt_ref = Self::bind_stmt_ref(&msg.payload);
2128 #[cfg(feature = "edge-proxy")]
2132 if stmt_ref.is_none() {
2133 edge_batch_bound_unnamed = true;
2134 }
2135 if let Some(name) = stmt_ref {
2136 batch_refs.push(name.to_string());
2137 }
2138 }
2139 MessageType::Describe => {
2140 if let Some(name) = Self::stmt_kind_name(&msg.payload) {
2141 batch_refs.push(name.to_string());
2142 }
2143 }
2144 MessageType::Close => {
2145 if let Some(name) = Self::stmt_kind_name(&msg.payload) {
2146 batch_closes.push(name.to_string());
2147 }
2148 }
2149 _ => {}
2150 }
2151 if add_to_pending {
2152 pending.extend_from_slice(&msg.encode());
2153 }
2154 }
2155
2156 MessageType::Sync | MessageType::Flush => {
2158 let wait_ready = msg.msg_type == MessageType::Sync;
2159 pending.extend_from_slice(&msg.encode());
2160 let batch = pending.split().freeze();
2161 let reprepare: Vec<String> = batch_refs
2165 .iter()
2166 .filter(|r| !batch_defines.contains(r))
2167 .cloned()
2168 .collect();
2169 let (used_node, sent) = Self::forward_extended_batch(
2170 stream,
2171 &batch,
2172 pending_route_sql.as_deref(),
2173 wait_ready,
2174 &mut conns,
2175 current_node.as_deref(),
2176 &stmt_registry,
2177 &reprepare,
2178 &batch_defines,
2179 held_unnamed.take(),
2180 session,
2181 state,
2182 config,
2183 )
2184 .await?;
2185 if let Some(n) = used_node {
2186 current_node = Some(n);
2187 }
2188 #[cfg(feature = "pool-modes")]
2192 if wait_ready {
2193 Self::release_to_pool_if_idle(
2194 &mut conns,
2195 current_node.as_deref(),
2196 session,
2197 state,
2198 config,
2199 )
2200 .await;
2201 }
2202 state.metrics.bytes_sent.fetch_add(sent, Ordering::Relaxed);
2203 #[cfg(feature = "edge-proxy")]
2216 if wait_ready && config.edge.enabled {
2217 if let Some(tables) = Self::edge_extended_batch_tables(
2218 &batch_refs,
2219 edge_batch_bound_unnamed,
2220 &edge_stmt_meta,
2221 &edge_unnamed_meta,
2222 ) {
2223 if session
2224 .copy_in_progress
2225 .load(std::sync::atomic::Ordering::Relaxed)
2226 {
2227 *session
2228 .pending_edge_copy_tables
2229 .lock()
2230 .unwrap_or_else(|e| e.into_inner()) = Some(tables);
2231 } else {
2232 Self::edge_invalidate_write(state, config, tables).await;
2233 }
2234 }
2235 edge_batch_bound_unnamed = false;
2236 }
2237 #[cfg(not(feature = "edge-proxy"))]
2240 for name in batch_closes.drain(..) {
2241 if let Some(removed) = stmt_registry.remove(&name) {
2242 stmt_registry_bytes =
2243 stmt_registry_bytes.saturating_sub(removed.len());
2244 }
2245 }
2246 #[cfg(feature = "edge-proxy")]
2258 {
2259 for name in &batch_closes {
2260 if let Some(removed) = stmt_registry.remove(name) {
2261 stmt_registry_bytes =
2262 stmt_registry_bytes.saturating_sub(removed.len());
2263 }
2264 }
2265 if wait_ready {
2266 for name in Self::edge_meta_prunable(&batch_closes, &batch_defines)
2267 {
2268 edge_stmt_meta.remove(name);
2269 }
2270 batch_closes.clear();
2271 }
2272 }
2273 if wait_ready {
2274 pending_route_sql = None;
2278 batch_defines.clear();
2279 batch_refs.clear();
2280 state
2281 .metrics
2282 .queries_processed
2283 .fetch_add(1, Ordering::Relaxed);
2284 }
2285 }
2286
2287 MessageType::CopyData | MessageType::CopyDone | MessageType::CopyFail => {
2289 let is_copy_end =
2290 matches!(msg.msg_type, MessageType::CopyDone | MessageType::CopyFail);
2291 let conn = current_node.as_ref().and_then(|n| conns.get_mut(n));
2292 match conn {
2293 Some(b) => {
2294 b.stream.write_all(&msg.encode()).await.map_err(|e| {
2295 ProxyError::Network(format!("Backend copy write error: {}", e))
2296 })?;
2297 if is_copy_end {
2298 let node = current_node.clone().unwrap();
2299 let r = Self::stream_until_ready(
2300 stream,
2301 &mut b.stream,
2302 session,
2303 state,
2304 )
2305 .await;
2306 session
2310 .copy_in_progress
2311 .store(false, std::sync::atomic::Ordering::Relaxed);
2312 match r {
2313 Ok(sent) => {
2314 state
2315 .metrics
2316 .bytes_sent
2317 .fetch_add(sent, Ordering::Relaxed);
2318 #[cfg(feature = "edge-proxy")]
2323 if config.edge.enabled {
2324 let stashed = session
2325 .pending_edge_copy_tables
2326 .lock()
2327 .unwrap_or_else(|e| e.into_inner())
2328 .take();
2329 if let Some(tables) = stashed {
2330 if matches!(msg.msg_type, MessageType::CopyDone)
2331 {
2332 Self::edge_invalidate_write(
2333 state, config, tables,
2334 )
2335 .await;
2336 }
2337 }
2338 }
2339 }
2340 Err(e) => {
2341 conns.remove(&node);
2342 return Err(e);
2343 }
2344 }
2345 }
2346 }
2347 None => {
2348 session
2354 .copy_in_progress
2355 .store(false, std::sync::atomic::Ordering::Relaxed);
2356 #[cfg(feature = "edge-proxy")]
2360 {
2361 *session
2362 .pending_edge_copy_tables
2363 .lock()
2364 .unwrap_or_else(|e| e.into_inner()) = None;
2365 }
2366 if is_copy_end {
2367 let emsg = Self::create_error_response(
2368 "57000",
2369 "COPY aborted: backend connection lost",
2370 );
2371 let _ = stream.write_all(&emsg).await;
2372 let _ =
2373 stream.write_all(&Self::create_ready_for_query(b'I')).await;
2374 }
2375 }
2376 }
2377 }
2378
2379 _ => {
2381 if let Some(ref node) = current_node {
2382 if let Some(b) = conns.get_mut(node) {
2383 let _ = b.stream.write_all(&msg.encode()).await;
2384 }
2385 }
2386 }
2387 }
2388 }
2389
2390 if pending.len() > Self::MAX_PENDING_BYTES {
2394 let emsg = Self::create_error_response(
2395 "53400",
2396 "un-flushed extended-protocol buffer exceeds per-session limit",
2397 );
2398 let _ = stream.write_all(&emsg).await;
2399 let _ = stream.write_all(&Self::create_ready_for_query(b'I')).await;
2400 tracing::warn!(
2401 client = %session.client_addr,
2402 pending = pending.len(),
2403 "pending extended-protocol buffer cap exceeded; closing connection"
2404 );
2405 return Ok(());
2406 }
2407 }
2408
2409 #[cfg(feature = "pool-modes")]
2413 if state.backend_pool.is_some() {
2414 let nodes: Vec<String> = conns.keys().cloned().collect();
2415 for node in nodes {
2416 Self::release_to_pool_if_idle(
2417 &mut conns,
2418 Some(node.as_str()),
2419 session,
2420 state,
2421 config,
2422 )
2423 .await;
2424 }
2425 }
2426
2427 Ok(())
2428 }
2429
2430 async fn negotiate_client_tls(
2437 mut tcp: TcpStream,
2438 state: &Arc<ServerState>,
2439 ) -> Result<(ClientStream, Option<StartupMessage>)> {
2440 let codec = ProtocolCodec::new();
2441 let mut buffer = BytesMut::with_capacity(1024);
2442 let mut read_buf = vec![0u8; 1024];
2443
2444 let first = loop {
2445 if let Some(msg) = codec.decode_startup(&mut buffer)? {
2446 break msg;
2447 }
2448 let n = tcp
2449 .read(&mut read_buf)
2450 .await
2451 .map_err(|e| ProxyError::Network(format!("Startup read error: {}", e)))?;
2452 if n == 0 {
2453 return Err(ProxyError::Connection(
2454 "client closed before startup".to_string(),
2455 ));
2456 }
2457 buffer.extend_from_slice(&read_buf[..n]);
2458 };
2459
2460 match first {
2461 StartupMessage::SSLRequest => match state.tls_acceptor.as_ref() {
2462 Some(acceptor) => {
2463 tcp.write_all(b"S")
2464 .await
2465 .map_err(|e| ProxyError::Network(format!("SSL accept write: {}", e)))?;
2466 let tls = acceptor
2467 .accept(tcp)
2468 .await
2469 .map_err(|e| ProxyError::Network(format!("TLS handshake failed: {}", e)))?;
2470 if tls.get_ref().1.peer_certificates().is_some() {
2471 tracing::debug!("client presented a certificate (mTLS)");
2472 }
2473 Ok((ClientStream::Tls(Box::new(tls)), None))
2474 }
2475 None => {
2476 tcp.write_all(b"N")
2477 .await
2478 .map_err(|e| ProxyError::Network(format!("SSL reject write: {}", e)))?;
2479 Ok((ClientStream::Plain(tcp), None))
2480 }
2481 },
2482 other => Ok((ClientStream::Plain(tcp), Some(other))),
2483 }
2484 }
2485
2486 async fn handle_startup(
2490 client_stream: &mut ClientStream,
2491 buffer: &mut BytesMut,
2492 codec: &ProtocolCodec,
2493 pre: Option<StartupMessage>,
2494 session: &Arc<ClientSession>,
2495 state: &Arc<ServerState>,
2496 config: &ProxyConfig,
2497 ) -> Result<(Option<TcpStream>, String)> {
2498 let startup_msg = match pre {
2501 Some(msg) => Some(msg),
2502 None => {
2503 let mut read_buf = vec![0u8; 1024];
2504 loop {
2505 if let Some(msg) = codec.decode_startup(buffer)? {
2506 break Some(msg);
2507 }
2508 let n = client_stream
2509 .read(&mut read_buf)
2510 .await
2511 .map_err(|e| ProxyError::Network(format!("Startup read error: {}", e)))?;
2512 if n == 0 {
2513 return Ok((None, String::new()));
2514 }
2515 buffer.extend_from_slice(&read_buf[..n]);
2516 }
2517 }
2518 };
2519
2520 match startup_msg {
2521 Some(StartupMessage::SSLRequest) => {
2522 client_stream
2525 .write_all(b"N")
2526 .await
2527 .map_err(|e| ProxyError::Network(format!("SSL reject error: {}", e)))?;
2528 Err(ProxyError::Protocol(
2529 "unexpected SSLRequest after startup".to_string(),
2530 ))
2531 }
2532 Some(StartupMessage::CancelRequest { pid, key }) => {
2533 Self::forward_cancel_request(state, pid, key).await;
2536 Ok((None, String::new()))
2537 }
2538 Some(StartupMessage::Startup { params, .. }) => {
2539 Self::connect_and_authenticate(client_stream, ¶ms, session, state, config).await
2540 }
2541 None => Err(ProxyError::Protocol(
2542 "Incomplete startup message".to_string(),
2543 )),
2544 }
2545 }
2546
2547 fn hba_admits(rules: &[HbaRule], ip: std::net::IpAddr, user: &str, database: &str) -> bool {
2550 for r in rules {
2551 let user_ok = r.user == "all" || r.user == user;
2552 let db_ok = r.database == "all" || r.database == database;
2553 if user_ok && db_ok && Self::hba_addr_matches(&r.address, ip) {
2554 return r.action == HbaAction::Allow;
2555 }
2556 }
2557 true
2558 }
2559
2560 fn hba_addr_matches(spec: &str, ip: std::net::IpAddr) -> bool {
2563 use std::net::IpAddr;
2564 if spec == "all" {
2565 return true;
2566 }
2567 if let Some((net, bits)) = spec.split_once('/') {
2568 let bits: u32 = match bits.parse() {
2569 Ok(b) => b,
2570 Err(_) => return false,
2571 };
2572 match (net.parse::<IpAddr>(), ip) {
2573 (Ok(IpAddr::V4(n)), IpAddr::V4(i)) if bits <= 32 => {
2574 let mask = if bits == 0 {
2575 0
2576 } else {
2577 u32::MAX << (32 - bits)
2578 };
2579 (u32::from(n) & mask) == (u32::from(i) & mask)
2580 }
2581 (Ok(IpAddr::V6(n)), IpAddr::V6(i)) if bits <= 128 => {
2582 let mask = if bits == 0 {
2583 0
2584 } else {
2585 u128::MAX << (128 - bits)
2586 };
2587 (u128::from(n) & mask) == (u128::from(i) & mask)
2588 }
2589 _ => false,
2590 }
2591 } else {
2592 spec.parse::<IpAddr>().map(|s| s == ip).unwrap_or(false)
2593 }
2594 }
2595
2596 async fn proxy_scram_auth(
2602 client: &mut ClientStream,
2603 user: &str,
2604 state: &Arc<ServerState>,
2605 ) -> std::result::Result<(), String> {
2606 use crate::auth_scram::ScramServer;
2607 let auth_file = state.auth_file.as_ref().ok_or("scram not configured")?;
2608
2609 let mut sasl = BytesMut::new();
2611 sasl.put_i32(10); sasl.extend_from_slice(b"SCRAM-SHA-256\0");
2613 sasl.put_u8(0); Self::write_auth_frame(client, &sasl).await?;
2615
2616 let init = Self::read_password_message(client).await?;
2618 let mech_end = init
2619 .iter()
2620 .position(|&b| b == 0)
2621 .ok_or("malformed SASLInitialResponse (no mechanism)")?;
2622 if init.len() < mech_end + 5 {
2623 return Err("short SASLInitialResponse".into());
2624 }
2625 let client_first =
2626 std::str::from_utf8(&init[mech_end + 5..]).map_err(|_| "client-first not UTF-8")?;
2627
2628 let verifier = auth_file.get(user).ok_or("no such user")?.clone();
2630
2631 let server_nonce = Self::random_nonce();
2633 let (server, server_first) = ScramServer::start(verifier, client_first, &server_nonce)?;
2634
2635 let mut cont = BytesMut::new();
2637 cont.put_i32(11);
2638 cont.extend_from_slice(server_first.as_bytes());
2639 Self::write_auth_frame(client, &cont).await?;
2640
2641 let client_final_raw = Self::read_password_message(client).await?;
2643 let client_final =
2644 std::str::from_utf8(&client_final_raw).map_err(|_| "client-final not UTF-8")?;
2645
2646 let server_final = server.finish(client_final)?;
2648
2649 let mut fin = BytesMut::new();
2651 fin.put_i32(12);
2652 fin.extend_from_slice(server_final.as_bytes());
2653 Self::write_auth_frame(client, &fin).await?;
2654 Ok(())
2655 }
2656
2657 async fn write_auth_frame(
2659 client: &mut ClientStream,
2660 payload: &[u8],
2661 ) -> std::result::Result<(), String> {
2662 let mut frame = BytesMut::with_capacity(payload.len() + 5);
2663 frame.put_u8(b'R');
2664 frame.put_u32((payload.len() + 4) as u32);
2665 frame.extend_from_slice(payload);
2666 client
2667 .write_all(&frame)
2668 .await
2669 .map_err(|e| format!("client write: {}", e))
2670 }
2671
2672 async fn read_password_message(
2675 client: &mut ClientStream,
2676 ) -> std::result::Result<BytesMut, String> {
2677 let codec = ProtocolCodec::new();
2678 let mut buffer = BytesMut::with_capacity(1024);
2679 let mut read_buf = vec![0u8; 1024];
2680 loop {
2681 if let Some(msg) = codec
2682 .decode_message(&mut buffer)
2683 .map_err(|e| format!("decode: {}", e))?
2684 {
2685 if msg.msg_type == MessageType::Password {
2686 return Ok(msg.payload);
2687 }
2688 return Err(format!("expected SASL response, got {:?}", msg.msg_type));
2689 }
2690 let n = client
2691 .read(&mut read_buf)
2692 .await
2693 .map_err(|e| format!("client read: {}", e))?;
2694 if n == 0 {
2695 return Err("client closed during SASL".into());
2696 }
2697 buffer.extend_from_slice(&read_buf[..n]);
2698 }
2699 }
2700
2701 fn random_nonce() -> String {
2703 use rand::Rng;
2704 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
2705 let mut rng = rand::thread_rng();
2706 (0..24)
2707 .map(|_| CHARS[rng.gen_range(0..CHARS.len())] as char)
2708 .collect()
2709 }
2710
2711 async fn connect_and_authenticate(
2713 client_stream: &mut ClientStream,
2714 params: &HashMap<String, String>,
2715 session: &Arc<ClientSession>,
2716 state: &Arc<ServerState>,
2717 config: &ProxyConfig,
2718 ) -> Result<(Option<TcpStream>, String)> {
2719 let user = params.get("user").map(String::as_str).unwrap_or("");
2722 let database = params.get("database").map(String::as_str).unwrap_or(user);
2723 if !Self::hba_admits(&config.hba, session.client_addr.ip(), user, database) {
2724 tracing::info!(%user, %database, client = %session.client_addr, "connection rejected by hba rule");
2725 let err = Self::create_error_response(
2726 "28000",
2727 "connection rejected by proxy admission rules",
2728 );
2729 let _ = client_stream.write_all(&err).await;
2730 return Ok((None, String::new()));
2731 }
2732
2733 if state.auth_file.is_some() {
2739 if let Err(e) = Self::proxy_scram_auth(client_stream, user, state).await {
2740 tracing::info!(%user, error = %e, "proxy SCRAM auth failed");
2741 let err =
2742 Self::create_error_response("28P01", &format!("authentication failed: {}", e));
2743 let _ = client_stream.write_all(&err).await;
2744 return Ok((None, String::new()));
2745 }
2746 tracing::debug!(%user, "client authenticated by proxy SCRAM");
2747 }
2748
2749 Self::apply_authenticate_hook(params, session, state).await?;
2755
2756 let cutover = state.cutover.load_full();
2760 let (node_addr, effective_params) = if let Some(t) = cutover.as_ref() {
2761 let mut p = params.clone();
2762 p.insert("user".to_string(), t.user.clone());
2763 if let Some(ref db) = t.database {
2764 p.insert("database".to_string(), db.clone());
2765 } else {
2766 p.remove("database");
2767 }
2768 tracing::debug!(target = %t.addr, "routing connection to cutover target");
2769 (t.addr.clone(), p)
2770 } else {
2771 (
2772 Self::select_node(session, state, config).await?,
2773 params.clone(),
2774 )
2775 };
2776
2777 let mut backend = match tokio::time::timeout(
2782 config.pool.acquire_timeout(),
2783 TcpStream::connect(&node_addr),
2784 )
2785 .await
2786 {
2787 Ok(Ok(s)) => s,
2788 Ok(Err(e)) => {
2789 let msg = format!("Failed to connect to {}: {}", node_addr, e);
2790 Self::note_backend_failure(state, &node_addr, &msg);
2791 return Err(ProxyError::Connection(msg));
2792 }
2793 Err(_) => {
2794 let msg = format!("Connection timeout to {}", node_addr);
2795 Self::note_backend_failure(state, &node_addr, &msg);
2796 return Err(ProxyError::Connection(msg));
2797 }
2798 };
2799 let _ = backend.set_nodelay(true);
2800
2801 let params = &effective_params;
2803 let startup_bytes = Self::build_startup_message(params);
2804 backend
2805 .write_all(&startup_bytes)
2806 .await
2807 .map_err(|e| ProxyError::Network(format!("Backend startup write error: {}", e)))?;
2808
2809 Self::proxy_authentication(client_stream, &mut backend, state, &node_addr).await?;
2813
2814 {
2816 let mut vars = session.variables.write().await;
2817 for (k, v) in params {
2818 vars.insert(k.clone(), v.clone());
2819 }
2820 }
2821
2822 Ok((Some(backend), node_addr))
2823 }
2824
2825 fn build_startup_message(params: &HashMap<String, String>) -> Vec<u8> {
2827 let mut payload = BytesMut::new();
2828
2829 payload.put_u32(196608);
2831
2832 for (key, value) in params {
2834 payload.extend_from_slice(key.as_bytes());
2835 payload.put_u8(0);
2836 payload.extend_from_slice(value.as_bytes());
2837 payload.put_u8(0);
2838 }
2839 payload.put_u8(0); let mut msg = BytesMut::new();
2843 msg.put_u32((payload.len() + 4) as u32);
2844 msg.extend_from_slice(&payload);
2845
2846 msg.to_vec()
2847 }
2848
2849 const MAX_CANCEL_KEYS: usize = 100_000;
2853
2854 const STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
2859 const BACKEND_WRITE_TIMEOUT: Duration = Duration::from_secs(30);
2863 const CLIENT_WRITE_TIMEOUT: Duration = Duration::from_secs(60);
2866 const REPREPARE_TIMEOUT: Duration = Duration::from_secs(15);
2869 const MAX_PREPARED_STATEMENTS: usize = 8192;
2872 const MAX_PREPARED_BYTES: usize = 64 * 1024 * 1024;
2878 const MAX_PENDING_BYTES: usize = 64 * 1024 * 1024;
2881 #[cfg(feature = "pool-modes")]
2885 const MAX_TOTAL_IDLE_BACKEND_CONNS: usize = 8192;
2886 const POOL_REAP_INTERVAL: Duration = Duration::from_secs(30);
2888
2889 fn register_cancel_key(state: &Arc<ServerState>, pid: u32, key: u32, node_addr: &str) {
2891 {
2895 let mut order = state.cancel_order.lock();
2896 while state.cancel_map.len() >= Self::MAX_CANCEL_KEYS {
2897 match order.pop_front() {
2898 Some(old) => {
2899 state.cancel_map.remove(&old);
2900 }
2901 None => {
2902 state.cancel_map.clear();
2905 break;
2906 }
2907 }
2908 }
2909 order.push_back((pid, key));
2910 }
2911 state.cancel_map.insert((pid, key), node_addr.to_string());
2912 }
2913
2914 async fn forward_cancel_request(state: &Arc<ServerState>, pid: u32, key: u32) {
2917 let Some(addr) = state.cancel_map.get(&(pid, key)).map(|e| e.clone()) else {
2918 tracing::debug!(pid, "cancel request for unknown key; ignoring");
2919 return;
2920 };
2921 let mut msg = BytesMut::with_capacity(16);
2923 msg.put_u32(16);
2924 msg.put_u32(80877102);
2925 msg.put_u32(pid);
2926 msg.put_u32(key);
2927 match tokio::time::timeout(Duration::from_secs(5), TcpStream::connect(&addr)).await {
2928 Ok(Ok(mut conn)) => {
2929 let _ = conn.set_nodelay(true);
2930 if let Err(e) = conn.write_all(&msg).await {
2931 tracing::warn!(node = %addr, error = %e, "failed to forward CancelRequest");
2932 }
2933 }
2935 other => {
2936 tracing::warn!(node = %addr, ?other, "could not connect to forward CancelRequest")
2937 }
2938 }
2939 }
2940
2941 async fn proxy_authentication(
2943 client_stream: &mut ClientStream,
2944 backend_stream: &mut TcpStream,
2945 state: &Arc<ServerState>,
2946 node_addr: &str,
2947 ) -> Result<()> {
2948 let mut backend_buffer = BytesMut::with_capacity(4096);
2964 let mut cbuf = vec![0u8; 4096];
2965 let mut bbuf = vec![0u8; 4096];
2966 let deadline = tokio::time::Instant::now() + Self::STARTUP_TIMEOUT;
2967
2968 loop {
2969 tokio::select! {
2970 biased;
2971 _ = tokio::time::sleep_until(deadline) => {
2972 return Err(ProxyError::Auth(
2973 "authentication timed out".to_string(),
2974 ));
2975 }
2976 r = backend_stream.read(&mut bbuf) => {
2979 let n = r.map_err(|e| {
2980 ProxyError::Network(format!("Backend auth read error: {}", e))
2981 })?;
2982 if n == 0 {
2983 return Err(ProxyError::Connection(
2984 "Backend closed during auth".to_string(),
2985 ));
2986 }
2987 client_stream
2988 .write_all(&bbuf[..n])
2989 .await
2990 .map_err(|e| ProxyError::Network(format!("Client auth write error: {}", e)))?;
2991 backend_buffer.extend_from_slice(&bbuf[..n]);
2992
2993 loop {
2995 if backend_buffer.len() < 5 {
2996 break;
2997 }
2998 let len = u32::from_be_bytes([
2999 backend_buffer[1],
3000 backend_buffer[2],
3001 backend_buffer[3],
3002 backend_buffer[4],
3003 ]) as usize;
3004 if len < 4 || backend_buffer.len() < len + 1 {
3005 break;
3006 }
3007 let tag = backend_buffer[0];
3008 let frame = backend_buffer.split_to(len + 1);
3009 match tag {
3010 b'K' if frame.len() >= 13 => {
3013 let pid = u32::from_be_bytes([
3014 frame[5], frame[6], frame[7], frame[8],
3015 ]);
3016 let key = u32::from_be_bytes([
3017 frame[9], frame[10], frame[11], frame[12],
3018 ]);
3019 Self::register_cancel_key(state, pid, key, node_addr);
3020 }
3021 b'Z' => return Ok(()),
3023 b'E' => {
3026 return Err(ProxyError::Auth("Authentication failed".to_string()));
3027 }
3028 _ => {}
3029 }
3030 }
3031 }
3032 r = client_stream.read(&mut cbuf) => {
3035 let n = r.map_err(|e| {
3036 ProxyError::Network(format!("Client auth read error: {}", e))
3037 })?;
3038 if n == 0 {
3039 return Err(ProxyError::Connection(
3040 "Client closed during auth".to_string(),
3041 ));
3042 }
3043 backend_stream
3044 .write_all(&cbuf[..n])
3045 .await
3046 .map_err(|e| {
3047 ProxyError::Network(format!("Backend password write error: {}", e))
3048 })?;
3049 }
3050 }
3051 }
3052 }
3053
3054 async fn choose_target_node(
3059 is_write: bool,
3060 forced_target: Option<String>,
3061 current_node: Option<&str>,
3062 session: &Arc<ClientSession>,
3063 state: &Arc<ServerState>,
3064 config: &ProxyConfig,
3065 ) -> Result<String> {
3066 if let Some(t) = state.cutover.load_full().as_ref() {
3069 return Ok(t.addr.clone());
3070 }
3071
3072 #[cfg(feature = "lag-routing")]
3076 if !is_write && forced_target.is_none() && config.lag_routing.enabled {
3077 let last_write = *session.last_write_at.read().await;
3078 if Self::ryw_pins_primary(last_write, config.lag_routing.ryw_window_ms) {
3079 tracing::debug!(target: "helios::routing", "read-your-writes: pinning read to primary");
3080 return Self::select_primary_with_timeout(session, state, config).await;
3081 }
3082 }
3083
3084 let need_switch = if let Some(ref forced) = forced_target {
3085 let health = state.health.load_full();
3086 let reuse = current_node
3087 .map(|c| c == forced && health.get(c).map(|h| h.healthy).unwrap_or(false))
3088 .unwrap_or(false);
3089 !reuse
3090 } else if let Some(current) = current_node {
3091 let health = state.health.load_full();
3092 let current_healthy = health.get(current).map(|h| h.healthy).unwrap_or(false);
3093 if !current_healthy {
3094 true
3095 } else if is_write {
3096 let is_primary = config
3097 .nodes
3098 .iter()
3099 .find(|n| n.address() == *current)
3100 .map(|n| n.role == NodeRole::Primary)
3101 .unwrap_or(false);
3102 !is_primary
3103 } else {
3104 false
3105 }
3106 } else {
3107 true
3108 };
3109
3110 if let Some(forced) = forced_target {
3111 let resolved = config
3116 .nodes
3117 .iter()
3118 .find(|n| n.name.as_deref() == Some(forced.as_str()) || n.address() == forced)
3119 .map(|n| n.address())
3120 .unwrap_or(forced);
3121 Ok(resolved)
3122 } else if need_switch {
3123 if is_write {
3124 Self::select_primary_with_timeout(session, state, config).await
3125 } else {
3126 Self::select_read_node(session, state, config).await
3127 }
3128 } else {
3129 Ok(current_node.unwrap().to_string())
3130 }
3131 }
3132
3133 async fn ensure_conn(
3138 conns: &mut HashMap<String, BackendConn>,
3139 target: &str,
3140 session: &Arc<ClientSession>,
3141 config: &ProxyConfig,
3142 _state: &Arc<ServerState>,
3143 ) -> Result<()> {
3144 if conns.contains_key(target) {
3145 return Ok(());
3146 }
3147
3148 #[cfg(feature = "pool-modes")]
3153 if let Some(pool) = _state.backend_pool.as_ref() {
3154 let key = Self::pool_key_for(target, session).await;
3155 if let Some(stream) = pool.checkout(&key) {
3156 tracing::info!(
3157 target: "helios::pool",
3158 node = %target,
3159 "reused pooled backend connection"
3160 );
3161 conns.insert(target.to_string(), BackendConn::new(stream));
3162 return Ok(());
3163 }
3164 }
3165
3166 let mut backend =
3167 tokio::time::timeout(config.pool.acquire_timeout(), TcpStream::connect(target))
3168 .await
3169 .map_err(|_| ProxyError::Connection(format!("Connection timeout to {}", target)))?
3170 .map_err(|e| {
3171 ProxyError::Connection(format!("Failed to connect to {}: {}", target, e))
3172 })?;
3173 let _ = backend.set_nodelay(true);
3174
3175 let params = session.variables.read().await.clone();
3176 let startup = Self::build_startup_message(¶ms);
3177 backend
3178 .write_all(&startup)
3179 .await
3180 .map_err(|e| ProxyError::Network(format!("Backend startup error: {}", e)))?;
3181 Self::complete_backend_auth(&mut backend).await?;
3182 #[cfg(feature = "pool-modes")]
3183 if _state.backend_pool.is_some() {
3184 tracing::debug!(target: "helios::pool", node = %target, "dialed fresh backend connection (pool miss)");
3185 }
3186 tracing::debug!(node = %target, "opened backend connection");
3187 conns.insert(target.to_string(), BackendConn::new(backend));
3188 Ok(())
3189 }
3190
3191 #[cfg(feature = "pool-modes")]
3198 const POOL_IDENTITY_PARAMS: &'static [&'static str] = &[
3199 "client_encoding",
3200 "DateStyle",
3201 "TimeZone",
3202 "IntervalStyle",
3203 "standard_conforming_strings",
3204 "options",
3205 ];
3206
3207 #[cfg(feature = "pool-modes")]
3215 async fn pool_key_for(target: &str, session: &Arc<ClientSession>) -> String {
3216 let vars = session.variables.read().await;
3217 let user = vars.get("user").map(|s| s.as_str()).unwrap_or("");
3218 let database = vars.get("database").map(|s| s.as_str()).unwrap_or(user);
3220 let base = crate::pool::pool_key(target, user, database);
3221
3222 use std::hash::{Hash, Hasher};
3223 let mut h = std::collections::hash_map::DefaultHasher::new();
3224 let mut any = false;
3225 for k in Self::POOL_IDENTITY_PARAMS {
3226 if let Some(v) = vars.get(*k) {
3227 any = true;
3228 k.hash(&mut h);
3229 v.hash(&mut h);
3230 }
3231 }
3232 if any {
3233 format!("{}\0{:016x}", base, h.finish())
3234 } else {
3235 base
3236 }
3237 }
3238
3239 #[cfg(feature = "pool-modes")]
3257 async fn reset_backend<S: AsyncReadExt + AsyncWriteExt + Unpin>(
3258 stream: &mut S,
3259 reset_sql: &str,
3260 ) -> Result<()> {
3261 let msg = crate::protocol::QueryMessage {
3262 query: reset_sql.to_string(),
3263 }
3264 .encode();
3265 tokio::time::timeout(Self::BACKEND_WRITE_TIMEOUT, stream.write_all(&msg.encode()))
3266 .await
3267 .map_err(|_| ProxyError::Network("reset write timeout".to_string()))?
3268 .map_err(|e| ProxyError::Network(format!("reset write error: {}", e)))?;
3269
3270 let mut buf = BytesMut::with_capacity(1024);
3271 let mut had_error = false;
3272 loop {
3273 let mut consumed = 0usize;
3276 let mut ready_status: Option<u8> = None;
3277 loop {
3278 let rem = &buf[consumed..];
3279 if rem.len() < 5 {
3280 break;
3281 }
3282 let len = u32::from_be_bytes([rem[1], rem[2], rem[3], rem[4]]) as usize;
3283 if len < 4 || rem.len() < len + 1 {
3284 break;
3285 }
3286 let mtype = rem[0];
3287 let frame_total = len + 1;
3288 if mtype == b'E' {
3289 had_error = true;
3290 }
3291 consumed += frame_total;
3292 if mtype == b'Z' {
3293 ready_status = Some(if frame_total >= 6 { rem[5] } else { b'I' });
3294 break;
3295 }
3296 }
3297 let _ = buf.split_to(consumed);
3298
3299 if let Some(status) = ready_status {
3300 if had_error || status != b'I' {
3301 return Err(ProxyError::Connection(format!(
3302 "reset query did not cleanly succeed (error={}, status={})",
3303 had_error, status as char
3304 )));
3305 }
3306 return Ok(());
3307 }
3308
3309 buf.reserve(1024);
3310 let n = tokio::time::timeout(Duration::from_secs(5), stream.read_buf(&mut buf))
3311 .await
3312 .map_err(|_| ProxyError::Network("reset drain timeout".to_string()))?
3313 .map_err(|e| ProxyError::Network(format!("reset drain read error: {}", e)))?;
3314 if n == 0 {
3315 return Err(ProxyError::Connection(
3316 "backend closed during reset".to_string(),
3317 ));
3318 }
3319 }
3320 }
3321
3322 #[cfg(feature = "pool-modes")]
3328 async fn release_to_pool_if_idle(
3329 conns: &mut HashMap<String, BackendConn>,
3330 node: Option<&str>,
3331 session: &Arc<ClientSession>,
3332 state: &Arc<ServerState>,
3333 config: &ProxyConfig,
3334 ) {
3335 let Some(pool) = state.backend_pool.as_ref() else {
3336 return;
3337 };
3338 let Some(node) = node else {
3339 return;
3340 };
3341 if session
3345 .in_transaction
3346 .load(std::sync::atomic::Ordering::Relaxed)
3347 || session
3348 .copy_in_progress
3349 .load(std::sync::atomic::Ordering::Relaxed)
3350 {
3351 return;
3352 }
3353 let Some(mut bc) = conns.remove(node) else {
3354 return;
3355 };
3356
3357 let clean = !bc.dirty && bc.prepared.is_empty() && bc.unnamed_sig.is_none();
3365 if config.pool_mode.skip_clean_reset && clean {
3366 let key = Self::pool_key_for(node, session).await;
3367 if pool.checkin(&key, bc.stream) {
3368 pool.note_reset_skipped();
3369 tracing::debug!(target: "helios::pool", node = %node, "parked clean backend connection (reset skipped)");
3370 }
3371 return;
3372 }
3373
3374 if Self::reset_backend(&mut bc.stream, &config.pool_mode.reset_query)
3375 .await
3376 .is_ok()
3377 {
3378 let key = Self::pool_key_for(node, session).await;
3379 if pool.checkin(&key, bc.stream) {
3380 tracing::debug!(target: "helios::pool", node = %node, "parked backend connection for reuse");
3381 }
3382 }
3383 }
3385
3386 async fn forward_simple_query(
3392 client: &mut ClientStream,
3393 msg: &Message,
3394 conns: &mut HashMap<String, BackendConn>,
3395 current_node: Option<&str>,
3396 session: &Arc<ClientSession>,
3397 state: &Arc<ServerState>,
3398 config: &ProxyConfig,
3399 ) -> Result<(Option<String>, u64)> {
3400 #[cfg(feature = "rate-limiting")]
3402 if let Some(mut resp) = Self::rate_limit_check(session, state, config).await {
3403 resp.extend_from_slice(&Self::create_ready_for_query(b'I'));
3404 client
3405 .write_all(&resp)
3406 .await
3407 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
3408 return Ok((None, resp.len() as u64));
3409 }
3410
3411 let default_is_write = Self::is_write_message(msg);
3412 let plugin_override = Self::apply_route_hook(msg, state, session);
3413
3414 if let RouteOverride::Block(reason) = plugin_override {
3416 let mut response = Vec::with_capacity(64 + reason.len());
3417 response.extend_from_slice(&Self::create_error_response(
3418 "42000",
3419 &format!("Query blocked by route plugin: {}", reason),
3420 ));
3421 response.extend_from_slice(&Self::create_ready_for_query(b'I'));
3422 client
3423 .write_all(&response)
3424 .await
3425 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
3426 return Ok((None, response.len() as u64));
3427 }
3428
3429 #[cfg(feature = "routing-hints")]
3433 let (route_override, default_is_write, stripped_msg) =
3434 Self::resolve_simple_route(msg, plugin_override, default_is_write, state);
3435 #[cfg(not(feature = "routing-hints"))]
3436 let (route_override, stripped_msg): (RouteOverride, Option<Message>) =
3437 (plugin_override, None);
3438
3439 let (is_write, forced_target) = match route_override {
3440 RouteOverride::None => (default_is_write, None),
3441 RouteOverride::Primary => (true, None),
3442 RouteOverride::Standby => (false, None),
3443 RouteOverride::Node(name) => (default_is_write, Some(name)),
3444 RouteOverride::Block(_) => unreachable!("handled above"),
3445 };
3446
3447 #[cfg(feature = "lag-routing")]
3450 if is_write && config.lag_routing.enabled {
3451 *session.last_write_at.write().await = Some(std::time::Instant::now());
3452 }
3453
3454 let forward_msg = stripped_msg.as_ref().unwrap_or(msg);
3457
3458 #[cfg(feature = "query-rewriting")]
3462 let rewritten_msg: Option<Message> = state.rewriter.as_ref().and_then(|rw| {
3463 let sql = crate::protocol::query_text(&forward_msg.payload)?;
3464 match rw.rewrite(sql) {
3465 Ok(res) if res.was_rewritten() => {
3466 tracing::debug!(target: "helios::rewrite", rules = ?res.rules_applied, "query rewritten");
3467 Some(crate::protocol::QueryMessage { query: res.query().to_string() }.encode())
3468 }
3469 _ => None,
3470 }
3471 });
3472 #[cfg(feature = "query-rewriting")]
3473 let forward_msg = rewritten_msg.as_ref().unwrap_or(forward_msg);
3474
3475 #[cfg(feature = "multi-tenancy")]
3479 let tenant_msg: Option<Message> = if let Some(tm) = state.tenant_manager.as_ref() {
3480 match crate::protocol::query_text(&forward_msg.payload) {
3481 Some(sql) => {
3482 let ctx = Self::tenant_request_ctx(session).await;
3483 match tm.identify_tenant(&ctx) {
3484 Some(tenant) => {
3485 let res = tm.transform_query(sql, &tenant);
3486 if res.transformed {
3487 tracing::debug!(target: "helios::tenant", tenant = %tenant.0, "tenant filter injected");
3488 Some(crate::protocol::QueryMessage { query: res.query }.encode())
3489 } else {
3490 None
3491 }
3492 }
3493 None => None,
3494 }
3495 }
3496 None => None,
3497 }
3498 } else {
3499 None
3500 };
3501 #[cfg(feature = "multi-tenancy")]
3502 let forward_msg = tenant_msg.as_ref().unwrap_or(forward_msg);
3503
3504 #[cfg(feature = "edge-proxy")]
3516 if config.edge.enabled
3517 && !session
3518 .edge_ineligible
3519 .load(std::sync::atomic::Ordering::Relaxed)
3520 {
3521 if let Some(sql) = crate::protocol::query_text(&forward_msg.payload) {
3522 if Self::stmt_leaves_session_state(sql) {
3523 session
3524 .edge_ineligible
3525 .store(true, std::sync::atomic::Ordering::Relaxed);
3526 }
3527 }
3528 }
3529 #[cfg(feature = "edge-proxy")]
3530 let edge_ctx: Option<(crate::edge::CacheKey, Vec<String>, u64, u64)> = if is_write
3531 || !config.edge.enabled
3532 || session
3533 .edge_ineligible
3534 .load(std::sync::atomic::Ordering::Relaxed)
3535 {
3536 None
3537 } else {
3538 let sql = crate::protocol::query_text(&forward_msg.payload).unwrap_or("");
3539 if Self::is_cacheable_read_sql(sql)
3542 && !session
3543 .in_transaction
3544 .load(std::sync::atomic::Ordering::Relaxed)
3545 {
3546 let (database, user, session_vars) = {
3551 let vars = session.variables.read().await;
3552 let database = vars
3553 .get("database")
3554 .cloned()
3555 .unwrap_or_else(|| "default".to_string());
3556 let user = vars.get("user").cloned().unwrap_or_default();
3557 let mut rest: Vec<(String, String)> = vars
3558 .iter()
3559 .filter(|(k, _)| k.as_str() != "database" && k.as_str() != "user")
3560 .map(|(k, v)| (k.clone(), v.clone()))
3561 .collect();
3562 rest.sort();
3563 (database, user, rest)
3564 };
3565 let fp = crate::edge::fingerprint::analyze(sql, &database, &user, &session_vars);
3566 if fp.tables.is_empty() {
3570 None
3571 } else {
3572 let key = crate::edge::CacheKey {
3573 fingerprint: fp.fingerprint,
3574 params_hash: fp.params_hash,
3575 database,
3576 user,
3577 };
3578 if let Some(entry) = state.edge_cache.get(&key) {
3579 tracing::debug!(target: "helios::edge", "edge cache hit");
3580 client.write_all(&entry.response_bytes).await.map_err(|e| {
3581 ProxyError::Network(format!("Client write error: {}", e))
3582 })?;
3583 return Ok((None, entry.response_bytes.len() as u64));
3584 }
3585 let (read_version, inval_epoch) =
3594 if config.edge.role == crate::edge::EdgeRole::Edge {
3595 (
3596 state.edge_cache.observed_home_version(),
3597 state.edge_cache.invalidation_epoch(),
3598 )
3599 } else {
3600 (state.edge_cache.next_version(), 0)
3601 };
3602 Some((key, fp.tables, read_version, inval_epoch))
3603 }
3604 } else {
3605 None
3606 }
3607 };
3608
3609 #[cfg(feature = "query-cache")]
3612 let cache_ctx: Option<crate::cache::CacheContext> = if is_write {
3613 None
3614 } else if let Some(qc) = state.query_cache.as_ref() {
3615 let sql = crate::protocol::query_text(&forward_msg.payload).unwrap_or("");
3616 match Self::cacheable_read_ctx(session, sql).await {
3617 Some(ctx) => {
3618 if let crate::cache::CacheLookup::Hit { result, level } =
3619 qc.get(sql, &ctx).await
3620 {
3621 tracing::debug!(target: "helios::cache", level = %level, "cache hit");
3622 client.write_all(&result.data).await.map_err(|e| {
3623 ProxyError::Network(format!("Client write error: {}", e))
3624 })?;
3625 return Ok((None, result.data.len() as u64));
3626 }
3627 Some(ctx)
3628 }
3629 None => None,
3630 }
3631 } else {
3632 None
3633 };
3634
3635 #[cfg(feature = "schema-routing")]
3638 let forced_target = match state.schema_analyzer.as_ref() {
3639 Some(analyzer)
3640 if forced_target.is_none()
3641 && !is_write
3642 && !config.schema_routing.analytics_node.is_empty() =>
3643 {
3644 match crate::protocol::query_text(&forward_msg.payload) {
3645 Some(sql) if analyzer.analyze(sql).is_analytics() => {
3646 tracing::debug!(target: "helios::schema", "OLAP query routed to analytics node");
3647 Some(config.schema_routing.analytics_node.clone())
3648 }
3649 _ => forced_target,
3650 }
3651 }
3652 _ => forced_target,
3653 };
3654
3655 #[cfg(feature = "query-analytics")]
3657 let analytics_sql =
3658 crate::protocol::query_text(&forward_msg.payload).map(|s| s.to_string());
3659 #[cfg(feature = "query-analytics")]
3660 let started = std::time::Instant::now();
3661
3662 let target = Self::choose_target_node(
3663 is_write,
3664 forced_target,
3665 current_node,
3666 session,
3667 state,
3668 config,
3669 )
3670 .await?;
3671 tracing::debug!(target: "helios::routing", node = %target, is_write, "routed simple query");
3672
3673 #[cfg(feature = "circuit-breaker")]
3675 if let Some(mut resp) = Self::circuit_fast_fail(state, &target) {
3676 resp.extend_from_slice(&Self::create_ready_for_query(b'I'));
3677 client
3678 .write_all(&resp)
3679 .await
3680 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
3681 return Ok((None, resp.len() as u64));
3682 }
3683
3684 if let Err(e) = Self::ensure_conn(conns, &target, session, config, state).await {
3686 Self::record_backend_failure(state, &target, &e.to_string());
3687 return Err(e);
3688 }
3689 let backend = conns.get_mut(&target).expect("just ensured");
3690
3691 #[cfg(feature = "pool-modes")]
3697 if config.pool_mode.skip_clean_reset && !backend.dirty {
3698 if let Some(sql) = crate::protocol::query_text(&forward_msg.payload) {
3699 if Self::stmt_leaves_session_state(sql) {
3700 backend.dirty = true;
3701 }
3702 }
3703 }
3704
3705 let backend_err = match tokio::time::timeout(
3706 Self::BACKEND_WRITE_TIMEOUT,
3707 backend.stream.write_all(&forward_msg.encode()),
3708 )
3709 .await
3710 {
3711 Ok(Ok(())) => None,
3712 Ok(Err(e)) => Some(format!("Backend write error: {}", e)),
3713 Err(_) => Some("Backend write timeout".to_string()),
3714 };
3715 if let Some(msg) = backend_err {
3716 let e = ProxyError::Network(msg);
3717 conns.remove(&target);
3718 Self::record_backend_failure(state, &target, &e.to_string());
3719 return Err(e);
3720 }
3721
3722 #[cfg(any(feature = "query-cache", feature = "edge-proxy"))]
3727 {
3728 #[cfg(feature = "query-cache")]
3729 let qc_wants = cache_ctx.is_some() && state.query_cache.is_some();
3730 #[cfg(not(feature = "query-cache"))]
3731 let qc_wants = false;
3732 #[cfg(feature = "edge-proxy")]
3733 let edge_wants = edge_ctx.is_some();
3734 #[cfg(not(feature = "edge-proxy"))]
3735 let edge_wants = false;
3736 if qc_wants || edge_wants {
3737 return match Self::stream_until_ready_capture(client, &mut backend.stream, session)
3738 .await
3739 {
3740 Ok((sent, captured, cacheable, rows)) => {
3741 #[cfg(not(feature = "query-cache"))]
3742 let _ = rows; #[cfg(feature = "circuit-breaker")]
3744 Self::circuit_record(state, &target, true, "");
3745 let body = bytes::Bytes::from(captured);
3748 #[cfg(feature = "edge-proxy")]
3755 if let Some((key, tables, read_version, inval_epoch)) = edge_ctx {
3756 if cacheable && !body.is_empty() {
3757 let entry = crate::edge::CacheEntry {
3758 version: read_version,
3759 response_bytes: body.clone(),
3760 tables,
3761 expires_at: std::time::Instant::now()
3762 + config.edge.default_ttl(),
3763 };
3764 let stored = if config.edge.role == crate::edge::EdgeRole::Edge {
3765 state.edge_cache.insert_if_epoch(key, entry, inval_epoch)
3766 } else {
3767 state.edge_cache.insert_if_fresh(key, entry)
3768 };
3769 if !stored {
3770 tracing::debug!(
3771 target: "helios::edge",
3772 "edge store skipped — invalidation raced the read"
3773 );
3774 }
3775 }
3776 }
3777 #[cfg(feature = "query-cache")]
3778 if let (Some(ctx), Some(qc)) =
3779 (cache_ctx.as_ref(), state.query_cache.as_ref())
3780 {
3781 if cacheable && !body.is_empty() {
3782 let sql =
3783 crate::protocol::query_text(&forward_msg.payload).unwrap_or("");
3784 qc.put(sql, ctx, body.clone(), rows, std::time::Duration::ZERO)
3785 .await;
3786 }
3787 }
3788 let _ = body;
3789 #[cfg(feature = "query-analytics")]
3790 if let Some(sql) = analytics_sql.as_deref() {
3791 Self::record_analytics(
3792 state,
3793 session,
3794 sql,
3795 &target,
3796 started.elapsed(),
3797 None,
3798 )
3799 .await;
3800 }
3801 Ok((Some(target), sent))
3802 }
3803 Err(e) => {
3804 conns.remove(&target);
3805 Self::record_backend_failure(state, &target, &e.to_string());
3806 Err(e)
3807 }
3808 };
3809 }
3810 }
3811
3812 match Self::stream_until_ready(client, &mut backend.stream, session, state).await {
3813 Ok(sent) => {
3814 #[cfg(feature = "circuit-breaker")]
3815 Self::circuit_record(state, &target, true, "");
3816 #[cfg(feature = "query-cache")]
3818 if is_write {
3819 if let Some(qc) = state.query_cache.as_ref() {
3820 let sql = crate::protocol::query_text(&forward_msg.payload).unwrap_or("");
3821 qc.invalidate_query(sql).await;
3822 }
3823 }
3824 #[cfg(feature = "edge-proxy")]
3834 if config.edge.enabled {
3835 let sql = crate::protocol::query_text(&forward_msg.payload).unwrap_or("");
3836 if Self::edge_write_needs_invalidation(is_write, sql) {
3837 let tables = crate::edge::fingerprint::tables_only(sql);
3844 if session
3845 .copy_in_progress
3846 .load(std::sync::atomic::Ordering::Relaxed)
3847 {
3848 *session
3849 .pending_edge_copy_tables
3850 .lock()
3851 .unwrap_or_else(|e| e.into_inner()) = Some(tables);
3852 } else {
3853 Self::edge_invalidate_write(state, config, tables).await;
3854 }
3855 }
3856 }
3857 #[cfg(feature = "ha-tr")]
3859 if is_write && config.tr_enabled {
3860 if let Some(sql) = crate::protocol::query_text(&forward_msg.payload) {
3861 Self::journal_write(state, session, sql).await;
3862 }
3863 }
3864 #[cfg(feature = "query-analytics")]
3865 if let Some(sql) = analytics_sql.as_deref() {
3866 Self::record_analytics(state, session, sql, &target, started.elapsed(), None)
3867 .await;
3868 }
3869 Ok((Some(target), sent))
3870 }
3871 Err(e) => {
3872 conns.remove(&target);
3874 Self::record_backend_failure(state, &target, &e.to_string());
3875 #[cfg(feature = "query-analytics")]
3876 if let Some(sql) = analytics_sql.as_deref() {
3877 Self::record_analytics(
3878 state,
3879 session,
3880 sql,
3881 &target,
3882 started.elapsed(),
3883 Some(e.to_string()),
3884 )
3885 .await;
3886 }
3887 Err(e)
3888 }
3889 }
3890 }
3891
3892 #[allow(clippy::too_many_arguments)]
3905 async fn forward_extended_batch(
3906 client: &mut ClientStream,
3907 batch: &[u8],
3908 route_sql: Option<&str>,
3909 wait_ready: bool,
3910 conns: &mut HashMap<String, BackendConn>,
3911 current_node: Option<&str>,
3912 registry: &HashMap<String, bytes::Bytes>,
3913 reprepare: &[String],
3914 defines: &[String],
3915 unnamed: Option<(bytes::Bytes, bytes::Bytes)>,
3916 session: &Arc<ClientSession>,
3917 state: &Arc<ServerState>,
3918 config: &ProxyConfig,
3919 ) -> Result<(Option<String>, u64)> {
3920 #[cfg(feature = "rate-limiting")]
3924 if let Some(mut resp) = Self::rate_limit_check(session, state, config).await {
3925 if wait_ready {
3926 resp.extend_from_slice(&Self::create_ready_for_query(b'I'));
3927 }
3928 client
3929 .write_all(&resp)
3930 .await
3931 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
3932 return Ok((None, resp.len() as u64));
3933 }
3934
3935 #[cfg(feature = "query-analytics")]
3937 let analytics_sql = route_sql.map(|s| s.to_string());
3938 #[cfg(feature = "query-analytics")]
3939 let started = std::time::Instant::now();
3940
3941 let target = match route_sql {
3942 Some(sql) => {
3943 #[cfg(feature = "routing-hints")]
3946 let (is_write, forced) = Self::extended_hint_route(state, sql)
3947 .unwrap_or_else(|| (Self::is_write_query(sql), None));
3948 #[cfg(not(feature = "routing-hints"))]
3949 let (is_write, forced): (bool, Option<String>) = (Self::is_write_query(sql), None);
3950 #[cfg(feature = "lag-routing")]
3951 if is_write && config.lag_routing.enabled {
3952 *session.last_write_at.write().await = Some(std::time::Instant::now());
3953 }
3954 Self::choose_target_node(is_write, forced, current_node, session, state, config)
3955 .await?
3956 }
3957 None => match current_node {
3961 Some(c) => c.to_string(),
3962 None => Self::select_read_node(session, state, config).await?,
3963 },
3964 };
3965
3966 #[cfg(feature = "circuit-breaker")]
3968 if let Some(mut resp) = Self::circuit_fast_fail(state, &target) {
3969 if wait_ready {
3970 resp.extend_from_slice(&Self::create_ready_for_query(b'I'));
3971 }
3972 client
3973 .write_all(&resp)
3974 .await
3975 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
3976 return Ok((None, resp.len() as u64));
3977 }
3978
3979 if let Err(e) = Self::ensure_conn(conns, &target, session, config, state).await {
3980 Self::record_backend_failure(state, &target, &e.to_string());
3981 return Err(e);
3982 }
3983 let backend = conns.get_mut(&target).expect("just ensured");
3984
3985 for name in reprepare {
3990 if backend.prepared.contains(name) {
3991 continue;
3992 }
3993 let Some(parse_bytes) = registry.get(name) else {
3994 continue; };
3996 match Self::reprepare_statement(&mut backend.stream, parse_bytes).await {
3997 Ok(()) => {
3998 backend.prepared.insert(name.clone());
3999 }
4000 Err(e) => {
4001 conns.remove(&target);
4002 return Err(e);
4003 }
4004 }
4005 }
4006
4007 let mut inject_parse_complete = false;
4014 let mut new_unnamed_sig: Option<bytes::Bytes> = None;
4015 if let Some((parse_msg, sig)) = unnamed.as_ref() {
4016 if backend.unnamed_sig.as_deref() == Some(&sig[..]) {
4017 inject_parse_complete = true;
4018 } else {
4019 if let Err(e) = backend
4020 .stream
4021 .write_all(parse_msg)
4022 .await
4023 .map_err(|e| ProxyError::Network(format!("Backend write error: {}", e)))
4024 {
4025 conns.remove(&target);
4026 return Err(e);
4027 }
4028 new_unnamed_sig = Some(sig.clone());
4029 }
4030 }
4031
4032 let batch_err = match tokio::time::timeout(
4033 Self::BACKEND_WRITE_TIMEOUT,
4034 backend.stream.write_all(batch),
4035 )
4036 .await
4037 {
4038 Ok(Ok(())) => None,
4039 Ok(Err(e)) => Some(format!("Backend write error: {}", e)),
4040 Err(_) => Some("Backend write timeout".to_string()),
4041 };
4042 if let Some(msg) = batch_err {
4043 let e = ProxyError::Network(msg);
4044 conns.remove(&target);
4045 Self::record_backend_failure(state, &target, &e.to_string());
4046 return Err(e);
4047 }
4048
4049 let mut injected: u64 = 0;
4052 if inject_parse_complete {
4053 if let Err(e) = client
4054 .write_all(&[b'1', 0, 0, 0, 4])
4055 .await
4056 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))
4057 {
4058 conns.remove(&target);
4059 return Err(e);
4060 }
4061 injected = 5;
4062 }
4063
4064 let r = if wait_ready {
4065 Self::stream_until_ready(client, &mut backend.stream, session, state).await
4066 } else {
4067 Self::stream_flush(client, &mut backend.stream, session, state).await
4068 };
4069 match r {
4070 Ok(sent) => {
4071 #[cfg(feature = "circuit-breaker")]
4072 Self::circuit_record(state, &target, true, "");
4073 #[cfg(feature = "query-analytics")]
4074 if let Some(sql) = analytics_sql.as_deref() {
4075 Self::record_analytics(state, session, sql, &target, started.elapsed(), None)
4076 .await;
4077 }
4078 for name in defines {
4080 backend.prepared.insert(name.clone());
4081 }
4082 if let Some(sig) = new_unnamed_sig {
4084 backend.unnamed_sig = Some(sig);
4085 }
4086 Ok((Some(target), sent + injected))
4087 }
4088 Err(e) => {
4089 conns.remove(&target);
4090 Self::record_backend_failure(state, &target, &e.to_string());
4091 #[cfg(feature = "query-analytics")]
4092 if let Some(sql) = analytics_sql.as_deref() {
4093 Self::record_analytics(
4094 state,
4095 session,
4096 sql,
4097 &target,
4098 started.elapsed(),
4099 Some(e.to_string()),
4100 )
4101 .await;
4102 }
4103 Err(e)
4104 }
4105 }
4106 }
4107
4108 async fn reprepare_statement<S: AsyncReadExt + AsyncWriteExt + Unpin>(
4114 backend: &mut S,
4115 parse_bytes: &[u8],
4116 ) -> Result<()> {
4117 tokio::time::timeout(Self::REPREPARE_TIMEOUT, backend.write_all(parse_bytes))
4118 .await
4119 .map_err(|_| ProxyError::Network("re-prepare write timeout".to_string()))?
4120 .map_err(|e| ProxyError::Network(format!("re-prepare write error: {}", e)))?;
4121 tokio::time::timeout(
4123 Self::REPREPARE_TIMEOUT,
4124 backend.write_all(&[b'H', 0, 0, 0, 4]),
4125 )
4126 .await
4127 .map_err(|_| ProxyError::Network("re-prepare flush timeout".to_string()))?
4128 .map_err(|e| ProxyError::Network(format!("re-prepare flush error: {}", e)))?;
4129 let mtype =
4130 tokio::time::timeout(Self::REPREPARE_TIMEOUT, Self::read_one_frame_type(backend))
4131 .await
4132 .map_err(|_| ProxyError::Network("re-prepare read timeout".to_string()))??;
4133 match mtype {
4134 b'1' => Ok(()), b'E' => Err(ProxyError::Protocol(
4136 "re-prepare rejected by backend".to_string(),
4137 )),
4138 other => Err(ProxyError::Protocol(format!(
4139 "unexpected re-prepare reply: {}",
4140 other as char
4141 ))),
4142 }
4143 }
4144
4145 async fn read_one_frame_type<S: AsyncReadExt + Unpin>(backend: &mut S) -> Result<u8> {
4149 let mut header = [0u8; 5];
4150 backend
4151 .read_exact(&mut header)
4152 .await
4153 .map_err(|e| ProxyError::Network(format!("re-prepare read error: {}", e)))?;
4154 let len = u32::from_be_bytes([header[1], header[2], header[3], header[4]]) as usize;
4155 let body_len = len.saturating_sub(4);
4156 if body_len > 0 {
4157 let mut body = vec![0u8; body_len];
4158 backend
4159 .read_exact(&mut body)
4160 .await
4161 .map_err(|e| ProxyError::Network(format!("re-prepare body read error: {}", e)))?;
4162 }
4163 Ok(header[0])
4164 }
4165
4166 fn parse_stmt_name(payload: &[u8]) -> &str {
4169 let end = payload.iter().position(|&b| b == 0).unwrap_or(0);
4170 std::str::from_utf8(&payload[..end]).unwrap_or("")
4171 }
4172
4173 fn bind_stmt_ref(payload: &[u8]) -> Option<&str> {
4177 let portal_end = payload.iter().position(|&b| b == 0)?;
4178 let rest = &payload[portal_end + 1..];
4179 let stmt_end = rest.iter().position(|&b| b == 0)?;
4180 let name = std::str::from_utf8(&rest[..stmt_end]).ok()?;
4181 (!name.is_empty()).then_some(name)
4182 }
4183
4184 fn stmt_kind_name(payload: &[u8]) -> Option<&str> {
4187 if payload.first() != Some(&b'S') {
4188 return None;
4189 }
4190 let rest = &payload[1..];
4191 let end = rest.iter().position(|&b| b == 0)?;
4192 let name = std::str::from_utf8(&rest[..end]).ok()?;
4193 (!name.is_empty()).then_some(name)
4194 }
4195
4196 async fn stream_until_ready(
4204 client: &mut ClientStream,
4205 backend: &mut TcpStream,
4206 session: &Arc<ClientSession>,
4207 state: &Arc<ServerState>,
4208 ) -> Result<u64> {
4209 let _ = state;
4210 let mut buf = BytesMut::with_capacity(16384);
4211 let mut sent: u64 = 0;
4212
4213 loop {
4214 let mut consumed = 0usize;
4216 let mut ready_status: Option<u8> = None;
4217 let mut yield_for_copy = false;
4218 loop {
4219 let rem = &buf[consumed..];
4220 if rem.len() < 5 {
4221 break;
4222 }
4223 let len = u32::from_be_bytes([rem[1], rem[2], rem[3], rem[4]]) as usize;
4224 if len < 4 || rem.len() < len + 1 {
4225 break; }
4227 let frame_total = len + 1;
4228 let mtype = rem[0];
4229 consumed += frame_total;
4230 if mtype == b'Z' {
4231 ready_status = Some(if frame_total >= 6 { rem[5] } else { b'I' });
4233 break;
4234 }
4235 if mtype == b'G' || mtype == b'W' {
4236 yield_for_copy = true;
4239 break;
4240 }
4241 }
4242
4243 if consumed > 0 {
4244 tokio::time::timeout(
4245 Self::CLIENT_WRITE_TIMEOUT,
4246 client.write_all(&buf[..consumed]),
4247 )
4248 .await
4249 .map_err(|_| ProxyError::Network("Client write timeout".to_string()))?
4250 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
4251 sent += consumed as u64;
4252 let _ = buf.split_to(consumed);
4253 }
4254
4255 if let Some(status) = ready_status {
4256 let st = TransactionStatus::from_byte(status);
4257 session.in_transaction.store(
4258 st != TransactionStatus::Idle,
4259 std::sync::atomic::Ordering::Relaxed,
4260 );
4261 return Ok(sent);
4262 }
4263 if yield_for_copy {
4264 session
4269 .copy_in_progress
4270 .store(true, std::sync::atomic::Ordering::Relaxed);
4271 return Ok(sent);
4272 }
4273
4274 buf.reserve(16384);
4277 let n = tokio::time::timeout(Duration::from_secs(30), backend.read_buf(&mut buf))
4278 .await
4279 .map_err(|_| ProxyError::Network("Backend read timeout".to_string()))?
4280 .map_err(|e| ProxyError::Network(format!("Backend read error: {}", e)))?;
4281 if n == 0 {
4282 return Err(ProxyError::Connection(
4283 "Backend closed mid-response".to_string(),
4284 ));
4285 }
4286 }
4287 }
4288
4289 #[cfg(any(feature = "query-cache", feature = "edge-proxy"))]
4302 async fn stream_until_ready_capture(
4303 client: &mut ClientStream,
4304 backend: &mut TcpStream,
4305 session: &Arc<ClientSession>,
4306 ) -> Result<(u64, Vec<u8>, bool, usize)> {
4307 let mut buf = BytesMut::with_capacity(16384);
4308 let mut sent: u64 = 0;
4309 let mut captured: Vec<u8> = Vec::with_capacity(4096);
4310 let mut had_error = false;
4311 let mut saw_async = false;
4312 let mut row_count: usize = 0;
4313
4314 loop {
4315 let mut consumed = 0usize;
4316 let mut ready_status: Option<u8> = None;
4317 let mut yield_for_copy = false;
4318 loop {
4319 let rem = &buf[consumed..];
4320 if rem.len() < 5 {
4321 break;
4322 }
4323 let len = u32::from_be_bytes([rem[1], rem[2], rem[3], rem[4]]) as usize;
4324 if len < 4 || rem.len() < len + 1 {
4325 break;
4326 }
4327 let frame_total = len + 1;
4328 let mtype = rem[0];
4329 if mtype == b'E' {
4330 had_error = true;
4331 }
4332 if mtype == b'A' || mtype == b'N' || mtype == b'S' {
4335 saw_async = true;
4336 }
4337 if mtype == b'C' {
4338 if let Some(tag) = rem.get(5..frame_total) {
4340 if let Some(end) = tag.iter().position(|&b| b == 0) {
4341 if let Ok(s) = std::str::from_utf8(&tag[..end]) {
4342 if let Some(n) =
4343 s.rsplit(' ').next().and_then(|x| x.parse::<usize>().ok())
4344 {
4345 row_count = n;
4346 }
4347 }
4348 }
4349 }
4350 }
4351 consumed += frame_total;
4352 if mtype == b'Z' {
4353 ready_status = Some(if frame_total >= 6 { rem[5] } else { b'I' });
4354 break;
4355 }
4356 if mtype == b'G' || mtype == b'W' {
4357 yield_for_copy = true;
4358 break;
4359 }
4360 }
4361
4362 if consumed > 0 {
4363 tokio::time::timeout(
4364 Self::CLIENT_WRITE_TIMEOUT,
4365 client.write_all(&buf[..consumed]),
4366 )
4367 .await
4368 .map_err(|_| ProxyError::Network("Client write timeout".to_string()))?
4369 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
4370 captured.extend_from_slice(&buf[..consumed]);
4371 sent += consumed as u64;
4372 let _ = buf.split_to(consumed);
4373 }
4374
4375 if let Some(status) = ready_status {
4376 let st = TransactionStatus::from_byte(status);
4377 session.in_transaction.store(
4378 st != TransactionStatus::Idle,
4379 std::sync::atomic::Ordering::Relaxed,
4380 );
4381 let cacheable = !had_error && status == b'I' && !saw_async;
4382 return Ok((sent, captured, cacheable, row_count));
4383 }
4384 if yield_for_copy {
4385 session
4386 .copy_in_progress
4387 .store(true, std::sync::atomic::Ordering::Relaxed);
4388 return Ok((sent, captured, false, row_count));
4389 }
4390
4391 buf.reserve(16384);
4393 let n = tokio::time::timeout(Duration::from_secs(30), backend.read_buf(&mut buf))
4394 .await
4395 .map_err(|_| ProxyError::Network("Backend read timeout".to_string()))?
4396 .map_err(|e| ProxyError::Network(format!("Backend read error: {}", e)))?;
4397 if n == 0 {
4398 return Err(ProxyError::Connection(
4399 "Backend closed mid-response".to_string(),
4400 ));
4401 }
4402 }
4403 }
4404
4405 async fn stream_flush(
4419 client: &mut ClientStream,
4420 backend: &mut TcpStream,
4421 session: &Arc<ClientSession>,
4422 state: &Arc<ServerState>,
4423 ) -> Result<u64> {
4424 let _ = (session, state);
4425 let mut read_buf = vec![0u8; 16384];
4426 let mut sent: u64 = 0;
4427 loop {
4428 match backend.try_read(&mut read_buf) {
4429 Ok(0) => {
4430 return Err(ProxyError::Connection(
4431 "Backend closed mid-flush".to_string(),
4432 ))
4433 }
4434 Ok(n) => {
4435 client
4436 .write_all(&read_buf[..n])
4437 .await
4438 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
4439 sent += n as u64;
4440 }
4441 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(sent),
4444 Err(e) => return Err(ProxyError::Network(format!("Backend read error: {}", e))),
4445 }
4446 }
4447 }
4448
4449 fn is_write_message(msg: &Message) -> bool {
4451 match msg.msg_type {
4452 MessageType::Query => {
4453 crate::protocol::query_text(&msg.payload)
4457 .map(Self::is_write_query)
4458 .unwrap_or(false)
4459 }
4460 MessageType::Parse => {
4461 msg.payload
4464 .iter()
4465 .position(|&b| b == 0)
4466 .and_then(|end| crate::protocol::query_text(&msg.payload[end + 1..]))
4467 .map(Self::is_write_query)
4468 .unwrap_or(false)
4469 }
4470 _ => false,
4472 }
4473 }
4474
4475 fn is_write_query(sql: &str) -> bool {
4477 use crate::protocol::starts_with_ci;
4478 let trimmed = sql.trim();
4479
4480 if starts_with_ci(trimmed, "INSERT")
4482 || starts_with_ci(trimmed, "UPDATE")
4483 || starts_with_ci(trimmed, "DELETE")
4484 || starts_with_ci(trimmed, "CREATE")
4485 || starts_with_ci(trimmed, "DROP")
4486 || starts_with_ci(trimmed, "ALTER")
4487 || starts_with_ci(trimmed, "TRUNCATE")
4488 || starts_with_ci(trimmed, "GRANT")
4489 || starts_with_ci(trimmed, "REVOKE")
4490 || starts_with_ci(trimmed, "VACUUM")
4491 || starts_with_ci(trimmed, "REINDEX")
4492 || starts_with_ci(trimmed, "CLUSTER")
4493 {
4494 return true;
4495 }
4496
4497 if starts_with_ci(trimmed, "BEGIN")
4499 || starts_with_ci(trimmed, "START")
4500 || starts_with_ci(trimmed, "COMMIT")
4501 || starts_with_ci(trimmed, "ROLLBACK")
4502 || starts_with_ci(trimmed, "SAVEPOINT")
4503 || starts_with_ci(trimmed, "RELEASE")
4504 {
4505 return true;
4506 }
4507
4508 if starts_with_ci(trimmed, "SET") && !starts_with_ci(trimmed, "SET TRANSACTION READ ONLY") {
4510 return true;
4511 }
4512
4513 false
4514 }
4515
4516 #[cfg(feature = "edge-proxy")]
4531 fn edge_write_needs_invalidation(is_write: bool, sql: &str) -> bool {
4532 use crate::protocol::starts_with_ci;
4533 let t = sql.trim();
4534 let core = t.strip_suffix(';').unwrap_or(t).trim_end();
4535 let multi_stmt = core.contains(';');
4536 if !multi_stmt
4537 && (starts_with_ci(core, "BEGIN")
4538 || starts_with_ci(core, "START")
4539 || starts_with_ci(core, "SAVEPOINT")
4540 || starts_with_ci(core, "RELEASE")
4541 || starts_with_ci(core, "ROLLBACK"))
4542 {
4543 return false;
4544 }
4545 is_write
4546 || multi_stmt
4547 || Self::is_edge_copy_write_sql(core)
4548 || Self::is_edge_procedural_sql(core)
4549 || Self::is_edge_txn_end_sql(core)
4550 }
4551
4552 #[cfg(feature = "edge-proxy")]
4556 fn is_edge_copy_write_sql(sql: &str) -> bool {
4557 crate::protocol::starts_with_ci(sql.trim_start(), "COPY")
4558 && Self::contains_word_ci(sql, "from")
4559 }
4560
4561 #[cfg(feature = "edge-proxy")]
4570 fn edge_meta_prunable<'a>(closes: &'a [String], defines: &[String]) -> Vec<&'a str> {
4571 closes
4572 .iter()
4573 .filter(|n| !defines.contains(n))
4574 .map(String::as_str)
4575 .collect()
4576 }
4577
4578 #[cfg(feature = "edge-proxy")]
4585 fn is_edge_procedural_sql(sql: &str) -> bool {
4586 use crate::protocol::starts_with_ci;
4587 let t = sql.trim_start();
4588 starts_with_ci(t, "EXECUTE") || starts_with_ci(t, "CALL") || starts_with_ci(t, "DO")
4589 }
4590
4591 #[cfg(feature = "edge-proxy")]
4600 fn is_edge_txn_end_sql(sql: &str) -> bool {
4601 use crate::protocol::starts_with_ci;
4602 let t = sql.trim_start();
4603 starts_with_ci(t, "COMMIT") || starts_with_ci(t, "END")
4604 }
4605
4606 #[cfg(feature = "edge-proxy")]
4614 fn is_edge_dml_sql(sql: &str) -> bool {
4615 use crate::protocol::starts_with_ci;
4616 let t = sql.trim_start();
4617 if starts_with_ci(t, "INSERT")
4618 || starts_with_ci(t, "UPDATE")
4619 || starts_with_ci(t, "DELETE")
4620 || starts_with_ci(t, "MERGE")
4621 || starts_with_ci(t, "CREATE")
4622 || starts_with_ci(t, "DROP")
4623 || starts_with_ci(t, "ALTER")
4624 || starts_with_ci(t, "TRUNCATE")
4625 || starts_with_ci(t, "GRANT")
4626 || starts_with_ci(t, "REVOKE")
4627 {
4628 return true;
4629 }
4630 if starts_with_ci(t, "COPY") {
4631 return Self::contains_word_ci(t, "from");
4632 }
4633 if starts_with_ci(t, "WITH") {
4634 return Self::contains_word_ci(t, "insert")
4635 || Self::contains_word_ci(t, "update")
4636 || Self::contains_word_ci(t, "delete")
4637 || Self::contains_word_ci(t, "merge");
4638 }
4639 false
4640 }
4641
4642 #[cfg(feature = "edge-proxy")]
4648 fn edge_extended_batch_tables(
4649 refs: &[String],
4650 bound_unnamed: bool,
4651 named_meta: &HashMap<String, Option<Vec<String>>>,
4652 unnamed_meta: &Option<Vec<String>>,
4653 ) -> Option<Vec<String>> {
4654 let mut any_dml = false;
4655 let mut wipe_all = false;
4656 let mut union: Vec<String> = Vec::new();
4657 {
4658 let mut consider = |meta: &Option<Vec<String>>| {
4659 if let Some(tables) = meta {
4660 any_dml = true;
4661 if tables.is_empty() {
4662 wipe_all = true;
4663 } else {
4664 for t in tables {
4665 if !union.contains(t) {
4666 union.push(t.clone());
4667 }
4668 }
4669 }
4670 }
4671 };
4672 for name in refs {
4673 if let Some(meta) = named_meta.get(name) {
4674 consider(meta);
4675 }
4676 }
4677 if bound_unnamed {
4678 consider(unnamed_meta);
4679 }
4680 }
4681 if !any_dml {
4682 None
4683 } else if wipe_all {
4684 Some(Vec::new())
4685 } else {
4686 Some(union)
4687 }
4688 }
4689
4690 #[cfg(feature = "edge-proxy")]
4695 async fn edge_invalidate_write(
4696 state: &Arc<ServerState>,
4697 config: &ProxyConfig,
4698 tables: Vec<String>,
4699 ) {
4700 if config.edge.role == crate::edge::EdgeRole::Home {
4701 let version = state.edge_cache.next_version();
4702 let dropped = state.edge_cache.invalidate(version, &tables);
4703 let (notified, pruned) = state
4704 .edge_registry
4705 .broadcast(crate::edge::InvalidationEvent {
4706 up_to_version: version,
4707 tables,
4708 committed_at: chrono::Utc::now().to_rfc3339(),
4709 epoch: state.edge_cache.epoch(),
4710 })
4711 .await;
4712 tracing::debug!(
4713 target: "helios::edge",
4714 version,
4715 dropped,
4716 notified,
4717 pruned,
4718 "write invalidation broadcast to edges"
4719 );
4720 } else {
4721 let version = state.edge_cache.observed_home_version();
4727 let dropped = state.edge_cache.invalidate(version, &tables);
4728 tracing::debug!(
4729 target: "helios::edge",
4730 version,
4731 dropped,
4732 "write invalidated local edge cache (edge role — home broadcasts)"
4733 );
4734 }
4735 }
4736
4737 #[cfg(any(feature = "pool-modes", feature = "edge-proxy"))]
4761 fn stmt_leaves_session_state(sql: &str) -> bool {
4762 use crate::protocol::{contains_ci, starts_with_ci};
4763 let t = sql.trim();
4764 if t.is_empty() {
4765 return false;
4766 }
4767 let core = t.strip_suffix(';').unwrap_or(t).trim_end();
4772 if core.contains(';') {
4773 return true;
4774 }
4775 let neutral_lead = starts_with_ci(core, "SELECT")
4780 || starts_with_ci(core, "INSERT")
4781 || starts_with_ci(core, "UPDATE")
4782 || starts_with_ci(core, "DELETE")
4783 || starts_with_ci(core, "WITH")
4784 || starts_with_ci(core, "VALUES")
4785 || starts_with_ci(core, "TABLE")
4786 || starts_with_ci(core, "SHOW")
4787 || starts_with_ci(core, "EXPLAIN")
4788 || starts_with_ci(core, "FETCH")
4789 || starts_with_ci(core, "BEGIN")
4790 || starts_with_ci(core, "START")
4791 || starts_with_ci(core, "COMMIT")
4792 || starts_with_ci(core, "END")
4793 || starts_with_ci(core, "ROLLBACK")
4794 || starts_with_ci(core, "ABORT")
4795 || starts_with_ci(core, "SAVEPOINT")
4796 || starts_with_ci(core, "RELEASE");
4797 if !neutral_lead {
4798 return true;
4799 }
4800 if (starts_with_ci(core, "SELECT") || starts_with_ci(core, "WITH"))
4809 && Self::contains_word_ci(core, "into")
4810 {
4811 return true;
4812 }
4813 const DIRTY_TOKENS: [&str; 4] = ["set_config", "advisory", "nextval", "setval"];
4814 DIRTY_TOKENS.iter().any(|tok| contains_ci(core, tok))
4815 }
4816
4817 #[cfg(any(
4823 feature = "pool-modes",
4824 feature = "edge-proxy",
4825 feature = "query-cache"
4826 ))]
4827 fn contains_word_ci(haystack: &str, word: &str) -> bool {
4828 let hb = haystack.as_bytes();
4829 let wb = word.as_bytes();
4830 if wb.is_empty() || hb.len() < wb.len() {
4831 return false;
4832 }
4833 let is_ident = |c: u8| c.is_ascii_alphanumeric() || c == b'_';
4834 let mut i = 0;
4835 while i + wb.len() <= hb.len() {
4836 if hb[i..i + wb.len()].eq_ignore_ascii_case(wb) {
4837 let before_ok = i == 0 || !is_ident(hb[i - 1]);
4838 let after = i + wb.len();
4839 let after_ok = after == hb.len() || !is_ident(hb[after]);
4840 if before_ok && after_ok {
4841 return true;
4842 }
4843 }
4844 i += 1;
4845 }
4846 false
4847 }
4848
4849 #[cfg(feature = "rate-limiting")]
4852 async fn rate_limit_key(
4853 session: &Arc<ClientSession>,
4854 config: &ProxyConfig,
4855 ) -> crate::rate_limit::LimiterKey {
4856 use crate::config::RateLimitKeyBy;
4857 use crate::rate_limit::LimiterKey;
4858 match config.rate_limit.key_by {
4859 RateLimitKeyBy::Global => LimiterKey::Global,
4860 RateLimitKeyBy::ClientIp => LimiterKey::ClientIp(session.client_addr.ip()),
4861 RateLimitKeyBy::Database => {
4862 let vars = session.variables.read().await;
4863 LimiterKey::Database(vars.get("database").cloned().unwrap_or_default())
4864 }
4865 RateLimitKeyBy::User => {
4866 let vars = session.variables.read().await;
4867 LimiterKey::User(vars.get("user").cloned().unwrap_or_default())
4868 }
4869 }
4870 }
4871
4872 #[cfg(feature = "rate-limiting")]
4878 async fn rate_limit_check(
4879 session: &Arc<ClientSession>,
4880 state: &Arc<ServerState>,
4881 config: &ProxyConfig,
4882 ) -> Option<Vec<u8>> {
4883 use crate::rate_limit::RateLimitResult;
4884 let limiter = state.rate_limiter.as_ref()?;
4885 let key = Self::rate_limit_key(session, config).await;
4886 match limiter.check(&key, 1) {
4887 RateLimitResult::Allowed => None,
4888 RateLimitResult::Warned(msg) => {
4889 tracing::warn!(key = %key, reason = %msg, "rate limit warning");
4890 None
4891 }
4892 RateLimitResult::Throttled(d) | RateLimitResult::Queued(d) => {
4893 tokio::time::sleep(d.min(Duration::from_secs(5))).await;
4896 None
4897 }
4898 RateLimitResult::Denied(exc) => {
4899 tracing::info!(key = %key, "rate limit exceeded");
4900 let msg = format!(
4901 "rate limit exceeded: {} (retry after {}ms)",
4902 exc.message,
4903 exc.retry_after.as_millis()
4904 );
4905 Some(Self::create_error_response("53400", &msg))
4906 }
4907 }
4908 }
4909
4910 fn is_backend_fault(err: &str) -> bool {
4936 !err.contains("Client") && !err.contains("Backend read timeout")
4937 }
4938
4939 fn note_backend_failure(state: &Arc<ServerState>, addr: &str, err: &str) {
4943 if !Self::is_backend_fault(err) {
4944 return;
4945 }
4946 let _writers = state.health_write.lock();
4955 let snapshot = state.health.load_full();
4956 if snapshot.get(addr).map(|h| h.healthy).unwrap_or(false) {
4959 let mut next = (*snapshot).clone();
4960 if let Some(nh) = next.get_mut(addr) {
4961 nh.healthy = false;
4962 nh.failure_count = nh.failure_count.saturating_add(1);
4963 nh.last_error = Some(format!("in-band failure: {}", err));
4964 tracing::warn!(
4965 node = %addr,
4966 error = %err,
4967 "in-band failure — node marked unhealthy for fast failover"
4968 );
4969 }
4970 state.health.store(Arc::new(next));
4971 }
4972 }
4973
4974 fn record_backend_failure(state: &Arc<ServerState>, node: &str, err: &str) {
4980 Self::note_backend_failure(state, node, err);
4981 #[cfg(feature = "circuit-breaker")]
4982 if Self::is_backend_fault(err) {
4983 Self::circuit_record(state, node, false, err);
4984 }
4985 }
4986
4987 #[cfg(feature = "circuit-breaker")]
4990 fn circuit_is_open(state: &Arc<ServerState>, node: &str) -> bool {
4991 state
4992 .circuit_breaker
4993 .as_ref()
4994 .map(|cb| {
4995 cb.get_breaker(node).get_state() == crate::circuit_breaker::CircuitState::Open
4996 })
4997 .unwrap_or(false)
4998 }
4999
5000 #[cfg(feature = "circuit-breaker")]
5002 fn circuit_record(state: &Arc<ServerState>, node: &str, success: bool, err: &str) {
5003 if let Some(cb) = state.circuit_breaker.as_ref() {
5004 let breaker = cb.get_breaker(node);
5005 if success {
5006 breaker.record_success();
5007 } else {
5008 breaker.record_failure(err);
5009 }
5010 }
5011 }
5012
5013 #[cfg(feature = "circuit-breaker")]
5017 fn circuit_fast_fail(state: &Arc<ServerState>, node: &str) -> Option<Vec<u8>> {
5018 if Self::circuit_is_open(state, node) {
5019 tracing::info!(node = %node, "circuit open — fast-failing");
5020 Some(Self::create_error_response(
5021 "08006",
5022 &format!("circuit open for node {node}: backend temporarily unavailable"),
5023 ))
5024 } else {
5025 None
5026 }
5027 }
5028
5029 #[cfg(feature = "lag-routing")]
5032 fn ryw_pins_primary(last_write: Option<std::time::Instant>, window_ms: u64) -> bool {
5033 window_ms > 0
5034 && last_write
5035 .map(|t| t.elapsed() < Duration::from_millis(window_ms))
5036 .unwrap_or(false)
5037 }
5038
5039 #[cfg(feature = "lag-routing")]
5043 fn lag_excludes_standby(lag_bytes: Option<u64>, max_lag_bytes: u64) -> bool {
5044 max_lag_bytes > 0 && lag_bytes.map(|l| l > max_lag_bytes).unwrap_or(false)
5045 }
5046
5047 #[cfg(any(feature = "query-cache", feature = "edge-proxy"))]
5052 fn is_cacheable_read_sql(sql: &str) -> bool {
5053 use crate::protocol::{contains_ci, starts_with_ci};
5054 let t = sql.trim_start();
5055 if !starts_with_ci(t, "SELECT") {
5056 return false;
5057 }
5058 let core = t.trim_end();
5064 let core = core.strip_suffix(';').map(str::trim_end).unwrap_or(core);
5065 if core.contains(';') {
5066 return false;
5067 }
5068 if Self::contains_word_ci(core, "into") {
5074 return false;
5075 }
5076 if contains_ci(t, "FOR UPDATE") || contains_ci(t, "FOR SHARE") {
5077 return false;
5078 }
5079 const VOLATILE: [&str; 11] = [
5083 "now(",
5084 "current_timestamp",
5085 "current_date",
5086 "current_time",
5087 "clock_timestamp",
5088 "statement_timestamp",
5089 "random(",
5090 "nextval(",
5091 "uuid_generate",
5092 "gen_random_uuid",
5093 "set_config(",
5094 ];
5095 !VOLATILE.iter().any(|v| contains_ci(t, v))
5096 }
5097
5098 #[cfg(feature = "query-cache")]
5102 async fn cacheable_read_ctx(
5103 session: &Arc<ClientSession>,
5104 sql: &str,
5105 ) -> Option<crate::cache::CacheContext> {
5106 if !Self::is_cacheable_read_sql(sql) {
5107 return None;
5108 }
5109 if session
5111 .in_transaction
5112 .load(std::sync::atomic::Ordering::Relaxed)
5113 {
5114 return None;
5115 }
5116 let (user, database) = {
5117 let vars = session.variables.read().await;
5118 (
5119 vars.get("user").cloned(),
5120 vars.get("database")
5121 .cloned()
5122 .unwrap_or_else(|| "default".to_string()),
5123 )
5124 };
5125 Some(crate::cache::CacheContext {
5126 database,
5127 user,
5128 branch: None,
5129 connection_id: Some(session.id.as_u64_pair().0),
5130 })
5131 }
5132
5133 #[cfg(feature = "multi-tenancy")]
5137 async fn tenant_request_ctx(
5138 session: &Arc<ClientSession>,
5139 ) -> crate::multi_tenancy::RequestContext {
5140 let vars = session.variables.read().await;
5141 crate::multi_tenancy::RequestContext {
5142 headers: vars.clone(),
5143 username: vars.get("user").cloned(),
5144 database: vars.get("database").cloned(),
5145 auth_token: None,
5146 sql_context: HashMap::new(),
5147 client_ip: Some(session.client_addr.ip().to_string()),
5148 connection_id: Some(session.id.as_u64_pair().0),
5149 }
5150 }
5151
5152 #[cfg(feature = "ha-tr")]
5157 async fn journal_write(state: &Arc<ServerState>, session: &Arc<ClientSession>, sql: &str) {
5158 let tx_id = uuid::Uuid::new_v4();
5159 let j = &state.transaction_journal;
5160 if j.begin_transaction(tx_id, session.id, crate::NodeId::new(), 0)
5161 .await
5162 .is_ok()
5163 {
5164 let _ = j
5165 .log_statement(tx_id, sql.to_string(), Vec::new(), None, None, 0)
5166 .await;
5167 }
5168 }
5169
5170 #[cfg(feature = "query-analytics")]
5173 async fn record_analytics(
5174 state: &Arc<ServerState>,
5175 session: &Arc<ClientSession>,
5176 sql: &str,
5177 node: &str,
5178 duration: Duration,
5179 error: Option<String>,
5180 ) {
5181 let Some(analytics) = state.analytics.as_ref() else {
5182 return;
5183 };
5184 let (user, database) = {
5185 let vars = session.variables.read().await;
5186 (
5187 vars.get("user").cloned().unwrap_or_default(),
5188 vars.get("database").cloned().unwrap_or_default(),
5189 )
5190 };
5191 let mut exec = crate::analytics::QueryExecution::new(sql, duration);
5192 exec.user = user;
5193 exec.database = database;
5194 exec.client_ip = session.client_addr.ip().to_string();
5195 exec.node = node.to_string();
5196 exec.session_id = Some(session.id.to_string());
5197 exec.error = error;
5198 analytics.record(exec);
5199 }
5200
5201 async fn select_primary_with_timeout(
5203 session: &Arc<ClientSession>,
5204 state: &Arc<ServerState>,
5205 config: &ProxyConfig,
5206 ) -> Result<String> {
5207 let timeout = config.write_timeout();
5208 let start = std::time::Instant::now();
5209 let check_interval = Duration::from_millis(100);
5212
5213 loop {
5214 let health = state.health.load_full();
5216 let primary = config
5217 .nodes
5218 .iter()
5219 .find(|n| n.role == NodeRole::Primary && n.enabled);
5220
5221 if let Some(primary_node) = primary {
5222 if let Some(node_health) = health.get(&primary_node.address()) {
5223 if node_health.healthy {
5224 let mut current = session.current_node.write().await;
5226 *current = Some(primary_node.address());
5227 return Ok(primary_node.address());
5228 }
5229 }
5230 }
5231 drop(health);
5232
5233 if start.elapsed() >= timeout {
5235 state.metrics.failovers.fetch_add(1, Ordering::Relaxed);
5236 return Err(ProxyError::NoHealthyNodes);
5237 }
5238
5239 tracing::warn!(
5240 "Primary unavailable, waiting for failover... ({:.1}s elapsed, {:.1}s timeout)",
5241 start.elapsed().as_secs_f64(),
5242 timeout.as_secs_f64()
5243 );
5244
5245 tokio::time::sleep(check_interval).await;
5247 }
5248 }
5249
5250 async fn select_read_node(
5252 session: &Arc<ClientSession>,
5253 state: &Arc<ServerState>,
5254 config: &ProxyConfig,
5255 ) -> Result<String> {
5256 if session
5258 .in_transaction
5259 .load(std::sync::atomic::Ordering::Relaxed)
5260 {
5261 if let Some(node) = session.current_node.read().await.clone() {
5262 return Ok(node);
5263 }
5264 }
5265
5266 let health = state.health.load_full();
5268 let healthy_standbys: Vec<&NodeConfig> = config
5269 .nodes
5270 .iter()
5271 .filter(|n| {
5272 let base = n.enabled
5273 && (n.role == NodeRole::Standby || n.role == NodeRole::ReadReplica)
5274 && health.get(&n.address()).map(|h| h.healthy).unwrap_or(false);
5275 #[cfg(feature = "circuit-breaker")]
5277 let base = base && !Self::circuit_is_open(state, &n.address());
5278 #[cfg(feature = "lag-routing")]
5280 let base = base
5281 && !Self::lag_excludes_standby(
5282 health
5283 .get(&n.address())
5284 .and_then(|h| h.replication_lag_bytes),
5285 config.lag_routing.max_lag_bytes,
5286 );
5287 base
5288 })
5289 .collect();
5290
5291 if !healthy_standbys.is_empty() {
5292 let ticket = state.lb_state.rr_counter.fetch_add(1, Ordering::Relaxed);
5294 let index = ticket as usize % healthy_standbys.len();
5295 let node_addr = healthy_standbys[index].address();
5296
5297 let mut current = session.current_node.write().await;
5298 *current = Some(node_addr.clone());
5299 return Ok(node_addr);
5300 }
5301
5302 Self::select_node(session, state, config).await
5304 }
5305
5306 async fn complete_backend_auth(backend: &mut TcpStream) -> Result<()> {
5309 let mut buffer = BytesMut::with_capacity(4096);
5310 let timeout = Duration::from_secs(10);
5311 let start = std::time::Instant::now();
5312
5313 loop {
5314 if start.elapsed() > timeout {
5315 return Err(ProxyError::Auth(
5316 "Backend authentication timeout".to_string(),
5317 ));
5318 }
5319
5320 buffer.reserve(4096);
5321 let n = tokio::time::timeout(Duration::from_secs(5), backend.read_buf(&mut buffer))
5322 .await
5323 .map_err(|_| ProxyError::Auth("Read timeout during backend auth".to_string()))?
5324 .map_err(|e| ProxyError::Network(format!("Backend auth read error: {}", e)))?;
5325
5326 if n == 0 {
5327 return Err(ProxyError::Connection(
5328 "Backend closed during auth".to_string(),
5329 ));
5330 }
5331
5332 loop {
5339 if buffer.len() < 5 {
5340 break;
5341 }
5342 let len = u32::from_be_bytes([buffer[1], buffer[2], buffer[3], buffer[4]]) as usize;
5343 if len < 4 || buffer.len() < len + 1 {
5344 break;
5345 }
5346 let tag = buffer[0];
5347 let frame = buffer.split_to(len + 1);
5348 match tag {
5349 b'Z' => return Ok(()),
5351 b'E' => {
5353 let payload = BytesMut::from(&frame[5..]);
5354 let err = ErrorResponse::parse(payload)
5355 .map(|e| e.message().unwrap_or("Unknown error").to_string())
5356 .unwrap_or_else(|_| "authentication failed".to_string());
5357 return Err(ProxyError::Auth(err));
5358 }
5359 _ => {}
5360 }
5361 }
5362 }
5363 }
5364
5365 fn create_error_response(code: &str, message: &str) -> Vec<u8> {
5367 let mut fields = HashMap::new();
5368 fields.insert('S', "ERROR".to_string());
5369 fields.insert('V', "ERROR".to_string());
5370 fields.insert('C', code.to_string());
5371 fields.insert('M', message.to_string());
5372
5373 let err = ErrorResponse { fields };
5374 err.encode().encode().to_vec()
5375 }
5376
5377 fn create_ready_for_query(status: u8) -> Vec<u8> {
5380 let mut payload = BytesMut::with_capacity(1);
5381 payload.put_u8(status);
5382 Message::new(MessageType::ReadyForQuery, payload)
5383 .encode()
5384 .to_vec()
5385 }
5386
5387 #[cfg(feature = "wasm-plugins")]
5425 fn synthesise_cached_response(bytes: &[u8]) -> Result<Vec<u8>> {
5426 use serde::Deserialize;
5427
5428 #[derive(Deserialize)]
5429 struct CachedPayload {
5430 columns: Vec<ColumnDef>,
5431 rows: Vec<Vec<Option<String>>>,
5432 }
5433
5434 #[derive(Deserialize)]
5435 struct ColumnDef {
5436 name: String,
5437 #[serde(default = "default_text_oid")]
5438 oid: u32,
5439 }
5440
5441 fn default_text_oid() -> u32 {
5442 25 }
5444
5445 let payload: CachedPayload = serde_json::from_slice(bytes)
5446 .map_err(|e| ProxyError::Protocol(format!("invalid cached payload JSON: {}", e)))?;
5447
5448 if payload.columns.is_empty() {
5449 return Err(ProxyError::Protocol(
5450 "cached payload must declare at least one column".to_string(),
5451 ));
5452 }
5453
5454 let mut reply = Vec::new();
5455
5456 let mut rd = BytesMut::new();
5458 rd.put_u16(payload.columns.len() as u16);
5459 for col in &payload.columns {
5460 rd.extend_from_slice(col.name.as_bytes());
5461 rd.put_u8(0); rd.put_i32(0); rd.put_i16(0); rd.put_u32(col.oid);
5465 rd.put_i16(-1); rd.put_i32(-1); rd.put_i16(0); }
5469 reply.extend_from_slice(&Message::new(MessageType::RowDescription, rd).encode());
5470
5471 let column_count = payload.columns.len();
5473 for row in &payload.rows {
5474 if row.len() != column_count {
5475 return Err(ProxyError::Protocol(format!(
5476 "cached row has {} values but {} columns are declared",
5477 row.len(),
5478 column_count
5479 )));
5480 }
5481 let mut dr = BytesMut::new();
5482 dr.put_u16(row.len() as u16);
5483 for value in row {
5484 match value {
5485 Some(s) => {
5486 dr.put_i32(s.len() as i32);
5487 dr.extend_from_slice(s.as_bytes());
5488 }
5489 None => {
5490 dr.put_i32(-1); }
5492 }
5493 }
5494 reply.extend_from_slice(&Message::new(MessageType::DataRow, dr).encode());
5495 }
5496
5497 let tag = format!("SELECT {}", payload.rows.len());
5499 let mut cc = BytesMut::new();
5500 cc.extend_from_slice(tag.as_bytes());
5501 cc.put_u8(0);
5502 reply.extend_from_slice(&Message::new(MessageType::CommandComplete, cc).encode());
5503
5504 reply.extend_from_slice(&Self::create_ready_for_query(b'I'));
5506
5507 Ok(reply)
5508 }
5509
5510 fn apply_pre_query_hook(
5520 msg: Message,
5521 state: &Arc<ServerState>,
5522 session: &Arc<ClientSession>,
5523 ) -> (Message, PreQueryAction) {
5524 #[cfg(feature = "wasm-plugins")]
5525 {
5526 let pm = match state.plugin_manager.as_ref() {
5527 Some(pm) => pm,
5528 None => return (msg, PreQueryAction::Forward),
5529 };
5530
5531 if msg.msg_type != MessageType::Query {
5532 return (msg, PreQueryAction::Forward);
5533 }
5534
5535 if !pm.has_hook(HookType::PreQuery) {
5538 return (msg, PreQueryAction::Forward);
5539 }
5540
5541 let query_msg = match QueryMessage::parse(msg.payload.clone()) {
5542 Ok(q) => q,
5543 Err(_) => return (msg, PreQueryAction::Forward),
5544 };
5545
5546 let ctx = Self::build_query_context(&query_msg.query, session);
5547
5548 match pm.execute_pre_query(&ctx) {
5549 PreQueryResult::Continue => (msg, PreQueryAction::Forward),
5550 PreQueryResult::Block(reason) => (msg, PreQueryAction::Block(reason)),
5551 PreQueryResult::Rewrite(new_sql) => {
5552 let rewritten = QueryMessage { query: new_sql }.encode();
5553 (rewritten, PreQueryAction::Forward)
5554 }
5555 PreQueryResult::Cached(bytes) => (msg, PreQueryAction::Cached(bytes)),
5556 }
5557 }
5558 #[cfg(not(feature = "wasm-plugins"))]
5559 {
5560 let _ = (state, session);
5561 (msg, PreQueryAction::Forward)
5562 }
5563 }
5564
5565 #[cfg(feature = "anomaly-detection")]
5571 fn record_anomaly_observation(
5572 msg: &Message,
5573 state: &Arc<ServerState>,
5574 session: &Arc<ClientSession>,
5575 ) {
5576 if msg.msg_type != MessageType::Query {
5577 return;
5578 }
5579 if let Some(query) = crate::protocol::query_text(&msg.payload) {
5582 Self::record_anomaly_sql(query, state, session);
5583 }
5584 }
5585
5586 #[cfg(feature = "anomaly-detection")]
5590 fn record_anomaly_sql(query: &str, state: &Arc<ServerState>, session: &Arc<ClientSession>) {
5591 let tenant = match session.variables.try_read() {
5598 Ok(vars) => vars
5599 .get("tenant_id")
5600 .or_else(|| vars.get("user"))
5601 .cloned()
5602 .unwrap_or_else(|| session.client_addr.ip().to_string()),
5603 Err(_) => session.client_addr.ip().to_string(),
5604 };
5605 let fingerprint = anomaly_fingerprint(query);
5606 let obs = crate::anomaly::QueryObservation {
5607 tenant,
5608 fingerprint,
5609 sql: query.to_string(),
5610 timestamp: std::time::Instant::now(),
5611 };
5612 for ev in state.anomaly_detector.record_query(&obs) {
5613 tracing::warn!(anomaly = ?ev, "anomaly detected");
5614 }
5615 }
5616
5617 async fn send_block_response(
5621 stream: &mut ClientStream,
5622 reason: &str,
5623 state: &Arc<ServerState>,
5624 ) -> Result<()> {
5625 let err =
5626 Self::create_error_response("42000", &format!("Query blocked by plugin: {}", reason));
5627 stream
5628 .write_all(&err)
5629 .await
5630 .map_err(|e| ProxyError::Network(format!("Write error: {}", e)))?;
5631 let rfq = Self::create_ready_for_query(b'I');
5632 stream
5633 .write_all(&rfq)
5634 .await
5635 .map_err(|e| ProxyError::Network(format!("Write error: {}", e)))?;
5636 state
5637 .metrics
5638 .bytes_sent
5639 .fetch_add((err.len() + rfq.len()) as u64, Ordering::Relaxed);
5640 Ok(())
5641 }
5642
5643 #[cfg(feature = "wasm-plugins")]
5649 fn build_query_context(query: &str, session: &Arc<ClientSession>) -> QueryContext {
5650 let is_read_only = !Self::is_write_query(query);
5651 let hook_context = HookContext {
5652 client_id: Some(session.id.to_string()),
5653 ..HookContext::default()
5654 };
5655 QueryContext {
5656 query: query.to_string(),
5657 normalized: query.to_string(),
5658 tables: Vec::new(),
5659 is_read_only,
5660 hook_context,
5661 }
5662 }
5663
5664 async fn apply_authenticate_hook(
5685 _params: &HashMap<String, String>,
5686 _session: &Arc<ClientSession>,
5687 _state: &Arc<ServerState>,
5688 ) -> Result<()> {
5689 #[cfg(feature = "wasm-plugins")]
5690 {
5691 let pm = match _state.plugin_manager.as_ref() {
5692 Some(pm) => pm,
5693 None => return Ok(()),
5694 };
5695
5696 let request = PluginAuthRequest {
5697 headers: HashMap::new(),
5698 username: _params.get("user").cloned(),
5699 password: None,
5700 client_ip: _session.client_addr.ip().to_string(),
5701 database: _params.get("database").cloned(),
5702 };
5703
5704 match pm.execute_authenticate(&request) {
5705 AuthResult::Defer => Ok(()),
5706 AuthResult::Success(identity) => {
5707 tracing::debug!(
5708 user = %identity.username,
5709 roles = ?identity.roles,
5710 "plugin authenticated user"
5711 );
5712 *_session.plugin_identity.write().await = Some(identity);
5713 Ok(())
5714 }
5715 AuthResult::Denied(reason) => {
5716 tracing::info!(
5717 reason = %reason,
5718 client = %_session.client_addr,
5719 user = ?_params.get("user"),
5720 "plugin denied authentication"
5721 );
5722 Err(ProxyError::Auth(format!(
5723 "authentication denied by plugin: {}",
5724 reason
5725 )))
5726 }
5727 }
5728 }
5729 #[cfg(not(feature = "wasm-plugins"))]
5730 {
5731 Ok(())
5732 }
5733 }
5734
5735 fn apply_route_hook(
5738 msg: &Message,
5739 state: &Arc<ServerState>,
5740 session: &Arc<ClientSession>,
5741 ) -> RouteOverride {
5742 #[cfg(feature = "wasm-plugins")]
5743 {
5744 let pm = match state.plugin_manager.as_ref() {
5745 Some(pm) => pm,
5746 None => return RouteOverride::None,
5747 };
5748 if msg.msg_type != MessageType::Query {
5749 return RouteOverride::None;
5750 }
5751 if !pm.has_hook(HookType::Route) {
5754 return RouteOverride::None;
5755 }
5756 let query_msg = match QueryMessage::parse(msg.payload.clone()) {
5757 Ok(q) => q,
5758 Err(_) => return RouteOverride::None,
5759 };
5760 let ctx = Self::build_query_context(&query_msg.query, session);
5761 match pm.execute_route(&ctx) {
5762 RouteResult::Default => RouteOverride::None,
5763 RouteResult::Primary => RouteOverride::Primary,
5764 RouteResult::Standby => RouteOverride::Standby,
5765 RouteResult::Node(name) => RouteOverride::Node(name),
5766 RouteResult::Block(reason) => RouteOverride::Block(reason),
5767 RouteResult::Branch(name) => {
5768 tracing::warn!(
5769 branch = %name,
5770 "Route hook returned Branch but branch routing is not yet wired — using default"
5771 );
5772 RouteOverride::None
5773 }
5774 }
5775 }
5776 #[cfg(not(feature = "wasm-plugins"))]
5777 {
5778 let _ = (msg, state, session);
5779 RouteOverride::None
5780 }
5781 }
5782
5783 #[cfg(feature = "routing-hints")]
5789 fn hint_to_override(hints: &crate::routing::ParsedHints) -> RouteOverride {
5790 use crate::routing::{ConsistencyLevel, RouteTarget};
5791 if let Some(node) = &hints.node {
5792 return RouteOverride::Node(node.clone());
5793 }
5794 if let Some(route) = hints.route {
5795 return match route {
5796 RouteTarget::Primary => RouteOverride::Primary,
5797 RouteTarget::Standby
5798 | RouteTarget::Sync
5799 | RouteTarget::SemiSync
5800 | RouteTarget::Async
5801 | RouteTarget::Local => RouteOverride::Standby,
5802 RouteTarget::Any | RouteTarget::Vector => RouteOverride::None,
5803 };
5804 }
5805 if hints.consistency == Some(ConsistencyLevel::Strong) {
5806 return RouteOverride::Primary;
5807 }
5808 RouteOverride::None
5809 }
5810
5811 #[cfg(feature = "routing-hints")]
5819 fn resolve_simple_route(
5820 msg: &Message,
5821 plugin_override: RouteOverride,
5822 default_is_write: bool,
5823 state: &Arc<ServerState>,
5824 ) -> (RouteOverride, bool, Option<Message>) {
5825 let parser = match state.hint_parser.as_ref() {
5826 Some(p) => p,
5827 None => return (plugin_override, default_is_write, None),
5828 };
5829 let sql = match crate::protocol::query_text(&msg.payload) {
5830 Some(s) => s,
5831 None => return (plugin_override, default_is_write, None),
5832 };
5833 let hints = parser.parse(sql);
5834 if hints.is_empty() {
5835 return (plugin_override, default_is_write, None);
5836 }
5837 let stripped = parser.strip(sql);
5838 let is_write = Self::is_write_query(&stripped);
5839 let effective = match Self::hint_to_override(&hints) {
5840 RouteOverride::None => plugin_override,
5841 hint_override => hint_override,
5842 };
5843 let forward = if parser.strip_hints {
5844 Some(crate::protocol::QueryMessage { query: stripped }.encode())
5845 } else {
5846 None
5847 };
5848 (effective, is_write, forward)
5849 }
5850
5851 #[cfg(feature = "routing-hints")]
5858 fn extended_hint_route(state: &Arc<ServerState>, sql: &str) -> Option<(bool, Option<String>)> {
5859 let parser = state.hint_parser.as_ref()?;
5860 let hints = parser.parse(sql);
5861 if hints.is_empty() {
5862 return None;
5863 }
5864 let stripped = parser.strip(sql);
5865 let is_write = Self::is_write_query(&stripped);
5866 match Self::hint_to_override(&hints) {
5867 RouteOverride::Primary => Some((true, None)),
5868 RouteOverride::Standby => Some((false, None)),
5869 RouteOverride::Node(n) => Some((is_write, Some(n))),
5870 _ => Some((is_write, None)),
5871 }
5872 }
5873
5874 #[cfg(feature = "wasm-plugins")]
5878 fn fire_post_query_hook(
5879 msg: &Message,
5880 session: &Arc<ClientSession>,
5881 state: &Arc<ServerState>,
5882 result: &Result<(Option<String>, u64)>,
5883 elapsed: Duration,
5884 ) {
5885 let pm = match state.plugin_manager.as_ref() {
5886 Some(pm) => pm,
5887 None => return,
5888 };
5889 if msg.msg_type != MessageType::Query {
5890 return;
5891 }
5892 if !pm.has_hook(HookType::PostQuery) {
5895 return;
5896 }
5897 let query_msg = match QueryMessage::parse(msg.payload.clone()) {
5898 Ok(q) => q,
5899 Err(_) => return,
5900 };
5901 let ctx = Self::build_query_context(&query_msg.query, session);
5902 let outcome = match result {
5903 Ok((node, bytes)) => PostQueryOutcome {
5904 success: true,
5905 target_node: node.clone(),
5906 elapsed_us: elapsed.as_micros() as u64,
5907 response_bytes: *bytes,
5908 error: None,
5909 },
5910 Err(e) => PostQueryOutcome {
5911 success: false,
5912 target_node: None,
5913 elapsed_us: elapsed.as_micros() as u64,
5914 response_bytes: 0,
5915 error: Some(e.to_string()),
5916 },
5917 };
5918 pm.execute_post_query(&ctx, &outcome);
5919 }
5920
5921 async fn select_node(
5925 session: &Arc<ClientSession>,
5926 state: &Arc<ServerState>,
5927 config: &ProxyConfig,
5928 ) -> Result<String> {
5929 if session
5931 .in_transaction
5932 .load(std::sync::atomic::Ordering::Relaxed)
5933 {
5934 if let Some(node) = session.current_node.read().await.clone() {
5935 return Ok(node);
5936 }
5937 }
5938
5939 let health = state.health.load_full();
5941 let healthy_nodes: Vec<&NodeConfig> = config
5942 .nodes
5943 .iter()
5944 .filter(|n| n.enabled && health.get(&n.address()).map(|h| h.healthy).unwrap_or(false))
5945 .collect();
5946
5947 if healthy_nodes.is_empty() {
5948 return Err(ProxyError::NoHealthyNodes);
5949 }
5950
5951 if let Some(primary) = healthy_nodes.iter().find(|n| n.role == NodeRole::Primary) {
5953 let node_addr = primary.address();
5954 let mut current = session.current_node.write().await;
5955 *current = Some(node_addr.clone());
5956 return Ok(node_addr);
5957 }
5958
5959 if let Some(standby) = healthy_nodes.iter().find(|n| n.role == NodeRole::Standby) {
5962 tracing::warn!("Primary unavailable, connecting to standby for initial session");
5963 let node_addr = standby.address();
5964 let mut current = session.current_node.write().await;
5965 *current = Some(node_addr.clone());
5966 return Ok(node_addr);
5967 }
5968
5969 Err(ProxyError::NoHealthyNodes)
5971 }
5972
5973 fn spawn_health_checker(&self) -> tokio::task::JoinHandle<()> {
5975 let state = self.state.clone();
5976 let mut shutdown_rx = self.shutdown_tx.subscribe();
5977
5978 tokio::spawn(async move {
5979 let interval_secs = state.live_config.load().health.check_interval_secs.max(1);
5985 let mut interval = tokio::time::interval(std::time::Duration::from_secs(interval_secs));
5986
5987 loop {
5988 tokio::select! {
5989 _ = interval.tick() => {
5990 let config = state.live_config.load_full();
5993 let st = state.clone();
5998 if let Err(e) = tokio::spawn(async move {
5999 Self::check_all_nodes(&st, &config).await;
6000 })
6001 .await
6002 {
6003 tracing::error!(error = %e, "health-check sweep panicked; health loop continuing");
6004 }
6005 }
6006 _ = shutdown_rx.recv() => {
6007 break;
6008 }
6009 }
6010 }
6011 })
6012 }
6013
6014 async fn check_all_nodes(state: &Arc<ServerState>, config: &ProxyConfig) {
6021 let timeout = Duration::from_secs(config.health.check_timeout_secs);
6024 let mut set = tokio::task::JoinSet::new();
6025 for node in &config.nodes {
6026 let addr = node.address();
6027 set.spawn(async move {
6028 let r = Self::check_node_addr(&addr, timeout).await;
6029 (addr, r)
6030 });
6031 }
6032 let mut results = Vec::with_capacity(config.nodes.len());
6033 while let Some(joined) = set.join_next().await {
6034 if let Ok(pair) = joined {
6035 results.push(pair);
6036 }
6037 }
6038
6039 let _writers = state.health_write.lock();
6045 let mut next = (*state.health.load_full()).clone();
6046 for (addr, result) in results {
6047 if let Some(node_health) = next.get_mut(&addr) {
6048 match result {
6049 Ok(latency) => {
6050 node_health.healthy = true;
6051 node_health.failure_count = 0;
6052 node_health.latency_ms = latency;
6053 node_health.last_error = None;
6054 }
6055 Err(e) => {
6056 node_health.failure_count += 1;
6057 node_health.last_error = Some(e.to_string());
6058 if node_health.failure_count >= config.health.failure_threshold {
6059 node_health.healthy = false;
6060 tracing::warn!(
6061 "Node {} marked unhealthy after {} failures",
6062 addr,
6063 node_health.failure_count
6064 );
6065 }
6066 }
6067 }
6068 node_health.last_check = chrono::Utc::now();
6069 }
6070 }
6071 state.health.store(Arc::new(next));
6072 }
6073
6074 async fn check_node_addr(addr: &str, timeout: Duration) -> Result<f64> {
6085 const SSL_REQUEST: [u8; 8] = [0, 0, 0, 8, 0x04, 0xD2, 0x16, 0x2F];
6087 let start = std::time::Instant::now();
6088 let mut stream = tokio::time::timeout(timeout, TcpStream::connect(addr))
6089 .await
6090 .map_err(|_| ProxyError::HealthCheck(format!("Timeout connecting to {}", addr)))?
6091 .map_err(|e| {
6092 ProxyError::HealthCheck(format!("Failed to connect to {}: {}", addr, e))
6093 })?;
6094
6095 let probe = async {
6096 stream.write_all(&SSL_REQUEST).await?;
6097 let mut resp = [0u8; 1];
6098 stream.read_exact(&mut resp).await?;
6099 Ok::<u8, std::io::Error>(resp[0])
6100 };
6101 let remaining = timeout
6103 .saturating_sub(start.elapsed())
6104 .max(Duration::from_millis(1));
6105 let byte = tokio::time::timeout(remaining, probe)
6106 .await
6107 .map_err(|_| {
6108 ProxyError::HealthCheck(format!("{} did not answer protocol probe in time", addr))
6109 })?
6110 .map_err(|e| {
6111 ProxyError::HealthCheck(format!("{} protocol probe error: {}", addr, e))
6112 })?;
6113 if byte != b'S' && byte != b'N' {
6116 return Err(ProxyError::HealthCheck(format!(
6117 "{} sent unexpected probe reply {:#x}",
6118 addr, byte
6119 )));
6120 }
6121 let latency = start.elapsed().as_secs_f64() * 1000.0;
6122 Ok(latency)
6123 }
6124
6125 fn spawn_pool_manager(&self) -> tokio::task::JoinHandle<()> {
6127 #[cfg(feature = "pool-modes")]
6129 let state = self.state.clone();
6130 let mut shutdown_rx = self.shutdown_tx.subscribe();
6131
6132 tokio::spawn(async move {
6133 let mut interval = tokio::time::interval(Self::POOL_REAP_INTERVAL);
6134
6135 loop {
6136 tokio::select! {
6137 _ = interval.tick() => {
6138 #[cfg(feature = "pool-modes")]
6140 if let Some(ref pool_manager) = state.pool_manager {
6141 pool_manager.evict_idle().await;
6142 tracing::trace!("Pool-modes idle eviction completed");
6143 }
6144 #[cfg(feature = "pool-modes")]
6149 if let Some(ref backend_pool) = state.backend_pool {
6150 let ttl = std::time::Duration::from_secs(
6151 state.live_config.load().pool_mode.idle_timeout_secs,
6152 );
6153 let n = if ttl.is_zero() {
6159 0
6160 } else {
6161 backend_pool.reap_idle(ttl)
6162 };
6163 if n > 0 {
6164 tracing::debug!(
6165 target: "helios::pool",
6166 reaped = n,
6167 idle_remaining = backend_pool.idle_count(),
6168 "reaped idle backend connections (TTL)"
6169 );
6170 }
6171 }
6172 }
6173 _ = shutdown_rx.recv() => {
6174 #[cfg(feature = "pool-modes")]
6176 if let Some(ref pool_manager) = state.pool_manager {
6177 pool_manager.close_all().await;
6178 tracing::info!("Pool-modes manager closed all connections");
6179 }
6180 break;
6181 }
6182 }
6183 }
6184 })
6185 }
6186
6187 #[cfg(feature = "edge-proxy")]
6194 fn spawn_edge_maintenance(&self) -> tokio::task::JoinHandle<()> {
6195 let state = self.state.clone();
6196 let gc_secs = self.config.edge.subscribe_gc_secs.max(1);
6199 let mut shutdown_rx = self.shutdown_tx.subscribe();
6200
6201 tokio::spawn(async move {
6202 let mut interval = tokio::time::interval(Duration::from_secs(gc_secs));
6203
6204 loop {
6205 tokio::select! {
6206 _ = interval.tick() => {
6207 let pruned = state.edge_registry.prune_stale();
6208 if pruned > 0 {
6209 tracing::debug!(
6210 target: "helios::edge",
6211 pruned,
6212 remaining = state.edge_registry.count(),
6213 "edge registry GC pruned stale edges"
6214 );
6215 }
6216 }
6217 _ = shutdown_rx.recv() => break,
6218 }
6219 }
6220 })
6221 }
6222
6223 pub fn shutdown(&self) {
6225 let _ = self.shutdown_tx.send(());
6226 }
6227
6228 #[cfg(feature = "pool-modes")]
6230 pub async fn pool_mode_stats(&self) -> Option<PoolModeStatsSnapshot> {
6231 if let Some(ref pool_manager) = self.state.pool_manager {
6232 let stats = pool_manager.get_stats().await;
6233 let metrics = pool_manager.metrics().snapshot();
6234 let default_mode = pool_manager.default_mode();
6235
6236 let avg_lease_duration_ms = metrics
6238 .mode_stats
6239 .get(&default_mode)
6240 .map(|s| s.avg_lease_duration_ms as u64)
6241 .unwrap_or(0);
6242
6243 Some(PoolModeStatsSnapshot {
6244 mode: format!("{:?}", default_mode),
6245 total_connections: stats.total_connections,
6246 active_leases: stats.active_connections,
6247 idle_connections: stats.idle_connections,
6248 node_count: stats.node_count,
6249 acquires: metrics.acquires,
6250 releases: metrics.releases,
6251 acquire_failures: metrics.acquire_failures,
6252 acquire_timeouts: metrics.acquire_timeouts,
6253 transactions_completed: metrics.transactions_completed,
6254 statements_executed: metrics.statements_executed,
6255 avg_lease_duration_ms,
6256 })
6257 } else {
6258 None
6259 }
6260 }
6261
6262 #[cfg(feature = "pool-modes")]
6264 pub async fn add_node_to_pool(&self, node: &NodeConfig) {
6265 if let Some(ref pool_manager) = self.state.pool_manager {
6266 let endpoint = NodeEndpoint::new(&node.host, node.port)
6267 .with_role(match node.role {
6268 NodeRole::Primary => crate::NodeRole::Primary,
6269 NodeRole::Standby => crate::NodeRole::Standby,
6270 NodeRole::ReadReplica => crate::NodeRole::ReadReplica,
6271 })
6272 .with_weight(node.weight);
6273 pool_manager.add_node(&endpoint).await;
6274 tracing::info!("Added node {} to pool manager", node.address());
6275 }
6276 }
6277
6278 pub fn metrics(&self) -> ServerMetricsSnapshot {
6280 ServerMetricsSnapshot {
6281 connections_accepted: self
6282 .state
6283 .metrics
6284 .connections_accepted
6285 .load(Ordering::Relaxed),
6286 connections_closed: self
6287 .state
6288 .metrics
6289 .connections_closed
6290 .load(Ordering::Relaxed),
6291 queries_processed: self.state.metrics.queries_processed.load(Ordering::Relaxed),
6292 bytes_received: self.state.metrics.bytes_received.load(Ordering::Relaxed),
6293 bytes_sent: self.state.metrics.bytes_sent.load(Ordering::Relaxed),
6294 failovers: self.state.metrics.failovers.load(Ordering::Relaxed),
6295 }
6296 }
6297}
6298
6299#[derive(Debug, Clone)]
6301pub struct ServerMetricsSnapshot {
6302 pub connections_accepted: u64,
6303 pub connections_closed: u64,
6304 pub queries_processed: u64,
6305 pub bytes_received: u64,
6306 pub bytes_sent: u64,
6307 pub failovers: u64,
6308}
6309
6310#[cfg(feature = "pool-modes")]
6312#[derive(Debug, Clone)]
6313pub struct PoolModeStatsSnapshot {
6314 pub mode: String,
6316 pub total_connections: usize,
6318 pub active_leases: usize,
6320 pub idle_connections: usize,
6322 pub node_count: usize,
6324 pub acquires: u64,
6326 pub releases: u64,
6328 pub acquire_failures: u64,
6330 pub acquire_timeouts: u64,
6332 pub transactions_completed: u64,
6334 pub statements_executed: u64,
6336 pub avg_lease_duration_ms: u64,
6338}
6339
6340#[cfg(test)]
6341mod tests {
6342 use super::*;
6343 use crate::config::{HealthConfig, LoadBalancerConfig, PoolConfig};
6344 #[cfg(not(feature = "wasm-plugins"))]
6345 use crate::protocol::QueryMessage;
6346
6347 fn test_config() -> ProxyConfig {
6348 let mut config = ProxyConfig::default();
6349 config.listen_address = "127.0.0.1:0".to_string();
6350 config.add_node("127.0.0.1:5432", "primary").unwrap();
6351 config
6352 }
6353
6354 #[test]
6355 fn test_server_creation() {
6356 let config = test_config();
6357 let server = ProxyServer::new(config);
6358 assert!(server.is_ok());
6359 }
6360
6361 #[test]
6362 fn is_backend_fault_excludes_client_and_slow_query_errors() {
6363 assert!(ProxyServer::is_backend_fault(
6365 "Backend read error: connection reset"
6366 ));
6367 assert!(ProxyServer::is_backend_fault(
6368 "Backend write error: broken pipe"
6369 ));
6370 assert!(ProxyServer::is_backend_fault("Backend write timeout"));
6371 assert!(ProxyServer::is_backend_fault(
6372 "Failed to connect to 127.0.0.1:5432: Connection refused"
6373 ));
6374 assert!(!ProxyServer::is_backend_fault("Backend read timeout"));
6377 assert!(!ProxyServer::is_backend_fault("Client write timeout"));
6378 assert!(!ProxyServer::is_backend_fault(
6379 "Client write error: broken pipe"
6380 ));
6381 assert!(!ProxyServer::is_backend_fault("Backend read timeout"));
6383 assert!(ProxyServer::is_backend_fault(
6384 "Backend read error: timed out"
6385 ));
6386 }
6387
6388 #[test]
6389 fn test_hba_addr_matches() {
6390 use std::net::IpAddr;
6391 let v4 = |s: &str| s.parse::<IpAddr>().unwrap();
6392 assert!(ProxyServer::hba_addr_matches("all", v4("203.0.113.7")));
6394 assert!(ProxyServer::hba_addr_matches("10.0.0.0/8", v4("10.1.2.3")));
6396 assert!(!ProxyServer::hba_addr_matches("10.0.0.0/8", v4("11.1.2.3")));
6397 assert!(ProxyServer::hba_addr_matches(
6398 "127.0.0.1/32",
6399 v4("127.0.0.1")
6400 ));
6401 assert!(!ProxyServer::hba_addr_matches(
6402 "127.0.0.1/32",
6403 v4("127.0.0.2")
6404 ));
6405 assert!(ProxyServer::hba_addr_matches(
6407 "192.168.1.1",
6408 v4("192.168.1.1")
6409 ));
6410 assert!(!ProxyServer::hba_addr_matches(
6411 "192.168.1.1",
6412 v4("192.168.1.2")
6413 ));
6414 assert!(ProxyServer::hba_addr_matches("::1/128", v4("::1")));
6416 assert!(ProxyServer::hba_addr_matches("0.0.0.0/0", v4("8.8.8.8")));
6417 }
6418
6419 #[test]
6420 fn test_hba_admits() {
6421 use crate::config::{HbaAction, HbaRule};
6422 use std::net::IpAddr;
6423 let ip: IpAddr = "10.0.0.5".parse().unwrap();
6424 assert!(ProxyServer::hba_admits(&[], ip, "bench", "benchdb"));
6426 let rules = vec![HbaRule {
6428 action: HbaAction::Reject,
6429 user: "bench".into(),
6430 database: "all".into(),
6431 address: "all".into(),
6432 }];
6433 assert!(!ProxyServer::hba_admits(&rules, ip, "bench", "benchdb"));
6434 assert!(ProxyServer::hba_admits(&rules, ip, "alice", "benchdb"));
6435 let rules = vec![
6437 HbaRule {
6438 action: HbaAction::Allow,
6439 user: "bench".into(),
6440 database: "all".into(),
6441 address: "10.0.0.0/8".into(),
6442 },
6443 HbaRule {
6444 action: HbaAction::Reject,
6445 user: "all".into(),
6446 database: "all".into(),
6447 address: "all".into(),
6448 },
6449 ];
6450 assert!(ProxyServer::hba_admits(&rules, ip, "bench", "benchdb"));
6451 assert!(!ProxyServer::hba_admits(
6452 &rules,
6453 "192.168.0.1".parse().unwrap(),
6454 "bench",
6455 "benchdb"
6456 ));
6457 assert!(!ProxyServer::hba_admits(&rules, ip, "alice", "benchdb"));
6458 }
6459
6460 #[test]
6461 fn test_initial_metrics() {
6462 let config = test_config();
6463 let server = ProxyServer::new(config).unwrap();
6464 let metrics = server.metrics();
6465 assert_eq!(metrics.connections_accepted, 0);
6466 assert_eq!(metrics.queries_processed, 0);
6467 }
6468
6469 #[tokio::test]
6470 async fn test_session_creation() {
6471 let config = test_config();
6472 let server = ProxyServer::new(config).unwrap();
6473
6474 assert!(server.state.sessions.is_empty());
6475 }
6476
6477 #[tokio::test]
6478 async fn test_node_health_initialization() {
6479 let config = test_config();
6480 let server = ProxyServer::new(config).unwrap();
6481
6482 let health = server.state.health.load_full();
6483 assert!(!health.is_empty());
6484
6485 for node_health in health.values() {
6486 assert!(node_health.healthy);
6487 assert_eq!(node_health.failure_count, 0);
6488 }
6489 }
6490
6491 fn make_test_session() -> Arc<ClientSession> {
6493 Arc::new(ClientSession {
6494 id: Uuid::new_v4(),
6495 client_addr: "127.0.0.1:0".parse().unwrap(),
6496 current_node: RwLock::new(None),
6497 in_transaction: std::sync::atomic::AtomicBool::new(false),
6498 copy_in_progress: std::sync::atomic::AtomicBool::new(false),
6499 tx_state: RwLock::new(TransactionState::default()),
6500 variables: RwLock::new(HashMap::new()),
6501 created_at: chrono::Utc::now(),
6502 tr_mode: crate::config::TrMode::default(),
6503 #[cfg(feature = "lag-routing")]
6504 last_write_at: RwLock::new(None),
6505 #[cfg(feature = "pool-modes")]
6506 pool_client_id: crate::pool::lease::ClientId::default(),
6507 #[cfg(feature = "wasm-plugins")]
6508 plugin_identity: RwLock::new(None),
6509 #[cfg(feature = "edge-proxy")]
6510 edge_ineligible: std::sync::atomic::AtomicBool::new(false),
6511 #[cfg(feature = "edge-proxy")]
6512 pending_edge_copy_tables: std::sync::Mutex::new(None),
6513 })
6514 }
6515
6516 #[tokio::test]
6520 async fn test_apply_route_hook_no_plugin_manager_returns_none() {
6521 let config = test_config();
6522 let server = ProxyServer::new(config).unwrap();
6523 let session = make_test_session();
6524
6525 let msg = QueryMessage {
6526 query: "SELECT * FROM users".to_string(),
6527 }
6528 .encode();
6529
6530 let decision = ProxyServer::apply_route_hook(&msg, &server.state, &session);
6531 assert!(matches!(decision, RouteOverride::None));
6532 }
6533
6534 #[tokio::test]
6538 async fn test_apply_pre_query_hook_no_plugin_manager_forwards() {
6539 let config = test_config();
6540 let server = ProxyServer::new(config).unwrap();
6541 let session = make_test_session();
6542
6543 let original = QueryMessage {
6544 query: "SELECT 1".to_string(),
6545 }
6546 .encode();
6547 let original_bytes = original.encode().to_vec();
6548
6549 let (msg_out, action) =
6550 ProxyServer::apply_pre_query_hook(original, &server.state, &session);
6551
6552 assert!(matches!(action, PreQueryAction::Forward));
6553 assert_eq!(msg_out.encode().to_vec(), original_bytes);
6555 }
6556
6557 #[tokio::test]
6561 async fn test_apply_route_hook_skips_non_query_messages() {
6562 let config = test_config();
6563 let server = ProxyServer::new(config).unwrap();
6564 let session = make_test_session();
6565
6566 let sync_msg = Message::empty(MessageType::Sync);
6567 let decision = ProxyServer::apply_route_hook(&sync_msg, &server.state, &session);
6568 assert!(matches!(decision, RouteOverride::None));
6569 }
6570
6571 #[cfg(feature = "wasm-plugins")]
6576 #[test]
6577 fn test_init_plugin_manager_disabled_by_default_returns_none() {
6578 let config = test_config();
6579 assert!(!config.plugins.enabled);
6580 let pm = ProxyServer::init_plugin_manager(&config.plugins);
6581 assert!(pm.is_none());
6582 }
6583
6584 #[cfg(feature = "wasm-plugins")]
6588 #[test]
6589 fn test_init_plugin_manager_missing_dir_logs_warning() {
6590 let mut config = test_config();
6591 config.plugins.enabled = true;
6592 config.plugins.plugin_dir = "/definitely/not/a/real/path".to_string();
6593
6594 let pm = ProxyServer::init_plugin_manager(&config.plugins);
6596 assert!(pm.is_some());
6597 }
6598
6599 #[tokio::test]
6603 async fn test_apply_authenticate_hook_no_plugin_manager_defers() {
6604 let config = test_config();
6605 let server = ProxyServer::new(config).unwrap();
6606 let session = make_test_session();
6607
6608 let mut params = HashMap::new();
6609 params.insert("user".to_string(), "alice".to_string());
6610 params.insert("database".to_string(), "app".to_string());
6611
6612 let result = ProxyServer::apply_authenticate_hook(¶ms, &session, &server.state).await;
6613 assert!(result.is_ok());
6614
6615 #[cfg(feature = "wasm-plugins")]
6617 {
6618 let ident = session.plugin_identity.read().await;
6619 assert!(ident.is_none());
6620 }
6621 }
6622
6623 #[cfg(feature = "wasm-plugins")]
6631 #[test]
6632 fn test_synthesise_cached_response_roundtrip() {
6633 let payload = br#"{
6634 "columns": [
6635 {"name": "id", "oid": 23},
6636 {"name": "email", "oid": 25}
6637 ],
6638 "rows": [
6639 ["1", "alice@example.com"],
6640 ["2", null]
6641 ]
6642 }"#;
6643 let reply = ProxyServer::synthesise_cached_response(payload).expect("synthesis");
6644
6645 let mut tags = Vec::new();
6648 let mut i = 0;
6649 while i < reply.len() {
6650 let tag = reply[i];
6651 let len = u32::from_be_bytes([reply[i + 1], reply[i + 2], reply[i + 3], reply[i + 4]])
6652 as usize;
6653 tags.push(tag);
6654 i += 1 + len;
6655 }
6656 assert_eq!(i, reply.len(), "no trailing bytes");
6657 assert_eq!(tags, vec![b'T', b'D', b'D', b'C', b'Z'], "wire frame order");
6658
6659 assert_eq!(*reply.last().unwrap(), b'I');
6661 }
6662
6663 #[cfg(feature = "wasm-plugins")]
6666 #[test]
6667 fn test_synthesise_cached_response_rejects_row_width_mismatch() {
6668 let payload = br#"{
6669 "columns": [{"name": "id", "oid": 23}, {"name": "name", "oid": 25}],
6670 "rows": [["1", "alice", "extra"]]
6671 }"#;
6672 let result = ProxyServer::synthesise_cached_response(payload);
6673 assert!(matches!(result, Err(ProxyError::Protocol(_))));
6674 }
6675
6676 #[cfg(feature = "wasm-plugins")]
6680 #[test]
6681 fn test_synthesise_cached_response_rejects_empty_columns() {
6682 let payload = br#"{ "columns": [], "rows": [] }"#;
6683 let result = ProxyServer::synthesise_cached_response(payload);
6684 assert!(matches!(result, Err(ProxyError::Protocol(_))));
6685 }
6686
6687 #[cfg(feature = "wasm-plugins")]
6690 #[test]
6691 fn test_synthesise_cached_response_rejects_bad_json() {
6692 let payload = b"not json at all";
6693 let result = ProxyServer::synthesise_cached_response(payload);
6694 assert!(matches!(result, Err(ProxyError::Protocol(_))));
6695 }
6696
6697 #[cfg(feature = "wasm-plugins")]
6706 #[tokio::test]
6707 async fn test_apply_authenticate_hook_with_manager_no_plugins_defers() {
6708 use crate::plugins::{PluginManager, PluginRuntimeConfig};
6709
6710 let config = test_config();
6711 let server = ProxyServer::new(config).unwrap();
6712 let session = make_test_session();
6713
6714 let pm = Arc::new(PluginManager::new(PluginRuntimeConfig::default()).unwrap());
6717 #[cfg(feature = "edge-proxy")]
6718 let edge_defaults = crate::edge::EdgeConfig::default();
6719 let augmented_state = Arc::new(ServerState {
6720 sessions: DashMap::new(),
6721 health: ArcSwap::from_pointee(HashMap::new()),
6722 health_write: parking_lot::Mutex::new(()),
6723 live_config: ArcSwap::from_pointee(ProxyConfig::default()),
6724 metrics: ServerMetrics::default(),
6725 cancel_map: Arc::new(DashMap::new()),
6726 cancel_order: Arc::new(parking_lot::Mutex::new(std::collections::VecDeque::new())),
6727 tls_acceptor: None,
6728 auth_file: None,
6729 mirror: None,
6730 cutover: Arc::new(ArcSwap::from_pointee(None)),
6731 lb_state: LoadBalancerState {
6732 rr_counter: AtomicU64::new(0),
6733 },
6734 #[cfg(feature = "routing-hints")]
6735 hint_parser: None,
6736 #[cfg(feature = "rate-limiting")]
6737 rate_limiter: None,
6738 #[cfg(feature = "circuit-breaker")]
6739 circuit_breaker: None,
6740 #[cfg(feature = "query-analytics")]
6741 analytics: None,
6742 #[cfg(feature = "query-cache")]
6743 query_cache: None,
6744 #[cfg(feature = "query-rewriting")]
6745 rewriter: None,
6746 #[cfg(feature = "multi-tenancy")]
6747 tenant_manager: None,
6748 #[cfg(feature = "schema-routing")]
6749 schema_analyzer: None,
6750 #[cfg(feature = "pool-modes")]
6751 pool_manager: None,
6752 #[cfg(feature = "pool-modes")]
6753 backend_pool: None,
6754 plugin_manager: Some(pm),
6755 #[cfg(feature = "ha-tr")]
6756 transaction_journal: Arc::new(crate::transaction_journal::TransactionJournal::new()),
6757 #[cfg(feature = "anomaly-detection")]
6758 anomaly_detector: Arc::new(crate::anomaly::AnomalyDetector::new(
6759 crate::anomaly::AnomalyConfig::default(),
6760 )),
6761 #[cfg(feature = "edge-proxy")]
6762 edge_cache: Arc::new(crate::edge::EdgeCache::new(
6763 edge_defaults.max_entries.max(1),
6764 )),
6765 #[cfg(feature = "edge-proxy")]
6766 edge_registry: Arc::new(crate::edge::EdgeRegistry::new(
6767 edge_defaults.max_edges,
6768 std::time::Duration::from_secs(edge_defaults.liveness_window_secs),
6769 )),
6770 });
6771
6772 let mut params = HashMap::new();
6773 params.insert("user".to_string(), "alice".to_string());
6774
6775 let result =
6776 ProxyServer::apply_authenticate_hook(¶ms, &session, &augmented_state).await;
6777 assert!(result.is_ok());
6778 let ident = session.plugin_identity.read().await;
6779 assert!(ident.is_none());
6780 let _ = server;
6782 }
6783
6784 fn cstr(s: &str) -> Vec<u8> {
6787 let mut v = s.as_bytes().to_vec();
6788 v.push(0);
6789 v
6790 }
6791
6792 #[test]
6793 fn parse_stmt_name_extracts_named_and_unnamed() {
6794 let mut named = cstr("ps1");
6796 named.extend_from_slice(&cstr("SELECT 1"));
6797 named.extend_from_slice(&[0, 0]);
6798 assert_eq!(ProxyServer::parse_stmt_name(&named), "ps1");
6799
6800 let mut unnamed = cstr("");
6801 unnamed.extend_from_slice(&cstr("SELECT 1"));
6802 unnamed.extend_from_slice(&[0, 0]);
6803 assert_eq!(ProxyServer::parse_stmt_name(&unnamed), "");
6804 }
6805
6806 #[test]
6807 fn bind_stmt_ref_reads_second_cstring() {
6808 let mut named = cstr("portal_a");
6810 named.extend_from_slice(&cstr("ps1"));
6811 named.extend_from_slice(&[0, 0]); assert_eq!(ProxyServer::bind_stmt_ref(&named), Some("ps1"));
6813
6814 let mut unnamed = cstr("");
6816 unnamed.extend_from_slice(&cstr(""));
6817 assert_eq!(ProxyServer::bind_stmt_ref(&unnamed), None);
6818 }
6819
6820 #[test]
6821 fn stmt_kind_name_only_matches_statement_kind() {
6822 let mut stmt = vec![b'S'];
6824 stmt.extend_from_slice(&cstr("ps1"));
6825 assert_eq!(ProxyServer::stmt_kind_name(&stmt), Some("ps1"));
6826
6827 let mut portal = vec![b'P'];
6829 portal.extend_from_slice(&cstr("portal_a"));
6830 assert_eq!(ProxyServer::stmt_kind_name(&portal), None);
6831
6832 let mut empty = vec![b'S'];
6834 empty.extend_from_slice(&cstr(""));
6835 assert_eq!(ProxyServer::stmt_kind_name(&empty), None);
6836 }
6837
6838 #[tokio::test]
6839 async fn read_one_frame_type_consumes_full_frame() {
6840 let (mut a, mut b) = tokio::io::duplex(64);
6843 let bytes = [b'1', 0, 0, 0, 4, b'Z', 0, 0, 0, 5, b'I'];
6845 b.write_all(&bytes).await.unwrap();
6846 let t = ProxyServer::read_one_frame_type(&mut a).await.unwrap();
6847 assert_eq!(t, b'1');
6848 let t2 = ProxyServer::read_one_frame_type(&mut a).await.unwrap();
6850 assert_eq!(t2, b'Z');
6851 }
6852
6853 #[tokio::test]
6854 async fn reprepare_statement_accepts_parse_complete_and_rejects_error() {
6855 let (mut client, mut backend) = tokio::io::duplex(64);
6857 backend.write_all(&[b'1', 0, 0, 0, 4]).await.unwrap();
6858 let parse = {
6859 let mut p = vec![b'P', 0, 0, 0, 0];
6860 p.extend_from_slice(&cstr("ps1"));
6861 p.extend_from_slice(&cstr("SELECT 1"));
6862 p.extend_from_slice(&[0, 0]);
6863 p
6864 };
6865 assert!(ProxyServer::reprepare_statement(&mut client, &parse)
6866 .await
6867 .is_ok());
6868
6869 let (mut client2, mut backend2) = tokio::io::duplex(64);
6871 backend2.write_all(&[b'E', 0, 0, 0, 4]).await.unwrap();
6872 assert!(ProxyServer::reprepare_statement(&mut client2, &parse)
6873 .await
6874 .is_err());
6875 }
6876
6877 #[cfg(feature = "routing-hints")]
6880 mod routing_hints {
6881 use super::*;
6882 use crate::routing::HintParser;
6883
6884 fn over(sql: &str) -> RouteOverride {
6885 let hints = HintParser::new().parse(sql);
6886 ProxyServer::hint_to_override(&hints)
6887 }
6888
6889 #[test]
6890 fn route_primary_maps_to_primary() {
6891 assert!(matches!(
6892 over("/*helios:route=primary*/ SELECT 1"),
6893 RouteOverride::Primary
6894 ));
6895 }
6896
6897 #[test]
6898 fn read_tier_targets_map_to_standby() {
6899 for t in ["standby", "sync", "semisync", "async", "local"] {
6900 assert!(
6901 matches!(
6902 over(&format!("/*helios:route={t}*/ SELECT 1")),
6903 RouteOverride::Standby
6904 ),
6905 "route={t} should map to Standby"
6906 );
6907 }
6908 }
6909
6910 #[test]
6911 fn any_and_vector_impose_no_constraint() {
6912 assert!(matches!(
6913 over("/*helios:route=any*/ SELECT 1"),
6914 RouteOverride::None
6915 ));
6916 assert!(matches!(
6917 over("/*helios:route=vector*/ SELECT 1"),
6918 RouteOverride::None
6919 ));
6920 }
6921
6922 #[test]
6923 fn node_hint_maps_to_node_and_wins_over_route() {
6924 match over("/*helios:node=pg-standby,route=primary*/ SELECT 1") {
6926 RouteOverride::Node(n) => assert_eq!(n, "pg-standby"),
6927 other => panic!("expected Node, got {other:?}"),
6928 }
6929 }
6930
6931 #[test]
6932 fn consistency_strong_forces_primary() {
6933 assert!(matches!(
6934 over("/*helios:consistency=strong*/ SELECT 1"),
6935 RouteOverride::Primary
6936 ));
6937 }
6938
6939 #[test]
6940 fn no_hint_yields_none() {
6941 assert!(matches!(over("SELECT 1"), RouteOverride::None));
6942 }
6943
6944 #[test]
6948 fn write_verb_classified_after_strip() {
6949 let parser = HintParser::new();
6950 let raw = "/*helios:route=primary*/ INSERT INTO t VALUES (1)";
6951 assert!(!ProxyServer::is_write_query(raw));
6954 assert!(ProxyServer::is_write_query(&parser.strip(raw)));
6956 }
6957
6958 #[test]
6959 fn strip_removes_hint_comment() {
6960 let parser = HintParser::new();
6961 assert_eq!(
6962 parser.strip("/*helios:route=standby*/ SELECT 42"),
6963 "SELECT 42"
6964 );
6965 }
6966 }
6967
6968 #[cfg(feature = "rate-limiting")]
6971 mod rate_limiting {
6972 use crate::rate_limit::{LimiterKey, RateLimitConfig, RateLimitResult, RateLimiter};
6973
6974 #[test]
6975 fn burst_allows_then_denies() {
6976 let cfg = RateLimitConfig {
6979 enabled: true,
6980 default_qps: 1,
6981 default_burst: 2,
6982 ..Default::default()
6983 };
6984 let limiter = RateLimiter::new(cfg);
6985 let key = LimiterKey::User("u".to_string());
6986
6987 assert!(matches!(limiter.check(&key, 1), RateLimitResult::Allowed));
6989 assert!(matches!(limiter.check(&key, 1), RateLimitResult::Allowed));
6990
6991 let mut denied = false;
6993 for _ in 0..5 {
6994 if matches!(limiter.check(&key, 1), RateLimitResult::Denied(_)) {
6995 denied = true;
6996 }
6997 }
6998 assert!(denied, "over-burst checks must yield a Denied verdict");
6999 }
7000
7001 #[test]
7002 fn distinct_keys_have_independent_buckets() {
7003 let cfg = RateLimitConfig {
7004 enabled: true,
7005 default_qps: 1,
7006 default_burst: 1,
7007 ..Default::default()
7008 };
7009 let limiter = RateLimiter::new(cfg);
7010 assert!(matches!(
7012 limiter.check(&LimiterKey::User("a".to_string()), 1),
7013 RateLimitResult::Allowed
7014 ));
7015 assert!(matches!(
7016 limiter.check(&LimiterKey::User("b".to_string()), 1),
7017 RateLimitResult::Allowed
7018 ));
7019 }
7020 }
7021
7022 #[cfg(feature = "circuit-breaker")]
7025 mod circuit_breaker {
7026 use crate::circuit_breaker::{
7027 CircuitBreakerConfig, CircuitBreakerManager, CircuitState, ManagerConfig,
7028 };
7029 use std::time::Duration;
7030
7031 fn mgr(threshold: u32) -> CircuitBreakerManager {
7032 let cfg = CircuitBreakerConfig {
7033 failure_threshold: threshold,
7034 cooldown: Duration::from_secs(10),
7035 ..Default::default()
7036 };
7037 CircuitBreakerManager::new(ManagerConfig::new(cfg))
7038 }
7039
7040 #[test]
7041 fn opens_after_threshold_failures() {
7042 let m = mgr(3);
7043 let b = m.get_breaker("n1");
7044 assert_eq!(b.get_state(), CircuitState::Closed);
7045 b.record_failure("boom");
7046 b.record_failure("boom");
7047 assert_eq!(b.get_state(), CircuitState::Closed);
7049 b.record_failure("boom");
7051 assert_eq!(b.get_state(), CircuitState::Open);
7052 }
7053
7054 #[test]
7055 fn healthy_node_stays_closed() {
7056 let m = mgr(3);
7057 let b = m.get_breaker("n2");
7058 b.record_success();
7059 b.record_success();
7060 assert_eq!(b.get_state(), CircuitState::Closed);
7061 }
7062 }
7063
7064 #[cfg(feature = "query-analytics")]
7067 mod query_analytics {
7068 use crate::analytics::{AnalyticsConfig, OrderBy, QueryAnalytics, QueryExecution};
7069 use std::time::Duration;
7070
7071 #[test]
7072 fn records_and_collapses_literals() {
7073 let a = QueryAnalytics::new(AnalyticsConfig::default());
7074 for n in [1, 2, 3] {
7075 a.record(QueryExecution::new(
7076 format!("select {n}"),
7077 Duration::from_millis(1),
7078 ));
7079 }
7080 let top = a.top_queries(OrderBy::Calls, 10);
7081 assert!(!top.is_empty(), "no fingerprints recorded");
7082 assert!(
7084 top.iter().any(|s| s.calls >= 3),
7085 "literals did not collapse: {:?}",
7086 top.iter()
7087 .map(|s| (s.normalized.clone(), s.calls))
7088 .collect::<Vec<_>>()
7089 );
7090 }
7091 }
7092
7093 #[cfg(feature = "lag-routing")]
7096 mod lag_routing {
7097 use super::ProxyServer;
7098
7099 #[test]
7100 fn ryw_pins_recent_write() {
7101 assert!(ProxyServer::ryw_pins_primary(
7103 Some(std::time::Instant::now()),
7104 1000
7105 ));
7106 }
7107
7108 #[test]
7109 fn ryw_releases_old_write() {
7110 let old = std::time::Instant::now()
7111 .checked_sub(std::time::Duration::from_secs(10))
7112 .unwrap();
7113 assert!(!ProxyServer::ryw_pins_primary(Some(old), 1000));
7114 }
7115
7116 #[test]
7117 fn ryw_no_write_or_disabled() {
7118 assert!(!ProxyServer::ryw_pins_primary(None, 1000));
7119 assert!(!ProxyServer::ryw_pins_primary(
7121 Some(std::time::Instant::now()),
7122 0
7123 ));
7124 }
7125
7126 #[test]
7127 fn lag_exclusion_thresholds() {
7128 assert!(!ProxyServer::lag_excludes_standby(Some(999_999), 0));
7130 assert!(!ProxyServer::lag_excludes_standby(None, 1000));
7132 assert!(!ProxyServer::lag_excludes_standby(Some(500), 1000));
7134 assert!(ProxyServer::lag_excludes_standby(Some(2000), 1000));
7136 }
7137 }
7138
7139 #[cfg(feature = "query-cache")]
7142 mod query_cache {
7143 use super::ProxyServer;
7144
7145 #[test]
7146 fn plain_selects_are_cacheable() {
7147 assert!(ProxyServer::is_cacheable_read_sql("select v from t"));
7148 assert!(ProxyServer::is_cacheable_read_sql(
7149 " SELECT a, b FROM users WHERE id = 5"
7150 ));
7151 }
7152
7153 #[test]
7154 fn writes_and_non_selects_are_not_cacheable() {
7155 assert!(!ProxyServer::is_cacheable_read_sql(
7156 "insert into t values (1)"
7157 ));
7158 assert!(!ProxyServer::is_cacheable_read_sql("update t set v = 1"));
7159 assert!(!ProxyServer::is_cacheable_read_sql("show search_path"));
7160 }
7161
7162 #[test]
7163 fn locking_and_volatile_selects_are_not_cacheable() {
7164 assert!(!ProxyServer::is_cacheable_read_sql(
7165 "select * from t for update"
7166 ));
7167 assert!(!ProxyServer::is_cacheable_read_sql("select now()"));
7168 assert!(!ProxyServer::is_cacheable_read_sql("select random()"));
7169 assert!(!ProxyServer::is_cacheable_read_sql("select nextval('s')"));
7170 assert!(!ProxyServer::is_cacheable_read_sql(
7173 "select set_config('timezone', 'UTC', false) from t"
7174 ));
7175 }
7176
7177 #[test]
7178 fn multi_statement_strings_are_not_cacheable() {
7179 assert!(!ProxyServer::is_cacheable_read_sql(
7182 "select v from t; update t set v = 1"
7183 ));
7184 assert!(!ProxyServer::is_cacheable_read_sql("select 1; select 2"));
7185 assert!(ProxyServer::is_cacheable_read_sql("select v from t;"));
7187 assert!(ProxyServer::is_cacheable_read_sql("SELECT v FROM t ; "));
7188 }
7189
7190 #[test]
7191 fn literal_semicolon_or_into_over_rejects_by_design() {
7192 assert!(!ProxyServer::is_cacheable_read_sql(
7201 "select v from t where url = 'a;b=c'"
7202 ));
7203 assert!(!ProxyServer::is_cacheable_read_sql(
7204 "select v from t where body like '%go into space%'"
7205 ));
7206 assert!(!ProxyServer::is_cacheable_read_sql(
7208 "select * into snapshot from t"
7209 ));
7210 }
7211
7212 #[test]
7213 fn select_into_is_not_cacheable() {
7214 assert!(!ProxyServer::is_cacheable_read_sql(
7217 "select * into report_tmp from src"
7218 ));
7219 assert!(!ProxyServer::is_cacheable_read_sql(
7221 "SELECT *\nINTO report_tmp\nFROM src"
7222 ));
7223 assert!(ProxyServer::is_cacheable_read_sql(
7225 "select into_total from t"
7226 ));
7227 }
7228 }
7229
7230 #[cfg(feature = "edge-proxy")]
7233 mod edge_proxy {
7234 use super::ProxyServer;
7235 use std::collections::HashMap;
7236
7237 #[test]
7238 fn bare_txn_control_is_exempt_from_invalidation() {
7239 for sql in [
7243 "BEGIN",
7244 "begin;",
7245 "START TRANSACTION",
7246 "SAVEPOINT sp1",
7247 "RELEASE SAVEPOINT sp1",
7248 "ROLLBACK",
7249 "ROLLBACK TO SAVEPOINT sp1;",
7250 ] {
7251 assert!(
7252 !ProxyServer::edge_write_needs_invalidation(true, sql),
7253 "{sql:?} must be exempt"
7254 );
7255 }
7256 assert!(ProxyServer::edge_write_needs_invalidation(true, "COMMIT"));
7259 assert!(ProxyServer::edge_write_needs_invalidation(
7260 true,
7261 "SET search_path TO tenant_b"
7262 ));
7263 }
7264
7265 #[test]
7266 fn compound_txn_strings_still_invalidate() {
7267 assert!(ProxyServer::edge_write_needs_invalidation(
7270 true,
7271 "BEGIN; UPDATE t SET v = 1; COMMIT"
7272 ));
7273 assert!(ProxyServer::edge_write_needs_invalidation(
7276 false,
7277 "SELECT v FROM t; UPDATE t SET v = 1"
7278 ));
7279 assert!(!ProxyServer::edge_write_needs_invalidation(
7281 false,
7282 "SELECT v FROM t"
7283 ));
7284 }
7285
7286 #[test]
7287 fn copy_from_counts_as_write() {
7288 assert!(ProxyServer::edge_write_needs_invalidation(
7289 false,
7290 "COPY t FROM STDIN"
7291 ));
7292 assert!(ProxyServer::is_edge_copy_write_sql("copy t from stdin"));
7293 assert!(!ProxyServer::edge_write_needs_invalidation(
7295 false,
7296 "COPY t TO STDOUT"
7297 ));
7298 }
7299
7300 #[test]
7301 fn procedural_and_txn_end_trigger_invalidation() {
7302 for sql in [
7306 "EXECUTE ins(1)",
7307 "execute ins(1)",
7308 "CALL do_write()",
7309 "DO $$ BEGIN PERFORM 1; END $$",
7310 ] {
7311 assert!(
7312 ProxyServer::is_edge_procedural_sql(sql),
7313 "{sql:?} is procedural"
7314 );
7315 assert!(
7316 ProxyServer::edge_write_needs_invalidation(false, sql),
7317 "{sql:?} must invalidate on the simple path"
7318 );
7319 }
7320 for sql in [
7321 "INSERT INTO t VALUES (1)",
7322 "SELECT 1 FROM t",
7323 "UPDATE t SET v=1",
7324 ] {
7325 assert!(
7326 !ProxyServer::is_edge_procedural_sql(sql),
7327 "{sql:?} is not procedural"
7328 );
7329 }
7330
7331 for sql in [
7334 "COMMIT",
7335 "commit work",
7336 "COMMIT PREPARED 'x'",
7337 "END",
7338 "END TRANSACTION",
7339 "end;",
7340 ] {
7341 assert!(ProxyServer::is_edge_txn_end_sql(sql), "{sql:?} ends a txn");
7342 assert!(
7343 ProxyServer::edge_write_needs_invalidation(false, sql),
7344 "{sql:?} must invalidate (commit-visibility window)"
7345 );
7346 }
7347 for sql in ["BEGIN", "START TRANSACTION", "SAVEPOINT s1", "ROLLBACK"] {
7348 assert!(
7349 !ProxyServer::is_edge_txn_end_sql(sql),
7350 "{sql:?} is not a txn-end"
7351 );
7352 assert!(
7353 !ProxyServer::edge_write_needs_invalidation(false, sql),
7354 "{sql:?} must stay exempt on the simple path"
7355 );
7356 }
7357 }
7358
7359 #[test]
7360 fn edge_meta_prunable_protects_reparsed_names() {
7361 let closes = vec!["S1".to_string(), "S2".to_string()];
7365 let none: Vec<String> = vec![];
7366 assert_eq!(
7367 ProxyServer::edge_meta_prunable(&closes, &none),
7368 vec!["S1", "S2"]
7369 );
7370 assert_eq!(
7372 ProxyServer::edge_meta_prunable(&closes, &["S1".to_string()]),
7373 vec!["S2"]
7374 );
7375 assert!(ProxyServer::edge_meta_prunable(&closes, &closes).is_empty());
7377 }
7378
7379 #[test]
7380 fn edge_dml_classifier_covers_dml_not_txn_control() {
7381 for sql in [
7382 "INSERT INTO t VALUES (1)",
7383 "update t set v = 1",
7384 "DELETE FROM t WHERE id = 1",
7385 "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET v = s.v",
7386 "TRUNCATE t",
7387 "ALTER TABLE t ADD COLUMN c int",
7388 "COPY t FROM STDIN",
7389 "WITH del AS (DELETE FROM t RETURNING id) SELECT * FROM del",
7390 ] {
7391 assert!(ProxyServer::is_edge_dml_sql(sql), "{sql:?} is DML");
7392 }
7393 for sql in [
7396 "BEGIN",
7397 "COMMIT",
7398 "ROLLBACK",
7399 "SET search_path TO x",
7400 "SELECT v FROM t",
7401 "WITH x AS (SELECT 1) SELECT * FROM x",
7402 "COPY t TO STDOUT",
7403 ] {
7404 assert!(!ProxyServer::is_edge_dml_sql(sql), "{sql:?} is not DML");
7405 }
7406 }
7407
7408 #[test]
7409 fn extended_batch_tables_union_and_wildcard() {
7410 let mut named: HashMap<String, Option<Vec<String>>> = HashMap::new();
7411 named.insert("ins".into(), Some(vec!["orders".into()]));
7412 named.insert("upd".into(), Some(vec!["users".into()]));
7413 named.insert("sel".into(), None);
7414 named.insert("weird".into(), Some(vec![])); let unnamed: Option<Vec<String>> = Some(vec!["events".into()]);
7416
7417 assert_eq!(
7419 ProxyServer::edge_extended_batch_tables(
7420 &["sel".to_string()],
7421 false,
7422 &named,
7423 &unnamed
7424 ),
7425 None
7426 );
7427 let t = ProxyServer::edge_extended_batch_tables(
7429 &["ins".to_string(), "upd".to_string(), "ins".to_string()],
7430 false,
7431 &named,
7432 &unnamed,
7433 )
7434 .expect("dml");
7435 assert_eq!(t, vec!["orders".to_string(), "users".to_string()]);
7436 let t = ProxyServer::edge_extended_batch_tables(&[], true, &named, &unnamed)
7438 .expect("unnamed dml");
7439 assert_eq!(t, vec!["events".to_string()]);
7440 let t = ProxyServer::edge_extended_batch_tables(
7442 &["ins".to_string(), "weird".to_string()],
7443 false,
7444 &named,
7445 &unnamed,
7446 )
7447 .expect("dml");
7448 assert!(t.is_empty(), "wildcard invalidation");
7449 assert_eq!(
7452 ProxyServer::edge_extended_batch_tables(
7453 &["ghost".to_string()],
7454 false,
7455 &named,
7456 &None
7457 ),
7458 None
7459 );
7460 }
7461 }
7462
7463 #[cfg(any(feature = "query-cache", feature = "edge-proxy"))]
7469 #[tokio::test]
7470 async fn capture_excludes_async_frames_from_cacheable() {
7471 use crate::client_tls::ClientStream;
7472 use tokio::io::AsyncReadExt;
7473 use tokio::io::AsyncWriteExt as _;
7474 use tokio::net::{TcpListener, TcpStream};
7475
7476 async fn pair() -> (TcpStream, TcpStream) {
7477 let l = TcpListener::bind("127.0.0.1:0").await.unwrap();
7478 let addr = l.local_addr().unwrap();
7479 let (accepted, connected) = tokio::join!(l.accept(), TcpStream::connect(addr));
7480 (accepted.unwrap().0, connected.unwrap())
7481 }
7482
7483 fn frame(mtype: u8, body: &[u8]) -> Vec<u8> {
7484 let mut v = vec![mtype];
7485 v.extend_from_slice(&((body.len() + 4) as u32).to_be_bytes());
7486 v.extend_from_slice(body);
7487 v
7488 }
7489
7490 let clean: Vec<u8> = [
7491 frame(b'T', b"rowdesc"),
7492 frame(b'D', b"row"),
7493 frame(b'C', b"SELECT 1\0"),
7494 frame(b'Z', b"I"),
7495 ]
7496 .concat();
7497 let with_notify: Vec<u8> = [
7498 frame(b'T', b"rowdesc"),
7499 frame(b'A', b"\x00\x00\x30\x39chan\0payload\0"),
7500 frame(b'D', b"row"),
7501 frame(b'C', b"SELECT 1\0"),
7502 frame(b'Z', b"I"),
7503 ]
7504 .concat();
7505 let with_param_status: Vec<u8> = [
7506 frame(b'D', b"row"),
7507 frame(b'C', b"SELECT 1\0"),
7508 frame(b'S', b"TimeZone\0UTC\0"),
7509 frame(b'Z', b"I"),
7510 ]
7511 .concat();
7512
7513 for (bytes, want_cacheable) in [
7514 (clean, true),
7515 (with_notify, false),
7516 (with_param_status, false),
7517 ] {
7518 let (mut backend, mut backend_peer) = pair().await;
7519 let (client_raw, mut client_peer) = pair().await;
7520 let mut client = ClientStream::Plain(client_raw);
7521 let session = make_test_session();
7522
7523 backend_peer.write_all(&bytes).await.unwrap();
7524 backend_peer.flush().await.unwrap();
7525
7526 let (sent, captured, cacheable, _rows) =
7527 ProxyServer::stream_until_ready_capture(&mut client, &mut backend, &session)
7528 .await
7529 .expect("capture ok");
7530 assert_eq!(cacheable, want_cacheable, "cacheable flag");
7531 assert_eq!(sent as usize, bytes.len());
7532 assert_eq!(captured, bytes, "capture is byte-exact");
7533
7534 let mut got = vec![0u8; bytes.len()];
7537 client_peer.read_exact(&mut got).await.unwrap();
7538 assert_eq!(got, bytes, "forwarding must not be filtered");
7539 }
7540 }
7541
7542 #[cfg(feature = "query-rewriting")]
7545 mod query_rewriting {
7546 use crate::rewriter::{
7547 QueryPattern, QueryRewriter, RewriteRule, RewriterConfig, Transformation,
7548 };
7549
7550 fn rw_with_table_replace() -> QueryRewriter {
7551 let rw = QueryRewriter::new(RewriterConfig {
7552 enabled: true,
7553 ..Default::default()
7554 });
7555 rw.add_rule(
7556 RewriteRule::build("t")
7557 .pattern(QueryPattern::Table("a".to_string()))
7558 .transform(Transformation::ReplaceTable {
7559 from: "a".to_string(),
7560 to: "b".to_string(),
7561 })
7562 .build(),
7563 );
7564 rw
7565 }
7566
7567 #[test]
7568 fn matching_query_is_rewritten() {
7569 let res = rw_with_table_replace().rewrite("select * from a").unwrap();
7570 assert!(res.was_rewritten(), "rule did not fire");
7571 assert!(res.query().contains('b'), "rewritten: {}", res.query());
7572 assert!(
7573 !res.query().contains("from a"),
7574 "still references a: {}",
7575 res.query()
7576 );
7577 }
7578
7579 #[test]
7580 fn unmatched_query_is_unchanged() {
7581 let res = rw_with_table_replace()
7582 .rewrite("select * from other")
7583 .unwrap();
7584 assert!(!res.was_rewritten());
7585 assert_eq!(res.query(), "select * from other");
7586 }
7587 }
7588
7589 #[cfg(feature = "multi-tenancy")]
7592 mod multi_tenancy {
7593 use crate::multi_tenancy::{
7594 IdentificationMethod, IsolationStrategy, MultiTenancyConfig, TenantConfig, TenantId,
7595 TenantManager, TenantManagerBuilder, TenantQueryTransformer,
7596 };
7597
7598 fn manager() -> TenantManager {
7599 let transformer = TenantQueryTransformer::new().register_tables(&["t"], "tid");
7600 let tm = TenantManagerBuilder::new()
7601 .config(MultiTenancyConfig {
7602 enabled: true,
7603 identification: IdentificationMethod::Header {
7604 header_name: "application_name".to_string(),
7605 },
7606 ..Default::default()
7607 })
7608 .query_transformer(transformer)
7609 .build();
7610 tm.register_tenant(TenantConfig::new(
7611 TenantId::new("acme"),
7612 IsolationStrategy::row("public", "tid"),
7613 ));
7614 tm
7615 }
7616
7617 #[test]
7618 fn tenant_table_gets_filter() {
7619 let res = manager().transform_query("select * from t", &TenantId::new("acme"));
7620 assert!(res.transformed, "expected a tenant filter to be injected");
7621 let q = res.query.to_lowercase();
7622 assert!(
7623 q.contains("tid") && q.contains("acme"),
7624 "filter missing: {}",
7625 res.query
7626 );
7627 }
7628
7629 #[test]
7630 fn non_tenant_table_passes_through() {
7631 let res = manager().transform_query("select * from other", &TenantId::new("acme"));
7632 assert!(!res.transformed);
7633 }
7634 }
7635
7636 #[cfg(feature = "ha-tr")]
7639 mod ha_tr {
7640 use crate::transaction_journal::TransactionJournal;
7641 use crate::NodeId;
7642
7643 #[tokio::test]
7644 async fn journal_records_and_windows_a_statement() {
7645 let j = TransactionJournal::new();
7646 let from = chrono::Utc::now() - chrono::Duration::seconds(60);
7647 let tx = uuid::Uuid::new_v4();
7648 j.begin_transaction(tx, uuid::Uuid::new_v4(), NodeId::new(), 0)
7649 .await
7650 .unwrap();
7651 j.log_statement(
7652 tx,
7653 "insert into t values (1)".to_string(),
7654 Vec::new(),
7655 None,
7656 None,
7657 0,
7658 )
7659 .await
7660 .unwrap();
7661 let to = chrono::Utc::now() + chrono::Duration::seconds(60);
7662 let entries = j.entries_in_window(from, to).await;
7663 assert_eq!(entries.len(), 1, "journaled statement should be in window");
7664 assert!(entries[0].1.statement.contains("insert"));
7665 }
7666 }
7667
7668 #[cfg(feature = "schema-routing")]
7671 mod schema_routing {
7672 use crate::schema_routing::{QueryAnalyzer, SchemaRegistry};
7673 use std::sync::Arc;
7674
7675 fn analyzer() -> QueryAnalyzer {
7676 QueryAnalyzer::new(Arc::new(SchemaRegistry::new()))
7677 }
7678
7679 #[test]
7680 fn aggregation_group_by_is_analytics() {
7681 let a = analyzer();
7682 assert!(a
7683 .analyze("select count(*) from orders group by region")
7684 .is_analytics());
7685 }
7686
7687 #[test]
7688 fn simple_point_query_is_not_analytics() {
7689 let a = analyzer();
7690 assert!(!a
7691 .analyze("select * from orders where id = 1")
7692 .is_analytics());
7693 }
7694 }
7695
7696 #[tokio::test]
7699 async fn session_guard_deregisters_on_drop() {
7700 let server = ProxyServer::new(test_config()).unwrap();
7701 let state = server.state.clone();
7702 let session = make_test_session();
7703 state.sessions.insert(session.id, session.clone());
7704 assert_eq!(state.sessions.len(), 1);
7705 let before = state.metrics.connections_closed.load(Ordering::Relaxed);
7706 {
7707 let _g = SessionGuard {
7708 state: state.clone(),
7709 session_id: session.id,
7710 };
7711 }
7712 assert!(state.sessions.is_empty(), "guard must deregister on drop");
7713 assert_eq!(
7714 state.metrics.connections_closed.load(Ordering::Relaxed),
7715 before + 1
7716 );
7717 }
7718
7719 #[tokio::test]
7722 async fn session_guard_deregisters_on_panic() {
7723 let server = ProxyServer::new(test_config()).unwrap();
7724 let state = server.state.clone();
7725 let session = make_test_session();
7726 state.sessions.insert(session.id, session.clone());
7727 let sid = session.id;
7728 let st = state.clone();
7729 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
7730 let _g = SessionGuard {
7731 state: st,
7732 session_id: sid,
7733 };
7734 panic!("simulated connection-task panic");
7735 }));
7736 assert!(r.is_err(), "closure must have panicked");
7737 assert!(
7738 state.sessions.is_empty(),
7739 "guard must deregister on a panic unwind"
7740 );
7741 }
7742
7743 #[cfg(feature = "pool-modes")]
7748 #[test]
7749 fn stmt_classifier_is_conservative() {
7750 let clean = ProxyServer::stmt_leaves_session_state;
7751 assert!(!clean(
7753 "SELECT abalance FROM pgbench_accounts WHERE aid = 12345"
7754 ));
7755 assert!(!clean("SELECT 1"));
7756 assert!(!clean("SELECT 1;")); assert!(!clean(" select now() ")); assert!(!clean("INSERT INTO t VALUES (1)")); assert!(!clean("UPDATE t SET c = 1 WHERE id = 2")); assert!(!clean("DELETE FROM t WHERE id = 3"));
7761 assert!(!clean("WITH x AS (SELECT 1) SELECT * FROM x"));
7762 assert!(!clean("SELECT into_total FROM ledger")); assert!(!clean("BEGIN"));
7764 assert!(!clean("COMMIT"));
7765 assert!(!clean("SELECT current_setting('work_mem')")); assert!(clean("SET work_mem = '1GB'"), "SET GUC");
7769 assert!(clean("set search_path to public"), "lowercase SET");
7770 assert!(clean("CREATE TEMP TABLE t(x int)"), "temp table");
7771 assert!(clean("CREATE TEMPORARY TABLE t(x int)"), "temp table");
7772 assert!(clean("SELECT * INTO TEMP t FROM src"), "SELECT INTO temp");
7773 assert!(clean("select a into t from s"), "SELECT INTO lowercase");
7774 assert!(clean("PREPARE p AS SELECT 1"), "prepared statement");
7775 assert!(clean("DEALLOCATE p"), "deallocate");
7776 assert!(
7777 clean("DECLARE c CURSOR WITH HOLD FOR SELECT 1"),
7778 "held cursor"
7779 );
7780 assert!(clean("LISTEN my_channel"), "listen");
7781 assert!(clean("SELECT pg_advisory_lock(42)"), "advisory lock");
7782 assert!(clean("SELECT pg_try_advisory_lock(1)"), "try advisory lock");
7783 assert!(
7784 clean("SELECT set_config('work_mem','1GB',false)"),
7785 "set_config fn"
7786 );
7787 assert!(clean("SELECT nextval('s')"), "sequence cache");
7788 assert!(clean("SET ROLE admin"), "set role");
7789 assert!(clean("SET SESSION AUTHORIZATION bob"), "session auth");
7790 assert!(clean("DISCARD ALL"), "explicit discard");
7791 assert!(clean("RESET ALL"), "reset");
7792 assert!(clean("SELECT 1; SET work_mem='1GB'"), "hidden SET after ;");
7794 assert!(
7795 clean("SELECT 1; CREATE TEMP TABLE t(x int)"),
7796 "hidden temp after ;"
7797 );
7798 assert!(clean("SELECT 'a;b'"), "semicolon in literal");
7800 assert!(clean("COPY t FROM STDIN"), "copy");
7802 assert!(clean("GRANT SELECT ON t TO bob"), "grant");
7803 assert!(clean("ALTER TABLE t ADD COLUMN c int"), "ddl");
7804 }
7805
7806 #[cfg(feature = "pool-modes")]
7811 #[tokio::test]
7812 async fn reset_backend_rejects_error_and_nonidle() {
7813 use tokio::io::AsyncWriteExt as _;
7814 fn frame(tag: u8, body: &[u8]) -> Vec<u8> {
7815 let mut v = vec![tag];
7816 v.extend_from_slice(&((body.len() + 4) as u32).to_be_bytes());
7817 v.extend_from_slice(body);
7818 v
7819 }
7820 let rfq = |st: u8| frame(b'Z', &[st]);
7821 let cc = frame(b'C', b"DISCARD ALL\0");
7822 let err = frame(b'E', b"SERROR\0C25P02\0Mreset failed\0\0");
7823
7824 let (mut client, mut server) = tokio::io::duplex(4096);
7826 let mut resp = cc.clone();
7827 resp.extend_from_slice(&rfq(b'I'));
7828 server.write_all(&resp).await.unwrap();
7829 assert!(
7830 ProxyServer::reset_backend(&mut client, "DISCARD ALL")
7831 .await
7832 .is_ok(),
7833 "clean reset must succeed"
7834 );
7835
7836 let (mut client, mut server) = tokio::io::duplex(4096);
7838 let mut resp = err.clone();
7839 resp.extend_from_slice(&rfq(b'I'));
7840 server.write_all(&resp).await.unwrap();
7841 assert!(
7842 ProxyServer::reset_backend(&mut client, "DISCARD ALL")
7843 .await
7844 .is_err(),
7845 "reset that errored must be rejected"
7846 );
7847
7848 let (mut client, mut server) = tokio::io::duplex(4096);
7850 let mut resp = cc.clone();
7851 resp.extend_from_slice(&rfq(b'T'));
7852 server.write_all(&resp).await.unwrap();
7853 assert!(
7854 ProxyServer::reset_backend(&mut client, "DISCARD ALL")
7855 .await
7856 .is_err(),
7857 "reset leaving a non-idle txn must be rejected"
7858 );
7859 }
7860
7861 #[cfg(feature = "pool-modes")]
7867 #[tokio::test]
7868 async fn pool_key_folds_startup_params() {
7869 let base = make_test_session();
7870 {
7871 let mut v = base.variables.write().await;
7872 v.insert("user".into(), "u".into());
7873 v.insert("database".into(), "d".into());
7874 }
7875 let k_plain = ProxyServer::pool_key_for("n:5432", &base).await;
7876 assert_eq!(k_plain, crate::pool::pool_key("n:5432", "u", "d"));
7877
7878 let utf8 = make_test_session();
7881 let latin1 = make_test_session();
7882 for (s, enc) in [(&utf8, "UTF8"), (&latin1, "LATIN1")] {
7883 let mut v = s.variables.write().await;
7884 v.insert("user".into(), "u".into());
7885 v.insert("database".into(), "d".into());
7886 v.insert("client_encoding".into(), enc.into());
7887 }
7888 let k_utf8 = ProxyServer::pool_key_for("n:5432", &utf8).await;
7889 let k_latin1 = ProxyServer::pool_key_for("n:5432", &latin1).await;
7890 assert_ne!(k_utf8, k_latin1, "different client_encoding must not share");
7891 assert_ne!(k_utf8, k_plain, "GUC-bearing key must differ from bare key");
7892 }
7893}