1use async_trait::async_trait;
16use serde::{Deserialize, Serialize};
17use sqlx::postgres::{PgConnectOptions, PgPoolOptions, PgSslMode};
18use sqlx::{Executor, PgPool, Postgres, Row, Transaction};
19use std::collections::HashMap;
20use std::ops::{Deref, DerefMut};
21use std::path::{Path, PathBuf};
22use std::str::FromStr;
23use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
24use std::sync::Arc;
25use std::time::{Duration, Instant};
26use tokio::sync::{Mutex, RwLock};
27use tracing::{debug, error, info, trace, warn};
28use uuid::Uuid;
29
30use super::backend::StateBackend;
31use crate::error::{StateError, StateResult};
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct PostgresSslConfig {
40 pub mode: String,
42 pub root_cert_path: Option<PathBuf>,
44 pub client_cert_path: Option<PathBuf>,
46 pub client_key_path: Option<PathBuf>,
48}
49
50impl Default for PostgresSslConfig {
51 fn default() -> Self {
52 Self {
53 mode: "prefer".to_string(),
54 root_cert_path: None,
55 client_cert_path: None,
56 client_key_path: None,
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct PostgresPoolConfig {
64 pub min_connections: u32,
66 pub max_connections: u32,
68 pub acquire_timeout: Duration,
70 pub idle_timeout: Option<Duration>,
72 pub max_lifetime: Option<Duration>,
74 pub health_check_interval: Duration,
76 pub test_on_acquire: bool,
78}
79
80impl Default for PostgresPoolConfig {
81 fn default() -> Self {
82 Self {
83 min_connections: 2,
84 max_connections: 20,
85 acquire_timeout: Duration::from_secs(30),
86 idle_timeout: Some(Duration::from_secs(600)),
87 max_lifetime: Some(Duration::from_secs(1800)),
88 health_check_interval: Duration::from_secs(30),
89 test_on_acquire: true,
90 }
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct PostgresReplicaConfig {
97 pub replica_urls: Vec<String>,
99 pub enable_read_splitting: bool,
101 pub max_replication_lag_ms: i64,
103 pub health_check_interval: Duration,
105 pub enable_auto_failover: bool,
107}
108
109impl Default for PostgresReplicaConfig {
110 fn default() -> Self {
111 Self {
112 replica_urls: Vec::new(),
113 enable_read_splitting: true,
114 max_replication_lag_ms: 1000,
115 health_check_interval: Duration::from_secs(10),
116 enable_auto_failover: true,
117 }
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123pub enum IsolationLevel {
124 ReadCommitted,
125 RepeatableRead,
126 Serializable,
127}
128
129impl IsolationLevel {
130 pub fn as_sql(&self) -> &'static str {
131 match self {
132 IsolationLevel::ReadCommitted => "READ COMMITTED",
133 IsolationLevel::RepeatableRead => "REPEATABLE READ",
134 IsolationLevel::Serializable => "SERIALIZABLE",
135 }
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub enum PartitionStrategy {
142 Hash { num_partitions: u32 },
144 TimeRange {
146 column: String,
147 interval: String, },
149 List { column: String, values: Vec<String> },
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct RetentionPolicy {
156 pub enabled: bool,
158 pub max_age: Duration,
160 pub cleanup_interval: Duration,
162 pub batch_size: usize,
164}
165
166impl Default for RetentionPolicy {
167 fn default() -> Self {
168 Self {
169 enabled: false,
170 max_age: Duration::from_secs(86400 * 30), cleanup_interval: Duration::from_secs(3600), batch_size: 1000,
173 }
174 }
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct EnhancedPostgresConfig {
180 pub primary_url: String,
182 pub pool: PostgresPoolConfig,
184 pub ssl: PostgresSslConfig,
186 pub replicas: Option<PostgresReplicaConfig>,
188 pub default_isolation_level: IsolationLevel,
190 pub table_name: String,
192 pub schema_name: String,
194 pub enable_prepared_statements: bool,
196 pub prepared_statement_cache_size: usize,
198 pub partition_strategy: Option<PartitionStrategy>,
200 pub retention_policy: RetentionPolicy,
202 pub query_timeout: Duration,
204 pub max_retries: u32,
206 pub retry_backoff: Duration,
208 pub enable_slow_query_log: bool,
210 pub slow_query_threshold: Duration,
212 pub log_statements: bool,
214 pub enable_metrics: bool,
216}
217
218impl Default for EnhancedPostgresConfig {
219 fn default() -> Self {
220 Self {
221 primary_url: "postgresql://postgres:postgres@localhost/optimizer".to_string(),
222 pool: PostgresPoolConfig::default(),
223 ssl: PostgresSslConfig::default(),
224 replicas: None,
225 default_isolation_level: IsolationLevel::ReadCommitted,
226 table_name: "state_entries".to_string(),
227 schema_name: "public".to_string(),
228 enable_prepared_statements: true,
229 prepared_statement_cache_size: 100,
230 partition_strategy: None,
231 retention_policy: RetentionPolicy::default(),
232 query_timeout: Duration::from_secs(60),
233 max_retries: 3,
234 retry_backoff: Duration::from_millis(100),
235 enable_slow_query_log: true,
236 slow_query_threshold: Duration::from_millis(1000),
237 log_statements: false,
238 enable_metrics: true,
239 }
240 }
241}
242
243impl EnhancedPostgresConfig {
244 pub fn new<S: Into<String>>(primary_url: S) -> Self {
245 Self {
246 primary_url: primary_url.into(),
247 ..Default::default()
248 }
249 }
250
251 pub fn with_pool_config(mut self, pool: PostgresPoolConfig) -> Self {
252 self.pool = pool;
253 self
254 }
255
256 pub fn with_ssl_config(mut self, ssl: PostgresSslConfig) -> Self {
257 self.ssl = ssl;
258 self
259 }
260
261 pub fn with_replicas(mut self, replicas: PostgresReplicaConfig) -> Self {
262 self.replicas = Some(replicas);
263 self
264 }
265
266 pub fn with_isolation_level(mut self, level: IsolationLevel) -> Self {
267 self.default_isolation_level = level;
268 self
269 }
270
271 pub fn with_partitioning(mut self, strategy: PartitionStrategy) -> Self {
272 self.partition_strategy = Some(strategy);
273 self
274 }
275
276 pub fn with_retention_policy(mut self, policy: RetentionPolicy) -> Self {
277 self.retention_policy = policy;
278 self
279 }
280}
281
282#[derive(Debug, Default, Serialize, Deserialize)]
288pub struct EnhancedPostgresStats {
289 pub get_count: AtomicU64,
291 pub put_count: AtomicU64,
292 pub delete_count: AtomicU64,
293 pub query_count: AtomicU64,
294
295 pub transaction_count: AtomicU64,
297 pub transaction_rollback_count: AtomicU64,
298 pub transaction_conflict_count: AtomicU64,
299
300 pub pool_acquire_count: AtomicU64,
302 pub pool_acquire_duration_ms: AtomicU64,
303 pub pool_timeout_count: AtomicU64,
304
305 pub replica_read_count: AtomicU64,
307 pub replica_failover_count: AtomicU64,
308 pub replication_lag_ms: AtomicU64,
309
310 pub slow_query_count: AtomicU64,
312 pub avg_query_duration_ms: AtomicU64,
313 pub retry_count: AtomicU64,
314
315 pub bytes_written: AtomicU64,
317 pub bytes_read: AtomicU64,
318 pub rows_inserted: AtomicU64,
319 pub rows_updated: AtomicU64,
320 pub rows_deleted: AtomicU64,
321
322 pub batch_insert_count: AtomicU64,
324 pub batch_insert_rows: AtomicU64,
325
326 pub advisory_lock_count: AtomicU64,
328 pub advisory_lock_wait_ms: AtomicU64,
329}
330
331impl EnhancedPostgresStats {
332 pub fn new() -> Self {
333 Self::default()
334 }
335
336 pub fn snapshot(&self) -> PostgresStatsSnapshot {
337 PostgresStatsSnapshot {
338 get_count: self.get_count.load(Ordering::Relaxed),
339 put_count: self.put_count.load(Ordering::Relaxed),
340 delete_count: self.delete_count.load(Ordering::Relaxed),
341 query_count: self.query_count.load(Ordering::Relaxed),
342 transaction_count: self.transaction_count.load(Ordering::Relaxed),
343 transaction_rollback_count: self.transaction_rollback_count.load(Ordering::Relaxed),
344 transaction_conflict_count: self.transaction_conflict_count.load(Ordering::Relaxed),
345 pool_acquire_count: self.pool_acquire_count.load(Ordering::Relaxed),
346 pool_acquire_duration_ms: self.pool_acquire_duration_ms.load(Ordering::Relaxed),
347 pool_timeout_count: self.pool_timeout_count.load(Ordering::Relaxed),
348 replica_read_count: self.replica_read_count.load(Ordering::Relaxed),
349 replica_failover_count: self.replica_failover_count.load(Ordering::Relaxed),
350 replication_lag_ms: self.replication_lag_ms.load(Ordering::Relaxed),
351 slow_query_count: self.slow_query_count.load(Ordering::Relaxed),
352 avg_query_duration_ms: self.avg_query_duration_ms.load(Ordering::Relaxed),
353 retry_count: self.retry_count.load(Ordering::Relaxed),
354 bytes_written: self.bytes_written.load(Ordering::Relaxed),
355 bytes_read: self.bytes_read.load(Ordering::Relaxed),
356 rows_inserted: self.rows_inserted.load(Ordering::Relaxed),
357 rows_updated: self.rows_updated.load(Ordering::Relaxed),
358 rows_deleted: self.rows_deleted.load(Ordering::Relaxed),
359 batch_insert_count: self.batch_insert_count.load(Ordering::Relaxed),
360 batch_insert_rows: self.batch_insert_rows.load(Ordering::Relaxed),
361 advisory_lock_count: self.advisory_lock_count.load(Ordering::Relaxed),
362 advisory_lock_wait_ms: self.advisory_lock_wait_ms.load(Ordering::Relaxed),
363 }
364 }
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct PostgresStatsSnapshot {
369 pub get_count: u64,
370 pub put_count: u64,
371 pub delete_count: u64,
372 pub query_count: u64,
373 pub transaction_count: u64,
374 pub transaction_rollback_count: u64,
375 pub transaction_conflict_count: u64,
376 pub pool_acquire_count: u64,
377 pub pool_acquire_duration_ms: u64,
378 pub pool_timeout_count: u64,
379 pub replica_read_count: u64,
380 pub replica_failover_count: u64,
381 pub replication_lag_ms: u64,
382 pub slow_query_count: u64,
383 pub avg_query_duration_ms: u64,
384 pub retry_count: u64,
385 pub bytes_written: u64,
386 pub bytes_read: u64,
387 pub rows_inserted: u64,
388 pub rows_updated: u64,
389 pub rows_deleted: u64,
390 pub batch_insert_count: u64,
391 pub batch_insert_rows: u64,
392 pub advisory_lock_count: u64,
393 pub advisory_lock_wait_ms: u64,
394}
395
396struct ReplicaInfo {
401 pool: PgPool,
402 url: String,
403 healthy: AtomicBool,
404 lag_ms: AtomicU64,
405 last_check: Mutex<Instant>,
406}
407
408struct ReplicaManager {
409 replicas: Vec<Arc<ReplicaInfo>>,
410 config: PostgresReplicaConfig,
411 current_index: AtomicU64,
412}
413
414impl ReplicaManager {
415 async fn new(config: PostgresReplicaConfig, ssl_config: &PostgresSslConfig) -> StateResult<Self> {
416 let mut replicas = Vec::new();
417
418 for url in &config.replica_urls {
419 match Self::create_replica_pool(url, ssl_config).await {
420 Ok(pool) => {
421 replicas.push(Arc::new(ReplicaInfo {
422 pool,
423 url: url.clone(),
424 healthy: AtomicBool::new(true),
425 lag_ms: AtomicU64::new(0),
426 last_check: Mutex::new(Instant::now()),
427 }));
428 }
429 Err(e) => {
430 warn!("Failed to connect to replica {}: {}", url, e);
431 }
432 }
433 }
434
435 Ok(Self {
436 replicas,
437 config,
438 current_index: AtomicU64::new(0),
439 })
440 }
441
442 async fn create_replica_pool(url: &str, ssl_config: &PostgresSslConfig) -> StateResult<PgPool> {
443 let ssl_mode = Self::parse_ssl_mode(&ssl_config.mode)?;
444 let mut connect_options = PgConnectOptions::from_str(url)
445 .map_err(|e| StateError::StorageError {
446 backend_type: "postgres".to_string(),
447 details: format!("Invalid replica URL: {}", e),
448 })?
449 .ssl_mode(ssl_mode);
450
451 if let Some(root_cert) = &ssl_config.root_cert_path {
452 connect_options = connect_options.ssl_root_cert(root_cert);
453 }
454
455 let pool = PgPoolOptions::new()
456 .min_connections(1)
457 .max_connections(10)
458 .acquire_timeout(Duration::from_secs(10))
459 .connect_with(connect_options)
460 .await
461 .map_err(|e| StateError::StorageError {
462 backend_type: "postgres".to_string(),
463 details: format!("Failed to create replica pool: {}", e),
464 })?;
465
466 Ok(pool)
467 }
468
469 fn parse_ssl_mode(mode: &str) -> StateResult<PgSslMode> {
470 match mode.to_lowercase().as_str() {
471 "disable" => Ok(PgSslMode::Disable),
472 "allow" => Ok(PgSslMode::Allow),
473 "prefer" => Ok(PgSslMode::Prefer),
474 "require" => Ok(PgSslMode::Require),
475 "verify-ca" => Ok(PgSslMode::VerifyCa),
476 "verify-full" => Ok(PgSslMode::VerifyFull),
477 _ => Err(StateError::StorageError {
478 backend_type: "postgres".to_string(),
479 details: format!("Invalid SSL mode: {}", mode),
480 }),
481 }
482 }
483
484 async fn get_read_pool(&self) -> Option<&PgPool> {
485 if self.replicas.is_empty() || !self.config.enable_read_splitting {
486 return None;
487 }
488
489 let start_idx = self.current_index.fetch_add(1, Ordering::Relaxed) as usize % self.replicas.len();
491
492 for i in 0..self.replicas.len() {
493 let idx = (start_idx + i) % self.replicas.len();
494 let replica = &self.replicas[idx];
495
496 if replica.healthy.load(Ordering::Relaxed) {
497 let lag = replica.lag_ms.load(Ordering::Relaxed);
498 if lag <= self.config.max_replication_lag_ms as u64 {
499 return Some(&replica.pool);
500 }
501 }
502 }
503
504 None
505 }
506
507 async fn check_replica_health(&self, stats: &EnhancedPostgresStats) {
508 for replica in &self.replicas {
509 let mut last_check = replica.last_check.lock().await;
510 if last_check.elapsed() < self.config.health_check_interval {
511 continue;
512 }
513 *last_check = Instant::now();
514 drop(last_check);
515
516 match sqlx::query("SELECT 1").fetch_one(&replica.pool).await {
518 Ok(_) => {
519 replica.healthy.store(true, Ordering::Relaxed);
520
521 if let Ok(lag) = self.measure_replication_lag(&replica.pool).await {
523 replica.lag_ms.store(lag as u64, Ordering::Relaxed);
524 stats.replication_lag_ms.store(lag as u64, Ordering::Relaxed);
525 }
526 }
527 Err(e) => {
528 warn!("Replica {} unhealthy: {}", replica.url, e);
529 replica.healthy.store(false, Ordering::Relaxed);
530
531 if self.config.enable_auto_failover {
532 stats.replica_failover_count.fetch_add(1, Ordering::Relaxed);
533 }
534 }
535 }
536 }
537 }
538
539 async fn measure_replication_lag(&self, pool: &PgPool) -> StateResult<i64> {
540 let row = sqlx::query(
541 "SELECT EXTRACT(EPOCH FROM (NOW() - pg_last_xact_replay_timestamp())) * 1000 AS lag_ms"
542 )
543 .fetch_one(pool)
544 .await
545 .map_err(|e| StateError::StorageError {
546 backend_type: "postgres".to_string(),
547 details: format!("Failed to measure replication lag: {}", e),
548 })?;
549
550 let lag: Option<f64> = row.try_get("lag_ms").ok();
551 Ok(lag.unwrap_or(0.0) as i64)
552 }
553}
554
555pub struct PostgresTransaction<'a> {
560 pub(crate) tx: Transaction<'a, Postgres>,
561 isolation_level: IsolationLevel,
562}
563
564impl<'a> PostgresTransaction<'a> {
565 async fn new(pool: &'a PgPool, isolation_level: IsolationLevel) -> StateResult<Self> {
566 let mut tx = pool.begin().await.map_err(|e| StateError::TransactionFailed {
567 operation: "begin".to_string(),
568 reason: e.to_string(),
569 })?;
570
571 sqlx::query(&format!("SET TRANSACTION ISOLATION LEVEL {}", isolation_level.as_sql()))
573 .execute(&mut *tx)
574 .await
575 .map_err(|e| StateError::TransactionFailed {
576 operation: "set_isolation_level".to_string(),
577 reason: e.to_string(),
578 })?;
579
580 Ok(Self { tx, isolation_level })
581 }
582
583 pub async fn commit(self) -> StateResult<()> {
584 self.tx.commit().await.map_err(|e| StateError::TransactionFailed {
585 operation: "commit".to_string(),
586 reason: e.to_string(),
587 })
588 }
589
590 pub async fn rollback(self) -> StateResult<()> {
591 self.tx.rollback().await.map_err(|e| StateError::TransactionFailed {
592 operation: "rollback".to_string(),
593 reason: e.to_string(),
594 })
595 }
596
597 pub async fn savepoint(&mut self, name: &str) -> StateResult<()> {
598 sqlx::query(&format!("SAVEPOINT {}", name))
599 .execute(&mut *self.tx)
600 .await
601 .map_err(|e| StateError::TransactionFailed {
602 operation: "savepoint".to_string(),
603 reason: e.to_string(),
604 })?;
605 Ok(())
606 }
607
608 pub async fn rollback_to_savepoint(&mut self, name: &str) -> StateResult<()> {
609 sqlx::query(&format!("ROLLBACK TO SAVEPOINT {}", name))
610 .execute(&mut *self.tx)
611 .await
612 .map_err(|e| StateError::TransactionFailed {
613 operation: "rollback_to_savepoint".to_string(),
614 reason: e.to_string(),
615 })?;
616 Ok(())
617 }
618
619 pub fn executor(&mut self) -> &mut Transaction<'a, Postgres> {
620 &mut self.tx
621 }
622}
623
624pub struct EnhancedPostgresBackend {
629 primary_pool: PgPool,
630 replica_manager: Option<Arc<ReplicaManager>>,
631 config: EnhancedPostgresConfig,
632 stats: Arc<EnhancedPostgresStats>,
633 prepared_statements: Arc<RwLock<HashMap<String, String>>>,
634 health_check_running: Arc<AtomicBool>,
635}
636
637impl EnhancedPostgresBackend {
638 pub async fn new(config: EnhancedPostgresConfig) -> StateResult<Self> {
639 info!("Initializing Enhanced PostgreSQL backend");
640 info!("Primary: {}", Self::sanitize_url(&config.primary_url));
641 info!("Pool: {:?}", config.pool);
642
643 let primary_pool = Self::create_primary_pool(&config).await?;
645
646 let replica_manager = if let Some(replica_config) = &config.replicas {
648 info!("Initializing {} read replicas", replica_config.replica_urls.len());
649 Some(Arc::new(ReplicaManager::new(replica_config.clone(), &config.ssl).await?))
650 } else {
651 None
652 };
653
654 let backend = Self {
655 primary_pool,
656 replica_manager,
657 config,
658 stats: Arc::new(EnhancedPostgresStats::new()),
659 prepared_statements: Arc::new(RwLock::new(HashMap::new())),
660 health_check_running: Arc::new(AtomicBool::new(false)),
661 };
662
663 backend.run_migrations().await?;
665
666 if backend.config.partition_strategy.is_some() {
668 backend.setup_partitioning().await?;
669 }
670
671 if backend.config.enable_metrics {
673 backend.start_health_check_task();
674 }
675
676 info!("Enhanced PostgreSQL backend initialized successfully");
677 Ok(backend)
678 }
679
680 async fn create_primary_pool(config: &EnhancedPostgresConfig) -> StateResult<PgPool> {
681 let ssl_mode = ReplicaManager::parse_ssl_mode(&config.ssl.mode)?;
682
683 let mut connect_options = PgConnectOptions::from_str(&config.primary_url)
684 .map_err(|e| StateError::StorageError {
685 backend_type: "postgres".to_string(),
686 details: format!("Invalid database URL: {}", e),
687 })?
688 .ssl_mode(ssl_mode);
689
690 if let Some(root_cert) = &config.ssl.root_cert_path {
691 connect_options = connect_options.ssl_root_cert(root_cert);
692 }
693
694 if let Some(client_cert) = &config.ssl.client_cert_path {
695 if let Some(client_key) = &config.ssl.client_key_path {
696 connect_options = connect_options
697 .ssl_client_cert(client_cert)
698 .ssl_client_key(client_key);
699 }
700 }
701
702 let mut pool_options = PgPoolOptions::new()
703 .min_connections(config.pool.min_connections)
704 .max_connections(config.pool.max_connections)
705 .acquire_timeout(config.pool.acquire_timeout);
706
707 if let Some(idle_timeout) = config.pool.idle_timeout {
708 pool_options = pool_options.idle_timeout(idle_timeout);
709 }
710
711 if let Some(max_lifetime) = config.pool.max_lifetime {
712 pool_options = pool_options.max_lifetime(max_lifetime);
713 }
714
715 if config.pool.test_on_acquire {
716 pool_options = pool_options.test_before_acquire(true);
717 }
718
719 let pool = pool_options
720 .connect_with(connect_options)
721 .await
722 .map_err(|e| StateError::StorageError {
723 backend_type: "postgres".to_string(),
724 details: format!("Failed to create connection pool: {}", e),
725 })?;
726
727 info!("Primary connection pool created successfully");
728 Ok(pool)
729 }
730
731 fn start_health_check_task(&self) {
732 if self.health_check_running.swap(true, Ordering::Relaxed) {
733 return; }
735
736 let replica_manager = self.replica_manager.clone();
737 let stats = self.stats.clone();
738 let interval = self.config.pool.health_check_interval;
739 let running = self.health_check_running.clone();
740
741 tokio::spawn(async move {
742 while running.load(Ordering::Relaxed) {
743 if let Some(rm) = &replica_manager {
744 rm.check_replica_health(&stats).await;
745 }
746 tokio::time::sleep(interval).await;
747 }
748 });
749 }
750
751 fn stop_health_check_task(&self) {
752 self.health_check_running.store(false, Ordering::Relaxed);
753 }
754
755 async fn run_migrations(&self) -> StateResult<()> {
756 info!("Running database migrations");
757 let migration_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("migrations");
758
759 if !migration_dir.exists() {
760 warn!("Migrations directory not found: {:?}", migration_dir);
761 return Ok(());
762 }
763
764 let mut migrations = Vec::new();
765 let mut entries = tokio::fs::read_dir(&migration_dir).await.map_err(|e| {
766 StateError::StorageError {
767 backend_type: "postgres".to_string(),
768 details: format!("Failed to read migrations: {}", e),
769 }
770 })?;
771
772 while let Some(entry) = entries.next_entry().await.map_err(|e| StateError::StorageError {
773 backend_type: "postgres".to_string(),
774 details: format!("Failed to read migration entry: {}", e),
775 })? {
776 let path = entry.path();
777 if path.extension().and_then(|s| s.to_str()) == Some("sql") {
778 migrations.push(path);
779 }
780 }
781
782 migrations.sort();
783
784 for migration_path in migrations {
785 let filename = migration_path
786 .file_name()
787 .and_then(|s| s.to_str())
788 .unwrap_or("unknown");
789
790 debug!("Applying migration: {}", filename);
791
792 let sql = tokio::fs::read_to_string(&migration_path)
793 .await
794 .map_err(|e| StateError::StorageError {
795 backend_type: "postgres".to_string(),
796 details: format!("Failed to read migration {}: {}", filename, e),
797 })?;
798
799 sqlx::raw_sql(&sql)
800 .execute(&self.primary_pool)
801 .await
802 .map_err(|e| StateError::StorageError {
803 backend_type: "postgres".to_string(),
804 details: format!("Failed to execute migration {}: {}", filename, e),
805 })?;
806 }
807
808 info!("All migrations applied successfully");
809 Ok(())
810 }
811
812 async fn setup_partitioning(&self) -> StateResult<()> {
813 if let Some(strategy) = &self.config.partition_strategy {
814 info!("Setting up table partitioning: {:?}", strategy);
815
816 match strategy {
817 PartitionStrategy::Hash { num_partitions } => {
818 self.setup_hash_partitioning(*num_partitions).await?;
819 }
820 PartitionStrategy::TimeRange { column, interval } => {
821 self.setup_time_range_partitioning(column, interval).await?;
822 }
823 PartitionStrategy::List { column, values } => {
824 self.setup_list_partitioning(column, values).await?;
825 }
826 }
827 }
828 Ok(())
829 }
830
831 async fn setup_hash_partitioning(&self, num_partitions: u32) -> StateResult<()> {
832 warn!("Hash partitioning requires manual table recreation. Please run:");
835 warn!(" CREATE TABLE {} PARTITION BY HASH (key);", self.config.table_name);
836 for i in 0..num_partitions {
837 warn!(" CREATE TABLE {}_p{} PARTITION OF {} FOR VALUES WITH (MODULUS {}, REMAINDER {});",
838 self.config.table_name, i, self.config.table_name, num_partitions, i);
839 }
840 Ok(())
841 }
842
843 async fn setup_time_range_partitioning(&self, column: &str, interval: &str) -> StateResult<()> {
844 warn!("Time-range partitioning requires manual table recreation. Please run:");
845 warn!(" CREATE TABLE {} PARTITION BY RANGE ({});", self.config.table_name, column);
846 warn!(" -- Then create partitions for each time range");
847 Ok(())
848 }
849
850 async fn setup_list_partitioning(&self, column: &str, values: &[String]) -> StateResult<()> {
851 warn!("List partitioning requires manual table recreation. Please run:");
852 warn!(" CREATE TABLE {} PARTITION BY LIST ({});", self.config.table_name, column);
853 Ok(())
854 }
855
856 fn sanitize_url(url: &str) -> String {
857 if let Some(at_pos) = url.rfind('@') {
858 if let Some(scheme_end) = url.find("://") {
859 let scheme = &url[..scheme_end + 3];
860 let host = &url[at_pos + 1..];
861 return format!("{}***@{}", scheme, host);
862 }
863 }
864 url.to_string()
865 }
866
867 async fn execute_tracked<F, T, Fut>(&self, operation: &str, f: F) -> StateResult<T>
873 where
874 F: FnOnce() -> Fut,
875 Fut: std::future::Future<Output = StateResult<T>>,
876 {
877 let start = Instant::now();
878 let result = f().await;
879 let duration = start.elapsed();
880
881 if self.config.enable_metrics {
882 self.stats.query_count.fetch_add(1, Ordering::Relaxed);
883
884 let duration_ms = duration.as_millis() as u64;
885 let current_avg = self.stats.avg_query_duration_ms.load(Ordering::Relaxed);
886 let new_avg = if current_avg == 0 {
887 duration_ms
888 } else {
889 (current_avg + duration_ms) / 2
890 };
891 self.stats.avg_query_duration_ms.store(new_avg, Ordering::Relaxed);
892
893 if duration > self.config.slow_query_threshold {
894 self.stats.slow_query_count.fetch_add(1, Ordering::Relaxed);
895 warn!(
896 "Slow query detected: {} took {:?} (threshold: {:?})",
897 operation, duration, self.config.slow_query_threshold
898 );
899 }
900 }
901
902 result
903 }
904
905 async fn get_pool_for_read(&self) -> &PgPool {
907 if let Some(replica_manager) = &self.replica_manager {
908 if let Some(pool) = replica_manager.get_read_pool().await {
909 self.stats.replica_read_count.fetch_add(1, Ordering::Relaxed);
910 return pool;
911 }
912 }
913 &self.primary_pool
914 }
915
916 fn next_prefix(prefix: &[u8]) -> Vec<u8> {
918 let mut next = prefix.to_vec();
919 for i in (0..next.len()).rev() {
920 if next[i] < 255 {
921 next[i] += 1;
922 return next;
923 }
924 }
925 next.push(0);
926 next
927 }
928
929 fn clone_handle(&self) -> Self {
930 Self {
931 primary_pool: self.primary_pool.clone(),
932 replica_manager: self.replica_manager.clone(),
933 config: self.config.clone(),
934 stats: self.stats.clone(),
935 prepared_statements: self.prepared_statements.clone(),
936 health_check_running: self.health_check_running.clone(),
937 }
938 }
939
940 pub async fn begin_transaction(&self, isolation_level: IsolationLevel) -> StateResult<PostgresTransaction<'_>> {
946 trace!("BEGIN TRANSACTION: isolation={:?}", isolation_level);
947
948 let tx = PostgresTransaction::new(&self.primary_pool, isolation_level).await?;
949 self.stats.transaction_count.fetch_add(1, Ordering::Relaxed);
950
951 Ok(tx)
952 }
953
954 pub async fn batch_copy(&self, entries: &[(&[u8], &[u8])]) -> StateResult<()> {
956 if entries.is_empty() {
957 return Ok(());
958 }
959
960 info!("Batch COPY: {} entries", entries.len());
961
962 let mut tx = self.primary_pool.begin().await.map_err(|e| {
963 StateError::TransactionFailed {
964 operation: "begin".to_string(),
965 reason: e.to_string(),
966 }
967 })?;
968
969 let values_per_batch = 1000;
971 for chunk in entries.chunks(values_per_batch) {
972 let mut values = Vec::new();
973 for (i, _) in chunk.iter().enumerate() {
974 values.push(format!("(${}::bytea, ${}::bytea, NOW())", i * 2 + 1, i * 2 + 2));
975 }
976
977 let sql = format!(
978 "INSERT INTO {} (key, value, updated_at) VALUES {}
979 ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()",
980 self.config.table_name,
981 values.join(", ")
982 );
983
984 let mut query = sqlx::query(&sql);
985 for (key, value) in chunk {
986 query = query.bind(*key).bind(*value);
987 }
988
989 query.execute(&mut *tx).await.map_err(|e| StateError::StorageError {
990 backend_type: "postgres".to_string(),
991 details: format!("Bulk insert failed: {}", e),
992 })?;
993 }
994
995 tx.commit().await.map_err(|e| StateError::TransactionFailed {
996 operation: "commit".to_string(),
997 reason: e.to_string(),
998 })?;
999
1000 self.stats.batch_insert_count.fetch_add(1, Ordering::Relaxed);
1001 self.stats.batch_insert_rows.fetch_add(entries.len() as u64, Ordering::Relaxed);
1002
1003 Ok(())
1004 }
1005
1006 pub async fn batch_put(&self, entries: &[(&[u8], &[u8])]) -> StateResult<()> {
1008 if entries.is_empty() {
1009 return Ok(());
1010 }
1011
1012 debug!("BATCH_PUT: {} entries", entries.len());
1013
1014 let mut tx = self.begin_transaction(self.config.default_isolation_level).await?;
1015
1016 for (key, value) in entries {
1017 sqlx::query(
1018 "INSERT INTO state_entries (key, value, updated_at)
1019 VALUES ($1, $2, NOW())
1020 ON CONFLICT (key) DO UPDATE
1021 SET value = EXCLUDED.value, updated_at = NOW()"
1022 )
1023 .bind(key)
1024 .bind(value)
1025 .execute(&mut **tx.executor())
1026 .await
1027 .map_err(|e| StateError::StorageError {
1028 backend_type: "postgres".to_string(),
1029 details: format!("Batch insert failed: {}", e),
1030 })?;
1031 }
1032
1033 tx.commit().await?;
1034 self.stats.put_count.fetch_add(entries.len() as u64, Ordering::Relaxed);
1035
1036 Ok(())
1037 }
1038
1039 pub async fn query_by_metadata(
1041 &self,
1042 json_path: &str,
1043 value: &serde_json::Value,
1044 ) -> StateResult<Vec<(Vec<u8>, Vec<u8>)>> {
1045 debug!("Querying by metadata: path={}", json_path);
1046
1047 let pool = self.get_pool_for_read().await;
1048
1049 let rows = sqlx::query(&format!(
1050 "SELECT key, value FROM {}
1051 WHERE metadata @> $1::jsonb
1052 AND (expires_at IS NULL OR expires_at > NOW())",
1053 self.config.table_name
1054 ))
1055 .bind(value)
1056 .fetch_all(pool)
1057 .await
1058 .map_err(|e| StateError::StorageError {
1059 backend_type: "postgres".to_string(),
1060 details: format!("Metadata query failed: {}", e),
1061 })?;
1062
1063 let mut results = Vec::new();
1064 for row in rows {
1065 let key: Vec<u8> = row.try_get("key").map_err(|e| StateError::StorageError {
1066 backend_type: "postgres".to_string(),
1067 details: format!("Failed to extract key: {}", e),
1068 })?;
1069
1070 let value: Vec<u8> = row.try_get("value").map_err(|e| StateError::StorageError {
1071 backend_type: "postgres".to_string(),
1072 details: format!("Failed to extract value: {}", e),
1073 })?;
1074
1075 results.push((key, value));
1076 }
1077
1078 Ok(results)
1079 }
1080
1081 pub async fn put_with_version(
1083 &self,
1084 key: &[u8],
1085 value: &[u8],
1086 expected_version: Option<i64>,
1087 ) -> StateResult<i64> {
1088 trace!("Put with version check: key_len={}", key.len());
1089
1090 let result = if let Some(expected) = expected_version {
1091 sqlx::query(
1093 "UPDATE state_entries
1094 SET value = $1, version = version + 1, updated_at = NOW()
1095 WHERE key = $2 AND version = $3
1096 RETURNING version",
1097 )
1098 .bind(value)
1099 .bind(key)
1100 .bind(expected)
1101 .fetch_optional(&self.primary_pool)
1102 .await
1103 } else {
1104 sqlx::query(
1106 "INSERT INTO state_entries (key, value, version, updated_at)
1107 VALUES ($1, $2, 1, NOW())
1108 ON CONFLICT (key) DO UPDATE
1109 SET value = EXCLUDED.value,
1110 version = state_entries.version + 1,
1111 updated_at = NOW()
1112 RETURNING version",
1113 )
1114 .bind(key)
1115 .bind(value)
1116 .fetch_optional(&self.primary_pool)
1117 .await
1118 }
1119 .map_err(|e| StateError::StorageError {
1120 backend_type: "postgres".to_string(),
1121 details: format!("Versioned put failed: {}", e),
1122 })?;
1123
1124 if let Some(row) = result {
1125 let version: i64 = row.try_get("version").map_err(|e| StateError::StorageError {
1126 backend_type: "postgres".to_string(),
1127 details: format!("Failed to get version: {}", e),
1128 })?;
1129
1130 self.stats.put_count.fetch_add(1, Ordering::Relaxed);
1131 self.stats.rows_updated.fetch_add(1, Ordering::Relaxed);
1132
1133 Ok(version)
1134 } else {
1135 Err(StateError::ConcurrentModification {
1136 key: String::from_utf8_lossy(key).to_string(),
1137 })
1138 }
1139 }
1140
1141 pub async fn put_with_metadata(
1143 &self,
1144 key: &[u8],
1145 value: &[u8],
1146 metadata: serde_json::Value,
1147 ) -> StateResult<()> {
1148 trace!("PUT_WITH_METADATA: key_len={}", key.len());
1149
1150 sqlx::query(
1151 "INSERT INTO state_entries (key, value, metadata, updated_at)
1152 VALUES ($1, $2, $3, NOW())
1153 ON CONFLICT (key) DO UPDATE
1154 SET value = EXCLUDED.value,
1155 metadata = EXCLUDED.metadata,
1156 updated_at = NOW()"
1157 )
1158 .bind(key)
1159 .bind(value)
1160 .bind(metadata)
1161 .execute(&self.primary_pool)
1162 .await
1163 .map_err(|e| StateError::StorageError {
1164 backend_type: "postgres".to_string(),
1165 details: format!("Put with metadata failed: {}", e),
1166 })?;
1167
1168 self.stats.put_count.fetch_add(1, Ordering::Relaxed);
1169 Ok(())
1170 }
1171
1172 pub async fn put_with_ttl(&self, key: &[u8], value: &[u8], ttl: Duration) -> StateResult<()> {
1174 trace!("PUT_WITH_TTL: key_len={}, ttl={:?}", key.len(), ttl);
1175
1176 let expires_at = chrono::Utc::now() + chrono::Duration::from_std(ttl).unwrap();
1177
1178 sqlx::query(
1179 "INSERT INTO state_entries (key, value, expires_at, updated_at)
1180 VALUES ($1, $2, $3, NOW())
1181 ON CONFLICT (key) DO UPDATE
1182 SET value = EXCLUDED.value,
1183 expires_at = EXCLUDED.expires_at,
1184 updated_at = NOW()"
1185 )
1186 .bind(key)
1187 .bind(value)
1188 .bind(expires_at)
1189 .execute(&self.primary_pool)
1190 .await
1191 .map_err(|e| StateError::StorageError {
1192 backend_type: "postgres".to_string(),
1193 details: format!("Put with TTL failed: {}", e),
1194 })?;
1195
1196 self.stats.put_count.fetch_add(1, Ordering::Relaxed);
1197 Ok(())
1198 }
1199
1200 pub async fn cleanup_expired(&self) -> StateResult<usize> {
1202 debug!("CLEANUP_EXPIRED");
1203
1204 let result = sqlx::query("SELECT cleanup_expired_state_entries()")
1205 .fetch_one(&self.primary_pool)
1206 .await
1207 .map_err(|e| StateError::StorageError {
1208 backend_type: "postgres".to_string(),
1209 details: format!("Cleanup failed: {}", e),
1210 })?;
1211
1212 let count: i64 = result.try_get(0).map_err(|e| StateError::StorageError {
1213 backend_type: "postgres".to_string(),
1214 details: format!("Failed to get cleanup count: {}", e),
1215 })?;
1216
1217 self.stats.rows_deleted.fetch_add(count as u64, Ordering::Relaxed);
1218 Ok(count as usize)
1219 }
1220
1221 pub async fn acquire_advisory_lock(&self, lock_id: i64) -> StateResult<bool> {
1227 trace!("Acquiring advisory lock: {}", lock_id);
1228
1229 let start = Instant::now();
1230
1231 let row = sqlx::query("SELECT pg_try_advisory_lock($1) AS acquired")
1232 .bind(lock_id)
1233 .fetch_one(&self.primary_pool)
1234 .await
1235 .map_err(|e| StateError::StorageError {
1236 backend_type: "postgres".to_string(),
1237 details: format!("Advisory lock failed: {}", e),
1238 })?;
1239
1240 let acquired: bool = row.try_get("acquired").unwrap_or(false);
1241
1242 if acquired {
1243 self.stats.advisory_lock_count.fetch_add(1, Ordering::Relaxed);
1244 self.stats.advisory_lock_wait_ms.fetch_add(
1245 start.elapsed().as_millis() as u64,
1246 Ordering::Relaxed,
1247 );
1248 }
1249
1250 Ok(acquired)
1251 }
1252
1253 pub async fn release_advisory_lock(&self, lock_id: i64) -> StateResult<()> {
1255 trace!("Releasing advisory lock: {}", lock_id);
1256
1257 sqlx::query("SELECT pg_advisory_unlock($1)")
1258 .bind(lock_id)
1259 .execute(&self.primary_pool)
1260 .await
1261 .map_err(|e| StateError::StorageError {
1262 backend_type: "postgres".to_string(),
1263 details: format!("Advisory unlock failed: {}", e),
1264 })?;
1265
1266 Ok(())
1267 }
1268
1269 pub async fn notify(&self, channel: &str, payload: &str) -> StateResult<()> {
1271 debug!("Sending notification: channel={}", channel);
1272
1273 sqlx::query("SELECT pg_notify($1, $2)")
1274 .bind(channel)
1275 .bind(payload)
1276 .execute(&self.primary_pool)
1277 .await
1278 .map_err(|e| StateError::StorageError {
1279 backend_type: "postgres".to_string(),
1280 details: format!("Notify failed: {}", e),
1281 })?;
1282
1283 Ok(())
1284 }
1285
1286 pub async fn try_become_leader(&self, election_key: &str, ttl: Duration) -> StateResult<bool> {
1288 info!("Attempting leader election: key={}", election_key);
1289
1290 let lock_id = {
1292 use sha2::{Digest, Sha256};
1293 let mut hasher = Sha256::new();
1294 hasher.update(election_key.as_bytes());
1295 let result = hasher.finalize();
1296 i64::from_le_bytes(result[0..8].try_into().unwrap())
1297 };
1298
1299 let acquired = self.acquire_advisory_lock(lock_id).await?;
1301
1302 if acquired {
1303 info!("Leader election won: key={}", election_key);
1304
1305 let backend = self.clone_handle();
1307 tokio::spawn(async move {
1308 tokio::time::sleep(ttl).await;
1309 if let Err(e) = backend.release_advisory_lock(lock_id).await {
1310 warn!("Failed to release leader lock: {}", e);
1311 }
1312 });
1313 }
1314
1315 Ok(acquired)
1316 }
1317
1318 pub async fn vacuum_analyze(&self, full: bool) -> StateResult<()> {
1324 info!("Running VACUUM{} ANALYZE", if full { " FULL" } else { "" });
1325
1326 let sql = if full {
1327 format!("VACUUM FULL ANALYZE {}", self.config.table_name)
1328 } else {
1329 format!("VACUUM ANALYZE {}", self.config.table_name)
1330 };
1331
1332 sqlx::query(&sql)
1333 .execute(&self.primary_pool)
1334 .await
1335 .map_err(|e| StateError::StorageError {
1336 backend_type: "postgres".to_string(),
1337 details: format!("VACUUM failed: {}", e),
1338 })?;
1339
1340 info!("VACUUM completed successfully");
1341 Ok(())
1342 }
1343
1344 pub async fn apply_retention_policy(&self) -> StateResult<usize> {
1346 if !self.config.retention_policy.enabled {
1347 return Ok(0);
1348 }
1349
1350 info!("Applying retention policy");
1351
1352 let cutoff_time = chrono::Utc::now()
1353 - chrono::Duration::from_std(self.config.retention_policy.max_age).unwrap();
1354
1355 let result = sqlx::query(&format!(
1356 "WITH deleted AS (
1357 DELETE FROM {}
1358 WHERE created_at < $1
1359 RETURNING 1
1360 )
1361 SELECT count(*) as count FROM deleted",
1362 self.config.table_name
1363 ))
1364 .bind(cutoff_time)
1365 .fetch_one(&self.primary_pool)
1366 .await
1367 .map_err(|e| StateError::StorageError {
1368 backend_type: "postgres".to_string(),
1369 details: format!("Retention policy failed: {}", e),
1370 })?;
1371
1372 let deleted: i64 = result.try_get("count").unwrap_or(0);
1373 info!("Retention policy: deleted {} entries", deleted);
1374
1375 self.stats.rows_deleted.fetch_add(deleted as u64, Ordering::Relaxed);
1376
1377 Ok(deleted as usize)
1378 }
1379
1380 pub async fn archive_old_data(&self, archive_table: &str, age_threshold: Duration) -> StateResult<usize> {
1382 info!("Archiving data older than {:?}", age_threshold);
1383
1384 let cutoff_time = chrono::Utc::now() - chrono::Duration::from_std(age_threshold).unwrap();
1385
1386 let mut tx = self.primary_pool.begin().await.map_err(|e| {
1387 StateError::TransactionFailed {
1388 operation: "begin".to_string(),
1389 reason: e.to_string(),
1390 }
1391 })?;
1392
1393 let result = sqlx::query(&format!(
1395 "INSERT INTO {} SELECT *, NOW() as archived_at FROM {} WHERE created_at < $1",
1396 archive_table, self.config.table_name
1397 ))
1398 .bind(cutoff_time)
1399 .execute(&mut *tx)
1400 .await
1401 .map_err(|e| StateError::StorageError {
1402 backend_type: "postgres".to_string(),
1403 details: format!("Archive insert failed: {}", e),
1404 })?;
1405
1406 let archived = result.rows_affected() as usize;
1407
1408 sqlx::query(&format!(
1410 "DELETE FROM {} WHERE created_at < $1",
1411 self.config.table_name
1412 ))
1413 .bind(cutoff_time)
1414 .execute(&mut *tx)
1415 .await
1416 .map_err(|e| StateError::StorageError {
1417 backend_type: "postgres".to_string(),
1418 details: format!("Archive delete failed: {}", e),
1419 })?;
1420
1421 tx.commit().await.map_err(|e| StateError::TransactionFailed {
1422 operation: "commit".to_string(),
1423 reason: e.to_string(),
1424 })?;
1425
1426 info!("Archived {} entries", archived);
1427 Ok(archived)
1428 }
1429
1430 pub async fn get_table_statistics(&self) -> StateResult<TableStatistics> {
1432 debug!("Fetching table statistics");
1433
1434 let pool = self.get_pool_for_read().await;
1435
1436 let row = sqlx::query("SELECT * FROM get_state_table_stats()")
1437 .fetch_one(pool)
1438 .await
1439 .map_err(|e| StateError::StorageError {
1440 backend_type: "postgres".to_string(),
1441 details: format!("Failed to get table stats: {}", e),
1442 })?;
1443
1444 Ok(TableStatistics {
1445 total_rows: row.try_get("total_rows").unwrap_or(0),
1446 active_rows: row.try_get("active_rows").unwrap_or(0),
1447 expired_rows: row.try_get("expired_rows").unwrap_or(0),
1448 total_size: row.try_get("total_size_bytes").unwrap_or(0),
1449 table_size: row.try_get("table_size_bytes").unwrap_or(0),
1450 indexes_size: row.try_get("indexes_size_bytes").unwrap_or(0),
1451 toast_size: row.try_get("toast_size_bytes").unwrap_or(0),
1452 })
1453 }
1454
1455 pub async fn check_replication_status(&self) -> StateResult<ReplicationStatus> {
1461 debug!("Checking replication status");
1462
1463 let row = sqlx::query("SELECT * FROM replication_lag")
1464 .fetch_one(&self.primary_pool)
1465 .await
1466 .map_err(|e| StateError::StorageError {
1467 backend_type: "postgres".to_string(),
1468 details: format!("Replication status check failed: {}", e),
1469 })?;
1470
1471 Ok(ReplicationStatus {
1472 is_replica: row.try_get("is_replica").unwrap_or(false),
1473 lag_ms: row.try_get::<Option<f64>, _>("lag_ms").unwrap_or(None).unwrap_or(0.0) as i64,
1474 replicas_connected: self.replica_manager.as_ref().map(|rm| rm.replicas.len()).unwrap_or(0),
1475 })
1476 }
1477
1478 pub async fn checkpoint(&self) -> StateResult<()> {
1480 info!("Triggering manual checkpoint");
1481
1482 sqlx::query("CHECKPOINT")
1483 .execute(&self.primary_pool)
1484 .await
1485 .map_err(|e| StateError::StorageError {
1486 backend_type: "postgres".to_string(),
1487 details: format!("Checkpoint failed: {}", e),
1488 })?;
1489
1490 Ok(())
1491 }
1492
1493 pub fn get_stats(&self) -> PostgresStatsSnapshot {
1499 self.stats.snapshot()
1500 }
1501
1502 pub fn get_pool_stats(&self) -> PoolStatistics {
1504 let size = self.primary_pool.size() as u64;
1505 let idle = self.primary_pool.num_idle() as u64;
1506 PoolStatistics {
1507 size,
1508 idle,
1509 active: size.saturating_sub(idle),
1510 max_size: self.config.pool.max_connections as u64,
1511 }
1512 }
1513
1514 pub async fn health_check(&self) -> StateResult<bool> {
1516 sqlx::query("SELECT 1")
1517 .fetch_one(&self.primary_pool)
1518 .await
1519 .map(|_| true)
1520 .map_err(|e| StateError::StorageError {
1521 backend_type: "postgres".to_string(),
1522 details: format!("Health check failed: {}", e),
1523 })
1524 }
1525
1526 pub async fn close(&self) {
1528 info!("Closing PostgreSQL connections");
1529 self.stop_health_check_task();
1530 self.primary_pool.close().await;
1531
1532 if let Some(replica_manager) = &self.replica_manager {
1533 for replica in &replica_manager.replicas {
1534 replica.pool.close().await;
1535 }
1536 }
1537 }
1538}
1539
1540#[async_trait]
1545impl StateBackend for EnhancedPostgresBackend {
1546 async fn get(&self, key: &[u8]) -> StateResult<Option<Vec<u8>>> {
1547 trace!("GET: key_len={}", key.len());
1548
1549 let pool = self.get_pool_for_read().await;
1550
1551 let result = sqlx::query(
1552 "SELECT value FROM state_entries
1553 WHERE key = $1
1554 AND (expires_at IS NULL OR expires_at > NOW())"
1555 )
1556 .bind(key)
1557 .fetch_optional(pool)
1558 .await
1559 .map_err(|e| StateError::StorageError {
1560 backend_type: "postgres".to_string(),
1561 details: format!("Get failed: {}", e),
1562 })?;
1563
1564 self.stats.get_count.fetch_add(1, Ordering::Relaxed);
1565
1566 if let Some(row) = result {
1567 let value: Vec<u8> = row.try_get("value").map_err(|e| StateError::StorageError {
1568 backend_type: "postgres".to_string(),
1569 details: format!("Failed to extract value: {}", e),
1570 })?;
1571
1572 self.stats.bytes_read.fetch_add(value.len() as u64, Ordering::Relaxed);
1573 Ok(Some(value))
1574 } else {
1575 Ok(None)
1576 }
1577 }
1578
1579 async fn put(&self, key: &[u8], value: &[u8]) -> StateResult<()> {
1580 trace!("PUT: key_len={}, value_len={}", key.len(), value.len());
1581
1582 let result = sqlx::query(
1583 "INSERT INTO state_entries (key, value, updated_at)
1584 VALUES ($1, $2, NOW())
1585 ON CONFLICT (key) DO UPDATE
1586 SET value = EXCLUDED.value, updated_at = NOW()"
1587 )
1588 .bind(key)
1589 .bind(value)
1590 .execute(&self.primary_pool)
1591 .await
1592 .map_err(|e| StateError::StorageError {
1593 backend_type: "postgres".to_string(),
1594 details: format!("Put failed: {}", e),
1595 })?;
1596
1597 self.stats.put_count.fetch_add(1, Ordering::Relaxed);
1598 self.stats.bytes_written.fetch_add(
1599 (key.len() + value.len()) as u64,
1600 Ordering::Relaxed,
1601 );
1602
1603 if result.rows_affected() > 0 {
1604 self.stats.rows_updated.fetch_add(1, Ordering::Relaxed);
1605 } else {
1606 self.stats.rows_inserted.fetch_add(1, Ordering::Relaxed);
1607 }
1608
1609 Ok(())
1610 }
1611
1612 async fn delete(&self, key: &[u8]) -> StateResult<()> {
1613 trace!("DELETE: key_len={}", key.len());
1614
1615 let result = sqlx::query("DELETE FROM state_entries WHERE key = $1")
1616 .bind(key)
1617 .execute(&self.primary_pool)
1618 .await
1619 .map_err(|e| StateError::StorageError {
1620 backend_type: "postgres".to_string(),
1621 details: format!("Delete failed: {}", e),
1622 })?;
1623
1624 self.stats.delete_count.fetch_add(1, Ordering::Relaxed);
1625 self.stats.rows_deleted.fetch_add(result.rows_affected(), Ordering::Relaxed);
1626
1627 Ok(())
1628 }
1629
1630 async fn list_keys(&self, prefix: &[u8]) -> StateResult<Vec<Vec<u8>>> {
1631 trace!("LIST_KEYS: prefix_len={}", prefix.len());
1632
1633 let pool = self.get_pool_for_read().await;
1634
1635 let rows = if prefix.is_empty() {
1636 sqlx::query("SELECT key FROM state_entries WHERE expires_at IS NULL OR expires_at > NOW()")
1637 .fetch_all(pool)
1638 .await
1639 } else {
1640 sqlx::query(
1641 "SELECT key FROM state_entries
1642 WHERE key >= $1 AND key < $2
1643 AND (expires_at IS NULL OR expires_at > NOW())"
1644 )
1645 .bind(prefix)
1646 .bind(Self::next_prefix(prefix))
1647 .fetch_all(pool)
1648 .await
1649 }
1650 .map_err(|e| StateError::StorageError {
1651 backend_type: "postgres".to_string(),
1652 details: format!("List keys failed: {}", e),
1653 })?;
1654
1655 let keys = rows
1656 .into_iter()
1657 .filter_map(|row| row.try_get::<Vec<u8>, _>("key").ok())
1658 .collect();
1659
1660 Ok(keys)
1661 }
1662
1663 async fn clear(&self) -> StateResult<()> {
1664 debug!("CLEAR: truncating table");
1665
1666 sqlx::query("TRUNCATE TABLE state_entries")
1667 .execute(&self.primary_pool)
1668 .await
1669 .map_err(|e| StateError::StorageError {
1670 backend_type: "postgres".to_string(),
1671 details: format!("Clear failed: {}", e),
1672 })?;
1673
1674 Ok(())
1675 }
1676
1677 async fn count(&self) -> StateResult<usize> {
1678 let pool = self.get_pool_for_read().await;
1679
1680 let row = sqlx::query(
1681 "SELECT COUNT(*) as count FROM state_entries
1682 WHERE expires_at IS NULL OR expires_at > NOW()"
1683 )
1684 .fetch_one(pool)
1685 .await
1686 .map_err(|e| StateError::StorageError {
1687 backend_type: "postgres".to_string(),
1688 details: format!("Count failed: {}", e),
1689 })?;
1690
1691 let count: i64 = row.try_get("count").map_err(|e| StateError::StorageError {
1692 backend_type: "postgres".to_string(),
1693 details: format!("Failed to extract count: {}", e),
1694 })?;
1695
1696 Ok(count as usize)
1697 }
1698
1699 async fn contains(&self, key: &[u8]) -> StateResult<bool> {
1700 let pool = self.get_pool_for_read().await;
1701
1702 let row = sqlx::query(
1703 "SELECT EXISTS(
1704 SELECT 1 FROM state_entries
1705 WHERE key = $1
1706 AND (expires_at IS NULL OR expires_at > NOW())
1707 ) as exists"
1708 )
1709 .bind(key)
1710 .fetch_one(pool)
1711 .await
1712 .map_err(|e| StateError::StorageError {
1713 backend_type: "postgres".to_string(),
1714 details: format!("Contains check failed: {}", e),
1715 })?;
1716
1717 let exists: bool = row.try_get("exists").unwrap_or(false);
1718 Ok(exists)
1719 }
1720}
1721
1722#[derive(Debug, Clone, Serialize, Deserialize)]
1727pub struct TableStatistics {
1728 pub total_rows: i64,
1729 pub active_rows: i64,
1730 pub expired_rows: i64,
1731 pub total_size: i64,
1732 pub table_size: i64,
1733 pub indexes_size: i64,
1734 pub toast_size: i64,
1735}
1736
1737#[derive(Debug, Clone, Serialize, Deserialize)]
1738pub struct ReplicationStatus {
1739 pub is_replica: bool,
1740 pub lag_ms: i64,
1741 pub replicas_connected: usize,
1742}
1743
1744#[derive(Debug, Clone, Serialize, Deserialize)]
1745pub struct PoolStatistics {
1746 pub size: u64,
1747 pub idle: u64,
1748 pub active: u64,
1749 pub max_size: u64,
1750}
1751
1752pub struct PostgresLockGuard {
1758 lock_id: i64,
1759 backend: EnhancedPostgresBackend,
1760 released: Arc<AtomicBool>,
1761}
1762
1763impl PostgresLockGuard {
1764 pub async fn new(
1765 backend: EnhancedPostgresBackend,
1766 lock_key: &str,
1767 timeout: Duration,
1768 ) -> StateResult<Option<Self>> {
1769 use sha2::{Digest, Sha256};
1771 let mut hasher = Sha256::new();
1772 hasher.update(lock_key.as_bytes());
1773 let result = hasher.finalize();
1774 let lock_id = i64::from_le_bytes(result[0..8].try_into().unwrap());
1775
1776 let deadline = Instant::now() + timeout;
1778 while Instant::now() < deadline {
1779 if backend.acquire_advisory_lock(lock_id).await? {
1780 return Ok(Some(Self {
1781 lock_id,
1782 backend,
1783 released: Arc::new(AtomicBool::new(false)),
1784 }));
1785 }
1786 tokio::time::sleep(Duration::from_millis(100)).await;
1787 }
1788
1789 Ok(None)
1790 }
1791
1792 pub async fn release(self) -> StateResult<()> {
1793 if !self.released.swap(true, Ordering::Relaxed) {
1794 self.backend.release_advisory_lock(self.lock_id).await?;
1795 }
1796 Ok(())
1797 }
1798}
1799
1800impl Drop for PostgresLockGuard {
1801 fn drop(&mut self) {
1802 if !self.released.swap(true, Ordering::Relaxed) {
1803 let lock_id = self.lock_id;
1804 let backend = self.backend.clone_handle();
1805
1806 tokio::spawn(async move {
1808 if let Err(e) = backend.release_advisory_lock(lock_id).await {
1809 warn!("Failed to release lock in drop: {}", e);
1810 }
1811 });
1812 }
1813 }
1814}
1815
1816pub struct PostgresListener {
1821 listener: sqlx::postgres::PgListener,
1822}
1823
1824impl PostgresListener {
1825 pub async fn new(database_url: &str, channel: &str) -> StateResult<Self> {
1826 let mut listener = sqlx::postgres::PgListener::connect(database_url)
1827 .await
1828 .map_err(|e| StateError::StorageError {
1829 backend_type: "postgres".to_string(),
1830 details: format!("Failed to create listener: {}", e),
1831 })?;
1832
1833 listener.listen(channel).await.map_err(|e| StateError::StorageError {
1834 backend_type: "postgres".to_string(),
1835 details: format!("Failed to listen on channel: {}", e),
1836 })?;
1837
1838 Ok(Self { listener })
1839 }
1840
1841 pub async fn recv(&mut self) -> StateResult<String> {
1842 let notification = self.listener.recv().await.map_err(|e| StateError::StorageError {
1843 backend_type: "postgres".to_string(),
1844 details: format!("Failed to receive notification: {}", e),
1845 })?;
1846
1847 Ok(notification.payload().to_string())
1848 }
1849
1850 pub async fn try_recv(&mut self) -> StateResult<Option<String>> {
1851 match self.listener.try_recv().await {
1852 Ok(Some(notification)) => Ok(Some(notification.payload().to_string())),
1853 Ok(None) => Ok(None),
1854 Err(e) => Err(StateError::StorageError {
1855 backend_type: "postgres".to_string(),
1856 details: format!("Failed to try_recv notification: {}", e),
1857 }),
1858 }
1859 }
1860}