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
153#[derive(Debug, Clone)]
159struct ResolvedLimits {
160 max_cancel_keys: usize,
161 startup_timeout: Duration,
162 backend_write_timeout: Duration,
163 backend_read_timeout: Duration,
164 client_write_timeout: Duration,
165 reprepare_timeout: Duration,
166 max_prepared_statements: usize,
167 max_prepared_bytes: usize,
168 max_pending_bytes: usize,
169 #[cfg(feature = "pool-modes")]
172 max_total_idle_backend_conns: usize,
173 pool_reap_interval: Duration,
174}
175
176impl ResolvedLimits {
177 fn from_toml(l: &crate::config::LimitsToml) -> Self {
178 Self {
179 max_cancel_keys: l.max_cancel_keys,
180 startup_timeout: Duration::from_secs(l.startup_timeout_secs),
181 backend_write_timeout: Duration::from_secs(l.backend_write_timeout_secs),
182 backend_read_timeout: Duration::from_secs(l.backend_read_timeout_secs),
183 client_write_timeout: Duration::from_secs(l.client_write_timeout_secs),
184 reprepare_timeout: Duration::from_secs(l.reprepare_timeout_secs),
185 max_prepared_statements: l.max_prepared_statements,
186 max_prepared_bytes: l.max_prepared_bytes,
187 max_pending_bytes: l.max_pending_bytes,
188 #[cfg(feature = "pool-modes")]
189 max_total_idle_backend_conns: l.max_total_idle_backend_conns,
190 pool_reap_interval: Duration::from_secs(l.pool_reap_interval_secs),
191 }
192 }
193}
194
195impl Default for ResolvedLimits {
196 fn default() -> Self {
197 Self::from_toml(&crate::config::LimitsToml::default())
198 }
199}
200
201struct ServerState {
203 limits: ResolvedLimits,
205 sessions: DashMap<Uuid, Arc<ClientSession>>,
211 health: ArcSwap<HashMap<String, NodeHealth>>,
217 health_write: parking_lot::Mutex<()>,
222 live_config: ArcSwap<ProxyConfig>,
229 metrics: ServerMetrics,
231 cancel_map: Arc<DashMap<(u32, u32), String>>,
237 cancel_order: Arc<parking_lot::Mutex<std::collections::VecDeque<(u32, u32)>>>,
241 tls_acceptor: Option<tokio_rustls::TlsAcceptor>,
244 auth_file: Option<Arc<crate::auth_scram::AuthFile>>,
248 mirror: Option<crate::mirror::MirrorHandle>,
251 cutover: Arc<ArcSwap<Option<Arc<crate::mirror::CutoverTarget>>>>,
255 lb_state: LoadBalancerState,
257 #[cfg(feature = "routing-hints")]
262 hint_parser: Option<crate::routing::HintParser>,
263 #[cfg(feature = "rate-limiting")]
266 rate_limiter: Option<Arc<crate::rate_limit::RateLimiter>>,
267 #[cfg(feature = "circuit-breaker")]
271 circuit_breaker: Option<Arc<crate::circuit_breaker::CircuitBreakerManager>>,
272 #[cfg(feature = "query-analytics")]
275 analytics: Option<Arc<crate::analytics::QueryAnalytics>>,
276 #[cfg(feature = "query-cache")]
279 query_cache: Option<Arc<crate::cache::QueryCache>>,
280 #[cfg(feature = "query-rewriting")]
283 rewriter: Option<Arc<crate::rewriter::QueryRewriter>>,
284 #[cfg(feature = "multi-tenancy")]
287 tenant_manager: Option<Arc<crate::multi_tenancy::TenantManager>>,
288 #[cfg(feature = "schema-routing")]
291 schema_analyzer: Option<Arc<crate::schema_routing::QueryAnalyzer>>,
292 #[cfg(feature = "pool-modes")]
294 pool_manager: Option<Arc<ConnectionPoolManager>>,
295 #[cfg(feature = "pool-modes")]
300 backend_pool: Option<Arc<crate::pool::BackendIdlePool>>,
301 #[cfg(feature = "wasm-plugins")]
305 plugin_manager: Option<Arc<PluginManager>>,
306 #[cfg(feature = "ha-tr")]
311 transaction_journal: Arc<crate::transaction_journal::TransactionJournal>,
312 #[cfg(feature = "anomaly-detection")]
315 anomaly_detector: Arc<crate::anomaly::AnomalyDetector>,
316 #[cfg(feature = "edge-proxy")]
320 edge_cache: Arc<crate::edge::EdgeCache>,
321 #[cfg(feature = "edge-proxy")]
322 edge_registry: Arc<crate::edge::EdgeRegistry>,
323}
324
325#[derive(Debug, Clone)]
327pub struct NodeHealth {
328 pub address: String,
330 pub healthy: bool,
332 pub last_check: chrono::DateTime<chrono::Utc>,
334 pub failure_count: u32,
336 pub last_error: Option<String>,
338 pub latency_ms: f64,
340 pub replication_lag_bytes: Option<u64>,
342}
343
344#[derive(Default)]
346struct ServerMetrics {
347 connections_accepted: AtomicU64,
349 connections_closed: AtomicU64,
351 queries_processed: AtomicU64,
353 bytes_received: AtomicU64,
355 bytes_sent: AtomicU64,
357 failovers: AtomicU64,
359}
360
361struct LoadBalancerState {
363 rr_counter: AtomicU64,
366}
367
368pub struct ClientSession {
370 pub id: Uuid,
372 pub client_addr: SocketAddr,
374 pub current_node: RwLock<Option<String>>,
376 pub in_transaction: std::sync::atomic::AtomicBool,
384 pub copy_in_progress: std::sync::atomic::AtomicBool,
392 pub tx_state: RwLock<TransactionState>,
396 pub variables: RwLock<HashMap<String, String>>,
398 pub created_at: chrono::DateTime<chrono::Utc>,
400 pub tr_mode: TrMode,
402 #[cfg(feature = "lag-routing")]
407 pub last_write_at: RwLock<Option<std::time::Instant>>,
408 #[cfg(feature = "pool-modes")]
410 pub pool_client_id: ClientId,
411 #[cfg(feature = "wasm-plugins")]
416 pub plugin_identity: RwLock<Option<Identity>>,
417 #[cfg(feature = "edge-proxy")]
424 pub edge_ineligible: std::sync::atomic::AtomicBool,
425 #[cfg(feature = "edge-proxy")]
429 pub pending_edge_copy_tables: std::sync::Mutex<Option<Vec<String>>>,
430}
431
432#[derive(Debug, Clone, Default)]
434pub struct TransactionState {
435 pub in_transaction: bool,
437 pub tx_id: Option<Uuid>,
439 pub statements: Vec<StatementLog>,
441 pub read_only: bool,
443 pub savepoints: Vec<String>,
445}
446
447#[derive(Debug, Clone)]
449pub struct StatementLog {
450 pub sql: String,
452 pub params: Vec<String>,
454 pub result_checksum: Option<u64>,
456 pub executed_at: chrono::DateTime<chrono::Utc>,
458}
459
460struct BackendConn {
473 stream: TcpStream,
474 prepared: HashSet<String>,
475 unnamed_sig: Option<bytes::Bytes>,
481 dirty: bool,
491}
492
493impl BackendConn {
494 fn new(stream: TcpStream) -> Self {
495 Self {
496 stream,
497 prepared: HashSet::new(),
498 unnamed_sig: None,
499 dirty: false,
500 }
501 }
502}
503
504pub(crate) fn bind_reuseport(addr: &str) -> Result<TcpListener> {
510 use socket2::{Domain, Protocol, Socket, Type};
511 let sockaddr: SocketAddr = addr
512 .parse()
513 .map_err(|e| ProxyError::Config(format!("invalid listen address '{}': {}", addr, e)))?;
514 let domain = if sockaddr.is_ipv6() {
515 Domain::IPV6
516 } else {
517 Domain::IPV4
518 };
519 let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))
520 .map_err(|e| ProxyError::Network(format!("socket(): {}", e)))?;
521 socket
522 .set_reuse_address(true)
523 .map_err(|e| ProxyError::Network(format!("SO_REUSEADDR: {}", e)))?;
524 #[cfg(all(unix, not(target_os = "solaris")))]
525 socket
526 .set_reuse_port(true)
527 .map_err(|e| ProxyError::Network(format!("SO_REUSEPORT: {}", e)))?;
528 socket
529 .set_nonblocking(true)
530 .map_err(|e| ProxyError::Network(format!("set_nonblocking: {}", e)))?;
531 socket
532 .bind(&sockaddr.into())
533 .map_err(|e| ProxyError::Network(format!("Failed to bind {}: {}", addr, e)))?;
534 socket
535 .listen(1024)
536 .map_err(|e| ProxyError::Network(format!("listen(): {}", e)))?;
537 let std_listener: std::net::TcpListener = socket.into();
538 TcpListener::from_std(std_listener)
539 .map_err(|e| ProxyError::Network(format!("from_std listener: {}", e)))
540}
541
542#[derive(Debug)]
548#[allow(dead_code)] enum PreQueryAction {
550 Forward,
552 Block(String),
555 Cached(Vec<u8>),
561}
562
563#[derive(Debug)]
569#[allow(dead_code)] enum RouteOverride {
571 None,
573 Primary,
575 Standby,
577 Node(String),
581 Block(String),
586}
587
588struct SessionGuard {
595 state: Arc<ServerState>,
596 session_id: Uuid,
597}
598
599impl Drop for SessionGuard {
600 fn drop(&mut self) {
601 self.state.sessions.remove(&self.session_id);
602 self.state
603 .metrics
604 .connections_closed
605 .fetch_add(1, Ordering::Relaxed);
606 #[cfg(feature = "query-cache")]
609 if let Some(ref qc) = self.state.query_cache {
610 qc.remove_l1_cache(self.session_id.as_u64_pair().0);
611 }
612 }
613}
614
615impl ProxyServer {
616 #[cfg(feature = "wasm-plugins")]
624 fn init_plugin_manager(
625 toml_cfg: &crate::config::PluginToml,
626 ) -> Option<Arc<crate::plugins::PluginManager>> {
627 if !toml_cfg.enabled {
628 return None;
629 }
630
631 let runtime_cfg = crate::plugins::PluginRuntimeConfig::from(toml_cfg);
632 let plugin_dir = runtime_cfg.plugin_dir.clone();
633
634 let pm = match crate::plugins::PluginManager::new(runtime_cfg) {
635 Ok(pm) => Arc::new(pm),
636 Err(e) => {
637 tracing::error!(error = %e, "Failed to create plugin manager; plugins disabled");
638 return None;
639 }
640 };
641
642 match std::fs::read_dir(&plugin_dir) {
643 Ok(entries) => {
644 let mut loaded = 0usize;
645 let mut failed = 0usize;
646 for entry in entries.flatten() {
647 let path = entry.path();
648 if path.extension().and_then(|s| s.to_str()) != Some("wasm") {
649 continue;
650 }
651 match pm.load_plugin(&path) {
652 Ok(()) => loaded += 1,
653 Err(e) => {
654 failed += 1;
655 tracing::warn!(
656 path = %path.display(),
657 error = %e,
658 "Failed to load plugin"
659 );
660 }
661 }
662 }
663 tracing::info!(
664 dir = %plugin_dir.display(),
665 loaded = loaded,
666 failed = failed,
667 "Plugin loading complete"
668 );
669 }
670 Err(e) => {
671 tracing::warn!(
672 dir = %plugin_dir.display(),
673 error = %e,
674 "Plugin directory not readable; no plugins loaded"
675 );
676 }
677 }
678
679 Some(pm)
680 }
681
682 pub fn new(config: ProxyConfig) -> Result<Self> {
684 let (shutdown_tx, _) = broadcast::channel(1);
685
686 let resolved_limits = ResolvedLimits::from_toml(&config.limits);
689
690 let mut health = HashMap::new();
692 for node in &config.nodes {
693 health.insert(
694 node.address(),
695 NodeHealth {
696 address: node.address(),
697 healthy: true, last_check: chrono::Utc::now(),
699 failure_count: 0,
700 last_error: None,
701 latency_ms: 0.0,
702 replication_lag_bytes: None,
703 },
704 );
705 }
706
707 #[cfg(feature = "pool-modes")]
709 let pool_manager = {
710 use crate::pool::PreparedStatementMode as PoolPreparedStatementMode;
711
712 let pool_config = PoolModeConfig {
713 default_mode: match config.pool_mode.mode {
714 crate::config::PoolingMode::Session => PoolingMode::Session,
715 crate::config::PoolingMode::Transaction => PoolingMode::Transaction,
716 crate::config::PoolingMode::Statement => PoolingMode::Statement,
717 },
718 max_pool_size: config.pool_mode.max_pool_size,
719 min_idle: config.pool_mode.min_idle,
720 idle_timeout_secs: config.pool_mode.idle_timeout_secs,
721 max_lifetime_secs: config.pool_mode.max_lifetime_secs,
722 acquire_timeout_secs: config.pool_mode.acquire_timeout_secs,
723 reset_query: config.pool_mode.reset_query.clone(),
724 prepared_statement_mode: match config.pool_mode.prepared_statement_mode {
725 crate::config::PreparedStatementMode::Disable => {
726 PoolPreparedStatementMode::Disable
727 }
728 crate::config::PreparedStatementMode::Track => PoolPreparedStatementMode::Track,
729 crate::config::PreparedStatementMode::Named => PoolPreparedStatementMode::Named,
730 },
731 test_on_acquire: config.pool.test_on_acquire,
732 validation_query: "SELECT 1".to_string(),
733 queue_timeout_secs: 30,
734 max_queue_size: 0,
735 };
736 Some(Arc::new(ConnectionPoolManager::new(pool_config)))
737 };
738
739 #[cfg(feature = "pool-modes")]
743 let backend_pool = match config.pool_mode.mode {
744 crate::config::PoolingMode::Transaction | crate::config::PoolingMode::Statement => {
745 tracing::info!(
746 mode = ?config.pool_mode.mode,
747 max_idle_per_identity = config.pool_mode.max_pool_size,
748 "pool-modes: data-path connection pooling enabled"
749 );
750 Some(Arc::new(crate::pool::BackendIdlePool::new(
751 config.pool_mode.max_pool_size as usize,
752 resolved_limits.max_total_idle_backend_conns,
753 )))
754 }
755 crate::config::PoolingMode::Session => None,
756 };
757
758 #[cfg(feature = "wasm-plugins")]
763 let plugin_manager = Self::init_plugin_manager(&config.plugins);
764
765 let tls_acceptor = match config.tls.as_ref() {
769 Some(tls) if tls.enabled => match build_tls_acceptor(tls) {
770 Ok(acc) => {
771 tracing::info!(
772 mtls = tls.require_client_cert,
773 "client TLS termination enabled"
774 );
775 Some(acc)
776 }
777 Err(e) => {
778 return Err(ProxyError::Config(format!("TLS init failed: {}", e)));
779 }
780 },
781 _ => None,
782 };
783
784 let auth_file = if config.auth.mode == crate::config::AuthMode::Scram {
787 let path = config.auth.auth_file.as_ref().ok_or_else(|| {
788 ProxyError::Config("auth mode 'scram' requires auth_file".to_string())
789 })?;
790 let af = crate::auth_scram::AuthFile::load(path)
791 .map_err(|e| ProxyError::Config(format!("auth_file: {}", e)))?;
792 tracing::info!(users = %(!af.is_empty()), "proxy SCRAM auth enabled");
793 Some(Arc::new(af))
794 } else {
795 None
796 };
797
798 let mirror = if config.mirror.enabled {
801 tracing::info!(target = %format!("{}:{}", config.mirror.backend_host, config.mirror.backend_port),
802 writes_only = config.mirror.writes_only, "traffic mirroring enabled");
803 Some(crate::mirror::spawn(config.mirror.clone()))
804 } else {
805 None
806 };
807
808 #[cfg(feature = "rate-limiting")]
810 let rate_limiter = if config.rate_limit.enabled {
811 let rl = &config.rate_limit;
812 tracing::info!(
813 qps = rl.default_qps,
814 burst = rl.default_burst,
815 key_by = ?rl.key_by,
816 "rate limiting enabled"
817 );
818 let rlc = crate::rate_limit::RateLimitConfig {
819 enabled: true,
820 default_qps: rl.default_qps,
821 default_burst: rl.default_burst,
822 default_concurrency: if rl.max_concurrent > 0 {
823 rl.max_concurrent
824 } else {
825 crate::rate_limit::RateLimitConfig::default().default_concurrency
826 },
827 ..Default::default()
828 };
829 Some(Arc::new(crate::rate_limit::RateLimiter::new(rlc)))
830 } else {
831 None
832 };
833
834 #[cfg(feature = "circuit-breaker")]
836 let circuit_breaker = if config.circuit_breaker.enabled {
837 let cb = &config.circuit_breaker;
838 tracing::info!(
839 failure_threshold = cb.failure_threshold,
840 open_secs = cb.open_secs,
841 "circuit breaker enabled"
842 );
843 let cbc = crate::circuit_breaker::CircuitBreakerConfig {
844 failure_threshold: cb.failure_threshold,
845 cooldown: Duration::from_secs(cb.open_secs),
846 half_open_success_threshold: cb.success_threshold,
847 ..Default::default()
848 };
849 let mgr = crate::circuit_breaker::CircuitBreakerManager::new(
850 crate::circuit_breaker::ManagerConfig::new(cbc),
851 );
852 Some(Arc::new(mgr))
853 } else {
854 None
855 };
856
857 #[cfg(feature = "query-analytics")]
859 let analytics = if config.analytics.enabled {
860 let a = &config.analytics;
861 tracing::info!(
862 slow_query_ms = a.slow_query_ms,
863 max_fingerprints = a.max_fingerprints,
864 "query analytics enabled"
865 );
866 let ac = crate::analytics::AnalyticsConfig {
867 enabled: true,
868 max_fingerprints: a.max_fingerprints as usize,
869 slow_query: crate::analytics::SlowQueryConfig {
870 threshold: Duration::from_millis(a.slow_query_ms),
871 ..Default::default()
872 },
873 ..Default::default()
874 };
875 Some(Arc::new(crate::analytics::QueryAnalytics::new(ac)))
876 } else {
877 None
878 };
879
880 #[cfg(feature = "query-cache")]
882 let query_cache = if config.cache.enabled {
883 let c = &config.cache;
884 tracing::info!(
885 ttl_secs = c.ttl_secs,
886 max_result_bytes = c.max_result_bytes,
887 "query cache enabled (L1 hot + L2 warm)"
888 );
889 let ttl = Duration::from_secs(c.ttl_secs);
890 let cc = crate::cache::CacheConfig {
891 enabled: true,
892 default_ttl: ttl,
893 max_result_size: c.max_result_bytes,
894 l1: crate::cache::L1Config {
895 ttl,
896 ..Default::default()
897 },
898 l2: crate::cache::L2Config {
899 ttl,
900 ..Default::default()
901 },
902 ..Default::default()
903 };
904 Some(Arc::new(crate::cache::QueryCache::new(cc)))
905 } else {
906 None
907 };
908
909 #[cfg(feature = "query-rewriting")]
911 let rewriter = if config.query_rewrite.enabled && !config.query_rewrite.rules.is_empty() {
912 use crate::rewriter::{
913 QueryPattern, QueryRewriter, RewriteRule, RewriterConfig, Transformation,
914 };
915 let rw = QueryRewriter::new(RewriterConfig {
916 enabled: true,
917 ..Default::default()
918 });
919 let mut n = 0usize;
920 for (i, r) in config.query_rewrite.rules.iter().enumerate() {
921 let transformation =
922 if let (Some(from), Some(to)) = (&r.match_table, &r.replace_table_with) {
923 Transformation::ReplaceTable {
924 from: from.clone(),
925 to: to.clone(),
926 }
927 } else if let Some(w) = &r.append_where {
928 Transformation::AppendWhereAnd(w.clone())
929 } else if let Some(limit) = r.add_limit {
930 Transformation::AddLimit(limit)
931 } else {
932 continue; };
934 let pattern = if let Some(t) = &r.match_table {
935 QueryPattern::Table(t.clone())
936 } else if let Some(re) = &r.match_regex {
937 QueryPattern::regex(re.clone())
938 } else {
939 QueryPattern::All
940 };
941 rw.add_rule(
942 RewriteRule::build(format!("rule-{i}"))
943 .pattern(pattern)
944 .transform(transformation)
945 .build(),
946 );
947 n += 1;
948 }
949 tracing::info!(rules = n, "query rewriting enabled");
950 Some(Arc::new(rw))
951 } else {
952 None
953 };
954
955 #[cfg(feature = "multi-tenancy")]
957 let tenant_manager =
958 if config.multi_tenancy.enabled && !config.multi_tenancy.tenants.is_empty() {
959 use crate::multi_tenancy::{
960 IdentificationMethod, IsolationStrategy, MultiTenancyConfig, TenantConfig,
961 TenantId, TenantManagerBuilder, TenantQueryTransformer,
962 };
963 let mt = &config.multi_tenancy;
964 let identification = match mt.identify_by.as_str() {
965 "database" => IdentificationMethod::DatabaseName,
966 param => IdentificationMethod::Header {
967 header_name: param.to_string(),
968 },
969 };
970 let mtc = MultiTenancyConfig {
971 enabled: true,
972 identification,
973 ..Default::default()
974 };
975 let table_refs: Vec<&str> = mt.tenant_tables.iter().map(|s| s.as_str()).collect();
977 let transformer = TenantQueryTransformer::new()
978 .register_tables(&table_refs, mt.tenant_column.clone());
979 let tm = TenantManagerBuilder::new()
980 .config(mtc)
981 .query_transformer(transformer)
982 .build();
983 for id in &mt.tenants {
984 tm.register_tenant(TenantConfig::new(
985 TenantId::new(id.clone()),
986 IsolationStrategy::row("public", mt.tenant_column.clone()),
987 ));
988 }
989 tracing::info!(
990 tenants = mt.tenants.len(),
991 identify_by = %mt.identify_by,
992 "multi-tenancy enabled"
993 );
994 Some(Arc::new(tm))
995 } else {
996 None
997 };
998
999 #[cfg(feature = "schema-routing")]
1001 let schema_analyzer =
1002 if config.schema_routing.enabled && !config.schema_routing.analytics_node.is_empty() {
1003 tracing::info!(
1004 analytics_node = %config.schema_routing.analytics_node,
1005 "schema/workload routing enabled (OLAP -> analytics node)"
1006 );
1007 let registry = Arc::new(crate::schema_routing::SchemaRegistry::new());
1008 Some(Arc::new(crate::schema_routing::QueryAnalyzer::new(
1009 registry,
1010 )))
1011 } else {
1012 None
1013 };
1014
1015 let state = Arc::new(ServerState {
1016 limits: resolved_limits,
1017 sessions: DashMap::new(),
1018 health: ArcSwap::from_pointee(health),
1019 health_write: parking_lot::Mutex::new(()),
1020 live_config: ArcSwap::from_pointee(config.clone()),
1021 metrics: ServerMetrics::default(),
1022 cancel_map: Arc::new(DashMap::new()),
1023 cancel_order: Arc::new(parking_lot::Mutex::new(std::collections::VecDeque::new())),
1024 tls_acceptor,
1025 auth_file,
1026 mirror,
1027 cutover: Arc::new(ArcSwap::from_pointee(None)),
1028 lb_state: LoadBalancerState {
1029 rr_counter: AtomicU64::new(0),
1030 },
1031 #[cfg(feature = "routing-hints")]
1032 hint_parser: if config.routing_hints.enabled {
1033 tracing::info!(
1034 strip = config.routing_hints.strip_hints,
1035 "SQL-comment routing hints enabled"
1036 );
1037 Some(if config.routing_hints.strip_hints {
1038 crate::routing::HintParser::new()
1039 } else {
1040 crate::routing::HintParser::without_stripping()
1041 })
1042 } else {
1043 None
1044 },
1045 #[cfg(feature = "rate-limiting")]
1046 rate_limiter,
1047 #[cfg(feature = "circuit-breaker")]
1048 circuit_breaker,
1049 #[cfg(feature = "query-analytics")]
1050 analytics,
1051 #[cfg(feature = "query-cache")]
1052 query_cache,
1053 #[cfg(feature = "query-rewriting")]
1054 rewriter,
1055 #[cfg(feature = "multi-tenancy")]
1056 tenant_manager,
1057 #[cfg(feature = "schema-routing")]
1058 schema_analyzer,
1059 #[cfg(feature = "pool-modes")]
1060 pool_manager,
1061 #[cfg(feature = "pool-modes")]
1062 backend_pool,
1063 #[cfg(feature = "wasm-plugins")]
1064 plugin_manager,
1065 #[cfg(feature = "ha-tr")]
1066 transaction_journal: Arc::new(crate::transaction_journal::TransactionJournal::new()),
1067 #[cfg(feature = "anomaly-detection")]
1068 anomaly_detector: Arc::new(crate::anomaly::AnomalyDetector::new(
1069 config.anomaly.to_anomaly_config(),
1070 )),
1071 #[cfg(feature = "edge-proxy")]
1072 edge_cache: Arc::new(crate::edge::EdgeCache::new(config.edge.max_entries.max(1))),
1073 #[cfg(feature = "edge-proxy")]
1074 edge_registry: Arc::new(crate::edge::EdgeRegistry::new(
1075 config.edge.max_edges,
1076 std::time::Duration::from_secs(config.edge.liveness_window_secs),
1077 )),
1078 });
1079
1080 Ok(Self {
1081 config,
1082 state,
1083 shutdown_tx,
1084 config_path: None,
1085 })
1086 }
1087
1088 pub fn with_config_path(mut self, path: Option<String>) -> Self {
1092 self.config_path = path;
1093 self
1094 }
1095
1096 #[cfg(unix)]
1099 fn hangup_stream() -> tokio::signal::unix::Signal {
1100 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup())
1101 .expect("failed to install SIGHUP handler")
1102 }
1103 #[cfg(not(unix))]
1104 fn hangup_stream() -> HangupNever {
1105 HangupNever
1106 }
1107
1108 #[cfg(unix)]
1111 fn usr2_stream() -> tokio::signal::unix::Signal {
1112 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::user_defined2())
1113 .expect("failed to install SIGUSR2 handler")
1114 }
1115 #[cfg(not(unix))]
1116 fn usr2_stream() -> HangupNever {
1117 HangupNever
1118 }
1119
1120 async fn drain_connections(state: &Arc<ServerState>, timeout: Duration) {
1124 let deadline = tokio::time::Instant::now() + timeout;
1125 loop {
1126 let active = state.sessions.len();
1127 if active == 0 {
1128 tracing::info!("drain complete — all in-flight connections finished");
1129 return;
1130 }
1131 if tokio::time::Instant::now() >= deadline {
1132 tracing::warn!(
1133 active,
1134 "drain timeout reached — exiting with connections still open"
1135 );
1136 return;
1137 }
1138 tokio::time::sleep(Duration::from_millis(200)).await;
1139 }
1140 }
1141
1142 fn drain_timeout(config_secs: u64) -> Duration {
1147 let secs = std::env::var("HELIOS_DRAIN_TIMEOUT_SECS")
1148 .ok()
1149 .and_then(|s| s.parse::<u64>().ok())
1150 .unwrap_or(config_secs);
1151 Duration::from_secs(secs)
1152 }
1153
1154 async fn reload_config(&self) {
1163 let Some(path) = self.config_path.as_deref() else {
1164 tracing::warn!(
1165 "SIGHUP received but config was not loaded from a file — nothing to reload"
1166 );
1167 return;
1168 };
1169 tracing::info!(path, "SIGHUP: reloading configuration");
1170 let mut new_config = match ProxyConfig::from_file(path) {
1171 Ok(c) => c,
1172 Err(e) => {
1173 tracing::error!(path, error = %e, "SIGHUP reload failed to parse — keeping current config");
1174 return;
1175 }
1176 };
1177 let old = self.state.live_config.load_full();
1178 if new_config.edge != old.edge {
1186 tracing::warn!(
1187 "[edge] configuration changed on SIGHUP but is applied at startup only — \
1188 keeping the running edge settings (restart to apply)"
1189 );
1190 new_config.edge = old.edge.clone();
1191 }
1192 if new_config.listen_address != old.listen_address {
1193 tracing::warn!(old = %old.listen_address, new = %new_config.listen_address,
1194 "listen_address change needs a restart/handoff; the bound socket is kept");
1195 }
1196 if new_config.admin_address != old.admin_address {
1197 tracing::warn!(old = %old.admin_address, new = %new_config.admin_address,
1198 "admin_address change needs a restart; the bound socket is kept");
1199 }
1200 Self::reconcile_health(&self.state, &new_config);
1203 let nodes = new_config.nodes.len();
1204 let hba_rules = new_config.hba.len();
1205 let pool_max = new_config.pool.max_connections;
1206 self.state.live_config.store(Arc::new(new_config));
1207 tracing::info!(
1208 nodes,
1209 hba_rules,
1210 pool_max,
1211 "SIGHUP: configuration reloaded — applies to new connections"
1212 );
1213 }
1214
1215 fn reconcile_health(state: &Arc<ServerState>, config: &ProxyConfig) {
1219 let _writers = state.health_write.lock();
1222 let current = state.health.load_full();
1223 let mut next: HashMap<String, NodeHealth> = HashMap::new();
1224 for node in &config.nodes {
1225 let addr = node.address();
1226 match current.get(&addr) {
1227 Some(existing) => {
1228 next.insert(addr, existing.clone());
1229 }
1230 None => {
1231 tracing::info!(node = %addr, "SIGHUP: new node added — seeding healthy");
1232 next.insert(
1233 addr.clone(),
1234 NodeHealth {
1235 address: addr,
1236 healthy: true,
1237 last_check: chrono::Utc::now(),
1238 failure_count: 0,
1239 last_error: None,
1240 latency_ms: 0.0,
1241 replication_lag_bytes: None,
1242 },
1243 );
1244 }
1245 }
1246 }
1247 for gone in current.keys().filter(|k| !next.contains_key(*k)) {
1248 tracing::info!(node = %gone, "SIGHUP: node removed from config");
1249 }
1250 state.health.store(Arc::new(next));
1251 }
1252
1253 pub async fn run(&self) -> Result<()> {
1255 let listener = bind_reuseport(&self.config.listen_address)?;
1261
1262 tracing::info!(
1263 "Proxy listening on {} (SO_REUSEPORT)",
1264 self.config.listen_address
1265 );
1266
1267 let health_task = self.spawn_health_checker();
1269 let pool_task = self.spawn_pool_manager();
1270
1271 #[cfg(feature = "edge-proxy")]
1273 let _edge_maintenance_task = if self.config.edge.enabled {
1274 Some(self.spawn_edge_maintenance())
1275 } else {
1276 None
1277 };
1278
1279 let admin_task = self.spawn_admin_server();
1281
1282 let mcp_task = if self.config.mcp.enabled {
1284 let mcp_cfg = self.config.mcp.clone();
1285 let contract = mcp_cfg.contract.as_ref().and_then(|id| {
1287 let found = self.config.agent_contracts.iter().find(|c| &c.id == id).cloned();
1288 if found.is_none() {
1289 tracing::warn!(%id, "mcp.contract names an unknown agent_contract; gateway runs with only the read-only guardrail");
1290 }
1291 found
1292 });
1293 Some(tokio::spawn(async move {
1294 if let Err(e) = crate::mcp::McpServer::new(mcp_cfg, contract).run().await {
1295 tracing::error!("MCP gateway error: {}", e);
1296 }
1297 }))
1298 } else {
1299 None
1300 };
1301
1302 let http_gw_task = if self.config.http_gateway.enabled {
1304 let gw_cfg = self.config.http_gateway.clone();
1305 Some(tokio::spawn(async move {
1306 if let Err(e) = crate::http_gateway::HttpGateway::new(gw_cfg).run().await {
1307 tracing::error!("HTTP gateway error: {}", e);
1308 }
1309 }))
1310 } else {
1311 None
1312 };
1313
1314 #[cfg(feature = "graphql-gateway")]
1316 let _graphql_gw_task = if self.config.graphql_gateway.enabled {
1317 let gw_cfg = self.config.graphql_gateway.clone();
1318 Some(tokio::spawn(async move {
1319 if let Err(e) = crate::graphql_gateway::GraphqlGateway::new(gw_cfg)
1320 .run()
1321 .await
1322 {
1323 tracing::error!("GraphQL gateway error: {}", e);
1324 }
1325 }))
1326 } else {
1327 None
1328 };
1329
1330 #[cfg(feature = "edge-proxy")]
1333 let _edge_client_task =
1334 if self.config.edge.enabled && self.config.edge.role == crate::edge::EdgeRole::Edge {
1335 tracing::info!(
1336 home_url = %self.config.edge.home_url,
1337 region = %self.config.edge.region,
1338 "edge role: subscribing to home invalidation stream"
1339 );
1340 Some(crate::edge::client::EdgeClient::spawn(
1341 self.config.edge.clone(),
1342 self.state.edge_cache.clone(),
1343 ))
1344 } else {
1345 None
1346 };
1347
1348 let mut shutdown_rx = self.shutdown_tx.subscribe();
1349
1350 let mut sighup = Self::hangup_stream();
1354 let mut sigusr2 = Self::usr2_stream();
1355 let mut graceful = false;
1356
1357 loop {
1358 tokio::select! {
1359 _ = sighup.recv() => {
1360 self.reload_config().await;
1361 }
1362 _ = sigusr2.recv() => {
1363 tracing::info!(
1364 "SIGUSR2: graceful binary-handoff drain — closing the listener so new \
1365 connections route to the sibling process; finishing in-flight connections"
1366 );
1367 graceful = true;
1368 break;
1369 }
1370 accept_result = listener.accept() => {
1371 match accept_result {
1372 Ok((stream, addr)) => {
1373 let _ = stream.set_nodelay(true);
1377 self.state.metrics.connections_accepted.fetch_add(1, Ordering::Relaxed);
1378 let state = self.state.clone();
1379 let config = (*self.state.live_config.load_full()).clone();
1383 let shutdown_tx = self.shutdown_tx.clone();
1384
1385 tokio::spawn(async move {
1386 if let Err(e) = Self::handle_client(stream, addr, state, config, shutdown_tx).await {
1387 tracing::error!("Client handler error: {}", e);
1388 }
1389 });
1390 }
1391 Err(e) => {
1392 tracing::error!("Accept error: {}", e);
1393 }
1394 }
1395 }
1396 _ = shutdown_rx.recv() => {
1397 tracing::info!("Shutdown signal received");
1398 break;
1399 }
1400 }
1401 }
1402
1403 drop(listener);
1407
1408 if graceful {
1411 let timeout =
1412 Self::drain_timeout(self.state.live_config.load().shutdown_drain_timeout_secs);
1413 tracing::info!(
1414 timeout_secs = timeout.as_secs(),
1415 "draining in-flight connections"
1416 );
1417 Self::drain_connections(&self.state, timeout).await;
1418 }
1419
1420 health_task.abort();
1422 pool_task.abort();
1423 admin_task.abort();
1424 if let Some(t) = mcp_task {
1425 t.abort();
1426 }
1427 if let Some(t) = http_gw_task {
1428 t.abort();
1429 }
1430
1431 Ok(())
1432 }
1433
1434 fn spawn_admin_server(&self) -> tokio::task::JoinHandle<()> {
1436 let config = self.config.clone();
1437 let state = self.state.clone();
1438 let mut shutdown_rx = self.shutdown_tx.subscribe();
1439
1440 tokio::spawn(async move {
1441 let admin_state = Arc::new(AdminState::new());
1443
1444 {
1446 let mut snapshot = admin_state.config_snapshot.write().await;
1447 *snapshot = ConfigSnapshot {
1448 listen_address: config.listen_address.clone(),
1449 admin_address: config.admin_address.clone(),
1450 tr_enabled: config.tr_enabled,
1451 tr_mode: format!("{:?}", config.tr_mode),
1452 pool_min_connections: config.pool.min_connections,
1453 pool_max_connections: config.pool.max_connections,
1454 nodes: config
1455 .nodes
1456 .iter()
1457 .map(|n| NodeSnapshot {
1458 address: n.address(),
1459 role: format!("{:?}", n.role),
1460 weight: n.weight,
1461 enabled: n.enabled,
1462 })
1463 .collect(),
1464 };
1465 }
1466
1467 admin_state.set_proxy_config(config.clone()).await;
1469
1470 admin_state
1472 .with_auth_token(config.admin_token.clone())
1473 .await;
1474
1475 if config.branch.enabled {
1477 admin_state.with_branch(config.branch.clone()).await;
1478 }
1479
1480 if let Some(ref mirror) = state.mirror {
1482 admin_state
1483 .with_migration(crate::admin::MigrationInfo {
1484 target: mirror.target().to_string(),
1485 writes_only: mirror.writes_only(),
1486 metrics: mirror.metrics.clone(),
1487 config: config.mirror.clone(),
1488 cutover: state.cutover.clone(),
1489 cutover_target: crate::mirror::CutoverTarget {
1490 addr: format!(
1491 "{}:{}",
1492 config.mirror.backend_host, config.mirror.backend_port
1493 ),
1494 user: config.mirror.backend_user.clone(),
1495 password: config.mirror.backend_password.clone(),
1496 database: config.mirror.backend_database.clone(),
1497 },
1498 })
1499 .await;
1500 }
1501
1502 #[cfg(feature = "wasm-plugins")]
1507 if let Some(ref pm) = state.plugin_manager {
1508 admin_state.with_plugin_manager(pm.clone()).await;
1509 }
1510
1511 #[cfg(feature = "pool-modes")]
1514 if let Some(ref pm) = state.pool_manager {
1515 admin_state.with_pool_manager(pm.clone()).await;
1516 }
1517
1518 #[cfg(feature = "circuit-breaker")]
1521 if let Some(ref cb) = state.circuit_breaker {
1522 admin_state.with_circuit_breaker(cb.clone()).await;
1523 }
1524
1525 #[cfg(feature = "ha-tr")]
1532 {
1533 let template = build_replay_backend_template(&config);
1534 let engine = Arc::new(crate::replay::ReplayEngine::new(
1535 state.transaction_journal.clone(),
1536 template,
1537 ));
1538 admin_state.with_replay_engine(engine).await;
1539 }
1540
1541 #[cfg(feature = "anomaly-detection")]
1545 admin_state
1546 .with_anomaly_detector(state.anomaly_detector.clone())
1547 .await;
1548
1549 #[cfg(feature = "query-analytics")]
1551 if let Some(a) = state.analytics.as_ref() {
1552 admin_state.with_analytics(a.clone()).await;
1553 }
1554
1555 #[cfg(feature = "edge-proxy")]
1558 admin_state
1559 .with_edge(state.edge_cache.clone(), state.edge_registry.clone())
1560 .await;
1561
1562 let admin_server = AdminServer::new(config.admin_address.clone(), admin_state.clone());
1564
1565 let admin_state_sync = admin_state.clone();
1567 let server_state = state.clone();
1568 let sync_task = tokio::spawn(async move {
1569 let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
1570 loop {
1571 interval.tick().await;
1572
1573 {
1575 let health = server_state.health.load_full();
1576 let mut admin_health = admin_state_sync.node_health.write().await;
1577 *admin_health = (*health).clone();
1578 }
1579
1580 {
1582 let metrics = ServerMetricsSnapshot {
1583 connections_accepted: server_state
1584 .metrics
1585 .connections_accepted
1586 .load(Ordering::Relaxed),
1587 connections_closed: server_state
1588 .metrics
1589 .connections_closed
1590 .load(Ordering::Relaxed),
1591 queries_processed: server_state
1592 .metrics
1593 .queries_processed
1594 .load(Ordering::Relaxed),
1595 bytes_received: server_state
1596 .metrics
1597 .bytes_received
1598 .load(Ordering::Relaxed),
1599 bytes_sent: server_state.metrics.bytes_sent.load(Ordering::Relaxed),
1600 failovers: server_state.metrics.failovers.load(Ordering::Relaxed),
1601 };
1602 let mut admin_metrics = admin_state_sync.metrics.write().await;
1603 *admin_metrics = metrics;
1604 }
1605
1606 {
1608 let mut admin_sessions = admin_state_sync.active_sessions.write().await;
1609 *admin_sessions = server_state.sessions.len() as u64;
1610 }
1611 }
1612 });
1613
1614 tokio::select! {
1616 result = admin_server.run() => {
1617 if let Err(e) = result {
1618 tracing::error!("Admin server error: {}", e);
1619 }
1620 }
1621 _ = shutdown_rx.recv() => {
1622 tracing::info!("Admin server shutting down");
1623 }
1624 }
1625
1626 sync_task.abort();
1627 })
1628 }
1629
1630 async fn handle_client(
1632 stream: TcpStream,
1633 addr: SocketAddr,
1634 state: Arc<ServerState>,
1635 config: ProxyConfig,
1636 _shutdown_tx: broadcast::Sender<()>,
1637 ) -> Result<()> {
1638 tracing::debug!("New client connection from {}", addr);
1639
1640 let session = Arc::new(ClientSession {
1642 id: Uuid::new_v4(),
1643 client_addr: addr,
1644 current_node: RwLock::new(None),
1645 in_transaction: std::sync::atomic::AtomicBool::new(false),
1646 copy_in_progress: std::sync::atomic::AtomicBool::new(false),
1647 tx_state: RwLock::new(TransactionState::default()),
1648 variables: RwLock::new(HashMap::new()),
1649 created_at: chrono::Utc::now(),
1650 tr_mode: config.tr_mode,
1651 #[cfg(feature = "lag-routing")]
1652 last_write_at: RwLock::new(None),
1653 #[cfg(feature = "pool-modes")]
1654 pool_client_id: ClientId::new(),
1655 #[cfg(feature = "wasm-plugins")]
1656 plugin_identity: RwLock::new(None),
1657 #[cfg(feature = "edge-proxy")]
1658 edge_ineligible: std::sync::atomic::AtomicBool::new(false),
1659 #[cfg(feature = "edge-proxy")]
1660 pending_edge_copy_tables: std::sync::Mutex::new(None),
1661 });
1662
1663 state.sessions.insert(session.id, session.clone());
1671 let _session_guard = SessionGuard {
1672 state: state.clone(),
1673 session_id: session.id,
1674 };
1675
1676 let negotiated = match tokio::time::timeout(
1687 state.limits.startup_timeout,
1688 Self::negotiate_client_tls(stream, &state),
1689 )
1690 .await
1691 {
1692 Ok(r) => r,
1693 Err(_) => {
1694 tracing::debug!(client = %addr, "pre-auth negotiation timed out; closing");
1695 Err(ProxyError::Connection(
1696 "startup negotiation timeout".to_string(),
1697 ))
1698 }
1699 };
1700 let result = match negotiated {
1701 Ok((mut client_stream, pre)) => {
1702 Self::client_loop(&mut client_stream, pre, &session, &state, &config).await
1703 }
1704 Err(e) => Err(e),
1705 };
1706
1707 result
1711 }
1712
1713 async fn client_loop(
1715 stream: &mut ClientStream,
1716 pre: Option<StartupMessage>,
1717 session: &Arc<ClientSession>,
1718 state: &Arc<ServerState>,
1719 config: &ProxyConfig,
1720 ) -> Result<()> {
1721 let codec = ProtocolCodec::new();
1722 let mut buffer = BytesMut::with_capacity(8192);
1723
1724 let mut conns: HashMap<String, BackendConn> = HashMap::new();
1741 let startup_result = match tokio::time::timeout(
1746 state.limits.startup_timeout,
1747 Self::handle_startup(stream, &mut buffer, &codec, pre, session, state, config),
1748 )
1749 .await
1750 {
1751 Ok(r) => r,
1752 Err(_) => Err(ProxyError::Connection("startup timeout".to_string())),
1753 };
1754 let mut current_node: Option<String> = match startup_result {
1755 Ok((Some(stream_conn), node_addr)) => {
1756 conns.insert(node_addr.clone(), BackendConn::new(stream_conn));
1757 Some(node_addr)
1758 }
1759 Ok((None, _)) => {
1760 return Ok(());
1762 }
1763 Err(e) => {
1764 tracing::error!("Startup failed: {}", e);
1765 let err_msg =
1767 Self::create_error_response("08006", &format!("Startup failed: {}", e));
1768 let _ = stream.write_all(&err_msg).await;
1769 return Err(e);
1770 }
1771 };
1772
1773 let mut pending = BytesMut::new();
1788 let mut pending_route_sql: Option<String> = None;
1789 let mut stmt_registry: HashMap<String, bytes::Bytes> = HashMap::new();
1797 let mut stmt_registry_bytes: usize = 0;
1800 let mut batch_defines: Vec<String> = Vec::new();
1801 let mut batch_refs: Vec<String> = Vec::new();
1802 let mut batch_closes: Vec<String> = Vec::new();
1803 #[cfg(feature = "edge-proxy")]
1811 let mut edge_stmt_meta: HashMap<String, Option<Vec<String>>> = HashMap::new();
1812 #[cfg(feature = "edge-proxy")]
1813 let mut edge_unnamed_meta: Option<Vec<String>> = None;
1814 #[cfg(feature = "edge-proxy")]
1815 let mut edge_batch_bound_unnamed = false;
1816 let promote_unnamed = config.optimize_unnamed_parse;
1822 let mut held_unnamed: Option<(bytes::Bytes, bytes::Bytes)> = None;
1823 loop {
1824 buffer.reserve(16384);
1839 let watch_node: Option<String> = if session
1840 .copy_in_progress
1841 .load(std::sync::atomic::Ordering::Relaxed)
1842 {
1843 None
1844 } else {
1845 current_node.clone().filter(|node| conns.contains_key(node))
1846 };
1847 let n: usize = 'client_read: {
1848 let Some(node) = watch_node.as_deref() else {
1849 break 'client_read stream
1850 .read_buf(&mut buffer)
1851 .await
1852 .map_err(|e| ProxyError::Network(format!("Read error: {}", e)))?;
1853 };
1854 loop {
1855 let mut backend_gone = false;
1856 let mut client_bytes: Option<usize> = None;
1857 {
1858 let bc = conns.get_mut(node).expect("watch_node is in conns");
1859 let mut abuf = [0u8; 16384];
1860 tokio::select! {
1861 r = stream.read_buf(&mut buffer) => {
1862 client_bytes = Some(r.map_err(|e| {
1863 ProxyError::Network(format!("Read error: {}", e))
1864 })?);
1865 }
1866 r = bc.stream.read(&mut abuf) => match r {
1867 Ok(0) => backend_gone = true,
1868 Ok(bn) => {
1869 stream.write_all(&abuf[..bn]).await.map_err(|e| {
1870 ProxyError::Network(format!("Client write error: {}", e))
1871 })?;
1872 state.metrics.bytes_sent.fetch_add(bn as u64, Ordering::Relaxed);
1873 }
1874 Err(e) => {
1875 tracing::debug!(node = %node, error = %e, "backend read error while idle; dropping cached connection");
1876 backend_gone = true;
1877 }
1878 },
1879 }
1880 }
1881 if backend_gone {
1882 conns.remove(node);
1886 if current_node.as_deref() == Some(node) {
1887 current_node = None;
1888 }
1889 break 'client_read stream
1890 .read_buf(&mut buffer)
1891 .await
1892 .map_err(|e| ProxyError::Network(format!("Read error: {}", e)))?;
1893 }
1894 if let Some(cn) = client_bytes {
1895 break 'client_read cn;
1896 }
1897 }
1899 };
1900
1901 if n == 0 {
1902 break;
1904 }
1905
1906 state
1907 .metrics
1908 .bytes_received
1909 .fetch_add(n as u64, Ordering::Relaxed);
1910
1911 if buffer.len() > state.limits.max_pending_bytes {
1915 let emsg =
1916 Self::create_error_response("53400", "message exceeds per-session size limit");
1917 let _ = stream.write_all(&emsg).await;
1918 let _ = stream.write_all(&Self::create_ready_for_query(b'I')).await;
1919 tracing::warn!(
1920 client = %session.client_addr,
1921 bytes = buffer.len(),
1922 "inbound message exceeds size cap; closing connection"
1923 );
1924 return Ok(());
1925 }
1926
1927 while let Some(msg) = codec.decode_message(&mut buffer)? {
1929 match msg.msg_type {
1930 MessageType::Terminate => return Ok(()),
1931
1932 MessageType::Query => {
1934 #[cfg(feature = "anomaly-detection")]
1938 Self::record_anomaly_observation(&msg, state, session);
1939
1940 let (msg, action) = Self::apply_pre_query_hook(msg, state, session);
1943
1944 if let PreQueryAction::Block(reason) = &action {
1945 tracing::info!(reason = %reason, "pre-query plugin blocked query");
1946 Self::send_block_response(stream, reason, state).await?;
1947 state
1948 .metrics
1949 .queries_processed
1950 .fetch_add(1, Ordering::Relaxed);
1951 continue;
1952 }
1953
1954 #[cfg(feature = "wasm-plugins")]
1955 if let PreQueryAction::Cached(bytes) = &action {
1956 match Self::synthesise_cached_response(bytes) {
1957 Ok(reply) => {
1958 stream.write_all(&reply).await.map_err(|e| {
1959 ProxyError::Network(format!("Write error: {}", e))
1960 })?;
1961 state
1962 .metrics
1963 .bytes_sent
1964 .fetch_add(reply.len() as u64, Ordering::Relaxed);
1965 state
1966 .metrics
1967 .queries_processed
1968 .fetch_add(1, Ordering::Relaxed);
1969 continue;
1970 }
1971 Err(e) => {
1972 tracing::warn!(error = %e, "failed to synthesise cached response; falling back to backend");
1973 }
1974 }
1975 }
1976
1977 if let Some(ref mirror) = state.mirror {
1981 if let Some(sql) = crate::protocol::query_text(&msg.payload) {
1982 mirror.offer(sql, Self::is_write_query(sql));
1983 }
1984 }
1985
1986 #[cfg(feature = "wasm-plugins")]
1987 let forward_start = std::time::Instant::now();
1988 let fr = Self::forward_simple_query(
1989 stream,
1990 &msg,
1991 &mut conns,
1992 current_node.as_deref(),
1993 session,
1994 state,
1995 config,
1996 )
1997 .await;
1998 #[cfg(feature = "wasm-plugins")]
1999 Self::fire_post_query_hook(
2000 &msg,
2001 session,
2002 state,
2003 &fr,
2004 forward_start.elapsed(),
2005 );
2006 let (used_node, sent) = fr?;
2007 if let Some(n) = used_node {
2008 current_node = Some(n);
2009 }
2010 #[cfg(feature = "pool-modes")]
2013 Self::release_to_pool_if_idle(
2014 &mut conns,
2015 current_node.as_deref(),
2016 session,
2017 state,
2018 config,
2019 )
2020 .await;
2021 state.metrics.bytes_sent.fetch_add(sent, Ordering::Relaxed);
2022 state
2023 .metrics
2024 .queries_processed
2025 .fetch_add(1, Ordering::Relaxed);
2026 }
2027
2028 MessageType::Parse
2030 | MessageType::Bind
2031 | MessageType::Describe
2032 | MessageType::Execute
2033 | MessageType::Close => {
2034 let mut add_to_pending = true;
2038 match msg.msg_type {
2039 MessageType::Parse => {
2040 let name = Self::parse_stmt_name(&msg.payload);
2044 let unnamed = name.is_empty();
2045 if !unnamed {
2046 let name = name.to_string();
2047 let existed = stmt_registry.contains_key(&name);
2048 if !existed
2052 && stmt_registry.len()
2053 >= state.limits.max_prepared_statements
2054 {
2055 let emsg = Self::create_error_response(
2056 "54000",
2057 "too many prepared statements for this session",
2058 );
2059 let _ = stream.write_all(&emsg).await;
2060 let _ = stream
2061 .write_all(&Self::create_ready_for_query(b'I'))
2062 .await;
2063 tracing::warn!(
2064 client = %session.client_addr,
2065 limit = state.limits.max_prepared_statements,
2066 "prepared-statement cap exceeded; closing connection"
2067 );
2068 return Ok(());
2069 }
2070 let encoded = msg.encode().freeze();
2071 let old_len =
2075 stmt_registry.get(&name).map(|b| b.len()).unwrap_or(0);
2076 let projected =
2077 stmt_registry_bytes.saturating_sub(old_len) + encoded.len();
2078 if projected > state.limits.max_prepared_bytes {
2079 let emsg = Self::create_error_response(
2080 "54000",
2081 "prepared-statement memory limit exceeded for this session",
2082 );
2083 let _ = stream.write_all(&emsg).await;
2084 let _ = stream
2085 .write_all(&Self::create_ready_for_query(b'I'))
2086 .await;
2087 tracing::warn!(
2088 client = %session.client_addr,
2089 limit = state.limits.max_prepared_bytes,
2090 "prepared-statement byte cap exceeded; closing connection"
2091 );
2092 return Ok(());
2093 }
2094 stmt_registry.insert(name.clone(), encoded);
2095 stmt_registry_bytes = projected;
2096 batch_defines.push(name);
2097 }
2098 if pending_route_sql.is_none() {
2099 if let Some(end) = msg.payload.iter().position(|&b| b == 0) {
2100 if let Some(q) =
2101 crate::protocol::query_text(&msg.payload[end + 1..])
2102 {
2103 if !q.is_empty() {
2104 pending_route_sql = Some(q.to_string());
2105 #[cfg(feature = "anomaly-detection")]
2106 Self::record_anomaly_sql(q, state, session);
2107 }
2108 }
2109 }
2110 }
2111 #[cfg(feature = "edge-proxy")]
2120 if config.edge.enabled {
2121 if let Some(end) = msg.payload.iter().position(|&b| b == 0) {
2122 if let Some(q) =
2123 crate::protocol::query_text(&msg.payload[end + 1..])
2124 {
2125 if !session
2126 .edge_ineligible
2127 .load(std::sync::atomic::Ordering::Relaxed)
2128 && Self::stmt_leaves_session_state(q)
2129 {
2130 session.edge_ineligible.store(
2131 true,
2132 std::sync::atomic::Ordering::Relaxed,
2133 );
2134 }
2135 let meta = if Self::is_edge_dml_sql(q) {
2136 Some(crate::edge::fingerprint::tables_only(q))
2137 } else if Self::is_edge_procedural_sql(q)
2138 || Self::is_edge_txn_end_sql(q)
2139 {
2140 Some(Vec::new())
2146 } else {
2147 None
2148 };
2149 if unnamed {
2150 edge_unnamed_meta = meta;
2151 } else {
2152 edge_stmt_meta.insert(
2153 Self::parse_stmt_name(&msg.payload).to_string(),
2154 meta,
2155 );
2156 }
2157 }
2158 }
2159 }
2160 if promote_unnamed
2167 && unnamed
2168 && pending.is_empty()
2169 && held_unnamed.is_none()
2170 {
2171 let sig = bytes::Bytes::copy_from_slice(&msg.payload[1..]);
2172 held_unnamed = Some((msg.encode().freeze(), sig));
2173 add_to_pending = false;
2174 } else if let Some((held_msg, _)) = held_unnamed.take() {
2175 let mut combined =
2176 BytesMut::with_capacity(held_msg.len() + pending.len());
2177 combined.extend_from_slice(&held_msg);
2178 combined.extend_from_slice(&pending);
2179 pending = combined;
2180 }
2181 }
2182 MessageType::Bind => {
2183 let stmt_ref = Self::bind_stmt_ref(&msg.payload);
2184 #[cfg(feature = "edge-proxy")]
2188 if stmt_ref.is_none() {
2189 edge_batch_bound_unnamed = true;
2190 }
2191 if let Some(name) = stmt_ref {
2192 batch_refs.push(name.to_string());
2193 }
2194 }
2195 MessageType::Describe => {
2196 if let Some(name) = Self::stmt_kind_name(&msg.payload) {
2197 batch_refs.push(name.to_string());
2198 }
2199 }
2200 MessageType::Close => {
2201 if let Some(name) = Self::stmt_kind_name(&msg.payload) {
2202 batch_closes.push(name.to_string());
2203 }
2204 }
2205 _ => {}
2206 }
2207 if add_to_pending {
2208 pending.extend_from_slice(&msg.encode());
2209 }
2210 }
2211
2212 MessageType::Sync | MessageType::Flush => {
2214 let wait_ready = msg.msg_type == MessageType::Sync;
2215 pending.extend_from_slice(&msg.encode());
2216 let batch = pending.split().freeze();
2217 let reprepare: Vec<String> = batch_refs
2221 .iter()
2222 .filter(|r| !batch_defines.contains(r))
2223 .cloned()
2224 .collect();
2225 let (used_node, sent) = Self::forward_extended_batch(
2226 stream,
2227 &batch,
2228 pending_route_sql.as_deref(),
2229 wait_ready,
2230 &mut conns,
2231 current_node.as_deref(),
2232 &stmt_registry,
2233 &reprepare,
2234 &batch_defines,
2235 held_unnamed.take(),
2236 session,
2237 state,
2238 config,
2239 )
2240 .await?;
2241 if let Some(n) = used_node {
2242 current_node = Some(n);
2243 }
2244 #[cfg(feature = "pool-modes")]
2248 if wait_ready {
2249 Self::release_to_pool_if_idle(
2250 &mut conns,
2251 current_node.as_deref(),
2252 session,
2253 state,
2254 config,
2255 )
2256 .await;
2257 }
2258 state.metrics.bytes_sent.fetch_add(sent, Ordering::Relaxed);
2259 #[cfg(feature = "edge-proxy")]
2272 if wait_ready && config.edge.enabled {
2273 if let Some(tables) = Self::edge_extended_batch_tables(
2274 &batch_refs,
2275 edge_batch_bound_unnamed,
2276 &edge_stmt_meta,
2277 &edge_unnamed_meta,
2278 ) {
2279 if session
2280 .copy_in_progress
2281 .load(std::sync::atomic::Ordering::Relaxed)
2282 {
2283 *session
2284 .pending_edge_copy_tables
2285 .lock()
2286 .unwrap_or_else(|e| e.into_inner()) = Some(tables);
2287 } else {
2288 Self::edge_invalidate_write(state, config, tables).await;
2289 }
2290 }
2291 edge_batch_bound_unnamed = false;
2292 }
2293 #[cfg(not(feature = "edge-proxy"))]
2296 for name in batch_closes.drain(..) {
2297 if let Some(removed) = stmt_registry.remove(&name) {
2298 stmt_registry_bytes =
2299 stmt_registry_bytes.saturating_sub(removed.len());
2300 }
2301 }
2302 #[cfg(feature = "edge-proxy")]
2314 {
2315 for name in &batch_closes {
2316 if let Some(removed) = stmt_registry.remove(name) {
2317 stmt_registry_bytes =
2318 stmt_registry_bytes.saturating_sub(removed.len());
2319 }
2320 }
2321 if wait_ready {
2322 for name in Self::edge_meta_prunable(&batch_closes, &batch_defines)
2323 {
2324 edge_stmt_meta.remove(name);
2325 }
2326 batch_closes.clear();
2327 }
2328 }
2329 if wait_ready {
2330 pending_route_sql = None;
2334 batch_defines.clear();
2335 batch_refs.clear();
2336 state
2337 .metrics
2338 .queries_processed
2339 .fetch_add(1, Ordering::Relaxed);
2340 }
2341 }
2342
2343 MessageType::CopyData | MessageType::CopyDone | MessageType::CopyFail => {
2345 let is_copy_end =
2346 matches!(msg.msg_type, MessageType::CopyDone | MessageType::CopyFail);
2347 let conn = current_node.as_ref().and_then(|n| conns.get_mut(n));
2348 match conn {
2349 Some(b) => {
2350 b.stream.write_all(&msg.encode()).await.map_err(|e| {
2351 ProxyError::Network(format!("Backend copy write error: {}", e))
2352 })?;
2353 if is_copy_end {
2354 let node = current_node.clone().unwrap();
2355 let r = Self::stream_until_ready(
2356 stream,
2357 &mut b.stream,
2358 session,
2359 state,
2360 )
2361 .await;
2362 session
2366 .copy_in_progress
2367 .store(false, std::sync::atomic::Ordering::Relaxed);
2368 match r {
2369 Ok(sent) => {
2370 state
2371 .metrics
2372 .bytes_sent
2373 .fetch_add(sent, Ordering::Relaxed);
2374 #[cfg(feature = "edge-proxy")]
2379 if config.edge.enabled {
2380 let stashed = session
2381 .pending_edge_copy_tables
2382 .lock()
2383 .unwrap_or_else(|e| e.into_inner())
2384 .take();
2385 if let Some(tables) = stashed {
2386 if matches!(msg.msg_type, MessageType::CopyDone)
2387 {
2388 Self::edge_invalidate_write(
2389 state, config, tables,
2390 )
2391 .await;
2392 }
2393 }
2394 }
2395 }
2396 Err(e) => {
2397 conns.remove(&node);
2398 return Err(e);
2399 }
2400 }
2401 }
2402 }
2403 None => {
2404 session
2410 .copy_in_progress
2411 .store(false, std::sync::atomic::Ordering::Relaxed);
2412 #[cfg(feature = "edge-proxy")]
2416 {
2417 *session
2418 .pending_edge_copy_tables
2419 .lock()
2420 .unwrap_or_else(|e| e.into_inner()) = None;
2421 }
2422 if is_copy_end {
2423 let emsg = Self::create_error_response(
2424 "57000",
2425 "COPY aborted: backend connection lost",
2426 );
2427 let _ = stream.write_all(&emsg).await;
2428 let _ =
2429 stream.write_all(&Self::create_ready_for_query(b'I')).await;
2430 }
2431 }
2432 }
2433 }
2434
2435 _ => {
2437 if let Some(ref node) = current_node {
2438 if let Some(b) = conns.get_mut(node) {
2439 let _ = b.stream.write_all(&msg.encode()).await;
2440 }
2441 }
2442 }
2443 }
2444 }
2445
2446 if pending.len() > state.limits.max_pending_bytes {
2450 let emsg = Self::create_error_response(
2451 "53400",
2452 "un-flushed extended-protocol buffer exceeds per-session limit",
2453 );
2454 let _ = stream.write_all(&emsg).await;
2455 let _ = stream.write_all(&Self::create_ready_for_query(b'I')).await;
2456 tracing::warn!(
2457 client = %session.client_addr,
2458 pending = pending.len(),
2459 "pending extended-protocol buffer cap exceeded; closing connection"
2460 );
2461 return Ok(());
2462 }
2463 }
2464
2465 #[cfg(feature = "pool-modes")]
2469 if state.backend_pool.is_some() {
2470 let nodes: Vec<String> = conns.keys().cloned().collect();
2471 for node in nodes {
2472 Self::release_to_pool_if_idle(
2473 &mut conns,
2474 Some(node.as_str()),
2475 session,
2476 state,
2477 config,
2478 )
2479 .await;
2480 }
2481 }
2482
2483 Ok(())
2484 }
2485
2486 async fn negotiate_client_tls(
2493 mut tcp: TcpStream,
2494 state: &Arc<ServerState>,
2495 ) -> Result<(ClientStream, Option<StartupMessage>)> {
2496 let codec = ProtocolCodec::new();
2497 let mut buffer = BytesMut::with_capacity(1024);
2498 let mut read_buf = vec![0u8; 1024];
2499
2500 let first = loop {
2501 if let Some(msg) = codec.decode_startup(&mut buffer)? {
2502 break msg;
2503 }
2504 let n = tcp
2505 .read(&mut read_buf)
2506 .await
2507 .map_err(|e| ProxyError::Network(format!("Startup read error: {}", e)))?;
2508 if n == 0 {
2509 return Err(ProxyError::Connection(
2510 "client closed before startup".to_string(),
2511 ));
2512 }
2513 buffer.extend_from_slice(&read_buf[..n]);
2514 };
2515
2516 match first {
2517 StartupMessage::SSLRequest => match state.tls_acceptor.as_ref() {
2518 Some(acceptor) => {
2519 tcp.write_all(b"S")
2520 .await
2521 .map_err(|e| ProxyError::Network(format!("SSL accept write: {}", e)))?;
2522 let tls = acceptor
2523 .accept(tcp)
2524 .await
2525 .map_err(|e| ProxyError::Network(format!("TLS handshake failed: {}", e)))?;
2526 if tls.get_ref().1.peer_certificates().is_some() {
2527 tracing::debug!("client presented a certificate (mTLS)");
2528 }
2529 Ok((ClientStream::Tls(Box::new(tls)), None))
2530 }
2531 None => {
2532 tcp.write_all(b"N")
2533 .await
2534 .map_err(|e| ProxyError::Network(format!("SSL reject write: {}", e)))?;
2535 Ok((ClientStream::Plain(tcp), None))
2536 }
2537 },
2538 other => Ok((ClientStream::Plain(tcp), Some(other))),
2539 }
2540 }
2541
2542 async fn handle_startup(
2546 client_stream: &mut ClientStream,
2547 buffer: &mut BytesMut,
2548 codec: &ProtocolCodec,
2549 pre: Option<StartupMessage>,
2550 session: &Arc<ClientSession>,
2551 state: &Arc<ServerState>,
2552 config: &ProxyConfig,
2553 ) -> Result<(Option<TcpStream>, String)> {
2554 let startup_msg = match pre {
2557 Some(msg) => Some(msg),
2558 None => {
2559 let mut read_buf = vec![0u8; 1024];
2560 loop {
2561 if let Some(msg) = codec.decode_startup(buffer)? {
2562 break Some(msg);
2563 }
2564 let n = client_stream
2565 .read(&mut read_buf)
2566 .await
2567 .map_err(|e| ProxyError::Network(format!("Startup read error: {}", e)))?;
2568 if n == 0 {
2569 return Ok((None, String::new()));
2570 }
2571 buffer.extend_from_slice(&read_buf[..n]);
2572 }
2573 }
2574 };
2575
2576 match startup_msg {
2577 Some(StartupMessage::SSLRequest) => {
2578 client_stream
2581 .write_all(b"N")
2582 .await
2583 .map_err(|e| ProxyError::Network(format!("SSL reject error: {}", e)))?;
2584 Err(ProxyError::Protocol(
2585 "unexpected SSLRequest after startup".to_string(),
2586 ))
2587 }
2588 Some(StartupMessage::CancelRequest { pid, key }) => {
2589 Self::forward_cancel_request(state, pid, key).await;
2592 Ok((None, String::new()))
2593 }
2594 Some(StartupMessage::Startup { params, .. }) => {
2595 Self::connect_and_authenticate(client_stream, ¶ms, session, state, config).await
2596 }
2597 None => Err(ProxyError::Protocol(
2598 "Incomplete startup message".to_string(),
2599 )),
2600 }
2601 }
2602
2603 fn hba_admits(rules: &[HbaRule], ip: std::net::IpAddr, user: &str, database: &str) -> bool {
2606 for r in rules {
2607 let user_ok = r.user == "all" || r.user == user;
2608 let db_ok = r.database == "all" || r.database == database;
2609 if user_ok && db_ok && Self::hba_addr_matches(&r.address, ip) {
2610 return r.action == HbaAction::Allow;
2611 }
2612 }
2613 true
2614 }
2615
2616 fn hba_addr_matches(spec: &str, ip: std::net::IpAddr) -> bool {
2619 use std::net::IpAddr;
2620 if spec == "all" {
2621 return true;
2622 }
2623 if let Some((net, bits)) = spec.split_once('/') {
2624 let bits: u32 = match bits.parse() {
2625 Ok(b) => b,
2626 Err(_) => return false,
2627 };
2628 match (net.parse::<IpAddr>(), ip) {
2629 (Ok(IpAddr::V4(n)), IpAddr::V4(i)) if bits <= 32 => {
2630 let mask = if bits == 0 {
2631 0
2632 } else {
2633 u32::MAX << (32 - bits)
2634 };
2635 (u32::from(n) & mask) == (u32::from(i) & mask)
2636 }
2637 (Ok(IpAddr::V6(n)), IpAddr::V6(i)) if bits <= 128 => {
2638 let mask = if bits == 0 {
2639 0
2640 } else {
2641 u128::MAX << (128 - bits)
2642 };
2643 (u128::from(n) & mask) == (u128::from(i) & mask)
2644 }
2645 _ => false,
2646 }
2647 } else {
2648 spec.parse::<IpAddr>().map(|s| s == ip).unwrap_or(false)
2649 }
2650 }
2651
2652 async fn proxy_scram_auth(
2658 client: &mut ClientStream,
2659 user: &str,
2660 state: &Arc<ServerState>,
2661 ) -> std::result::Result<(), String> {
2662 use crate::auth_scram::ScramServer;
2663 let auth_file = state.auth_file.as_ref().ok_or("scram not configured")?;
2664
2665 let mut sasl = BytesMut::new();
2667 sasl.put_i32(10); sasl.extend_from_slice(b"SCRAM-SHA-256\0");
2669 sasl.put_u8(0); Self::write_auth_frame(client, &sasl).await?;
2671
2672 let init = Self::read_password_message(client).await?;
2674 let mech_end = init
2675 .iter()
2676 .position(|&b| b == 0)
2677 .ok_or("malformed SASLInitialResponse (no mechanism)")?;
2678 if init.len() < mech_end + 5 {
2679 return Err("short SASLInitialResponse".into());
2680 }
2681 let client_first =
2682 std::str::from_utf8(&init[mech_end + 5..]).map_err(|_| "client-first not UTF-8")?;
2683
2684 let verifier = auth_file.get(user).ok_or("no such user")?.clone();
2686
2687 let server_nonce = Self::random_nonce();
2689 let (server, server_first) = ScramServer::start(verifier, client_first, &server_nonce)?;
2690
2691 let mut cont = BytesMut::new();
2693 cont.put_i32(11);
2694 cont.extend_from_slice(server_first.as_bytes());
2695 Self::write_auth_frame(client, &cont).await?;
2696
2697 let client_final_raw = Self::read_password_message(client).await?;
2699 let client_final =
2700 std::str::from_utf8(&client_final_raw).map_err(|_| "client-final not UTF-8")?;
2701
2702 let server_final = server.finish(client_final)?;
2704
2705 let mut fin = BytesMut::new();
2707 fin.put_i32(12);
2708 fin.extend_from_slice(server_final.as_bytes());
2709 Self::write_auth_frame(client, &fin).await?;
2710 Ok(())
2711 }
2712
2713 async fn write_auth_frame(
2715 client: &mut ClientStream,
2716 payload: &[u8],
2717 ) -> std::result::Result<(), String> {
2718 let mut frame = BytesMut::with_capacity(payload.len() + 5);
2719 frame.put_u8(b'R');
2720 frame.put_u32((payload.len() + 4) as u32);
2721 frame.extend_from_slice(payload);
2722 client
2723 .write_all(&frame)
2724 .await
2725 .map_err(|e| format!("client write: {}", e))
2726 }
2727
2728 async fn read_password_message(
2731 client: &mut ClientStream,
2732 ) -> std::result::Result<BytesMut, String> {
2733 let codec = ProtocolCodec::new();
2734 let mut buffer = BytesMut::with_capacity(1024);
2735 let mut read_buf = vec![0u8; 1024];
2736 loop {
2737 if let Some(msg) = codec
2738 .decode_message(&mut buffer)
2739 .map_err(|e| format!("decode: {}", e))?
2740 {
2741 if msg.msg_type == MessageType::Password {
2742 return Ok(msg.payload);
2743 }
2744 return Err(format!("expected SASL response, got {:?}", msg.msg_type));
2745 }
2746 let n = client
2747 .read(&mut read_buf)
2748 .await
2749 .map_err(|e| format!("client read: {}", e))?;
2750 if n == 0 {
2751 return Err("client closed during SASL".into());
2752 }
2753 buffer.extend_from_slice(&read_buf[..n]);
2754 }
2755 }
2756
2757 fn random_nonce() -> String {
2759 use rand::Rng;
2760 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
2761 let mut rng = rand::thread_rng();
2762 (0..24)
2763 .map(|_| CHARS[rng.gen_range(0..CHARS.len())] as char)
2764 .collect()
2765 }
2766
2767 async fn connect_and_authenticate(
2769 client_stream: &mut ClientStream,
2770 params: &HashMap<String, String>,
2771 session: &Arc<ClientSession>,
2772 state: &Arc<ServerState>,
2773 config: &ProxyConfig,
2774 ) -> Result<(Option<TcpStream>, String)> {
2775 let user = params.get("user").map(String::as_str).unwrap_or("");
2778 let database = params.get("database").map(String::as_str).unwrap_or(user);
2779 if !Self::hba_admits(&config.hba, session.client_addr.ip(), user, database) {
2780 tracing::info!(%user, %database, client = %session.client_addr, "connection rejected by hba rule");
2781 let err = Self::create_error_response(
2782 "28000",
2783 "connection rejected by proxy admission rules",
2784 );
2785 let _ = client_stream.write_all(&err).await;
2786 return Ok((None, String::new()));
2787 }
2788
2789 if state.auth_file.is_some() {
2795 if let Err(e) = Self::proxy_scram_auth(client_stream, user, state).await {
2796 tracing::info!(%user, error = %e, "proxy SCRAM auth failed");
2797 let err =
2798 Self::create_error_response("28P01", &format!("authentication failed: {}", e));
2799 let _ = client_stream.write_all(&err).await;
2800 return Ok((None, String::new()));
2801 }
2802 tracing::debug!(%user, "client authenticated by proxy SCRAM");
2803 }
2804
2805 Self::apply_authenticate_hook(params, session, state).await?;
2811
2812 let cutover = state.cutover.load_full();
2816 let (node_addr, effective_params) = if let Some(t) = cutover.as_ref() {
2817 let mut p = params.clone();
2818 p.insert("user".to_string(), t.user.clone());
2819 if let Some(ref db) = t.database {
2820 p.insert("database".to_string(), db.clone());
2821 } else {
2822 p.remove("database");
2823 }
2824 tracing::debug!(target = %t.addr, "routing connection to cutover target");
2825 (t.addr.clone(), p)
2826 } else {
2827 (
2828 Self::select_node(session, state, config).await?,
2829 params.clone(),
2830 )
2831 };
2832
2833 let mut backend = match tokio::time::timeout(
2838 config.pool.acquire_timeout(),
2839 TcpStream::connect(&node_addr),
2840 )
2841 .await
2842 {
2843 Ok(Ok(s)) => s,
2844 Ok(Err(e)) => {
2845 let msg = format!("Failed to connect to {}: {}", node_addr, e);
2846 Self::note_backend_failure(state, &node_addr, &msg);
2847 return Err(ProxyError::Connection(msg));
2848 }
2849 Err(_) => {
2850 let msg = format!("Connection timeout to {}", node_addr);
2851 Self::note_backend_failure(state, &node_addr, &msg);
2852 return Err(ProxyError::Connection(msg));
2853 }
2854 };
2855 let _ = backend.set_nodelay(true);
2856
2857 let params = &effective_params;
2859 let startup_bytes = Self::build_startup_message(params);
2860 backend
2861 .write_all(&startup_bytes)
2862 .await
2863 .map_err(|e| ProxyError::Network(format!("Backend startup write error: {}", e)))?;
2864
2865 Self::proxy_authentication(client_stream, &mut backend, state, &node_addr).await?;
2869
2870 {
2872 let mut vars = session.variables.write().await;
2873 for (k, v) in params {
2874 vars.insert(k.clone(), v.clone());
2875 }
2876 }
2877
2878 Ok((Some(backend), node_addr))
2879 }
2880
2881 fn build_startup_message(params: &HashMap<String, String>) -> Vec<u8> {
2883 let mut payload = BytesMut::new();
2884
2885 payload.put_u32(196608);
2887
2888 for (key, value) in params {
2890 payload.extend_from_slice(key.as_bytes());
2891 payload.put_u8(0);
2892 payload.extend_from_slice(value.as_bytes());
2893 payload.put_u8(0);
2894 }
2895 payload.put_u8(0); let mut msg = BytesMut::new();
2899 msg.put_u32((payload.len() + 4) as u32);
2900 msg.extend_from_slice(&payload);
2901
2902 msg.to_vec()
2903 }
2904
2905 fn register_cancel_key(state: &Arc<ServerState>, pid: u32, key: u32, node_addr: &str) {
2912 {
2916 let mut order = state.cancel_order.lock();
2917 while state.cancel_map.len() >= state.limits.max_cancel_keys {
2918 match order.pop_front() {
2919 Some(old) => {
2920 state.cancel_map.remove(&old);
2921 }
2922 None => {
2923 state.cancel_map.clear();
2926 break;
2927 }
2928 }
2929 }
2930 order.push_back((pid, key));
2931 }
2932 state.cancel_map.insert((pid, key), node_addr.to_string());
2933 }
2934
2935 async fn forward_cancel_request(state: &Arc<ServerState>, pid: u32, key: u32) {
2938 let Some(addr) = state.cancel_map.get(&(pid, key)).map(|e| e.clone()) else {
2939 tracing::debug!(pid, "cancel request for unknown key; ignoring");
2940 return;
2941 };
2942 let mut msg = BytesMut::with_capacity(16);
2944 msg.put_u32(16);
2945 msg.put_u32(80877102);
2946 msg.put_u32(pid);
2947 msg.put_u32(key);
2948 match tokio::time::timeout(Duration::from_secs(5), TcpStream::connect(&addr)).await {
2949 Ok(Ok(mut conn)) => {
2950 let _ = conn.set_nodelay(true);
2951 if let Err(e) = conn.write_all(&msg).await {
2952 tracing::warn!(node = %addr, error = %e, "failed to forward CancelRequest");
2953 }
2954 }
2956 other => {
2957 tracing::warn!(node = %addr, ?other, "could not connect to forward CancelRequest")
2958 }
2959 }
2960 }
2961
2962 async fn proxy_authentication(
2964 client_stream: &mut ClientStream,
2965 backend_stream: &mut TcpStream,
2966 state: &Arc<ServerState>,
2967 node_addr: &str,
2968 ) -> Result<()> {
2969 let mut backend_buffer = BytesMut::with_capacity(4096);
2985 let mut cbuf = vec![0u8; 4096];
2986 let mut bbuf = vec![0u8; 4096];
2987 let deadline = tokio::time::Instant::now() + state.limits.startup_timeout;
2988
2989 loop {
2990 tokio::select! {
2991 biased;
2992 _ = tokio::time::sleep_until(deadline) => {
2993 return Err(ProxyError::Auth(
2994 "authentication timed out".to_string(),
2995 ));
2996 }
2997 r = backend_stream.read(&mut bbuf) => {
3000 let n = r.map_err(|e| {
3001 ProxyError::Network(format!("Backend auth read error: {}", e))
3002 })?;
3003 if n == 0 {
3004 return Err(ProxyError::Connection(
3005 "Backend closed during auth".to_string(),
3006 ));
3007 }
3008 client_stream
3009 .write_all(&bbuf[..n])
3010 .await
3011 .map_err(|e| ProxyError::Network(format!("Client auth write error: {}", e)))?;
3012 backend_buffer.extend_from_slice(&bbuf[..n]);
3013
3014 loop {
3016 if backend_buffer.len() < 5 {
3017 break;
3018 }
3019 let len = u32::from_be_bytes([
3020 backend_buffer[1],
3021 backend_buffer[2],
3022 backend_buffer[3],
3023 backend_buffer[4],
3024 ]) as usize;
3025 if len < 4 || backend_buffer.len() < len + 1 {
3026 break;
3027 }
3028 let tag = backend_buffer[0];
3029 let frame = backend_buffer.split_to(len + 1);
3030 match tag {
3031 b'K' if frame.len() >= 13 => {
3034 let pid = u32::from_be_bytes([
3035 frame[5], frame[6], frame[7], frame[8],
3036 ]);
3037 let key = u32::from_be_bytes([
3038 frame[9], frame[10], frame[11], frame[12],
3039 ]);
3040 Self::register_cancel_key(state, pid, key, node_addr);
3041 }
3042 b'Z' => return Ok(()),
3044 b'E' => {
3047 return Err(ProxyError::Auth("Authentication failed".to_string()));
3048 }
3049 _ => {}
3050 }
3051 }
3052 }
3053 r = client_stream.read(&mut cbuf) => {
3056 let n = r.map_err(|e| {
3057 ProxyError::Network(format!("Client auth read error: {}", e))
3058 })?;
3059 if n == 0 {
3060 return Err(ProxyError::Connection(
3061 "Client closed during auth".to_string(),
3062 ));
3063 }
3064 backend_stream
3065 .write_all(&cbuf[..n])
3066 .await
3067 .map_err(|e| {
3068 ProxyError::Network(format!("Backend password write error: {}", e))
3069 })?;
3070 }
3071 }
3072 }
3073 }
3074
3075 async fn choose_target_node(
3080 is_write: bool,
3081 forced_target: Option<String>,
3082 current_node: Option<&str>,
3083 session: &Arc<ClientSession>,
3084 state: &Arc<ServerState>,
3085 config: &ProxyConfig,
3086 ) -> Result<String> {
3087 if let Some(t) = state.cutover.load_full().as_ref() {
3090 return Ok(t.addr.clone());
3091 }
3092
3093 #[cfg(feature = "lag-routing")]
3097 if !is_write && forced_target.is_none() && config.lag_routing.enabled {
3098 let last_write = *session.last_write_at.read().await;
3099 if Self::ryw_pins_primary(last_write, config.lag_routing.ryw_window_ms) {
3100 tracing::debug!(target: "helios::routing", "read-your-writes: pinning read to primary");
3101 return Self::select_primary_with_timeout(session, state, config).await;
3102 }
3103 }
3104
3105 let need_switch = if let Some(ref forced) = forced_target {
3106 let health = state.health.load_full();
3107 let reuse = current_node
3108 .map(|c| c == forced && health.get(c).map(|h| h.healthy).unwrap_or(false))
3109 .unwrap_or(false);
3110 !reuse
3111 } else if let Some(current) = current_node {
3112 let health = state.health.load_full();
3113 let current_healthy = health.get(current).map(|h| h.healthy).unwrap_or(false);
3114 if !current_healthy {
3115 true
3116 } else if is_write {
3117 let is_primary = config
3118 .nodes
3119 .iter()
3120 .find(|n| n.address() == *current)
3121 .map(|n| n.role == NodeRole::Primary)
3122 .unwrap_or(false);
3123 !is_primary
3124 } else {
3125 false
3126 }
3127 } else {
3128 true
3129 };
3130
3131 if let Some(forced) = forced_target {
3132 let resolved = config
3137 .nodes
3138 .iter()
3139 .find(|n| n.name.as_deref() == Some(forced.as_str()) || n.address() == forced)
3140 .map(|n| n.address())
3141 .unwrap_or(forced);
3142 Ok(resolved)
3143 } else if need_switch {
3144 if is_write {
3145 Self::select_primary_with_timeout(session, state, config).await
3146 } else {
3147 Self::select_read_node(session, state, config).await
3148 }
3149 } else {
3150 Ok(current_node.unwrap().to_string())
3151 }
3152 }
3153
3154 async fn ensure_conn(
3159 conns: &mut HashMap<String, BackendConn>,
3160 target: &str,
3161 session: &Arc<ClientSession>,
3162 config: &ProxyConfig,
3163 _state: &Arc<ServerState>,
3164 ) -> Result<()> {
3165 if conns.contains_key(target) {
3166 return Ok(());
3167 }
3168
3169 #[cfg(feature = "pool-modes")]
3174 if let Some(pool) = _state.backend_pool.as_ref() {
3175 let key = Self::pool_key_for(target, session).await;
3176 if let Some(stream) = pool.checkout(&key) {
3177 tracing::info!(
3178 target: "helios::pool",
3179 node = %target,
3180 "reused pooled backend connection"
3181 );
3182 conns.insert(target.to_string(), BackendConn::new(stream));
3183 return Ok(());
3184 }
3185 }
3186
3187 let mut backend =
3188 tokio::time::timeout(config.pool.acquire_timeout(), TcpStream::connect(target))
3189 .await
3190 .map_err(|_| ProxyError::Connection(format!("Connection timeout to {}", target)))?
3191 .map_err(|e| {
3192 ProxyError::Connection(format!("Failed to connect to {}: {}", target, e))
3193 })?;
3194 let _ = backend.set_nodelay(true);
3195
3196 let params = session.variables.read().await.clone();
3197 let startup = Self::build_startup_message(¶ms);
3198 backend
3199 .write_all(&startup)
3200 .await
3201 .map_err(|e| ProxyError::Network(format!("Backend startup error: {}", e)))?;
3202 Self::complete_backend_auth(&mut backend).await?;
3203 #[cfg(feature = "pool-modes")]
3204 if _state.backend_pool.is_some() {
3205 tracing::debug!(target: "helios::pool", node = %target, "dialed fresh backend connection (pool miss)");
3206 }
3207 tracing::debug!(node = %target, "opened backend connection");
3208 conns.insert(target.to_string(), BackendConn::new(backend));
3209 Ok(())
3210 }
3211
3212 #[cfg(feature = "pool-modes")]
3219 const POOL_IDENTITY_PARAMS: &'static [&'static str] = &[
3220 "client_encoding",
3221 "DateStyle",
3222 "TimeZone",
3223 "IntervalStyle",
3224 "standard_conforming_strings",
3225 "options",
3226 ];
3227
3228 #[cfg(feature = "pool-modes")]
3236 async fn pool_key_for(target: &str, session: &Arc<ClientSession>) -> String {
3237 let vars = session.variables.read().await;
3238 let user = vars.get("user").map(|s| s.as_str()).unwrap_or("");
3239 let database = vars.get("database").map(|s| s.as_str()).unwrap_or(user);
3241 let base = crate::pool::pool_key(target, user, database);
3242
3243 use std::hash::{Hash, Hasher};
3244 let mut h = std::collections::hash_map::DefaultHasher::new();
3245 let mut any = false;
3246 for k in Self::POOL_IDENTITY_PARAMS {
3247 if let Some(v) = vars.get(*k) {
3248 any = true;
3249 k.hash(&mut h);
3250 v.hash(&mut h);
3251 }
3252 }
3253 if any {
3254 format!("{}\0{:016x}", base, h.finish())
3255 } else {
3256 base
3257 }
3258 }
3259
3260 #[cfg(feature = "pool-modes")]
3278 async fn reset_backend<S: AsyncReadExt + AsyncWriteExt + Unpin>(
3279 stream: &mut S,
3280 reset_sql: &str,
3281 backend_write_timeout: Duration,
3282 ) -> Result<()> {
3283 let msg = crate::protocol::QueryMessage {
3284 query: reset_sql.to_string(),
3285 }
3286 .encode();
3287 tokio::time::timeout(backend_write_timeout, stream.write_all(&msg.encode()))
3288 .await
3289 .map_err(|_| ProxyError::Network("reset write timeout".to_string()))?
3290 .map_err(|e| ProxyError::Network(format!("reset write error: {}", e)))?;
3291
3292 let mut buf = BytesMut::with_capacity(1024);
3293 let mut had_error = false;
3294 loop {
3295 let mut consumed = 0usize;
3298 let mut ready_status: Option<u8> = None;
3299 loop {
3300 let rem = &buf[consumed..];
3301 if rem.len() < 5 {
3302 break;
3303 }
3304 let len = u32::from_be_bytes([rem[1], rem[2], rem[3], rem[4]]) as usize;
3305 if len < 4 || rem.len() < len + 1 {
3306 break;
3307 }
3308 let mtype = rem[0];
3309 let frame_total = len + 1;
3310 if mtype == b'E' {
3311 had_error = true;
3312 }
3313 consumed += frame_total;
3314 if mtype == b'Z' {
3315 ready_status = Some(if frame_total >= 6 { rem[5] } else { b'I' });
3316 break;
3317 }
3318 }
3319 let _ = buf.split_to(consumed);
3320
3321 if let Some(status) = ready_status {
3322 if had_error || status != b'I' {
3323 return Err(ProxyError::Connection(format!(
3324 "reset query did not cleanly succeed (error={}, status={})",
3325 had_error, status as char
3326 )));
3327 }
3328 return Ok(());
3329 }
3330
3331 buf.reserve(1024);
3332 let n = tokio::time::timeout(Duration::from_secs(5), stream.read_buf(&mut buf))
3333 .await
3334 .map_err(|_| ProxyError::Network("reset drain timeout".to_string()))?
3335 .map_err(|e| ProxyError::Network(format!("reset drain read error: {}", e)))?;
3336 if n == 0 {
3337 return Err(ProxyError::Connection(
3338 "backend closed during reset".to_string(),
3339 ));
3340 }
3341 }
3342 }
3343
3344 #[cfg(feature = "pool-modes")]
3350 async fn release_to_pool_if_idle(
3351 conns: &mut HashMap<String, BackendConn>,
3352 node: Option<&str>,
3353 session: &Arc<ClientSession>,
3354 state: &Arc<ServerState>,
3355 config: &ProxyConfig,
3356 ) {
3357 let Some(pool) = state.backend_pool.as_ref() else {
3358 return;
3359 };
3360 let Some(node) = node else {
3361 return;
3362 };
3363 if session
3367 .in_transaction
3368 .load(std::sync::atomic::Ordering::Relaxed)
3369 || session
3370 .copy_in_progress
3371 .load(std::sync::atomic::Ordering::Relaxed)
3372 {
3373 return;
3374 }
3375 let Some(mut bc) = conns.remove(node) else {
3376 return;
3377 };
3378
3379 let clean = !bc.dirty && bc.prepared.is_empty() && bc.unnamed_sig.is_none();
3387 if config.pool_mode.skip_clean_reset && clean {
3388 let key = Self::pool_key_for(node, session).await;
3389 if pool.checkin(&key, bc.stream) {
3390 pool.note_reset_skipped();
3391 tracing::debug!(target: "helios::pool", node = %node, "parked clean backend connection (reset skipped)");
3392 }
3393 return;
3394 }
3395
3396 if Self::reset_backend(
3397 &mut bc.stream,
3398 &config.pool_mode.reset_query,
3399 state.limits.backend_write_timeout,
3400 )
3401 .await
3402 .is_ok()
3403 {
3404 let key = Self::pool_key_for(node, session).await;
3405 if pool.checkin(&key, bc.stream) {
3406 tracing::debug!(target: "helios::pool", node = %node, "parked backend connection for reuse");
3407 }
3408 }
3409 }
3411
3412 async fn forward_simple_query(
3418 client: &mut ClientStream,
3419 msg: &Message,
3420 conns: &mut HashMap<String, BackendConn>,
3421 current_node: Option<&str>,
3422 session: &Arc<ClientSession>,
3423 state: &Arc<ServerState>,
3424 config: &ProxyConfig,
3425 ) -> Result<(Option<String>, u64)> {
3426 #[cfg(feature = "rate-limiting")]
3428 if let Some(mut resp) = Self::rate_limit_check(session, state, config).await {
3429 resp.extend_from_slice(&Self::create_ready_for_query(b'I'));
3430 client
3431 .write_all(&resp)
3432 .await
3433 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
3434 return Ok((None, resp.len() as u64));
3435 }
3436
3437 let default_is_write = Self::is_write_message(msg);
3438 let plugin_override = Self::apply_route_hook(msg, state, session);
3439
3440 if let RouteOverride::Block(reason) = plugin_override {
3442 let mut response = Vec::with_capacity(64 + reason.len());
3443 response.extend_from_slice(&Self::create_error_response(
3444 "42000",
3445 &format!("Query blocked by route plugin: {}", reason),
3446 ));
3447 response.extend_from_slice(&Self::create_ready_for_query(b'I'));
3448 client
3449 .write_all(&response)
3450 .await
3451 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
3452 return Ok((None, response.len() as u64));
3453 }
3454
3455 #[cfg(feature = "routing-hints")]
3459 let (route_override, default_is_write, stripped_msg) =
3460 Self::resolve_simple_route(msg, plugin_override, default_is_write, state);
3461 #[cfg(not(feature = "routing-hints"))]
3462 let (route_override, stripped_msg): (RouteOverride, Option<Message>) =
3463 (plugin_override, None);
3464
3465 let (is_write, forced_target) = match route_override {
3466 RouteOverride::None => (default_is_write, None),
3467 RouteOverride::Primary => (true, None),
3468 RouteOverride::Standby => (false, None),
3469 RouteOverride::Node(name) => (default_is_write, Some(name)),
3470 RouteOverride::Block(_) => unreachable!("handled above"),
3471 };
3472
3473 #[cfg(feature = "lag-routing")]
3476 if is_write && config.lag_routing.enabled {
3477 *session.last_write_at.write().await = Some(std::time::Instant::now());
3478 }
3479
3480 let forward_msg = stripped_msg.as_ref().unwrap_or(msg);
3483
3484 #[cfg(feature = "query-rewriting")]
3488 let rewritten_msg: Option<Message> = state.rewriter.as_ref().and_then(|rw| {
3489 let sql = crate::protocol::query_text(&forward_msg.payload)?;
3490 match rw.rewrite(sql) {
3491 Ok(res) if res.was_rewritten() => {
3492 tracing::debug!(target: "helios::rewrite", rules = ?res.rules_applied, "query rewritten");
3493 Some(crate::protocol::QueryMessage { query: res.query().to_string() }.encode())
3494 }
3495 _ => None,
3496 }
3497 });
3498 #[cfg(feature = "query-rewriting")]
3499 let forward_msg = rewritten_msg.as_ref().unwrap_or(forward_msg);
3500
3501 #[cfg(feature = "multi-tenancy")]
3505 let tenant_msg: Option<Message> = if let Some(tm) = state.tenant_manager.as_ref() {
3506 match crate::protocol::query_text(&forward_msg.payload) {
3507 Some(sql) => {
3508 let ctx = Self::tenant_request_ctx(session).await;
3509 match tm.identify_tenant(&ctx) {
3510 Some(tenant) => {
3511 let res = tm.transform_query(sql, &tenant);
3512 if res.transformed {
3513 tracing::debug!(target: "helios::tenant", tenant = %tenant.0, "tenant filter injected");
3514 Some(crate::protocol::QueryMessage { query: res.query }.encode())
3515 } else {
3516 None
3517 }
3518 }
3519 None => None,
3520 }
3521 }
3522 None => None,
3523 }
3524 } else {
3525 None
3526 };
3527 #[cfg(feature = "multi-tenancy")]
3528 let forward_msg = tenant_msg.as_ref().unwrap_or(forward_msg);
3529
3530 #[cfg(feature = "edge-proxy")]
3542 if config.edge.enabled
3543 && !session
3544 .edge_ineligible
3545 .load(std::sync::atomic::Ordering::Relaxed)
3546 {
3547 if let Some(sql) = crate::protocol::query_text(&forward_msg.payload) {
3548 if Self::stmt_leaves_session_state(sql) {
3549 session
3550 .edge_ineligible
3551 .store(true, std::sync::atomic::Ordering::Relaxed);
3552 }
3553 }
3554 }
3555 #[cfg(feature = "edge-proxy")]
3556 let edge_ctx: Option<(crate::edge::CacheKey, Vec<String>, u64, u64)> = if is_write
3557 || !config.edge.enabled
3558 || session
3559 .edge_ineligible
3560 .load(std::sync::atomic::Ordering::Relaxed)
3561 {
3562 None
3563 } else {
3564 let sql = crate::protocol::query_text(&forward_msg.payload).unwrap_or("");
3565 if Self::is_cacheable_read_sql(sql)
3568 && !session
3569 .in_transaction
3570 .load(std::sync::atomic::Ordering::Relaxed)
3571 {
3572 let (database, user, session_vars) = {
3577 let vars = session.variables.read().await;
3578 let database = vars
3579 .get("database")
3580 .cloned()
3581 .unwrap_or_else(|| "default".to_string());
3582 let user = vars.get("user").cloned().unwrap_or_default();
3583 let mut rest: Vec<(String, String)> = vars
3584 .iter()
3585 .filter(|(k, _)| k.as_str() != "database" && k.as_str() != "user")
3586 .map(|(k, v)| (k.clone(), v.clone()))
3587 .collect();
3588 rest.sort();
3589 (database, user, rest)
3590 };
3591 let fp = crate::edge::fingerprint::analyze(sql, &database, &user, &session_vars);
3592 if fp.tables.is_empty() {
3596 None
3597 } else {
3598 let key = crate::edge::CacheKey {
3599 fingerprint: fp.fingerprint,
3600 params_hash: fp.params_hash,
3601 database,
3602 user,
3603 };
3604 if let Some(entry) = state.edge_cache.get(&key) {
3605 tracing::debug!(target: "helios::edge", "edge cache hit");
3606 client.write_all(&entry.response_bytes).await.map_err(|e| {
3607 ProxyError::Network(format!("Client write error: {}", e))
3608 })?;
3609 return Ok((None, entry.response_bytes.len() as u64));
3610 }
3611 let (read_version, inval_epoch) =
3620 if config.edge.role == crate::edge::EdgeRole::Edge {
3621 (
3622 state.edge_cache.observed_home_version(),
3623 state.edge_cache.invalidation_epoch(),
3624 )
3625 } else {
3626 (state.edge_cache.next_version(), 0)
3627 };
3628 Some((key, fp.tables, read_version, inval_epoch))
3629 }
3630 } else {
3631 None
3632 }
3633 };
3634
3635 #[cfg(feature = "query-cache")]
3638 let cache_ctx: Option<crate::cache::CacheContext> = if is_write {
3639 None
3640 } else if let Some(qc) = state.query_cache.as_ref() {
3641 let sql = crate::protocol::query_text(&forward_msg.payload).unwrap_or("");
3642 match Self::cacheable_read_ctx(session, sql).await {
3643 Some(ctx) => {
3644 if let crate::cache::CacheLookup::Hit { result, level } =
3645 qc.get(sql, &ctx).await
3646 {
3647 tracing::debug!(target: "helios::cache", level = %level, "cache hit");
3648 client.write_all(&result.data).await.map_err(|e| {
3649 ProxyError::Network(format!("Client write error: {}", e))
3650 })?;
3651 return Ok((None, result.data.len() as u64));
3652 }
3653 Some(ctx)
3654 }
3655 None => None,
3656 }
3657 } else {
3658 None
3659 };
3660
3661 #[cfg(feature = "schema-routing")]
3664 let forced_target = match state.schema_analyzer.as_ref() {
3665 Some(analyzer)
3666 if forced_target.is_none()
3667 && !is_write
3668 && !config.schema_routing.analytics_node.is_empty() =>
3669 {
3670 match crate::protocol::query_text(&forward_msg.payload) {
3671 Some(sql) if analyzer.analyze(sql).is_analytics() => {
3672 tracing::debug!(target: "helios::schema", "OLAP query routed to analytics node");
3673 Some(config.schema_routing.analytics_node.clone())
3674 }
3675 _ => forced_target,
3676 }
3677 }
3678 _ => forced_target,
3679 };
3680
3681 #[cfg(feature = "query-analytics")]
3683 let analytics_sql =
3684 crate::protocol::query_text(&forward_msg.payload).map(|s| s.to_string());
3685 #[cfg(feature = "query-analytics")]
3686 let started = std::time::Instant::now();
3687
3688 let target = Self::choose_target_node(
3689 is_write,
3690 forced_target,
3691 current_node,
3692 session,
3693 state,
3694 config,
3695 )
3696 .await?;
3697 tracing::debug!(target: "helios::routing", node = %target, is_write, "routed simple query");
3698
3699 #[cfg(feature = "circuit-breaker")]
3701 if let Some(mut resp) = Self::circuit_fast_fail(state, &target) {
3702 resp.extend_from_slice(&Self::create_ready_for_query(b'I'));
3703 client
3704 .write_all(&resp)
3705 .await
3706 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
3707 return Ok((None, resp.len() as u64));
3708 }
3709
3710 if let Err(e) = Self::ensure_conn(conns, &target, session, config, state).await {
3712 Self::record_backend_failure(state, &target, &e.to_string());
3713 return Err(e);
3714 }
3715 let backend = conns.get_mut(&target).expect("just ensured");
3716
3717 #[cfg(feature = "pool-modes")]
3723 if config.pool_mode.skip_clean_reset && !backend.dirty {
3724 if let Some(sql) = crate::protocol::query_text(&forward_msg.payload) {
3725 if Self::stmt_leaves_session_state(sql) {
3726 backend.dirty = true;
3727 }
3728 }
3729 }
3730
3731 let backend_err = match tokio::time::timeout(
3732 state.limits.backend_write_timeout,
3733 backend.stream.write_all(&forward_msg.encode()),
3734 )
3735 .await
3736 {
3737 Ok(Ok(())) => None,
3738 Ok(Err(e)) => Some(format!("Backend write error: {}", e)),
3739 Err(_) => Some("Backend write timeout".to_string()),
3740 };
3741 if let Some(msg) = backend_err {
3742 let e = ProxyError::Network(msg);
3743 conns.remove(&target);
3744 Self::record_backend_failure(state, &target, &e.to_string());
3745 return Err(e);
3746 }
3747
3748 #[cfg(any(feature = "query-cache", feature = "edge-proxy"))]
3753 {
3754 #[cfg(feature = "query-cache")]
3755 let qc_wants = cache_ctx.is_some() && state.query_cache.is_some();
3756 #[cfg(not(feature = "query-cache"))]
3757 let qc_wants = false;
3758 #[cfg(feature = "edge-proxy")]
3759 let edge_wants = edge_ctx.is_some();
3760 #[cfg(not(feature = "edge-proxy"))]
3761 let edge_wants = false;
3762 if qc_wants || edge_wants {
3763 return match Self::stream_until_ready_capture(
3764 client,
3765 &mut backend.stream,
3766 session,
3767 state.limits.client_write_timeout,
3768 state.limits.backend_read_timeout,
3769 )
3770 .await
3771 {
3772 Ok((sent, captured, cacheable, rows)) => {
3773 #[cfg(not(feature = "query-cache"))]
3774 let _ = rows; #[cfg(feature = "circuit-breaker")]
3776 Self::circuit_record(state, &target, true, "");
3777 let body = bytes::Bytes::from(captured);
3780 #[cfg(feature = "edge-proxy")]
3787 if let Some((key, tables, read_version, inval_epoch)) = edge_ctx {
3788 if cacheable && !body.is_empty() {
3789 let entry = crate::edge::CacheEntry {
3790 version: read_version,
3791 response_bytes: body.clone(),
3792 tables,
3793 expires_at: std::time::Instant::now()
3794 + config.edge.default_ttl(),
3795 };
3796 let stored = if config.edge.role == crate::edge::EdgeRole::Edge {
3797 state.edge_cache.insert_if_epoch(key, entry, inval_epoch)
3798 } else {
3799 state.edge_cache.insert_if_fresh(key, entry)
3800 };
3801 if !stored {
3802 tracing::debug!(
3803 target: "helios::edge",
3804 "edge store skipped — invalidation raced the read"
3805 );
3806 }
3807 }
3808 }
3809 #[cfg(feature = "query-cache")]
3810 if let (Some(ctx), Some(qc)) =
3811 (cache_ctx.as_ref(), state.query_cache.as_ref())
3812 {
3813 if cacheable && !body.is_empty() {
3814 let sql =
3815 crate::protocol::query_text(&forward_msg.payload).unwrap_or("");
3816 qc.put(sql, ctx, body.clone(), rows, std::time::Duration::ZERO)
3817 .await;
3818 }
3819 }
3820 let _ = body;
3821 #[cfg(feature = "query-analytics")]
3822 if let Some(sql) = analytics_sql.as_deref() {
3823 Self::record_analytics(
3824 state,
3825 session,
3826 sql,
3827 &target,
3828 started.elapsed(),
3829 None,
3830 )
3831 .await;
3832 }
3833 Ok((Some(target), sent))
3834 }
3835 Err(e) => {
3836 conns.remove(&target);
3837 Self::record_backend_failure(state, &target, &e.to_string());
3838 Err(e)
3839 }
3840 };
3841 }
3842 }
3843
3844 match Self::stream_until_ready(client, &mut backend.stream, session, state).await {
3845 Ok(sent) => {
3846 #[cfg(feature = "circuit-breaker")]
3847 Self::circuit_record(state, &target, true, "");
3848 #[cfg(feature = "query-cache")]
3850 if is_write {
3851 if let Some(qc) = state.query_cache.as_ref() {
3852 let sql = crate::protocol::query_text(&forward_msg.payload).unwrap_or("");
3853 qc.invalidate_query(sql).await;
3854 }
3855 }
3856 #[cfg(feature = "edge-proxy")]
3866 if config.edge.enabled {
3867 let sql = crate::protocol::query_text(&forward_msg.payload).unwrap_or("");
3868 if Self::edge_write_needs_invalidation(is_write, sql) {
3869 let tables = crate::edge::fingerprint::tables_only(sql);
3876 if session
3877 .copy_in_progress
3878 .load(std::sync::atomic::Ordering::Relaxed)
3879 {
3880 *session
3881 .pending_edge_copy_tables
3882 .lock()
3883 .unwrap_or_else(|e| e.into_inner()) = Some(tables);
3884 } else {
3885 Self::edge_invalidate_write(state, config, tables).await;
3886 }
3887 }
3888 }
3889 #[cfg(feature = "ha-tr")]
3891 if is_write && config.tr_enabled {
3892 if let Some(sql) = crate::protocol::query_text(&forward_msg.payload) {
3893 Self::journal_write(state, session, sql).await;
3894 }
3895 }
3896 #[cfg(feature = "query-analytics")]
3897 if let Some(sql) = analytics_sql.as_deref() {
3898 Self::record_analytics(state, session, sql, &target, started.elapsed(), None)
3899 .await;
3900 }
3901 Ok((Some(target), sent))
3902 }
3903 Err(e) => {
3904 conns.remove(&target);
3906 Self::record_backend_failure(state, &target, &e.to_string());
3907 #[cfg(feature = "query-analytics")]
3908 if let Some(sql) = analytics_sql.as_deref() {
3909 Self::record_analytics(
3910 state,
3911 session,
3912 sql,
3913 &target,
3914 started.elapsed(),
3915 Some(e.to_string()),
3916 )
3917 .await;
3918 }
3919 Err(e)
3920 }
3921 }
3922 }
3923
3924 #[allow(clippy::too_many_arguments)]
3937 async fn forward_extended_batch(
3938 client: &mut ClientStream,
3939 batch: &[u8],
3940 route_sql: Option<&str>,
3941 wait_ready: bool,
3942 conns: &mut HashMap<String, BackendConn>,
3943 current_node: Option<&str>,
3944 registry: &HashMap<String, bytes::Bytes>,
3945 reprepare: &[String],
3946 defines: &[String],
3947 unnamed: Option<(bytes::Bytes, bytes::Bytes)>,
3948 session: &Arc<ClientSession>,
3949 state: &Arc<ServerState>,
3950 config: &ProxyConfig,
3951 ) -> Result<(Option<String>, u64)> {
3952 #[cfg(feature = "rate-limiting")]
3956 if let Some(mut resp) = Self::rate_limit_check(session, state, config).await {
3957 if wait_ready {
3958 resp.extend_from_slice(&Self::create_ready_for_query(b'I'));
3959 }
3960 client
3961 .write_all(&resp)
3962 .await
3963 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
3964 return Ok((None, resp.len() as u64));
3965 }
3966
3967 #[cfg(feature = "query-analytics")]
3969 let analytics_sql = route_sql.map(|s| s.to_string());
3970 #[cfg(feature = "query-analytics")]
3971 let started = std::time::Instant::now();
3972
3973 let target = match route_sql {
3974 Some(sql) => {
3975 #[cfg(feature = "routing-hints")]
3978 let (is_write, forced) = Self::extended_hint_route(state, sql)
3979 .unwrap_or_else(|| (Self::is_write_query(sql), None));
3980 #[cfg(not(feature = "routing-hints"))]
3981 let (is_write, forced): (bool, Option<String>) = (Self::is_write_query(sql), None);
3982 #[cfg(feature = "lag-routing")]
3983 if is_write && config.lag_routing.enabled {
3984 *session.last_write_at.write().await = Some(std::time::Instant::now());
3985 }
3986 Self::choose_target_node(is_write, forced, current_node, session, state, config)
3987 .await?
3988 }
3989 None => match current_node {
3993 Some(c) => c.to_string(),
3994 None => Self::select_read_node(session, state, config).await?,
3995 },
3996 };
3997
3998 #[cfg(feature = "circuit-breaker")]
4000 if let Some(mut resp) = Self::circuit_fast_fail(state, &target) {
4001 if wait_ready {
4002 resp.extend_from_slice(&Self::create_ready_for_query(b'I'));
4003 }
4004 client
4005 .write_all(&resp)
4006 .await
4007 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
4008 return Ok((None, resp.len() as u64));
4009 }
4010
4011 if let Err(e) = Self::ensure_conn(conns, &target, session, config, state).await {
4012 Self::record_backend_failure(state, &target, &e.to_string());
4013 return Err(e);
4014 }
4015 let backend = conns.get_mut(&target).expect("just ensured");
4016
4017 for name in reprepare {
4022 if backend.prepared.contains(name) {
4023 continue;
4024 }
4025 let Some(parse_bytes) = registry.get(name) else {
4026 continue; };
4028 match Self::reprepare_statement(
4029 &mut backend.stream,
4030 parse_bytes,
4031 state.limits.reprepare_timeout,
4032 )
4033 .await
4034 {
4035 Ok(()) => {
4036 backend.prepared.insert(name.clone());
4037 }
4038 Err(e) => {
4039 conns.remove(&target);
4040 return Err(e);
4041 }
4042 }
4043 }
4044
4045 let mut inject_parse_complete = false;
4052 let mut new_unnamed_sig: Option<bytes::Bytes> = None;
4053 if let Some((parse_msg, sig)) = unnamed.as_ref() {
4054 if backend.unnamed_sig.as_deref() == Some(&sig[..]) {
4055 inject_parse_complete = true;
4056 } else {
4057 if let Err(e) = backend
4058 .stream
4059 .write_all(parse_msg)
4060 .await
4061 .map_err(|e| ProxyError::Network(format!("Backend write error: {}", e)))
4062 {
4063 conns.remove(&target);
4064 return Err(e);
4065 }
4066 new_unnamed_sig = Some(sig.clone());
4067 }
4068 }
4069
4070 let batch_err = match tokio::time::timeout(
4071 state.limits.backend_write_timeout,
4072 backend.stream.write_all(batch),
4073 )
4074 .await
4075 {
4076 Ok(Ok(())) => None,
4077 Ok(Err(e)) => Some(format!("Backend write error: {}", e)),
4078 Err(_) => Some("Backend write timeout".to_string()),
4079 };
4080 if let Some(msg) = batch_err {
4081 let e = ProxyError::Network(msg);
4082 conns.remove(&target);
4083 Self::record_backend_failure(state, &target, &e.to_string());
4084 return Err(e);
4085 }
4086
4087 let mut injected: u64 = 0;
4090 if inject_parse_complete {
4091 if let Err(e) = client
4092 .write_all(&[b'1', 0, 0, 0, 4])
4093 .await
4094 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))
4095 {
4096 conns.remove(&target);
4097 return Err(e);
4098 }
4099 injected = 5;
4100 }
4101
4102 let r = if wait_ready {
4103 Self::stream_until_ready(client, &mut backend.stream, session, state).await
4104 } else {
4105 Self::stream_flush(client, &mut backend.stream, session, state).await
4106 };
4107 match r {
4108 Ok(sent) => {
4109 #[cfg(feature = "circuit-breaker")]
4110 Self::circuit_record(state, &target, true, "");
4111 #[cfg(feature = "query-analytics")]
4112 if let Some(sql) = analytics_sql.as_deref() {
4113 Self::record_analytics(state, session, sql, &target, started.elapsed(), None)
4114 .await;
4115 }
4116 for name in defines {
4118 backend.prepared.insert(name.clone());
4119 }
4120 if let Some(sig) = new_unnamed_sig {
4122 backend.unnamed_sig = Some(sig);
4123 }
4124 Ok((Some(target), sent + injected))
4125 }
4126 Err(e) => {
4127 conns.remove(&target);
4128 Self::record_backend_failure(state, &target, &e.to_string());
4129 #[cfg(feature = "query-analytics")]
4130 if let Some(sql) = analytics_sql.as_deref() {
4131 Self::record_analytics(
4132 state,
4133 session,
4134 sql,
4135 &target,
4136 started.elapsed(),
4137 Some(e.to_string()),
4138 )
4139 .await;
4140 }
4141 Err(e)
4142 }
4143 }
4144 }
4145
4146 async fn reprepare_statement<S: AsyncReadExt + AsyncWriteExt + Unpin>(
4152 backend: &mut S,
4153 parse_bytes: &[u8],
4154 reprepare_timeout: Duration,
4155 ) -> Result<()> {
4156 tokio::time::timeout(reprepare_timeout, backend.write_all(parse_bytes))
4157 .await
4158 .map_err(|_| ProxyError::Network("re-prepare write timeout".to_string()))?
4159 .map_err(|e| ProxyError::Network(format!("re-prepare write error: {}", e)))?;
4160 tokio::time::timeout(reprepare_timeout, backend.write_all(&[b'H', 0, 0, 0, 4]))
4162 .await
4163 .map_err(|_| ProxyError::Network("re-prepare flush timeout".to_string()))?
4164 .map_err(|e| ProxyError::Network(format!("re-prepare flush error: {}", e)))?;
4165 let mtype = tokio::time::timeout(reprepare_timeout, Self::read_one_frame_type(backend))
4166 .await
4167 .map_err(|_| ProxyError::Network("re-prepare read timeout".to_string()))??;
4168 match mtype {
4169 b'1' => Ok(()), b'E' => Err(ProxyError::Protocol(
4171 "re-prepare rejected by backend".to_string(),
4172 )),
4173 other => Err(ProxyError::Protocol(format!(
4174 "unexpected re-prepare reply: {}",
4175 other as char
4176 ))),
4177 }
4178 }
4179
4180 async fn read_one_frame_type<S: AsyncReadExt + Unpin>(backend: &mut S) -> Result<u8> {
4184 let mut header = [0u8; 5];
4185 backend
4186 .read_exact(&mut header)
4187 .await
4188 .map_err(|e| ProxyError::Network(format!("re-prepare read error: {}", e)))?;
4189 let len = u32::from_be_bytes([header[1], header[2], header[3], header[4]]) as usize;
4190 let body_len = len.saturating_sub(4);
4191 if body_len > 0 {
4192 let mut body = vec![0u8; body_len];
4193 backend
4194 .read_exact(&mut body)
4195 .await
4196 .map_err(|e| ProxyError::Network(format!("re-prepare body read error: {}", e)))?;
4197 }
4198 Ok(header[0])
4199 }
4200
4201 fn parse_stmt_name(payload: &[u8]) -> &str {
4204 let end = payload.iter().position(|&b| b == 0).unwrap_or(0);
4205 std::str::from_utf8(&payload[..end]).unwrap_or("")
4206 }
4207
4208 fn bind_stmt_ref(payload: &[u8]) -> Option<&str> {
4212 let portal_end = payload.iter().position(|&b| b == 0)?;
4213 let rest = &payload[portal_end + 1..];
4214 let stmt_end = rest.iter().position(|&b| b == 0)?;
4215 let name = std::str::from_utf8(&rest[..stmt_end]).ok()?;
4216 (!name.is_empty()).then_some(name)
4217 }
4218
4219 fn stmt_kind_name(payload: &[u8]) -> Option<&str> {
4222 if payload.first() != Some(&b'S') {
4223 return None;
4224 }
4225 let rest = &payload[1..];
4226 let end = rest.iter().position(|&b| b == 0)?;
4227 let name = std::str::from_utf8(&rest[..end]).ok()?;
4228 (!name.is_empty()).then_some(name)
4229 }
4230
4231 async fn stream_until_ready(
4239 client: &mut ClientStream,
4240 backend: &mut TcpStream,
4241 session: &Arc<ClientSession>,
4242 state: &Arc<ServerState>,
4243 ) -> Result<u64> {
4244 let client_write_timeout = state.limits.client_write_timeout;
4245 let backend_read_timeout = state.limits.backend_read_timeout;
4246 let mut buf = BytesMut::with_capacity(16384);
4247 let mut sent: u64 = 0;
4248
4249 loop {
4250 let mut consumed = 0usize;
4252 let mut ready_status: Option<u8> = None;
4253 let mut yield_for_copy = false;
4254 loop {
4255 let rem = &buf[consumed..];
4256 if rem.len() < 5 {
4257 break;
4258 }
4259 let len = u32::from_be_bytes([rem[1], rem[2], rem[3], rem[4]]) as usize;
4260 if len < 4 || rem.len() < len + 1 {
4261 break; }
4263 let frame_total = len + 1;
4264 let mtype = rem[0];
4265 consumed += frame_total;
4266 if mtype == b'Z' {
4267 ready_status = Some(if frame_total >= 6 { rem[5] } else { b'I' });
4269 break;
4270 }
4271 if mtype == b'G' || mtype == b'W' {
4272 yield_for_copy = true;
4275 break;
4276 }
4277 }
4278
4279 if consumed > 0 {
4280 tokio::time::timeout(client_write_timeout, client.write_all(&buf[..consumed]))
4281 .await
4282 .map_err(|_| ProxyError::Network("Client write timeout".to_string()))?
4283 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
4284 sent += consumed as u64;
4285 let _ = buf.split_to(consumed);
4286 }
4287
4288 if let Some(status) = ready_status {
4289 let st = TransactionStatus::from_byte(status);
4290 session.in_transaction.store(
4291 st != TransactionStatus::Idle,
4292 std::sync::atomic::Ordering::Relaxed,
4293 );
4294 return Ok(sent);
4295 }
4296 if yield_for_copy {
4297 session
4302 .copy_in_progress
4303 .store(true, std::sync::atomic::Ordering::Relaxed);
4304 return Ok(sent);
4305 }
4306
4307 buf.reserve(16384);
4310 let n = tokio::time::timeout(backend_read_timeout, backend.read_buf(&mut buf))
4311 .await
4312 .map_err(|_| ProxyError::Network("Backend read timeout".to_string()))?
4313 .map_err(|e| ProxyError::Network(format!("Backend read error: {}", e)))?;
4314 if n == 0 {
4315 return Err(ProxyError::Connection(
4316 "Backend closed mid-response".to_string(),
4317 ));
4318 }
4319 }
4320 }
4321
4322 #[cfg(any(feature = "query-cache", feature = "edge-proxy"))]
4335 async fn stream_until_ready_capture(
4336 client: &mut ClientStream,
4337 backend: &mut TcpStream,
4338 session: &Arc<ClientSession>,
4339 client_write_timeout: Duration,
4340 backend_read_timeout: Duration,
4341 ) -> Result<(u64, Vec<u8>, bool, usize)> {
4342 let mut buf = BytesMut::with_capacity(16384);
4343 let mut sent: u64 = 0;
4344 let mut captured: Vec<u8> = Vec::with_capacity(4096);
4345 let mut had_error = false;
4346 let mut saw_async = false;
4347 let mut row_count: usize = 0;
4348
4349 loop {
4350 let mut consumed = 0usize;
4351 let mut ready_status: Option<u8> = None;
4352 let mut yield_for_copy = false;
4353 loop {
4354 let rem = &buf[consumed..];
4355 if rem.len() < 5 {
4356 break;
4357 }
4358 let len = u32::from_be_bytes([rem[1], rem[2], rem[3], rem[4]]) as usize;
4359 if len < 4 || rem.len() < len + 1 {
4360 break;
4361 }
4362 let frame_total = len + 1;
4363 let mtype = rem[0];
4364 if mtype == b'E' {
4365 had_error = true;
4366 }
4367 if mtype == b'A' || mtype == b'N' || mtype == b'S' {
4370 saw_async = true;
4371 }
4372 if mtype == b'C' {
4373 if let Some(tag) = rem.get(5..frame_total) {
4375 if let Some(end) = tag.iter().position(|&b| b == 0) {
4376 if let Ok(s) = std::str::from_utf8(&tag[..end]) {
4377 if let Some(n) =
4378 s.rsplit(' ').next().and_then(|x| x.parse::<usize>().ok())
4379 {
4380 row_count = n;
4381 }
4382 }
4383 }
4384 }
4385 }
4386 consumed += frame_total;
4387 if mtype == b'Z' {
4388 ready_status = Some(if frame_total >= 6 { rem[5] } else { b'I' });
4389 break;
4390 }
4391 if mtype == b'G' || mtype == b'W' {
4392 yield_for_copy = true;
4393 break;
4394 }
4395 }
4396
4397 if consumed > 0 {
4398 tokio::time::timeout(client_write_timeout, client.write_all(&buf[..consumed]))
4399 .await
4400 .map_err(|_| ProxyError::Network("Client write timeout".to_string()))?
4401 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
4402 captured.extend_from_slice(&buf[..consumed]);
4403 sent += consumed as u64;
4404 let _ = buf.split_to(consumed);
4405 }
4406
4407 if let Some(status) = ready_status {
4408 let st = TransactionStatus::from_byte(status);
4409 session.in_transaction.store(
4410 st != TransactionStatus::Idle,
4411 std::sync::atomic::Ordering::Relaxed,
4412 );
4413 let cacheable = !had_error && status == b'I' && !saw_async;
4414 return Ok((sent, captured, cacheable, row_count));
4415 }
4416 if yield_for_copy {
4417 session
4418 .copy_in_progress
4419 .store(true, std::sync::atomic::Ordering::Relaxed);
4420 return Ok((sent, captured, false, row_count));
4421 }
4422
4423 buf.reserve(16384);
4425 let n = tokio::time::timeout(backend_read_timeout, backend.read_buf(&mut buf))
4426 .await
4427 .map_err(|_| ProxyError::Network("Backend read timeout".to_string()))?
4428 .map_err(|e| ProxyError::Network(format!("Backend read error: {}", e)))?;
4429 if n == 0 {
4430 return Err(ProxyError::Connection(
4431 "Backend closed mid-response".to_string(),
4432 ));
4433 }
4434 }
4435 }
4436
4437 async fn stream_flush(
4451 client: &mut ClientStream,
4452 backend: &mut TcpStream,
4453 session: &Arc<ClientSession>,
4454 state: &Arc<ServerState>,
4455 ) -> Result<u64> {
4456 let _ = (session, state);
4457 let mut read_buf = vec![0u8; 16384];
4458 let mut sent: u64 = 0;
4459 loop {
4460 match backend.try_read(&mut read_buf) {
4461 Ok(0) => {
4462 return Err(ProxyError::Connection(
4463 "Backend closed mid-flush".to_string(),
4464 ))
4465 }
4466 Ok(n) => {
4467 client
4468 .write_all(&read_buf[..n])
4469 .await
4470 .map_err(|e| ProxyError::Network(format!("Client write error: {}", e)))?;
4471 sent += n as u64;
4472 }
4473 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(sent),
4476 Err(e) => return Err(ProxyError::Network(format!("Backend read error: {}", e))),
4477 }
4478 }
4479 }
4480
4481 fn is_write_message(msg: &Message) -> bool {
4483 match msg.msg_type {
4484 MessageType::Query => {
4485 crate::protocol::query_text(&msg.payload)
4489 .map(Self::is_write_query)
4490 .unwrap_or(false)
4491 }
4492 MessageType::Parse => {
4493 msg.payload
4496 .iter()
4497 .position(|&b| b == 0)
4498 .and_then(|end| crate::protocol::query_text(&msg.payload[end + 1..]))
4499 .map(Self::is_write_query)
4500 .unwrap_or(false)
4501 }
4502 _ => false,
4504 }
4505 }
4506
4507 fn is_write_query(sql: &str) -> bool {
4509 use crate::protocol::starts_with_ci;
4510 let trimmed = sql.trim();
4511
4512 if starts_with_ci(trimmed, "INSERT")
4514 || starts_with_ci(trimmed, "UPDATE")
4515 || starts_with_ci(trimmed, "DELETE")
4516 || starts_with_ci(trimmed, "CREATE")
4517 || starts_with_ci(trimmed, "DROP")
4518 || starts_with_ci(trimmed, "ALTER")
4519 || starts_with_ci(trimmed, "TRUNCATE")
4520 || starts_with_ci(trimmed, "GRANT")
4521 || starts_with_ci(trimmed, "REVOKE")
4522 || starts_with_ci(trimmed, "VACUUM")
4523 || starts_with_ci(trimmed, "REINDEX")
4524 || starts_with_ci(trimmed, "CLUSTER")
4525 {
4526 return true;
4527 }
4528
4529 if starts_with_ci(trimmed, "BEGIN")
4531 || starts_with_ci(trimmed, "START")
4532 || starts_with_ci(trimmed, "COMMIT")
4533 || starts_with_ci(trimmed, "ROLLBACK")
4534 || starts_with_ci(trimmed, "SAVEPOINT")
4535 || starts_with_ci(trimmed, "RELEASE")
4536 {
4537 return true;
4538 }
4539
4540 if starts_with_ci(trimmed, "SET") && !starts_with_ci(trimmed, "SET TRANSACTION READ ONLY") {
4542 return true;
4543 }
4544
4545 false
4546 }
4547
4548 #[cfg(feature = "edge-proxy")]
4563 fn edge_write_needs_invalidation(is_write: bool, sql: &str) -> bool {
4564 use crate::protocol::starts_with_ci;
4565 let t = sql.trim();
4566 let core = t.strip_suffix(';').unwrap_or(t).trim_end();
4567 let multi_stmt = core.contains(';');
4568 if !multi_stmt
4569 && (starts_with_ci(core, "BEGIN")
4570 || starts_with_ci(core, "START")
4571 || starts_with_ci(core, "SAVEPOINT")
4572 || starts_with_ci(core, "RELEASE")
4573 || starts_with_ci(core, "ROLLBACK"))
4574 {
4575 return false;
4576 }
4577 is_write
4578 || multi_stmt
4579 || Self::is_edge_copy_write_sql(core)
4580 || Self::is_edge_procedural_sql(core)
4581 || Self::is_edge_txn_end_sql(core)
4582 }
4583
4584 #[cfg(feature = "edge-proxy")]
4588 fn is_edge_copy_write_sql(sql: &str) -> bool {
4589 crate::protocol::starts_with_ci(sql.trim_start(), "COPY")
4590 && Self::contains_word_ci(sql, "from")
4591 }
4592
4593 #[cfg(feature = "edge-proxy")]
4602 fn edge_meta_prunable<'a>(closes: &'a [String], defines: &[String]) -> Vec<&'a str> {
4603 closes
4604 .iter()
4605 .filter(|n| !defines.contains(n))
4606 .map(String::as_str)
4607 .collect()
4608 }
4609
4610 #[cfg(feature = "edge-proxy")]
4617 fn is_edge_procedural_sql(sql: &str) -> bool {
4618 use crate::protocol::starts_with_ci;
4619 let t = sql.trim_start();
4620 starts_with_ci(t, "EXECUTE") || starts_with_ci(t, "CALL") || starts_with_ci(t, "DO")
4621 }
4622
4623 #[cfg(feature = "edge-proxy")]
4632 fn is_edge_txn_end_sql(sql: &str) -> bool {
4633 use crate::protocol::starts_with_ci;
4634 let t = sql.trim_start();
4635 starts_with_ci(t, "COMMIT") || starts_with_ci(t, "END")
4636 }
4637
4638 #[cfg(feature = "edge-proxy")]
4646 fn is_edge_dml_sql(sql: &str) -> bool {
4647 use crate::protocol::starts_with_ci;
4648 let t = sql.trim_start();
4649 if starts_with_ci(t, "INSERT")
4650 || starts_with_ci(t, "UPDATE")
4651 || starts_with_ci(t, "DELETE")
4652 || starts_with_ci(t, "MERGE")
4653 || starts_with_ci(t, "CREATE")
4654 || starts_with_ci(t, "DROP")
4655 || starts_with_ci(t, "ALTER")
4656 || starts_with_ci(t, "TRUNCATE")
4657 || starts_with_ci(t, "GRANT")
4658 || starts_with_ci(t, "REVOKE")
4659 {
4660 return true;
4661 }
4662 if starts_with_ci(t, "COPY") {
4663 return Self::contains_word_ci(t, "from");
4664 }
4665 if starts_with_ci(t, "WITH") {
4666 return Self::contains_word_ci(t, "insert")
4667 || Self::contains_word_ci(t, "update")
4668 || Self::contains_word_ci(t, "delete")
4669 || Self::contains_word_ci(t, "merge");
4670 }
4671 false
4672 }
4673
4674 #[cfg(feature = "edge-proxy")]
4680 fn edge_extended_batch_tables(
4681 refs: &[String],
4682 bound_unnamed: bool,
4683 named_meta: &HashMap<String, Option<Vec<String>>>,
4684 unnamed_meta: &Option<Vec<String>>,
4685 ) -> Option<Vec<String>> {
4686 let mut any_dml = false;
4687 let mut wipe_all = false;
4688 let mut union: Vec<String> = Vec::new();
4689 {
4690 let mut consider = |meta: &Option<Vec<String>>| {
4691 if let Some(tables) = meta {
4692 any_dml = true;
4693 if tables.is_empty() {
4694 wipe_all = true;
4695 } else {
4696 for t in tables {
4697 if !union.contains(t) {
4698 union.push(t.clone());
4699 }
4700 }
4701 }
4702 }
4703 };
4704 for name in refs {
4705 if let Some(meta) = named_meta.get(name) {
4706 consider(meta);
4707 }
4708 }
4709 if bound_unnamed {
4710 consider(unnamed_meta);
4711 }
4712 }
4713 if !any_dml {
4714 None
4715 } else if wipe_all {
4716 Some(Vec::new())
4717 } else {
4718 Some(union)
4719 }
4720 }
4721
4722 #[cfg(feature = "edge-proxy")]
4727 async fn edge_invalidate_write(
4728 state: &Arc<ServerState>,
4729 config: &ProxyConfig,
4730 tables: Vec<String>,
4731 ) {
4732 if config.edge.role == crate::edge::EdgeRole::Home {
4733 let version = state.edge_cache.next_version();
4734 let dropped = state.edge_cache.invalidate(version, &tables);
4735 let (notified, pruned) = state
4736 .edge_registry
4737 .broadcast(crate::edge::InvalidationEvent {
4738 up_to_version: version,
4739 tables,
4740 committed_at: chrono::Utc::now().to_rfc3339(),
4741 epoch: state.edge_cache.epoch(),
4742 })
4743 .await;
4744 tracing::debug!(
4745 target: "helios::edge",
4746 version,
4747 dropped,
4748 notified,
4749 pruned,
4750 "write invalidation broadcast to edges"
4751 );
4752 } else {
4753 let version = state.edge_cache.observed_home_version();
4759 let dropped = state.edge_cache.invalidate(version, &tables);
4760 tracing::debug!(
4761 target: "helios::edge",
4762 version,
4763 dropped,
4764 "write invalidated local edge cache (edge role — home broadcasts)"
4765 );
4766 }
4767 }
4768
4769 #[cfg(any(feature = "pool-modes", feature = "edge-proxy"))]
4793 fn stmt_leaves_session_state(sql: &str) -> bool {
4794 use crate::protocol::{contains_ci, starts_with_ci};
4795 let t = sql.trim();
4796 if t.is_empty() {
4797 return false;
4798 }
4799 let core = t.strip_suffix(';').unwrap_or(t).trim_end();
4804 if core.contains(';') {
4805 return true;
4806 }
4807 let neutral_lead = starts_with_ci(core, "SELECT")
4812 || starts_with_ci(core, "INSERT")
4813 || starts_with_ci(core, "UPDATE")
4814 || starts_with_ci(core, "DELETE")
4815 || starts_with_ci(core, "WITH")
4816 || starts_with_ci(core, "VALUES")
4817 || starts_with_ci(core, "TABLE")
4818 || starts_with_ci(core, "SHOW")
4819 || starts_with_ci(core, "EXPLAIN")
4820 || starts_with_ci(core, "FETCH")
4821 || starts_with_ci(core, "BEGIN")
4822 || starts_with_ci(core, "START")
4823 || starts_with_ci(core, "COMMIT")
4824 || starts_with_ci(core, "END")
4825 || starts_with_ci(core, "ROLLBACK")
4826 || starts_with_ci(core, "ABORT")
4827 || starts_with_ci(core, "SAVEPOINT")
4828 || starts_with_ci(core, "RELEASE");
4829 if !neutral_lead {
4830 return true;
4831 }
4832 if (starts_with_ci(core, "SELECT") || starts_with_ci(core, "WITH"))
4841 && Self::contains_word_ci(core, "into")
4842 {
4843 return true;
4844 }
4845 const DIRTY_TOKENS: [&str; 4] = ["set_config", "advisory", "nextval", "setval"];
4846 DIRTY_TOKENS.iter().any(|tok| contains_ci(core, tok))
4847 }
4848
4849 #[cfg(any(
4855 feature = "pool-modes",
4856 feature = "edge-proxy",
4857 feature = "query-cache"
4858 ))]
4859 fn contains_word_ci(haystack: &str, word: &str) -> bool {
4860 let hb = haystack.as_bytes();
4861 let wb = word.as_bytes();
4862 if wb.is_empty() || hb.len() < wb.len() {
4863 return false;
4864 }
4865 let is_ident = |c: u8| c.is_ascii_alphanumeric() || c == b'_';
4866 let mut i = 0;
4867 while i + wb.len() <= hb.len() {
4868 if hb[i..i + wb.len()].eq_ignore_ascii_case(wb) {
4869 let before_ok = i == 0 || !is_ident(hb[i - 1]);
4870 let after = i + wb.len();
4871 let after_ok = after == hb.len() || !is_ident(hb[after]);
4872 if before_ok && after_ok {
4873 return true;
4874 }
4875 }
4876 i += 1;
4877 }
4878 false
4879 }
4880
4881 #[cfg(feature = "rate-limiting")]
4884 async fn rate_limit_key(
4885 session: &Arc<ClientSession>,
4886 config: &ProxyConfig,
4887 ) -> crate::rate_limit::LimiterKey {
4888 use crate::config::RateLimitKeyBy;
4889 use crate::rate_limit::LimiterKey;
4890 match config.rate_limit.key_by {
4891 RateLimitKeyBy::Global => LimiterKey::Global,
4892 RateLimitKeyBy::ClientIp => LimiterKey::ClientIp(session.client_addr.ip()),
4893 RateLimitKeyBy::Database => {
4894 let vars = session.variables.read().await;
4895 LimiterKey::Database(vars.get("database").cloned().unwrap_or_default())
4896 }
4897 RateLimitKeyBy::User => {
4898 let vars = session.variables.read().await;
4899 LimiterKey::User(vars.get("user").cloned().unwrap_or_default())
4900 }
4901 }
4902 }
4903
4904 #[cfg(feature = "rate-limiting")]
4910 async fn rate_limit_check(
4911 session: &Arc<ClientSession>,
4912 state: &Arc<ServerState>,
4913 config: &ProxyConfig,
4914 ) -> Option<Vec<u8>> {
4915 use crate::rate_limit::RateLimitResult;
4916 let limiter = state.rate_limiter.as_ref()?;
4917 let key = Self::rate_limit_key(session, config).await;
4918 match limiter.check(&key, 1) {
4919 RateLimitResult::Allowed => None,
4920 RateLimitResult::Warned(msg) => {
4921 tracing::warn!(key = %key, reason = %msg, "rate limit warning");
4922 None
4923 }
4924 RateLimitResult::Throttled(d) | RateLimitResult::Queued(d) => {
4925 tokio::time::sleep(d.min(Duration::from_secs(5))).await;
4928 None
4929 }
4930 RateLimitResult::Denied(exc) => {
4931 tracing::info!(key = %key, "rate limit exceeded");
4932 let msg = format!(
4933 "rate limit exceeded: {} (retry after {}ms)",
4934 exc.message,
4935 exc.retry_after.as_millis()
4936 );
4937 Some(Self::create_error_response("53400", &msg))
4938 }
4939 }
4940 }
4941
4942 fn is_backend_fault(err: &str) -> bool {
4968 !err.contains("Client") && !err.contains("Backend read timeout")
4969 }
4970
4971 fn note_backend_failure(state: &Arc<ServerState>, addr: &str, err: &str) {
4975 if !Self::is_backend_fault(err) {
4976 return;
4977 }
4978 let _writers = state.health_write.lock();
4987 let snapshot = state.health.load_full();
4988 if snapshot.get(addr).map(|h| h.healthy).unwrap_or(false) {
4991 let mut next = (*snapshot).clone();
4992 if let Some(nh) = next.get_mut(addr) {
4993 nh.healthy = false;
4994 nh.failure_count = nh.failure_count.saturating_add(1);
4995 nh.last_error = Some(format!("in-band failure: {}", err));
4996 tracing::warn!(
4997 node = %addr,
4998 error = %err,
4999 "in-band failure — node marked unhealthy for fast failover"
5000 );
5001 }
5002 state.health.store(Arc::new(next));
5003 }
5004 }
5005
5006 fn record_backend_failure(state: &Arc<ServerState>, node: &str, err: &str) {
5012 Self::note_backend_failure(state, node, err);
5013 #[cfg(feature = "circuit-breaker")]
5014 if Self::is_backend_fault(err) {
5015 Self::circuit_record(state, node, false, err);
5016 }
5017 }
5018
5019 #[cfg(feature = "circuit-breaker")]
5022 fn circuit_is_open(state: &Arc<ServerState>, node: &str) -> bool {
5023 state
5024 .circuit_breaker
5025 .as_ref()
5026 .map(|cb| {
5027 cb.get_breaker(node).get_state() == crate::circuit_breaker::CircuitState::Open
5028 })
5029 .unwrap_or(false)
5030 }
5031
5032 #[cfg(feature = "circuit-breaker")]
5034 fn circuit_record(state: &Arc<ServerState>, node: &str, success: bool, err: &str) {
5035 if let Some(cb) = state.circuit_breaker.as_ref() {
5036 let breaker = cb.get_breaker(node);
5037 if success {
5038 breaker.record_success();
5039 } else {
5040 breaker.record_failure(err);
5041 }
5042 }
5043 }
5044
5045 #[cfg(feature = "circuit-breaker")]
5049 fn circuit_fast_fail(state: &Arc<ServerState>, node: &str) -> Option<Vec<u8>> {
5050 if Self::circuit_is_open(state, node) {
5051 tracing::info!(node = %node, "circuit open — fast-failing");
5052 Some(Self::create_error_response(
5053 "08006",
5054 &format!("circuit open for node {node}: backend temporarily unavailable"),
5055 ))
5056 } else {
5057 None
5058 }
5059 }
5060
5061 #[cfg(feature = "lag-routing")]
5064 fn ryw_pins_primary(last_write: Option<std::time::Instant>, window_ms: u64) -> bool {
5065 window_ms > 0
5066 && last_write
5067 .map(|t| t.elapsed() < Duration::from_millis(window_ms))
5068 .unwrap_or(false)
5069 }
5070
5071 #[cfg(feature = "lag-routing")]
5075 fn lag_excludes_standby(lag_bytes: Option<u64>, max_lag_bytes: u64) -> bool {
5076 max_lag_bytes > 0 && lag_bytes.map(|l| l > max_lag_bytes).unwrap_or(false)
5077 }
5078
5079 #[cfg(any(feature = "query-cache", feature = "edge-proxy"))]
5084 fn is_cacheable_read_sql(sql: &str) -> bool {
5085 use crate::protocol::{contains_ci, starts_with_ci};
5086 let t = sql.trim_start();
5087 if !starts_with_ci(t, "SELECT") {
5088 return false;
5089 }
5090 let core = t.trim_end();
5096 let core = core.strip_suffix(';').map(str::trim_end).unwrap_or(core);
5097 if core.contains(';') {
5098 return false;
5099 }
5100 if Self::contains_word_ci(core, "into") {
5106 return false;
5107 }
5108 if contains_ci(t, "FOR UPDATE") || contains_ci(t, "FOR SHARE") {
5109 return false;
5110 }
5111 const VOLATILE: [&str; 11] = [
5115 "now(",
5116 "current_timestamp",
5117 "current_date",
5118 "current_time",
5119 "clock_timestamp",
5120 "statement_timestamp",
5121 "random(",
5122 "nextval(",
5123 "uuid_generate",
5124 "gen_random_uuid",
5125 "set_config(",
5126 ];
5127 !VOLATILE.iter().any(|v| contains_ci(t, v))
5128 }
5129
5130 #[cfg(feature = "query-cache")]
5134 async fn cacheable_read_ctx(
5135 session: &Arc<ClientSession>,
5136 sql: &str,
5137 ) -> Option<crate::cache::CacheContext> {
5138 if !Self::is_cacheable_read_sql(sql) {
5139 return None;
5140 }
5141 if session
5143 .in_transaction
5144 .load(std::sync::atomic::Ordering::Relaxed)
5145 {
5146 return None;
5147 }
5148 let (user, database) = {
5149 let vars = session.variables.read().await;
5150 (
5151 vars.get("user").cloned(),
5152 vars.get("database")
5153 .cloned()
5154 .unwrap_or_else(|| "default".to_string()),
5155 )
5156 };
5157 Some(crate::cache::CacheContext {
5158 database,
5159 user,
5160 branch: None,
5161 connection_id: Some(session.id.as_u64_pair().0),
5162 })
5163 }
5164
5165 #[cfg(feature = "multi-tenancy")]
5169 async fn tenant_request_ctx(
5170 session: &Arc<ClientSession>,
5171 ) -> crate::multi_tenancy::RequestContext {
5172 let vars = session.variables.read().await;
5173 crate::multi_tenancy::RequestContext {
5174 headers: vars.clone(),
5175 username: vars.get("user").cloned(),
5176 database: vars.get("database").cloned(),
5177 auth_token: None,
5178 sql_context: HashMap::new(),
5179 client_ip: Some(session.client_addr.ip().to_string()),
5180 connection_id: Some(session.id.as_u64_pair().0),
5181 }
5182 }
5183
5184 #[cfg(feature = "ha-tr")]
5189 async fn journal_write(state: &Arc<ServerState>, session: &Arc<ClientSession>, sql: &str) {
5190 let tx_id = uuid::Uuid::new_v4();
5191 let j = &state.transaction_journal;
5192 if j.begin_transaction(tx_id, session.id, crate::NodeId::new(), 0)
5193 .await
5194 .is_ok()
5195 {
5196 let _ = j
5197 .log_statement(tx_id, sql.to_string(), Vec::new(), None, None, 0)
5198 .await;
5199 }
5200 }
5201
5202 #[cfg(feature = "query-analytics")]
5205 async fn record_analytics(
5206 state: &Arc<ServerState>,
5207 session: &Arc<ClientSession>,
5208 sql: &str,
5209 node: &str,
5210 duration: Duration,
5211 error: Option<String>,
5212 ) {
5213 let Some(analytics) = state.analytics.as_ref() else {
5214 return;
5215 };
5216 let (user, database) = {
5217 let vars = session.variables.read().await;
5218 (
5219 vars.get("user").cloned().unwrap_or_default(),
5220 vars.get("database").cloned().unwrap_or_default(),
5221 )
5222 };
5223 let mut exec = crate::analytics::QueryExecution::new(sql, duration);
5224 exec.user = user;
5225 exec.database = database;
5226 exec.client_ip = session.client_addr.ip().to_string();
5227 exec.node = node.to_string();
5228 exec.session_id = Some(session.id.to_string());
5229 exec.error = error;
5230 analytics.record(exec);
5231 }
5232
5233 async fn select_primary_with_timeout(
5235 session: &Arc<ClientSession>,
5236 state: &Arc<ServerState>,
5237 config: &ProxyConfig,
5238 ) -> Result<String> {
5239 let timeout = config.write_timeout();
5240 let start = std::time::Instant::now();
5241 let check_interval = Duration::from_millis(100);
5244
5245 loop {
5246 let health = state.health.load_full();
5248 let primary = config
5249 .nodes
5250 .iter()
5251 .find(|n| n.role == NodeRole::Primary && n.enabled);
5252
5253 if let Some(primary_node) = primary {
5254 if let Some(node_health) = health.get(&primary_node.address()) {
5255 if node_health.healthy {
5256 let mut current = session.current_node.write().await;
5258 *current = Some(primary_node.address());
5259 return Ok(primary_node.address());
5260 }
5261 }
5262 }
5263 drop(health);
5264
5265 if start.elapsed() >= timeout {
5267 state.metrics.failovers.fetch_add(1, Ordering::Relaxed);
5268 return Err(ProxyError::NoHealthyNodes);
5269 }
5270
5271 tracing::warn!(
5272 "Primary unavailable, waiting for failover... ({:.1}s elapsed, {:.1}s timeout)",
5273 start.elapsed().as_secs_f64(),
5274 timeout.as_secs_f64()
5275 );
5276
5277 tokio::time::sleep(check_interval).await;
5279 }
5280 }
5281
5282 async fn select_read_node(
5284 session: &Arc<ClientSession>,
5285 state: &Arc<ServerState>,
5286 config: &ProxyConfig,
5287 ) -> Result<String> {
5288 if session
5290 .in_transaction
5291 .load(std::sync::atomic::Ordering::Relaxed)
5292 {
5293 if let Some(node) = session.current_node.read().await.clone() {
5294 return Ok(node);
5295 }
5296 }
5297
5298 let health = state.health.load_full();
5300 let healthy_standbys: Vec<&NodeConfig> = config
5301 .nodes
5302 .iter()
5303 .filter(|n| {
5304 let base = n.enabled
5305 && (n.role == NodeRole::Standby || n.role == NodeRole::ReadReplica)
5306 && health.get(&n.address()).map(|h| h.healthy).unwrap_or(false);
5307 #[cfg(feature = "circuit-breaker")]
5309 let base = base && !Self::circuit_is_open(state, &n.address());
5310 #[cfg(feature = "lag-routing")]
5312 let base = base
5313 && !Self::lag_excludes_standby(
5314 health
5315 .get(&n.address())
5316 .and_then(|h| h.replication_lag_bytes),
5317 config.lag_routing.max_lag_bytes,
5318 );
5319 base
5320 })
5321 .collect();
5322
5323 if !healthy_standbys.is_empty() {
5324 let ticket = state.lb_state.rr_counter.fetch_add(1, Ordering::Relaxed);
5326 let index = ticket as usize % healthy_standbys.len();
5327 let node_addr = healthy_standbys[index].address();
5328
5329 let mut current = session.current_node.write().await;
5330 *current = Some(node_addr.clone());
5331 return Ok(node_addr);
5332 }
5333
5334 Self::select_node(session, state, config).await
5336 }
5337
5338 async fn complete_backend_auth(backend: &mut TcpStream) -> Result<()> {
5341 let mut buffer = BytesMut::with_capacity(4096);
5342 let timeout = Duration::from_secs(10);
5343 let start = std::time::Instant::now();
5344
5345 loop {
5346 if start.elapsed() > timeout {
5347 return Err(ProxyError::Auth(
5348 "Backend authentication timeout".to_string(),
5349 ));
5350 }
5351
5352 buffer.reserve(4096);
5353 let n = tokio::time::timeout(Duration::from_secs(5), backend.read_buf(&mut buffer))
5354 .await
5355 .map_err(|_| ProxyError::Auth("Read timeout during backend auth".to_string()))?
5356 .map_err(|e| ProxyError::Network(format!("Backend auth read error: {}", e)))?;
5357
5358 if n == 0 {
5359 return Err(ProxyError::Connection(
5360 "Backend closed during auth".to_string(),
5361 ));
5362 }
5363
5364 loop {
5371 if buffer.len() < 5 {
5372 break;
5373 }
5374 let len = u32::from_be_bytes([buffer[1], buffer[2], buffer[3], buffer[4]]) as usize;
5375 if len < 4 || buffer.len() < len + 1 {
5376 break;
5377 }
5378 let tag = buffer[0];
5379 let frame = buffer.split_to(len + 1);
5380 match tag {
5381 b'Z' => return Ok(()),
5383 b'E' => {
5385 let payload = BytesMut::from(&frame[5..]);
5386 let err = ErrorResponse::parse(payload)
5387 .map(|e| e.message().unwrap_or("Unknown error").to_string())
5388 .unwrap_or_else(|_| "authentication failed".to_string());
5389 return Err(ProxyError::Auth(err));
5390 }
5391 _ => {}
5392 }
5393 }
5394 }
5395 }
5396
5397 fn create_error_response(code: &str, message: &str) -> Vec<u8> {
5399 let mut fields = HashMap::new();
5400 fields.insert('S', "ERROR".to_string());
5401 fields.insert('V', "ERROR".to_string());
5402 fields.insert('C', code.to_string());
5403 fields.insert('M', message.to_string());
5404
5405 let err = ErrorResponse { fields };
5406 err.encode().encode().to_vec()
5407 }
5408
5409 fn create_ready_for_query(status: u8) -> Vec<u8> {
5412 let mut payload = BytesMut::with_capacity(1);
5413 payload.put_u8(status);
5414 Message::new(MessageType::ReadyForQuery, payload)
5415 .encode()
5416 .to_vec()
5417 }
5418
5419 #[cfg(feature = "wasm-plugins")]
5457 fn synthesise_cached_response(bytes: &[u8]) -> Result<Vec<u8>> {
5458 use serde::Deserialize;
5459
5460 #[derive(Deserialize)]
5461 struct CachedPayload {
5462 columns: Vec<ColumnDef>,
5463 rows: Vec<Vec<Option<String>>>,
5464 }
5465
5466 #[derive(Deserialize)]
5467 struct ColumnDef {
5468 name: String,
5469 #[serde(default = "default_text_oid")]
5470 oid: u32,
5471 }
5472
5473 fn default_text_oid() -> u32 {
5474 25 }
5476
5477 let payload: CachedPayload = serde_json::from_slice(bytes)
5478 .map_err(|e| ProxyError::Protocol(format!("invalid cached payload JSON: {}", e)))?;
5479
5480 if payload.columns.is_empty() {
5481 return Err(ProxyError::Protocol(
5482 "cached payload must declare at least one column".to_string(),
5483 ));
5484 }
5485
5486 let mut reply = Vec::new();
5487
5488 let mut rd = BytesMut::new();
5490 rd.put_u16(payload.columns.len() as u16);
5491 for col in &payload.columns {
5492 rd.extend_from_slice(col.name.as_bytes());
5493 rd.put_u8(0); rd.put_i32(0); rd.put_i16(0); rd.put_u32(col.oid);
5497 rd.put_i16(-1); rd.put_i32(-1); rd.put_i16(0); }
5501 reply.extend_from_slice(&Message::new(MessageType::RowDescription, rd).encode());
5502
5503 let column_count = payload.columns.len();
5505 for row in &payload.rows {
5506 if row.len() != column_count {
5507 return Err(ProxyError::Protocol(format!(
5508 "cached row has {} values but {} columns are declared",
5509 row.len(),
5510 column_count
5511 )));
5512 }
5513 let mut dr = BytesMut::new();
5514 dr.put_u16(row.len() as u16);
5515 for value in row {
5516 match value {
5517 Some(s) => {
5518 dr.put_i32(s.len() as i32);
5519 dr.extend_from_slice(s.as_bytes());
5520 }
5521 None => {
5522 dr.put_i32(-1); }
5524 }
5525 }
5526 reply.extend_from_slice(&Message::new(MessageType::DataRow, dr).encode());
5527 }
5528
5529 let tag = format!("SELECT {}", payload.rows.len());
5531 let mut cc = BytesMut::new();
5532 cc.extend_from_slice(tag.as_bytes());
5533 cc.put_u8(0);
5534 reply.extend_from_slice(&Message::new(MessageType::CommandComplete, cc).encode());
5535
5536 reply.extend_from_slice(&Self::create_ready_for_query(b'I'));
5538
5539 Ok(reply)
5540 }
5541
5542 fn apply_pre_query_hook(
5552 msg: Message,
5553 state: &Arc<ServerState>,
5554 session: &Arc<ClientSession>,
5555 ) -> (Message, PreQueryAction) {
5556 #[cfg(feature = "wasm-plugins")]
5557 {
5558 let pm = match state.plugin_manager.as_ref() {
5559 Some(pm) => pm,
5560 None => return (msg, PreQueryAction::Forward),
5561 };
5562
5563 if msg.msg_type != MessageType::Query {
5564 return (msg, PreQueryAction::Forward);
5565 }
5566
5567 if !pm.has_hook(HookType::PreQuery) {
5570 return (msg, PreQueryAction::Forward);
5571 }
5572
5573 let query_msg = match QueryMessage::parse(msg.payload.clone()) {
5574 Ok(q) => q,
5575 Err(_) => return (msg, PreQueryAction::Forward),
5576 };
5577
5578 let ctx = Self::build_query_context(&query_msg.query, session);
5579
5580 match pm.execute_pre_query(&ctx) {
5581 PreQueryResult::Continue => (msg, PreQueryAction::Forward),
5582 PreQueryResult::Block(reason) => (msg, PreQueryAction::Block(reason)),
5583 PreQueryResult::Rewrite(new_sql) => {
5584 let rewritten = QueryMessage { query: new_sql }.encode();
5585 (rewritten, PreQueryAction::Forward)
5586 }
5587 PreQueryResult::Cached(bytes) => (msg, PreQueryAction::Cached(bytes)),
5588 }
5589 }
5590 #[cfg(not(feature = "wasm-plugins"))]
5591 {
5592 let _ = (state, session);
5593 (msg, PreQueryAction::Forward)
5594 }
5595 }
5596
5597 #[cfg(feature = "anomaly-detection")]
5603 fn record_anomaly_observation(
5604 msg: &Message,
5605 state: &Arc<ServerState>,
5606 session: &Arc<ClientSession>,
5607 ) {
5608 if msg.msg_type != MessageType::Query {
5609 return;
5610 }
5611 if let Some(query) = crate::protocol::query_text(&msg.payload) {
5614 Self::record_anomaly_sql(query, state, session);
5615 }
5616 }
5617
5618 #[cfg(feature = "anomaly-detection")]
5622 fn record_anomaly_sql(query: &str, state: &Arc<ServerState>, session: &Arc<ClientSession>) {
5623 let tenant = match session.variables.try_read() {
5630 Ok(vars) => vars
5631 .get("tenant_id")
5632 .or_else(|| vars.get("user"))
5633 .cloned()
5634 .unwrap_or_else(|| session.client_addr.ip().to_string()),
5635 Err(_) => session.client_addr.ip().to_string(),
5636 };
5637 let fingerprint = anomaly_fingerprint(query);
5638 let obs = crate::anomaly::QueryObservation {
5639 tenant,
5640 fingerprint,
5641 sql: query.to_string(),
5642 timestamp: std::time::Instant::now(),
5643 };
5644 for ev in state.anomaly_detector.record_query(&obs) {
5645 tracing::warn!(anomaly = ?ev, "anomaly detected");
5646 }
5647 }
5648
5649 async fn send_block_response(
5653 stream: &mut ClientStream,
5654 reason: &str,
5655 state: &Arc<ServerState>,
5656 ) -> Result<()> {
5657 let err =
5658 Self::create_error_response("42000", &format!("Query blocked by plugin: {}", reason));
5659 stream
5660 .write_all(&err)
5661 .await
5662 .map_err(|e| ProxyError::Network(format!("Write error: {}", e)))?;
5663 let rfq = Self::create_ready_for_query(b'I');
5664 stream
5665 .write_all(&rfq)
5666 .await
5667 .map_err(|e| ProxyError::Network(format!("Write error: {}", e)))?;
5668 state
5669 .metrics
5670 .bytes_sent
5671 .fetch_add((err.len() + rfq.len()) as u64, Ordering::Relaxed);
5672 Ok(())
5673 }
5674
5675 #[cfg(feature = "wasm-plugins")]
5681 fn build_query_context(query: &str, session: &Arc<ClientSession>) -> QueryContext {
5682 let is_read_only = !Self::is_write_query(query);
5683 let hook_context = HookContext {
5684 client_id: Some(session.id.to_string()),
5685 ..HookContext::default()
5686 };
5687 QueryContext {
5688 query: query.to_string(),
5689 normalized: query.to_string(),
5690 tables: Vec::new(),
5691 is_read_only,
5692 hook_context,
5693 }
5694 }
5695
5696 async fn apply_authenticate_hook(
5717 _params: &HashMap<String, String>,
5718 _session: &Arc<ClientSession>,
5719 _state: &Arc<ServerState>,
5720 ) -> Result<()> {
5721 #[cfg(feature = "wasm-plugins")]
5722 {
5723 let pm = match _state.plugin_manager.as_ref() {
5724 Some(pm) => pm,
5725 None => return Ok(()),
5726 };
5727
5728 let request = PluginAuthRequest {
5729 headers: HashMap::new(),
5730 username: _params.get("user").cloned(),
5731 password: None,
5732 client_ip: _session.client_addr.ip().to_string(),
5733 database: _params.get("database").cloned(),
5734 };
5735
5736 match pm.execute_authenticate(&request) {
5737 AuthResult::Defer => Ok(()),
5738 AuthResult::Success(identity) => {
5739 tracing::debug!(
5740 user = %identity.username,
5741 roles = ?identity.roles,
5742 "plugin authenticated user"
5743 );
5744 *_session.plugin_identity.write().await = Some(identity);
5745 Ok(())
5746 }
5747 AuthResult::Denied(reason) => {
5748 tracing::info!(
5749 reason = %reason,
5750 client = %_session.client_addr,
5751 user = ?_params.get("user"),
5752 "plugin denied authentication"
5753 );
5754 Err(ProxyError::Auth(format!(
5755 "authentication denied by plugin: {}",
5756 reason
5757 )))
5758 }
5759 }
5760 }
5761 #[cfg(not(feature = "wasm-plugins"))]
5762 {
5763 Ok(())
5764 }
5765 }
5766
5767 fn apply_route_hook(
5770 msg: &Message,
5771 state: &Arc<ServerState>,
5772 session: &Arc<ClientSession>,
5773 ) -> RouteOverride {
5774 #[cfg(feature = "wasm-plugins")]
5775 {
5776 let pm = match state.plugin_manager.as_ref() {
5777 Some(pm) => pm,
5778 None => return RouteOverride::None,
5779 };
5780 if msg.msg_type != MessageType::Query {
5781 return RouteOverride::None;
5782 }
5783 if !pm.has_hook(HookType::Route) {
5786 return RouteOverride::None;
5787 }
5788 let query_msg = match QueryMessage::parse(msg.payload.clone()) {
5789 Ok(q) => q,
5790 Err(_) => return RouteOverride::None,
5791 };
5792 let ctx = Self::build_query_context(&query_msg.query, session);
5793 match pm.execute_route(&ctx) {
5794 RouteResult::Default => RouteOverride::None,
5795 RouteResult::Primary => RouteOverride::Primary,
5796 RouteResult::Standby => RouteOverride::Standby,
5797 RouteResult::Node(name) => RouteOverride::Node(name),
5798 RouteResult::Block(reason) => RouteOverride::Block(reason),
5799 RouteResult::Branch(name) => {
5800 tracing::warn!(
5801 branch = %name,
5802 "Route hook returned Branch but branch routing is not yet wired — using default"
5803 );
5804 RouteOverride::None
5805 }
5806 }
5807 }
5808 #[cfg(not(feature = "wasm-plugins"))]
5809 {
5810 let _ = (msg, state, session);
5811 RouteOverride::None
5812 }
5813 }
5814
5815 #[cfg(feature = "routing-hints")]
5821 fn hint_to_override(hints: &crate::routing::ParsedHints) -> RouteOverride {
5822 use crate::routing::{ConsistencyLevel, RouteTarget};
5823 if let Some(node) = &hints.node {
5824 return RouteOverride::Node(node.clone());
5825 }
5826 if let Some(route) = hints.route {
5827 return match route {
5828 RouteTarget::Primary => RouteOverride::Primary,
5829 RouteTarget::Standby
5830 | RouteTarget::Sync
5831 | RouteTarget::SemiSync
5832 | RouteTarget::Async
5833 | RouteTarget::Local => RouteOverride::Standby,
5834 RouteTarget::Any | RouteTarget::Vector => RouteOverride::None,
5835 };
5836 }
5837 if hints.consistency == Some(ConsistencyLevel::Strong) {
5838 return RouteOverride::Primary;
5839 }
5840 RouteOverride::None
5841 }
5842
5843 #[cfg(feature = "routing-hints")]
5851 fn resolve_simple_route(
5852 msg: &Message,
5853 plugin_override: RouteOverride,
5854 default_is_write: bool,
5855 state: &Arc<ServerState>,
5856 ) -> (RouteOverride, bool, Option<Message>) {
5857 let parser = match state.hint_parser.as_ref() {
5858 Some(p) => p,
5859 None => return (plugin_override, default_is_write, None),
5860 };
5861 let sql = match crate::protocol::query_text(&msg.payload) {
5862 Some(s) => s,
5863 None => return (plugin_override, default_is_write, None),
5864 };
5865 let hints = parser.parse(sql);
5866 if hints.is_empty() {
5867 return (plugin_override, default_is_write, None);
5868 }
5869 let stripped = parser.strip(sql);
5870 let is_write = Self::is_write_query(&stripped);
5871 let effective = match Self::hint_to_override(&hints) {
5872 RouteOverride::None => plugin_override,
5873 hint_override => hint_override,
5874 };
5875 let forward = if parser.strip_hints {
5876 Some(crate::protocol::QueryMessage { query: stripped }.encode())
5877 } else {
5878 None
5879 };
5880 (effective, is_write, forward)
5881 }
5882
5883 #[cfg(feature = "routing-hints")]
5890 fn extended_hint_route(state: &Arc<ServerState>, sql: &str) -> Option<(bool, Option<String>)> {
5891 let parser = state.hint_parser.as_ref()?;
5892 let hints = parser.parse(sql);
5893 if hints.is_empty() {
5894 return None;
5895 }
5896 let stripped = parser.strip(sql);
5897 let is_write = Self::is_write_query(&stripped);
5898 match Self::hint_to_override(&hints) {
5899 RouteOverride::Primary => Some((true, None)),
5900 RouteOverride::Standby => Some((false, None)),
5901 RouteOverride::Node(n) => Some((is_write, Some(n))),
5902 _ => Some((is_write, None)),
5903 }
5904 }
5905
5906 #[cfg(feature = "wasm-plugins")]
5910 fn fire_post_query_hook(
5911 msg: &Message,
5912 session: &Arc<ClientSession>,
5913 state: &Arc<ServerState>,
5914 result: &Result<(Option<String>, u64)>,
5915 elapsed: Duration,
5916 ) {
5917 let pm = match state.plugin_manager.as_ref() {
5918 Some(pm) => pm,
5919 None => return,
5920 };
5921 if msg.msg_type != MessageType::Query {
5922 return;
5923 }
5924 if !pm.has_hook(HookType::PostQuery) {
5927 return;
5928 }
5929 let query_msg = match QueryMessage::parse(msg.payload.clone()) {
5930 Ok(q) => q,
5931 Err(_) => return,
5932 };
5933 let ctx = Self::build_query_context(&query_msg.query, session);
5934 let outcome = match result {
5935 Ok((node, bytes)) => PostQueryOutcome {
5936 success: true,
5937 target_node: node.clone(),
5938 elapsed_us: elapsed.as_micros() as u64,
5939 response_bytes: *bytes,
5940 error: None,
5941 },
5942 Err(e) => PostQueryOutcome {
5943 success: false,
5944 target_node: None,
5945 elapsed_us: elapsed.as_micros() as u64,
5946 response_bytes: 0,
5947 error: Some(e.to_string()),
5948 },
5949 };
5950 pm.execute_post_query(&ctx, &outcome);
5951 }
5952
5953 async fn select_node(
5957 session: &Arc<ClientSession>,
5958 state: &Arc<ServerState>,
5959 config: &ProxyConfig,
5960 ) -> Result<String> {
5961 if session
5963 .in_transaction
5964 .load(std::sync::atomic::Ordering::Relaxed)
5965 {
5966 if let Some(node) = session.current_node.read().await.clone() {
5967 return Ok(node);
5968 }
5969 }
5970
5971 let health = state.health.load_full();
5973 let healthy_nodes: Vec<&NodeConfig> = config
5974 .nodes
5975 .iter()
5976 .filter(|n| n.enabled && health.get(&n.address()).map(|h| h.healthy).unwrap_or(false))
5977 .collect();
5978
5979 if healthy_nodes.is_empty() {
5980 return Err(ProxyError::NoHealthyNodes);
5981 }
5982
5983 if let Some(primary) = healthy_nodes.iter().find(|n| n.role == NodeRole::Primary) {
5985 let node_addr = primary.address();
5986 let mut current = session.current_node.write().await;
5987 *current = Some(node_addr.clone());
5988 return Ok(node_addr);
5989 }
5990
5991 if let Some(standby) = healthy_nodes.iter().find(|n| n.role == NodeRole::Standby) {
5994 tracing::warn!("Primary unavailable, connecting to standby for initial session");
5995 let node_addr = standby.address();
5996 let mut current = session.current_node.write().await;
5997 *current = Some(node_addr.clone());
5998 return Ok(node_addr);
5999 }
6000
6001 Err(ProxyError::NoHealthyNodes)
6003 }
6004
6005 fn spawn_health_checker(&self) -> tokio::task::JoinHandle<()> {
6007 let state = self.state.clone();
6008 let mut shutdown_rx = self.shutdown_tx.subscribe();
6009
6010 tokio::spawn(async move {
6011 let interval_secs = state.live_config.load().health.check_interval_secs.max(1);
6017 let mut interval = tokio::time::interval(std::time::Duration::from_secs(interval_secs));
6018
6019 loop {
6020 tokio::select! {
6021 _ = interval.tick() => {
6022 let config = state.live_config.load_full();
6025 let st = state.clone();
6030 if let Err(e) = tokio::spawn(async move {
6031 Self::check_all_nodes(&st, &config).await;
6032 })
6033 .await
6034 {
6035 tracing::error!(error = %e, "health-check sweep panicked; health loop continuing");
6036 }
6037 }
6038 _ = shutdown_rx.recv() => {
6039 break;
6040 }
6041 }
6042 }
6043 })
6044 }
6045
6046 async fn check_all_nodes(state: &Arc<ServerState>, config: &ProxyConfig) {
6053 let timeout = Duration::from_secs(config.health.check_timeout_secs);
6056 let mut set = tokio::task::JoinSet::new();
6057 for node in &config.nodes {
6058 let addr = node.address();
6059 set.spawn(async move {
6060 let r = Self::check_node_addr(&addr, timeout).await;
6061 (addr, r)
6062 });
6063 }
6064 let mut results = Vec::with_capacity(config.nodes.len());
6065 while let Some(joined) = set.join_next().await {
6066 if let Ok(pair) = joined {
6067 results.push(pair);
6068 }
6069 }
6070
6071 let _writers = state.health_write.lock();
6077 let mut next = (*state.health.load_full()).clone();
6078 for (addr, result) in results {
6079 if let Some(node_health) = next.get_mut(&addr) {
6080 match result {
6081 Ok(latency) => {
6082 node_health.healthy = true;
6083 node_health.failure_count = 0;
6084 node_health.latency_ms = latency;
6085 node_health.last_error = None;
6086 }
6087 Err(e) => {
6088 node_health.failure_count += 1;
6089 node_health.last_error = Some(e.to_string());
6090 if node_health.failure_count >= config.health.failure_threshold {
6091 node_health.healthy = false;
6092 tracing::warn!(
6093 "Node {} marked unhealthy after {} failures",
6094 addr,
6095 node_health.failure_count
6096 );
6097 }
6098 }
6099 }
6100 node_health.last_check = chrono::Utc::now();
6101 }
6102 }
6103 state.health.store(Arc::new(next));
6104 }
6105
6106 async fn check_node_addr(addr: &str, timeout: Duration) -> Result<f64> {
6117 const SSL_REQUEST: [u8; 8] = [0, 0, 0, 8, 0x04, 0xD2, 0x16, 0x2F];
6119 let start = std::time::Instant::now();
6120 let mut stream = tokio::time::timeout(timeout, TcpStream::connect(addr))
6121 .await
6122 .map_err(|_| ProxyError::HealthCheck(format!("Timeout connecting to {}", addr)))?
6123 .map_err(|e| {
6124 ProxyError::HealthCheck(format!("Failed to connect to {}: {}", addr, e))
6125 })?;
6126
6127 let probe = async {
6128 stream.write_all(&SSL_REQUEST).await?;
6129 let mut resp = [0u8; 1];
6130 stream.read_exact(&mut resp).await?;
6131 Ok::<u8, std::io::Error>(resp[0])
6132 };
6133 let remaining = timeout
6135 .saturating_sub(start.elapsed())
6136 .max(Duration::from_millis(1));
6137 let byte = tokio::time::timeout(remaining, probe)
6138 .await
6139 .map_err(|_| {
6140 ProxyError::HealthCheck(format!("{} did not answer protocol probe in time", addr))
6141 })?
6142 .map_err(|e| {
6143 ProxyError::HealthCheck(format!("{} protocol probe error: {}", addr, e))
6144 })?;
6145 if byte != b'S' && byte != b'N' {
6148 return Err(ProxyError::HealthCheck(format!(
6149 "{} sent unexpected probe reply {:#x}",
6150 addr, byte
6151 )));
6152 }
6153 let latency = start.elapsed().as_secs_f64() * 1000.0;
6154 Ok(latency)
6155 }
6156
6157 fn spawn_pool_manager(&self) -> tokio::task::JoinHandle<()> {
6159 #[cfg(feature = "pool-modes")]
6161 let state = self.state.clone();
6162 let reap_interval = self.state.limits.pool_reap_interval;
6165 let mut shutdown_rx = self.shutdown_tx.subscribe();
6166
6167 tokio::spawn(async move {
6168 let mut interval = tokio::time::interval(reap_interval);
6169
6170 loop {
6171 tokio::select! {
6172 _ = interval.tick() => {
6173 #[cfg(feature = "pool-modes")]
6175 if let Some(ref pool_manager) = state.pool_manager {
6176 pool_manager.evict_idle().await;
6177 tracing::trace!("Pool-modes idle eviction completed");
6178 }
6179 #[cfg(feature = "pool-modes")]
6184 if let Some(ref backend_pool) = state.backend_pool {
6185 let ttl = std::time::Duration::from_secs(
6186 state.live_config.load().pool_mode.idle_timeout_secs,
6187 );
6188 let n = if ttl.is_zero() {
6194 0
6195 } else {
6196 backend_pool.reap_idle(ttl)
6197 };
6198 if n > 0 {
6199 tracing::debug!(
6200 target: "helios::pool",
6201 reaped = n,
6202 idle_remaining = backend_pool.idle_count(),
6203 "reaped idle backend connections (TTL)"
6204 );
6205 }
6206 }
6207 }
6208 _ = shutdown_rx.recv() => {
6209 #[cfg(feature = "pool-modes")]
6211 if let Some(ref pool_manager) = state.pool_manager {
6212 pool_manager.close_all().await;
6213 tracing::info!("Pool-modes manager closed all connections");
6214 }
6215 break;
6216 }
6217 }
6218 }
6219 })
6220 }
6221
6222 #[cfg(feature = "edge-proxy")]
6229 fn spawn_edge_maintenance(&self) -> tokio::task::JoinHandle<()> {
6230 let state = self.state.clone();
6231 let gc_secs = self.config.edge.subscribe_gc_secs.max(1);
6234 let mut shutdown_rx = self.shutdown_tx.subscribe();
6235
6236 tokio::spawn(async move {
6237 let mut interval = tokio::time::interval(Duration::from_secs(gc_secs));
6238
6239 loop {
6240 tokio::select! {
6241 _ = interval.tick() => {
6242 let pruned = state.edge_registry.prune_stale();
6243 if pruned > 0 {
6244 tracing::debug!(
6245 target: "helios::edge",
6246 pruned,
6247 remaining = state.edge_registry.count(),
6248 "edge registry GC pruned stale edges"
6249 );
6250 }
6251 }
6252 _ = shutdown_rx.recv() => break,
6253 }
6254 }
6255 })
6256 }
6257
6258 pub fn shutdown(&self) {
6260 let _ = self.shutdown_tx.send(());
6261 }
6262
6263 #[cfg(feature = "pool-modes")]
6265 pub async fn pool_mode_stats(&self) -> Option<PoolModeStatsSnapshot> {
6266 if let Some(ref pool_manager) = self.state.pool_manager {
6267 let stats = pool_manager.get_stats().await;
6268 let metrics = pool_manager.metrics().snapshot();
6269 let default_mode = pool_manager.default_mode();
6270
6271 let avg_lease_duration_ms = metrics
6273 .mode_stats
6274 .get(&default_mode)
6275 .map(|s| s.avg_lease_duration_ms as u64)
6276 .unwrap_or(0);
6277
6278 Some(PoolModeStatsSnapshot {
6279 mode: format!("{:?}", default_mode),
6280 total_connections: stats.total_connections,
6281 active_leases: stats.active_connections,
6282 idle_connections: stats.idle_connections,
6283 node_count: stats.node_count,
6284 acquires: metrics.acquires,
6285 releases: metrics.releases,
6286 acquire_failures: metrics.acquire_failures,
6287 acquire_timeouts: metrics.acquire_timeouts,
6288 transactions_completed: metrics.transactions_completed,
6289 statements_executed: metrics.statements_executed,
6290 avg_lease_duration_ms,
6291 })
6292 } else {
6293 None
6294 }
6295 }
6296
6297 #[cfg(feature = "pool-modes")]
6299 pub async fn add_node_to_pool(&self, node: &NodeConfig) {
6300 if let Some(ref pool_manager) = self.state.pool_manager {
6301 let endpoint = NodeEndpoint::new(&node.host, node.port)
6302 .with_role(match node.role {
6303 NodeRole::Primary => crate::NodeRole::Primary,
6304 NodeRole::Standby => crate::NodeRole::Standby,
6305 NodeRole::ReadReplica => crate::NodeRole::ReadReplica,
6306 })
6307 .with_weight(node.weight);
6308 pool_manager.add_node(&endpoint).await;
6309 tracing::info!("Added node {} to pool manager", node.address());
6310 }
6311 }
6312
6313 pub fn metrics(&self) -> ServerMetricsSnapshot {
6315 ServerMetricsSnapshot {
6316 connections_accepted: self
6317 .state
6318 .metrics
6319 .connections_accepted
6320 .load(Ordering::Relaxed),
6321 connections_closed: self
6322 .state
6323 .metrics
6324 .connections_closed
6325 .load(Ordering::Relaxed),
6326 queries_processed: self.state.metrics.queries_processed.load(Ordering::Relaxed),
6327 bytes_received: self.state.metrics.bytes_received.load(Ordering::Relaxed),
6328 bytes_sent: self.state.metrics.bytes_sent.load(Ordering::Relaxed),
6329 failovers: self.state.metrics.failovers.load(Ordering::Relaxed),
6330 }
6331 }
6332}
6333
6334#[derive(Debug, Clone)]
6336pub struct ServerMetricsSnapshot {
6337 pub connections_accepted: u64,
6338 pub connections_closed: u64,
6339 pub queries_processed: u64,
6340 pub bytes_received: u64,
6341 pub bytes_sent: u64,
6342 pub failovers: u64,
6343}
6344
6345#[cfg(feature = "pool-modes")]
6347#[derive(Debug, Clone)]
6348pub struct PoolModeStatsSnapshot {
6349 pub mode: String,
6351 pub total_connections: usize,
6353 pub active_leases: usize,
6355 pub idle_connections: usize,
6357 pub node_count: usize,
6359 pub acquires: u64,
6361 pub releases: u64,
6363 pub acquire_failures: u64,
6365 pub acquire_timeouts: u64,
6367 pub transactions_completed: u64,
6369 pub statements_executed: u64,
6371 pub avg_lease_duration_ms: u64,
6373}
6374
6375#[cfg(test)]
6376mod tests {
6377 use super::*;
6378 #[cfg(not(feature = "wasm-plugins"))]
6379 use crate::protocol::QueryMessage;
6380
6381 fn test_config() -> ProxyConfig {
6382 let mut config = ProxyConfig {
6383 listen_address: "127.0.0.1:0".to_string(),
6384 ..Default::default()
6385 };
6386 config.add_node("127.0.0.1:5432", "primary").unwrap();
6387 config
6388 }
6389
6390 #[test]
6391 fn test_server_creation() {
6392 let config = test_config();
6393 let server = ProxyServer::new(config);
6394 assert!(server.is_ok());
6395 }
6396
6397 #[test]
6398 fn is_backend_fault_excludes_client_and_slow_query_errors() {
6399 assert!(ProxyServer::is_backend_fault(
6401 "Backend read error: connection reset"
6402 ));
6403 assert!(ProxyServer::is_backend_fault(
6404 "Backend write error: broken pipe"
6405 ));
6406 assert!(ProxyServer::is_backend_fault("Backend write timeout"));
6407 assert!(ProxyServer::is_backend_fault(
6408 "Failed to connect to 127.0.0.1:5432: Connection refused"
6409 ));
6410 assert!(!ProxyServer::is_backend_fault("Backend read timeout"));
6413 assert!(!ProxyServer::is_backend_fault("Client write timeout"));
6414 assert!(!ProxyServer::is_backend_fault(
6415 "Client write error: broken pipe"
6416 ));
6417 assert!(!ProxyServer::is_backend_fault("Backend read timeout"));
6419 assert!(ProxyServer::is_backend_fault(
6420 "Backend read error: timed out"
6421 ));
6422 }
6423
6424 #[test]
6425 fn test_hba_addr_matches() {
6426 use std::net::IpAddr;
6427 let v4 = |s: &str| s.parse::<IpAddr>().unwrap();
6428 assert!(ProxyServer::hba_addr_matches("all", v4("203.0.113.7")));
6430 assert!(ProxyServer::hba_addr_matches("10.0.0.0/8", v4("10.1.2.3")));
6432 assert!(!ProxyServer::hba_addr_matches("10.0.0.0/8", v4("11.1.2.3")));
6433 assert!(ProxyServer::hba_addr_matches(
6434 "127.0.0.1/32",
6435 v4("127.0.0.1")
6436 ));
6437 assert!(!ProxyServer::hba_addr_matches(
6438 "127.0.0.1/32",
6439 v4("127.0.0.2")
6440 ));
6441 assert!(ProxyServer::hba_addr_matches(
6443 "192.168.1.1",
6444 v4("192.168.1.1")
6445 ));
6446 assert!(!ProxyServer::hba_addr_matches(
6447 "192.168.1.1",
6448 v4("192.168.1.2")
6449 ));
6450 assert!(ProxyServer::hba_addr_matches("::1/128", v4("::1")));
6452 assert!(ProxyServer::hba_addr_matches("0.0.0.0/0", v4("8.8.8.8")));
6453 }
6454
6455 #[test]
6456 fn test_hba_admits() {
6457 use crate::config::{HbaAction, HbaRule};
6458 use std::net::IpAddr;
6459 let ip: IpAddr = "10.0.0.5".parse().unwrap();
6460 assert!(ProxyServer::hba_admits(&[], ip, "bench", "benchdb"));
6462 let rules = vec![HbaRule {
6464 action: HbaAction::Reject,
6465 user: "bench".into(),
6466 database: "all".into(),
6467 address: "all".into(),
6468 }];
6469 assert!(!ProxyServer::hba_admits(&rules, ip, "bench", "benchdb"));
6470 assert!(ProxyServer::hba_admits(&rules, ip, "alice", "benchdb"));
6471 let rules = vec![
6473 HbaRule {
6474 action: HbaAction::Allow,
6475 user: "bench".into(),
6476 database: "all".into(),
6477 address: "10.0.0.0/8".into(),
6478 },
6479 HbaRule {
6480 action: HbaAction::Reject,
6481 user: "all".into(),
6482 database: "all".into(),
6483 address: "all".into(),
6484 },
6485 ];
6486 assert!(ProxyServer::hba_admits(&rules, ip, "bench", "benchdb"));
6487 assert!(!ProxyServer::hba_admits(
6488 &rules,
6489 "192.168.0.1".parse().unwrap(),
6490 "bench",
6491 "benchdb"
6492 ));
6493 assert!(!ProxyServer::hba_admits(&rules, ip, "alice", "benchdb"));
6494 }
6495
6496 #[test]
6497 fn test_initial_metrics() {
6498 let config = test_config();
6499 let server = ProxyServer::new(config).unwrap();
6500 let metrics = server.metrics();
6501 assert_eq!(metrics.connections_accepted, 0);
6502 assert_eq!(metrics.queries_processed, 0);
6503 }
6504
6505 #[tokio::test]
6506 async fn test_session_creation() {
6507 let config = test_config();
6508 let server = ProxyServer::new(config).unwrap();
6509
6510 assert!(server.state.sessions.is_empty());
6511 }
6512
6513 #[tokio::test]
6514 async fn test_node_health_initialization() {
6515 let config = test_config();
6516 let server = ProxyServer::new(config).unwrap();
6517
6518 let health = server.state.health.load_full();
6519 assert!(!health.is_empty());
6520
6521 for node_health in health.values() {
6522 assert!(node_health.healthy);
6523 assert_eq!(node_health.failure_count, 0);
6524 }
6525 }
6526
6527 fn make_test_session() -> Arc<ClientSession> {
6529 Arc::new(ClientSession {
6530 id: Uuid::new_v4(),
6531 client_addr: "127.0.0.1:0".parse().unwrap(),
6532 current_node: RwLock::new(None),
6533 in_transaction: std::sync::atomic::AtomicBool::new(false),
6534 copy_in_progress: std::sync::atomic::AtomicBool::new(false),
6535 tx_state: RwLock::new(TransactionState::default()),
6536 variables: RwLock::new(HashMap::new()),
6537 created_at: chrono::Utc::now(),
6538 tr_mode: crate::config::TrMode::default(),
6539 #[cfg(feature = "lag-routing")]
6540 last_write_at: RwLock::new(None),
6541 #[cfg(feature = "pool-modes")]
6542 pool_client_id: crate::pool::lease::ClientId::default(),
6543 #[cfg(feature = "wasm-plugins")]
6544 plugin_identity: RwLock::new(None),
6545 #[cfg(feature = "edge-proxy")]
6546 edge_ineligible: std::sync::atomic::AtomicBool::new(false),
6547 #[cfg(feature = "edge-proxy")]
6548 pending_edge_copy_tables: std::sync::Mutex::new(None),
6549 })
6550 }
6551
6552 #[tokio::test]
6556 async fn test_apply_route_hook_no_plugin_manager_returns_none() {
6557 let config = test_config();
6558 let server = ProxyServer::new(config).unwrap();
6559 let session = make_test_session();
6560
6561 let msg = QueryMessage {
6562 query: "SELECT * FROM users".to_string(),
6563 }
6564 .encode();
6565
6566 let decision = ProxyServer::apply_route_hook(&msg, &server.state, &session);
6567 assert!(matches!(decision, RouteOverride::None));
6568 }
6569
6570 #[tokio::test]
6574 async fn test_apply_pre_query_hook_no_plugin_manager_forwards() {
6575 let config = test_config();
6576 let server = ProxyServer::new(config).unwrap();
6577 let session = make_test_session();
6578
6579 let original = QueryMessage {
6580 query: "SELECT 1".to_string(),
6581 }
6582 .encode();
6583 let original_bytes = original.encode().to_vec();
6584
6585 let (msg_out, action) =
6586 ProxyServer::apply_pre_query_hook(original, &server.state, &session);
6587
6588 assert!(matches!(action, PreQueryAction::Forward));
6589 assert_eq!(msg_out.encode().to_vec(), original_bytes);
6591 }
6592
6593 #[tokio::test]
6597 async fn test_apply_route_hook_skips_non_query_messages() {
6598 let config = test_config();
6599 let server = ProxyServer::new(config).unwrap();
6600 let session = make_test_session();
6601
6602 let sync_msg = Message::empty(MessageType::Sync);
6603 let decision = ProxyServer::apply_route_hook(&sync_msg, &server.state, &session);
6604 assert!(matches!(decision, RouteOverride::None));
6605 }
6606
6607 #[cfg(feature = "wasm-plugins")]
6612 #[test]
6613 fn test_init_plugin_manager_disabled_by_default_returns_none() {
6614 let config = test_config();
6615 assert!(!config.plugins.enabled);
6616 let pm = ProxyServer::init_plugin_manager(&config.plugins);
6617 assert!(pm.is_none());
6618 }
6619
6620 #[cfg(feature = "wasm-plugins")]
6624 #[test]
6625 fn test_init_plugin_manager_missing_dir_logs_warning() {
6626 let mut config = test_config();
6627 config.plugins.enabled = true;
6628 config.plugins.plugin_dir = "/definitely/not/a/real/path".to_string();
6629
6630 let pm = ProxyServer::init_plugin_manager(&config.plugins);
6632 assert!(pm.is_some());
6633 }
6634
6635 #[tokio::test]
6639 async fn test_apply_authenticate_hook_no_plugin_manager_defers() {
6640 let config = test_config();
6641 let server = ProxyServer::new(config).unwrap();
6642 let session = make_test_session();
6643
6644 let mut params = HashMap::new();
6645 params.insert("user".to_string(), "alice".to_string());
6646 params.insert("database".to_string(), "app".to_string());
6647
6648 let result = ProxyServer::apply_authenticate_hook(¶ms, &session, &server.state).await;
6649 assert!(result.is_ok());
6650
6651 #[cfg(feature = "wasm-plugins")]
6653 {
6654 let ident = session.plugin_identity.read().await;
6655 assert!(ident.is_none());
6656 }
6657 }
6658
6659 #[cfg(feature = "wasm-plugins")]
6667 #[test]
6668 fn test_synthesise_cached_response_roundtrip() {
6669 let payload = br#"{
6670 "columns": [
6671 {"name": "id", "oid": 23},
6672 {"name": "email", "oid": 25}
6673 ],
6674 "rows": [
6675 ["1", "alice@example.com"],
6676 ["2", null]
6677 ]
6678 }"#;
6679 let reply = ProxyServer::synthesise_cached_response(payload).expect("synthesis");
6680
6681 let mut tags = Vec::new();
6684 let mut i = 0;
6685 while i < reply.len() {
6686 let tag = reply[i];
6687 let len = u32::from_be_bytes([reply[i + 1], reply[i + 2], reply[i + 3], reply[i + 4]])
6688 as usize;
6689 tags.push(tag);
6690 i += 1 + len;
6691 }
6692 assert_eq!(i, reply.len(), "no trailing bytes");
6693 assert_eq!(tags, vec![b'T', b'D', b'D', b'C', b'Z'], "wire frame order");
6694
6695 assert_eq!(*reply.last().unwrap(), b'I');
6697 }
6698
6699 #[cfg(feature = "wasm-plugins")]
6702 #[test]
6703 fn test_synthesise_cached_response_rejects_row_width_mismatch() {
6704 let payload = br#"{
6705 "columns": [{"name": "id", "oid": 23}, {"name": "name", "oid": 25}],
6706 "rows": [["1", "alice", "extra"]]
6707 }"#;
6708 let result = ProxyServer::synthesise_cached_response(payload);
6709 assert!(matches!(result, Err(ProxyError::Protocol(_))));
6710 }
6711
6712 #[cfg(feature = "wasm-plugins")]
6716 #[test]
6717 fn test_synthesise_cached_response_rejects_empty_columns() {
6718 let payload = br#"{ "columns": [], "rows": [] }"#;
6719 let result = ProxyServer::synthesise_cached_response(payload);
6720 assert!(matches!(result, Err(ProxyError::Protocol(_))));
6721 }
6722
6723 #[cfg(feature = "wasm-plugins")]
6726 #[test]
6727 fn test_synthesise_cached_response_rejects_bad_json() {
6728 let payload = b"not json at all";
6729 let result = ProxyServer::synthesise_cached_response(payload);
6730 assert!(matches!(result, Err(ProxyError::Protocol(_))));
6731 }
6732
6733 #[cfg(feature = "wasm-plugins")]
6742 #[tokio::test]
6743 async fn test_apply_authenticate_hook_with_manager_no_plugins_defers() {
6744 use crate::plugins::{PluginManager, PluginRuntimeConfig};
6745
6746 let config = test_config();
6747 let server = ProxyServer::new(config).unwrap();
6748 let session = make_test_session();
6749
6750 let pm = Arc::new(PluginManager::new(PluginRuntimeConfig::default()).unwrap());
6753 #[cfg(feature = "edge-proxy")]
6754 let edge_defaults = crate::edge::EdgeConfig::default();
6755 let augmented_state = Arc::new(ServerState {
6756 limits: ResolvedLimits::default(),
6757 sessions: DashMap::new(),
6758 health: ArcSwap::from_pointee(HashMap::new()),
6759 health_write: parking_lot::Mutex::new(()),
6760 live_config: ArcSwap::from_pointee(ProxyConfig::default()),
6761 metrics: ServerMetrics::default(),
6762 cancel_map: Arc::new(DashMap::new()),
6763 cancel_order: Arc::new(parking_lot::Mutex::new(std::collections::VecDeque::new())),
6764 tls_acceptor: None,
6765 auth_file: None,
6766 mirror: None,
6767 cutover: Arc::new(ArcSwap::from_pointee(None)),
6768 lb_state: LoadBalancerState {
6769 rr_counter: AtomicU64::new(0),
6770 },
6771 #[cfg(feature = "routing-hints")]
6772 hint_parser: None,
6773 #[cfg(feature = "rate-limiting")]
6774 rate_limiter: None,
6775 #[cfg(feature = "circuit-breaker")]
6776 circuit_breaker: None,
6777 #[cfg(feature = "query-analytics")]
6778 analytics: None,
6779 #[cfg(feature = "query-cache")]
6780 query_cache: None,
6781 #[cfg(feature = "query-rewriting")]
6782 rewriter: None,
6783 #[cfg(feature = "multi-tenancy")]
6784 tenant_manager: None,
6785 #[cfg(feature = "schema-routing")]
6786 schema_analyzer: None,
6787 #[cfg(feature = "pool-modes")]
6788 pool_manager: None,
6789 #[cfg(feature = "pool-modes")]
6790 backend_pool: None,
6791 plugin_manager: Some(pm),
6792 #[cfg(feature = "ha-tr")]
6793 transaction_journal: Arc::new(crate::transaction_journal::TransactionJournal::new()),
6794 #[cfg(feature = "anomaly-detection")]
6795 anomaly_detector: Arc::new(crate::anomaly::AnomalyDetector::new(
6796 ProxyConfig::default().anomaly.to_anomaly_config(),
6797 )),
6798 #[cfg(feature = "edge-proxy")]
6799 edge_cache: Arc::new(crate::edge::EdgeCache::new(
6800 edge_defaults.max_entries.max(1),
6801 )),
6802 #[cfg(feature = "edge-proxy")]
6803 edge_registry: Arc::new(crate::edge::EdgeRegistry::new(
6804 edge_defaults.max_edges,
6805 std::time::Duration::from_secs(edge_defaults.liveness_window_secs),
6806 )),
6807 });
6808
6809 let mut params = HashMap::new();
6810 params.insert("user".to_string(), "alice".to_string());
6811
6812 let result =
6813 ProxyServer::apply_authenticate_hook(¶ms, &session, &augmented_state).await;
6814 assert!(result.is_ok());
6815 let ident = session.plugin_identity.read().await;
6816 assert!(ident.is_none());
6817 let _ = server;
6819 }
6820
6821 fn cstr(s: &str) -> Vec<u8> {
6824 let mut v = s.as_bytes().to_vec();
6825 v.push(0);
6826 v
6827 }
6828
6829 #[test]
6830 fn parse_stmt_name_extracts_named_and_unnamed() {
6831 let mut named = cstr("ps1");
6833 named.extend_from_slice(&cstr("SELECT 1"));
6834 named.extend_from_slice(&[0, 0]);
6835 assert_eq!(ProxyServer::parse_stmt_name(&named), "ps1");
6836
6837 let mut unnamed = cstr("");
6838 unnamed.extend_from_slice(&cstr("SELECT 1"));
6839 unnamed.extend_from_slice(&[0, 0]);
6840 assert_eq!(ProxyServer::parse_stmt_name(&unnamed), "");
6841 }
6842
6843 #[test]
6844 fn bind_stmt_ref_reads_second_cstring() {
6845 let mut named = cstr("portal_a");
6847 named.extend_from_slice(&cstr("ps1"));
6848 named.extend_from_slice(&[0, 0]); assert_eq!(ProxyServer::bind_stmt_ref(&named), Some("ps1"));
6850
6851 let mut unnamed = cstr("");
6853 unnamed.extend_from_slice(&cstr(""));
6854 assert_eq!(ProxyServer::bind_stmt_ref(&unnamed), None);
6855 }
6856
6857 #[test]
6858 fn stmt_kind_name_only_matches_statement_kind() {
6859 let mut stmt = vec![b'S'];
6861 stmt.extend_from_slice(&cstr("ps1"));
6862 assert_eq!(ProxyServer::stmt_kind_name(&stmt), Some("ps1"));
6863
6864 let mut portal = vec![b'P'];
6866 portal.extend_from_slice(&cstr("portal_a"));
6867 assert_eq!(ProxyServer::stmt_kind_name(&portal), None);
6868
6869 let mut empty = vec![b'S'];
6871 empty.extend_from_slice(&cstr(""));
6872 assert_eq!(ProxyServer::stmt_kind_name(&empty), None);
6873 }
6874
6875 #[tokio::test]
6876 async fn read_one_frame_type_consumes_full_frame() {
6877 let (mut a, mut b) = tokio::io::duplex(64);
6880 let bytes = [b'1', 0, 0, 0, 4, b'Z', 0, 0, 0, 5, b'I'];
6882 b.write_all(&bytes).await.unwrap();
6883 let t = ProxyServer::read_one_frame_type(&mut a).await.unwrap();
6884 assert_eq!(t, b'1');
6885 let t2 = ProxyServer::read_one_frame_type(&mut a).await.unwrap();
6887 assert_eq!(t2, b'Z');
6888 }
6889
6890 #[tokio::test]
6891 async fn reprepare_statement_accepts_parse_complete_and_rejects_error() {
6892 let (mut client, mut backend) = tokio::io::duplex(64);
6894 backend.write_all(&[b'1', 0, 0, 0, 4]).await.unwrap();
6895 let parse = {
6896 let mut p = vec![b'P', 0, 0, 0, 0];
6897 p.extend_from_slice(&cstr("ps1"));
6898 p.extend_from_slice(&cstr("SELECT 1"));
6899 p.extend_from_slice(&[0, 0]);
6900 p
6901 };
6902 assert!(
6903 ProxyServer::reprepare_statement(&mut client, &parse, Duration::from_secs(15))
6904 .await
6905 .is_ok()
6906 );
6907
6908 let (mut client2, mut backend2) = tokio::io::duplex(64);
6910 backend2.write_all(&[b'E', 0, 0, 0, 4]).await.unwrap();
6911 assert!(
6912 ProxyServer::reprepare_statement(&mut client2, &parse, Duration::from_secs(15))
6913 .await
6914 .is_err()
6915 );
6916 }
6917
6918 #[cfg(feature = "routing-hints")]
6921 mod routing_hints {
6922 use super::*;
6923 use crate::routing::HintParser;
6924
6925 fn over(sql: &str) -> RouteOverride {
6926 let hints = HintParser::new().parse(sql);
6927 ProxyServer::hint_to_override(&hints)
6928 }
6929
6930 #[test]
6931 fn route_primary_maps_to_primary() {
6932 assert!(matches!(
6933 over("/*helios:route=primary*/ SELECT 1"),
6934 RouteOverride::Primary
6935 ));
6936 }
6937
6938 #[test]
6939 fn read_tier_targets_map_to_standby() {
6940 for t in ["standby", "sync", "semisync", "async", "local"] {
6941 assert!(
6942 matches!(
6943 over(&format!("/*helios:route={t}*/ SELECT 1")),
6944 RouteOverride::Standby
6945 ),
6946 "route={t} should map to Standby"
6947 );
6948 }
6949 }
6950
6951 #[test]
6952 fn any_and_vector_impose_no_constraint() {
6953 assert!(matches!(
6954 over("/*helios:route=any*/ SELECT 1"),
6955 RouteOverride::None
6956 ));
6957 assert!(matches!(
6958 over("/*helios:route=vector*/ SELECT 1"),
6959 RouteOverride::None
6960 ));
6961 }
6962
6963 #[test]
6964 fn node_hint_maps_to_node_and_wins_over_route() {
6965 match over("/*helios:node=pg-standby,route=primary*/ SELECT 1") {
6967 RouteOverride::Node(n) => assert_eq!(n, "pg-standby"),
6968 other => panic!("expected Node, got {other:?}"),
6969 }
6970 }
6971
6972 #[test]
6973 fn consistency_strong_forces_primary() {
6974 assert!(matches!(
6975 over("/*helios:consistency=strong*/ SELECT 1"),
6976 RouteOverride::Primary
6977 ));
6978 }
6979
6980 #[test]
6981 fn no_hint_yields_none() {
6982 assert!(matches!(over("SELECT 1"), RouteOverride::None));
6983 }
6984
6985 #[test]
6989 fn write_verb_classified_after_strip() {
6990 let parser = HintParser::new();
6991 let raw = "/*helios:route=primary*/ INSERT INTO t VALUES (1)";
6992 assert!(!ProxyServer::is_write_query(raw));
6995 assert!(ProxyServer::is_write_query(&parser.strip(raw)));
6997 }
6998
6999 #[test]
7000 fn strip_removes_hint_comment() {
7001 let parser = HintParser::new();
7002 assert_eq!(
7003 parser.strip("/*helios:route=standby*/ SELECT 42"),
7004 "SELECT 42"
7005 );
7006 }
7007 }
7008
7009 #[cfg(feature = "rate-limiting")]
7012 mod rate_limiting {
7013 use crate::rate_limit::{LimiterKey, RateLimitConfig, RateLimitResult, RateLimiter};
7014
7015 #[test]
7016 fn burst_allows_then_denies() {
7017 let cfg = RateLimitConfig {
7020 enabled: true,
7021 default_qps: 1,
7022 default_burst: 2,
7023 ..Default::default()
7024 };
7025 let limiter = RateLimiter::new(cfg);
7026 let key = LimiterKey::User("u".to_string());
7027
7028 assert!(matches!(limiter.check(&key, 1), RateLimitResult::Allowed));
7030 assert!(matches!(limiter.check(&key, 1), RateLimitResult::Allowed));
7031
7032 let mut denied = false;
7034 for _ in 0..5 {
7035 if matches!(limiter.check(&key, 1), RateLimitResult::Denied(_)) {
7036 denied = true;
7037 }
7038 }
7039 assert!(denied, "over-burst checks must yield a Denied verdict");
7040 }
7041
7042 #[test]
7043 fn distinct_keys_have_independent_buckets() {
7044 let cfg = RateLimitConfig {
7045 enabled: true,
7046 default_qps: 1,
7047 default_burst: 1,
7048 ..Default::default()
7049 };
7050 let limiter = RateLimiter::new(cfg);
7051 assert!(matches!(
7053 limiter.check(&LimiterKey::User("a".to_string()), 1),
7054 RateLimitResult::Allowed
7055 ));
7056 assert!(matches!(
7057 limiter.check(&LimiterKey::User("b".to_string()), 1),
7058 RateLimitResult::Allowed
7059 ));
7060 }
7061 }
7062
7063 #[cfg(feature = "circuit-breaker")]
7066 mod circuit_breaker {
7067 use crate::circuit_breaker::{
7068 CircuitBreakerConfig, CircuitBreakerManager, CircuitState, ManagerConfig,
7069 };
7070 use std::time::Duration;
7071
7072 fn mgr(threshold: u32) -> CircuitBreakerManager {
7073 let cfg = CircuitBreakerConfig {
7074 failure_threshold: threshold,
7075 cooldown: Duration::from_secs(10),
7076 ..Default::default()
7077 };
7078 CircuitBreakerManager::new(ManagerConfig::new(cfg))
7079 }
7080
7081 #[test]
7082 fn opens_after_threshold_failures() {
7083 let m = mgr(3);
7084 let b = m.get_breaker("n1");
7085 assert_eq!(b.get_state(), CircuitState::Closed);
7086 b.record_failure("boom");
7087 b.record_failure("boom");
7088 assert_eq!(b.get_state(), CircuitState::Closed);
7090 b.record_failure("boom");
7092 assert_eq!(b.get_state(), CircuitState::Open);
7093 }
7094
7095 #[test]
7096 fn healthy_node_stays_closed() {
7097 let m = mgr(3);
7098 let b = m.get_breaker("n2");
7099 b.record_success();
7100 b.record_success();
7101 assert_eq!(b.get_state(), CircuitState::Closed);
7102 }
7103 }
7104
7105 #[cfg(feature = "query-analytics")]
7108 mod query_analytics {
7109 use crate::analytics::{AnalyticsConfig, OrderBy, QueryAnalytics, QueryExecution};
7110 use std::time::Duration;
7111
7112 #[test]
7113 fn records_and_collapses_literals() {
7114 let a = QueryAnalytics::new(AnalyticsConfig::default());
7115 for n in [1, 2, 3] {
7116 a.record(QueryExecution::new(
7117 format!("select {n}"),
7118 Duration::from_millis(1),
7119 ));
7120 }
7121 let top = a.top_queries(OrderBy::Calls, 10);
7122 assert!(!top.is_empty(), "no fingerprints recorded");
7123 assert!(
7125 top.iter().any(|s| s.calls >= 3),
7126 "literals did not collapse: {:?}",
7127 top.iter()
7128 .map(|s| (s.normalized.clone(), s.calls))
7129 .collect::<Vec<_>>()
7130 );
7131 }
7132 }
7133
7134 #[cfg(feature = "lag-routing")]
7137 mod lag_routing {
7138 use super::ProxyServer;
7139
7140 #[test]
7141 fn ryw_pins_recent_write() {
7142 assert!(ProxyServer::ryw_pins_primary(
7144 Some(std::time::Instant::now()),
7145 1000
7146 ));
7147 }
7148
7149 #[test]
7150 fn ryw_releases_old_write() {
7151 let old = std::time::Instant::now()
7152 .checked_sub(std::time::Duration::from_secs(10))
7153 .unwrap();
7154 assert!(!ProxyServer::ryw_pins_primary(Some(old), 1000));
7155 }
7156
7157 #[test]
7158 fn ryw_no_write_or_disabled() {
7159 assert!(!ProxyServer::ryw_pins_primary(None, 1000));
7160 assert!(!ProxyServer::ryw_pins_primary(
7162 Some(std::time::Instant::now()),
7163 0
7164 ));
7165 }
7166
7167 #[test]
7168 fn lag_exclusion_thresholds() {
7169 assert!(!ProxyServer::lag_excludes_standby(Some(999_999), 0));
7171 assert!(!ProxyServer::lag_excludes_standby(None, 1000));
7173 assert!(!ProxyServer::lag_excludes_standby(Some(500), 1000));
7175 assert!(ProxyServer::lag_excludes_standby(Some(2000), 1000));
7177 }
7178 }
7179
7180 #[cfg(feature = "query-cache")]
7183 mod query_cache {
7184 use super::ProxyServer;
7185
7186 #[test]
7187 fn plain_selects_are_cacheable() {
7188 assert!(ProxyServer::is_cacheable_read_sql("select v from t"));
7189 assert!(ProxyServer::is_cacheable_read_sql(
7190 " SELECT a, b FROM users WHERE id = 5"
7191 ));
7192 }
7193
7194 #[test]
7195 fn writes_and_non_selects_are_not_cacheable() {
7196 assert!(!ProxyServer::is_cacheable_read_sql(
7197 "insert into t values (1)"
7198 ));
7199 assert!(!ProxyServer::is_cacheable_read_sql("update t set v = 1"));
7200 assert!(!ProxyServer::is_cacheable_read_sql("show search_path"));
7201 }
7202
7203 #[test]
7204 fn locking_and_volatile_selects_are_not_cacheable() {
7205 assert!(!ProxyServer::is_cacheable_read_sql(
7206 "select * from t for update"
7207 ));
7208 assert!(!ProxyServer::is_cacheable_read_sql("select now()"));
7209 assert!(!ProxyServer::is_cacheable_read_sql("select random()"));
7210 assert!(!ProxyServer::is_cacheable_read_sql("select nextval('s')"));
7211 assert!(!ProxyServer::is_cacheable_read_sql(
7214 "select set_config('timezone', 'UTC', false) from t"
7215 ));
7216 }
7217
7218 #[test]
7219 fn multi_statement_strings_are_not_cacheable() {
7220 assert!(!ProxyServer::is_cacheable_read_sql(
7223 "select v from t; update t set v = 1"
7224 ));
7225 assert!(!ProxyServer::is_cacheable_read_sql("select 1; select 2"));
7226 assert!(ProxyServer::is_cacheable_read_sql("select v from t;"));
7228 assert!(ProxyServer::is_cacheable_read_sql("SELECT v FROM t ; "));
7229 }
7230
7231 #[test]
7232 fn literal_semicolon_or_into_over_rejects_by_design() {
7233 assert!(!ProxyServer::is_cacheable_read_sql(
7242 "select v from t where url = 'a;b=c'"
7243 ));
7244 assert!(!ProxyServer::is_cacheable_read_sql(
7245 "select v from t where body like '%go into space%'"
7246 ));
7247 assert!(!ProxyServer::is_cacheable_read_sql(
7249 "select * into snapshot from t"
7250 ));
7251 }
7252
7253 #[test]
7254 fn select_into_is_not_cacheable() {
7255 assert!(!ProxyServer::is_cacheable_read_sql(
7258 "select * into report_tmp from src"
7259 ));
7260 assert!(!ProxyServer::is_cacheable_read_sql(
7262 "SELECT *\nINTO report_tmp\nFROM src"
7263 ));
7264 assert!(ProxyServer::is_cacheable_read_sql(
7266 "select into_total from t"
7267 ));
7268 }
7269 }
7270
7271 #[cfg(feature = "edge-proxy")]
7274 mod edge_proxy {
7275 use super::ProxyServer;
7276 use std::collections::HashMap;
7277
7278 #[test]
7279 fn bare_txn_control_is_exempt_from_invalidation() {
7280 for sql in [
7284 "BEGIN",
7285 "begin;",
7286 "START TRANSACTION",
7287 "SAVEPOINT sp1",
7288 "RELEASE SAVEPOINT sp1",
7289 "ROLLBACK",
7290 "ROLLBACK TO SAVEPOINT sp1;",
7291 ] {
7292 assert!(
7293 !ProxyServer::edge_write_needs_invalidation(true, sql),
7294 "{sql:?} must be exempt"
7295 );
7296 }
7297 assert!(ProxyServer::edge_write_needs_invalidation(true, "COMMIT"));
7300 assert!(ProxyServer::edge_write_needs_invalidation(
7301 true,
7302 "SET search_path TO tenant_b"
7303 ));
7304 }
7305
7306 #[test]
7307 fn compound_txn_strings_still_invalidate() {
7308 assert!(ProxyServer::edge_write_needs_invalidation(
7311 true,
7312 "BEGIN; UPDATE t SET v = 1; COMMIT"
7313 ));
7314 assert!(ProxyServer::edge_write_needs_invalidation(
7317 false,
7318 "SELECT v FROM t; UPDATE t SET v = 1"
7319 ));
7320 assert!(!ProxyServer::edge_write_needs_invalidation(
7322 false,
7323 "SELECT v FROM t"
7324 ));
7325 }
7326
7327 #[test]
7328 fn copy_from_counts_as_write() {
7329 assert!(ProxyServer::edge_write_needs_invalidation(
7330 false,
7331 "COPY t FROM STDIN"
7332 ));
7333 assert!(ProxyServer::is_edge_copy_write_sql("copy t from stdin"));
7334 assert!(!ProxyServer::edge_write_needs_invalidation(
7336 false,
7337 "COPY t TO STDOUT"
7338 ));
7339 }
7340
7341 #[test]
7342 fn procedural_and_txn_end_trigger_invalidation() {
7343 for sql in [
7347 "EXECUTE ins(1)",
7348 "execute ins(1)",
7349 "CALL do_write()",
7350 "DO $$ BEGIN PERFORM 1; END $$",
7351 ] {
7352 assert!(
7353 ProxyServer::is_edge_procedural_sql(sql),
7354 "{sql:?} is procedural"
7355 );
7356 assert!(
7357 ProxyServer::edge_write_needs_invalidation(false, sql),
7358 "{sql:?} must invalidate on the simple path"
7359 );
7360 }
7361 for sql in [
7362 "INSERT INTO t VALUES (1)",
7363 "SELECT 1 FROM t",
7364 "UPDATE t SET v=1",
7365 ] {
7366 assert!(
7367 !ProxyServer::is_edge_procedural_sql(sql),
7368 "{sql:?} is not procedural"
7369 );
7370 }
7371
7372 for sql in [
7375 "COMMIT",
7376 "commit work",
7377 "COMMIT PREPARED 'x'",
7378 "END",
7379 "END TRANSACTION",
7380 "end;",
7381 ] {
7382 assert!(ProxyServer::is_edge_txn_end_sql(sql), "{sql:?} ends a txn");
7383 assert!(
7384 ProxyServer::edge_write_needs_invalidation(false, sql),
7385 "{sql:?} must invalidate (commit-visibility window)"
7386 );
7387 }
7388 for sql in ["BEGIN", "START TRANSACTION", "SAVEPOINT s1", "ROLLBACK"] {
7389 assert!(
7390 !ProxyServer::is_edge_txn_end_sql(sql),
7391 "{sql:?} is not a txn-end"
7392 );
7393 assert!(
7394 !ProxyServer::edge_write_needs_invalidation(false, sql),
7395 "{sql:?} must stay exempt on the simple path"
7396 );
7397 }
7398 }
7399
7400 #[test]
7401 fn edge_meta_prunable_protects_reparsed_names() {
7402 let closes = vec!["S1".to_string(), "S2".to_string()];
7406 let none: Vec<String> = vec![];
7407 assert_eq!(
7408 ProxyServer::edge_meta_prunable(&closes, &none),
7409 vec!["S1", "S2"]
7410 );
7411 assert_eq!(
7413 ProxyServer::edge_meta_prunable(&closes, &["S1".to_string()]),
7414 vec!["S2"]
7415 );
7416 assert!(ProxyServer::edge_meta_prunable(&closes, &closes).is_empty());
7418 }
7419
7420 #[test]
7421 fn edge_dml_classifier_covers_dml_not_txn_control() {
7422 for sql in [
7423 "INSERT INTO t VALUES (1)",
7424 "update t set v = 1",
7425 "DELETE FROM t WHERE id = 1",
7426 "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET v = s.v",
7427 "TRUNCATE t",
7428 "ALTER TABLE t ADD COLUMN c int",
7429 "COPY t FROM STDIN",
7430 "WITH del AS (DELETE FROM t RETURNING id) SELECT * FROM del",
7431 ] {
7432 assert!(ProxyServer::is_edge_dml_sql(sql), "{sql:?} is DML");
7433 }
7434 for sql in [
7437 "BEGIN",
7438 "COMMIT",
7439 "ROLLBACK",
7440 "SET search_path TO x",
7441 "SELECT v FROM t",
7442 "WITH x AS (SELECT 1) SELECT * FROM x",
7443 "COPY t TO STDOUT",
7444 ] {
7445 assert!(!ProxyServer::is_edge_dml_sql(sql), "{sql:?} is not DML");
7446 }
7447 }
7448
7449 #[test]
7450 fn extended_batch_tables_union_and_wildcard() {
7451 let mut named: HashMap<String, Option<Vec<String>>> = HashMap::new();
7452 named.insert("ins".into(), Some(vec!["orders".into()]));
7453 named.insert("upd".into(), Some(vec!["users".into()]));
7454 named.insert("sel".into(), None);
7455 named.insert("weird".into(), Some(vec![])); let unnamed: Option<Vec<String>> = Some(vec!["events".into()]);
7457
7458 assert_eq!(
7460 ProxyServer::edge_extended_batch_tables(
7461 &["sel".to_string()],
7462 false,
7463 &named,
7464 &unnamed
7465 ),
7466 None
7467 );
7468 let t = ProxyServer::edge_extended_batch_tables(
7470 &["ins".to_string(), "upd".to_string(), "ins".to_string()],
7471 false,
7472 &named,
7473 &unnamed,
7474 )
7475 .expect("dml");
7476 assert_eq!(t, vec!["orders".to_string(), "users".to_string()]);
7477 let t = ProxyServer::edge_extended_batch_tables(&[], true, &named, &unnamed)
7479 .expect("unnamed dml");
7480 assert_eq!(t, vec!["events".to_string()]);
7481 let t = ProxyServer::edge_extended_batch_tables(
7483 &["ins".to_string(), "weird".to_string()],
7484 false,
7485 &named,
7486 &unnamed,
7487 )
7488 .expect("dml");
7489 assert!(t.is_empty(), "wildcard invalidation");
7490 assert_eq!(
7493 ProxyServer::edge_extended_batch_tables(
7494 &["ghost".to_string()],
7495 false,
7496 &named,
7497 &None
7498 ),
7499 None
7500 );
7501 }
7502 }
7503
7504 #[cfg(any(feature = "query-cache", feature = "edge-proxy"))]
7510 #[tokio::test]
7511 async fn capture_excludes_async_frames_from_cacheable() {
7512 use crate::client_tls::ClientStream;
7513 use tokio::io::AsyncReadExt;
7514 use tokio::io::AsyncWriteExt as _;
7515 use tokio::net::{TcpListener, TcpStream};
7516
7517 async fn pair() -> (TcpStream, TcpStream) {
7518 let l = TcpListener::bind("127.0.0.1:0").await.unwrap();
7519 let addr = l.local_addr().unwrap();
7520 let (accepted, connected) = tokio::join!(l.accept(), TcpStream::connect(addr));
7521 (accepted.unwrap().0, connected.unwrap())
7522 }
7523
7524 fn frame(mtype: u8, body: &[u8]) -> Vec<u8> {
7525 let mut v = vec![mtype];
7526 v.extend_from_slice(&((body.len() + 4) as u32).to_be_bytes());
7527 v.extend_from_slice(body);
7528 v
7529 }
7530
7531 let clean: Vec<u8> = [
7532 frame(b'T', b"rowdesc"),
7533 frame(b'D', b"row"),
7534 frame(b'C', b"SELECT 1\0"),
7535 frame(b'Z', b"I"),
7536 ]
7537 .concat();
7538 let with_notify: Vec<u8> = [
7539 frame(b'T', b"rowdesc"),
7540 frame(b'A', b"\x00\x00\x30\x39chan\0payload\0"),
7541 frame(b'D', b"row"),
7542 frame(b'C', b"SELECT 1\0"),
7543 frame(b'Z', b"I"),
7544 ]
7545 .concat();
7546 let with_param_status: Vec<u8> = [
7547 frame(b'D', b"row"),
7548 frame(b'C', b"SELECT 1\0"),
7549 frame(b'S', b"TimeZone\0UTC\0"),
7550 frame(b'Z', b"I"),
7551 ]
7552 .concat();
7553
7554 for (bytes, want_cacheable) in [
7555 (clean, true),
7556 (with_notify, false),
7557 (with_param_status, false),
7558 ] {
7559 let (mut backend, mut backend_peer) = pair().await;
7560 let (client_raw, mut client_peer) = pair().await;
7561 let mut client = ClientStream::Plain(client_raw);
7562 let session = make_test_session();
7563
7564 backend_peer.write_all(&bytes).await.unwrap();
7565 backend_peer.flush().await.unwrap();
7566
7567 let (sent, captured, cacheable, _rows) = ProxyServer::stream_until_ready_capture(
7568 &mut client,
7569 &mut backend,
7570 &session,
7571 Duration::from_secs(60),
7572 Duration::from_secs(30),
7573 )
7574 .await
7575 .expect("capture ok");
7576 assert_eq!(cacheable, want_cacheable, "cacheable flag");
7577 assert_eq!(sent as usize, bytes.len());
7578 assert_eq!(captured, bytes, "capture is byte-exact");
7579
7580 let mut got = vec![0u8; bytes.len()];
7583 client_peer.read_exact(&mut got).await.unwrap();
7584 assert_eq!(got, bytes, "forwarding must not be filtered");
7585 }
7586 }
7587
7588 #[cfg(feature = "query-rewriting")]
7591 mod query_rewriting {
7592 use crate::rewriter::{
7593 QueryPattern, QueryRewriter, RewriteRule, RewriterConfig, Transformation,
7594 };
7595
7596 fn rw_with_table_replace() -> QueryRewriter {
7597 let rw = QueryRewriter::new(RewriterConfig {
7598 enabled: true,
7599 ..Default::default()
7600 });
7601 rw.add_rule(
7602 RewriteRule::build("t")
7603 .pattern(QueryPattern::Table("a".to_string()))
7604 .transform(Transformation::ReplaceTable {
7605 from: "a".to_string(),
7606 to: "b".to_string(),
7607 })
7608 .build(),
7609 );
7610 rw
7611 }
7612
7613 #[test]
7614 fn matching_query_is_rewritten() {
7615 let res = rw_with_table_replace().rewrite("select * from a").unwrap();
7616 assert!(res.was_rewritten(), "rule did not fire");
7617 assert!(res.query().contains('b'), "rewritten: {}", res.query());
7618 assert!(
7619 !res.query().contains("from a"),
7620 "still references a: {}",
7621 res.query()
7622 );
7623 }
7624
7625 #[test]
7626 fn unmatched_query_is_unchanged() {
7627 let res = rw_with_table_replace()
7628 .rewrite("select * from other")
7629 .unwrap();
7630 assert!(!res.was_rewritten());
7631 assert_eq!(res.query(), "select * from other");
7632 }
7633 }
7634
7635 #[cfg(feature = "multi-tenancy")]
7638 mod multi_tenancy {
7639 use crate::multi_tenancy::{
7640 IdentificationMethod, IsolationStrategy, MultiTenancyConfig, TenantConfig, TenantId,
7641 TenantManager, TenantManagerBuilder, TenantQueryTransformer,
7642 };
7643
7644 fn manager() -> TenantManager {
7645 let transformer = TenantQueryTransformer::new().register_tables(&["t"], "tid");
7646 let tm = TenantManagerBuilder::new()
7647 .config(MultiTenancyConfig {
7648 enabled: true,
7649 identification: IdentificationMethod::Header {
7650 header_name: "application_name".to_string(),
7651 },
7652 ..Default::default()
7653 })
7654 .query_transformer(transformer)
7655 .build();
7656 tm.register_tenant(TenantConfig::new(
7657 TenantId::new("acme"),
7658 IsolationStrategy::row("public", "tid"),
7659 ));
7660 tm
7661 }
7662
7663 #[test]
7664 fn tenant_table_gets_filter() {
7665 let res = manager().transform_query("select * from t", &TenantId::new("acme"));
7666 assert!(res.transformed, "expected a tenant filter to be injected");
7667 let q = res.query.to_lowercase();
7668 assert!(
7669 q.contains("tid") && q.contains("acme"),
7670 "filter missing: {}",
7671 res.query
7672 );
7673 }
7674
7675 #[test]
7676 fn non_tenant_table_passes_through() {
7677 let res = manager().transform_query("select * from other", &TenantId::new("acme"));
7678 assert!(!res.transformed);
7679 }
7680 }
7681
7682 #[cfg(feature = "ha-tr")]
7685 mod ha_tr {
7686 use crate::transaction_journal::TransactionJournal;
7687 use crate::NodeId;
7688
7689 #[tokio::test]
7690 async fn journal_records_and_windows_a_statement() {
7691 let j = TransactionJournal::new();
7692 let from = chrono::Utc::now() - chrono::Duration::seconds(60);
7693 let tx = uuid::Uuid::new_v4();
7694 j.begin_transaction(tx, uuid::Uuid::new_v4(), NodeId::new(), 0)
7695 .await
7696 .unwrap();
7697 j.log_statement(
7698 tx,
7699 "insert into t values (1)".to_string(),
7700 Vec::new(),
7701 None,
7702 None,
7703 0,
7704 )
7705 .await
7706 .unwrap();
7707 let to = chrono::Utc::now() + chrono::Duration::seconds(60);
7708 let entries = j.entries_in_window(from, to).await;
7709 assert_eq!(entries.len(), 1, "journaled statement should be in window");
7710 assert!(entries[0].1.statement.contains("insert"));
7711 }
7712 }
7713
7714 #[cfg(feature = "schema-routing")]
7717 mod schema_routing {
7718 use crate::schema_routing::{QueryAnalyzer, SchemaRegistry};
7719 use std::sync::Arc;
7720
7721 fn analyzer() -> QueryAnalyzer {
7722 QueryAnalyzer::new(Arc::new(SchemaRegistry::new()))
7723 }
7724
7725 #[test]
7726 fn aggregation_group_by_is_analytics() {
7727 let a = analyzer();
7728 assert!(a
7729 .analyze("select count(*) from orders group by region")
7730 .is_analytics());
7731 }
7732
7733 #[test]
7734 fn simple_point_query_is_not_analytics() {
7735 let a = analyzer();
7736 assert!(!a
7737 .analyze("select * from orders where id = 1")
7738 .is_analytics());
7739 }
7740 }
7741
7742 #[tokio::test]
7745 async fn session_guard_deregisters_on_drop() {
7746 let server = ProxyServer::new(test_config()).unwrap();
7747 let state = server.state.clone();
7748 let session = make_test_session();
7749 state.sessions.insert(session.id, session.clone());
7750 assert_eq!(state.sessions.len(), 1);
7751 let before = state.metrics.connections_closed.load(Ordering::Relaxed);
7752 {
7753 let _g = SessionGuard {
7754 state: state.clone(),
7755 session_id: session.id,
7756 };
7757 }
7758 assert!(state.sessions.is_empty(), "guard must deregister on drop");
7759 assert_eq!(
7760 state.metrics.connections_closed.load(Ordering::Relaxed),
7761 before + 1
7762 );
7763 }
7764
7765 #[tokio::test]
7768 async fn session_guard_deregisters_on_panic() {
7769 let server = ProxyServer::new(test_config()).unwrap();
7770 let state = server.state.clone();
7771 let session = make_test_session();
7772 state.sessions.insert(session.id, session.clone());
7773 let sid = session.id;
7774 let st = state.clone();
7775 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
7776 let _g = SessionGuard {
7777 state: st,
7778 session_id: sid,
7779 };
7780 panic!("simulated connection-task panic");
7781 }));
7782 assert!(r.is_err(), "closure must have panicked");
7783 assert!(
7784 state.sessions.is_empty(),
7785 "guard must deregister on a panic unwind"
7786 );
7787 }
7788
7789 #[cfg(feature = "pool-modes")]
7794 #[test]
7795 fn stmt_classifier_is_conservative() {
7796 let clean = ProxyServer::stmt_leaves_session_state;
7797 assert!(!clean(
7799 "SELECT abalance FROM pgbench_accounts WHERE aid = 12345"
7800 ));
7801 assert!(!clean("SELECT 1"));
7802 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"));
7807 assert!(!clean("WITH x AS (SELECT 1) SELECT * FROM x"));
7808 assert!(!clean("SELECT into_total FROM ledger")); assert!(!clean("BEGIN"));
7810 assert!(!clean("COMMIT"));
7811 assert!(!clean("SELECT current_setting('work_mem')")); assert!(clean("SET work_mem = '1GB'"), "SET GUC");
7815 assert!(clean("set search_path to public"), "lowercase SET");
7816 assert!(clean("CREATE TEMP TABLE t(x int)"), "temp table");
7817 assert!(clean("CREATE TEMPORARY TABLE t(x int)"), "temp table");
7818 assert!(clean("SELECT * INTO TEMP t FROM src"), "SELECT INTO temp");
7819 assert!(clean("select a into t from s"), "SELECT INTO lowercase");
7820 assert!(clean("PREPARE p AS SELECT 1"), "prepared statement");
7821 assert!(clean("DEALLOCATE p"), "deallocate");
7822 assert!(
7823 clean("DECLARE c CURSOR WITH HOLD FOR SELECT 1"),
7824 "held cursor"
7825 );
7826 assert!(clean("LISTEN my_channel"), "listen");
7827 assert!(clean("SELECT pg_advisory_lock(42)"), "advisory lock");
7828 assert!(clean("SELECT pg_try_advisory_lock(1)"), "try advisory lock");
7829 assert!(
7830 clean("SELECT set_config('work_mem','1GB',false)"),
7831 "set_config fn"
7832 );
7833 assert!(clean("SELECT nextval('s')"), "sequence cache");
7834 assert!(clean("SET ROLE admin"), "set role");
7835 assert!(clean("SET SESSION AUTHORIZATION bob"), "session auth");
7836 assert!(clean("DISCARD ALL"), "explicit discard");
7837 assert!(clean("RESET ALL"), "reset");
7838 assert!(clean("SELECT 1; SET work_mem='1GB'"), "hidden SET after ;");
7840 assert!(
7841 clean("SELECT 1; CREATE TEMP TABLE t(x int)"),
7842 "hidden temp after ;"
7843 );
7844 assert!(clean("SELECT 'a;b'"), "semicolon in literal");
7846 assert!(clean("COPY t FROM STDIN"), "copy");
7848 assert!(clean("GRANT SELECT ON t TO bob"), "grant");
7849 assert!(clean("ALTER TABLE t ADD COLUMN c int"), "ddl");
7850 }
7851
7852 #[cfg(feature = "pool-modes")]
7857 #[tokio::test]
7858 async fn reset_backend_rejects_error_and_nonidle() {
7859 use tokio::io::AsyncWriteExt as _;
7860 fn frame(tag: u8, body: &[u8]) -> Vec<u8> {
7861 let mut v = vec![tag];
7862 v.extend_from_slice(&((body.len() + 4) as u32).to_be_bytes());
7863 v.extend_from_slice(body);
7864 v
7865 }
7866 let rfq = |st: u8| frame(b'Z', &[st]);
7867 let cc = frame(b'C', b"DISCARD ALL\0");
7868 let err = frame(b'E', b"SERROR\0C25P02\0Mreset failed\0\0");
7869
7870 let (mut client, mut server) = tokio::io::duplex(4096);
7872 let mut resp = cc.clone();
7873 resp.extend_from_slice(&rfq(b'I'));
7874 server.write_all(&resp).await.unwrap();
7875 assert!(
7876 ProxyServer::reset_backend(&mut client, "DISCARD ALL", Duration::from_secs(30))
7877 .await
7878 .is_ok(),
7879 "clean reset must succeed"
7880 );
7881
7882 let (mut client, mut server) = tokio::io::duplex(4096);
7884 let mut resp = err.clone();
7885 resp.extend_from_slice(&rfq(b'I'));
7886 server.write_all(&resp).await.unwrap();
7887 assert!(
7888 ProxyServer::reset_backend(&mut client, "DISCARD ALL", Duration::from_secs(30))
7889 .await
7890 .is_err(),
7891 "reset that errored must be rejected"
7892 );
7893
7894 let (mut client, mut server) = tokio::io::duplex(4096);
7896 let mut resp = cc.clone();
7897 resp.extend_from_slice(&rfq(b'T'));
7898 server.write_all(&resp).await.unwrap();
7899 assert!(
7900 ProxyServer::reset_backend(&mut client, "DISCARD ALL", Duration::from_secs(30))
7901 .await
7902 .is_err(),
7903 "reset leaving a non-idle txn must be rejected"
7904 );
7905 }
7906
7907 #[cfg(feature = "pool-modes")]
7913 #[tokio::test]
7914 async fn pool_key_folds_startup_params() {
7915 let base = make_test_session();
7916 {
7917 let mut v = base.variables.write().await;
7918 v.insert("user".into(), "u".into());
7919 v.insert("database".into(), "d".into());
7920 }
7921 let k_plain = ProxyServer::pool_key_for("n:5432", &base).await;
7922 assert_eq!(k_plain, crate::pool::pool_key("n:5432", "u", "d"));
7923
7924 let utf8 = make_test_session();
7927 let latin1 = make_test_session();
7928 for (s, enc) in [(&utf8, "UTF8"), (&latin1, "LATIN1")] {
7929 let mut v = s.variables.write().await;
7930 v.insert("user".into(), "u".into());
7931 v.insert("database".into(), "d".into());
7932 v.insert("client_encoding".into(), enc.into());
7933 }
7934 let k_utf8 = ProxyServer::pool_key_for("n:5432", &utf8).await;
7935 let k_latin1 = ProxyServer::pool_key_for("n:5432", &latin1).await;
7936 assert_ne!(k_utf8, k_latin1, "different client_encoding must not share");
7937 assert_ne!(k_utf8, k_plain, "GUC-bearing key must differ from bare key");
7938 }
7939}