1use std::sync::Arc;
2use std::time::Duration;
3
4use color_eyre::Result;
5use deadpool_redis::{Config, Metrics, Pool, Runtime};
6use tracing::{debug, info, warn};
7
8use crate::config::ServerConfig;
9
10pub const BOOTSTRAP_LOCK_TTL_SECS: u64 = 120;
17
18pub const LOCK_WAIT_MAX_SECS: u64 = 130;
21
22pub const LOCK_POLL_INTERVAL_MS: u64 = 500;
24
25#[derive(Clone, Debug)]
34pub struct RedisConnections {
35 primary_pool: Arc<Pool>,
37 reader_pool: Arc<Pool>,
39}
40
41impl RedisConnections {
42 pub fn new_single_pool(pool: Arc<Pool>) -> Self {
49 Self {
50 primary_pool: pool.clone(),
51 reader_pool: pool,
52 }
53 }
54
55 pub fn primary(&self) -> &Arc<Pool> {
60 &self.primary_pool
61 }
62
63 pub fn reader(&self) -> &Arc<Pool> {
70 &self.reader_pool
71 }
72}
73
74async fn create_pool(url: &str, pool_max_size: usize, config: &ServerConfig) -> Result<Arc<Pool>> {
76 let cfg = Config::from_url(url);
77
78 let pool = cfg
79 .builder()
80 .map_err(|e| eyre::eyre!("Failed to create Redis pool builder for {}: {}", url, e))?
81 .max_size(pool_max_size)
82 .wait_timeout(Some(Duration::from_millis(config.redis_pool_timeout_ms)))
83 .create_timeout(Some(Duration::from_millis(
84 config.redis_connection_timeout_ms,
85 )))
86 .recycle_timeout(Some(Duration::from_millis(
87 config.redis_connection_timeout_ms,
88 )))
89 .runtime(Runtime::Tokio1)
90 .build()
91 .map_err(|e| eyre::eyre!("Failed to build Redis pool for {}: {}", url, e))?;
92
93 let conn = pool
95 .get()
96 .await
97 .map_err(|e| eyre::eyre!("Failed to get initial Redis connection from {}: {}", url, e))?;
98 drop(conn);
99
100 Ok(Arc::new(pool))
101}
102
103pub async fn initialize_redis_connections(config: &ServerConfig) -> Result<Arc<RedisConnections>> {
132 let primary_pool_size = config.redis_pool_max_size;
133 let primary_pool = create_pool(&config.redis_url, primary_pool_size, config).await?;
134
135 info!(
136 primary_url = %config.redis_url,
137 primary_pool_size = %primary_pool_size,
138 "initializing primary Redis connection pool"
139 );
140 let reader_pool = match &config.redis_reader_url {
141 Some(reader_url) if !reader_url.is_empty() => {
142 let reader_pool_size = config.redis_reader_pool_max_size;
143
144 info!(
145 primary_url = %config.redis_url,
146 reader_url = %reader_url,
147 primary_pool_size = %primary_pool_size,
148 reader_pool_size = %reader_pool_size,
149 "Using separate reader endpoint for read operations"
150 );
151 create_pool(reader_url, reader_pool_size, config).await?
152 }
153 _ => {
154 debug!(
155 primary_url = %config.redis_url,
156 pool_size = %primary_pool_size,
157 "No reader URL configured, using primary for all operations"
158 );
159 primary_pool.clone()
160 }
161 };
162
163 spawn_pool_age_recycler(&primary_pool, config.redis_connection_max_age_ms, "primary");
170 if !Arc::ptr_eq(&reader_pool, &primary_pool) {
171 spawn_pool_age_recycler(&reader_pool, config.redis_connection_max_age_ms, "reader");
172 }
173
174 Ok(Arc::new(RedisConnections {
175 primary_pool,
176 reader_pool,
177 }))
178}
179
180fn should_retain_connection(metrics: &Metrics, max_age: Duration) -> bool {
183 metrics.created.elapsed() < max_age
184}
185
186fn spawn_pool_age_recycler(pool: &Arc<Pool>, max_age_ms: u64, pool_label: &'static str) {
203 if max_age_ms == 0 {
204 return;
205 }
206
207 let max_age = Duration::from_millis(max_age_ms);
208 let pool = Arc::downgrade(pool);
209
210 let interval = max_age
213 .div_f64(2.0)
214 .clamp(Duration::from_secs(1), Duration::from_secs(30));
215
216 tokio::spawn(async move {
217 loop {
218 tokio::time::sleep(interval).await;
219 let Some(pool) = pool.upgrade() else {
220 break;
221 };
222 let result = pool.retain(|_, metrics| should_retain_connection(&metrics, max_age));
223 if !result.removed.is_empty() {
224 debug!(
225 pool = pool_label,
226 removed = result.removed.len(),
227 retained = result.retained,
228 max_age_ms = max_age_ms,
229 "recycled aged Redis connections"
230 );
231 }
232 }
233 });
234}
235
236#[derive(Clone)]
268pub struct DistributedLock {
269 pool: Arc<Pool>,
270 lock_key: String,
271 ttl: Duration,
272}
273
274impl DistributedLock {
275 pub fn new(pool: Arc<Pool>, lock_key: &str, ttl: Duration) -> Self {
283 Self {
284 pool,
285 lock_key: lock_key.to_string(),
286 ttl,
287 }
288 }
289
290 pub async fn try_acquire(&self) -> Result<Option<LockGuard>> {
305 let lock_value = generate_lock_value();
306 let mut conn = self.pool.get().await?;
307
308 let result: Option<String> = redis::cmd("SET")
310 .arg(&self.lock_key)
311 .arg(&lock_value)
312 .arg("NX")
313 .arg("EX")
314 .arg(self.ttl.as_secs())
315 .query_async(&mut conn)
316 .await?;
317
318 match result {
319 Some(_) => {
320 debug!(
321 lock_key = %self.lock_key,
322 ttl_secs = %self.ttl.as_secs(),
323 "distributed lock acquired"
324 );
325 Ok(Some(LockGuard {
326 release_info: Some(LockReleaseInfo {
327 pool: self.pool.clone(),
328 lock_key: self.lock_key.clone(),
329 lock_value,
330 }),
331 }))
332 }
333 None => {
334 debug!(
335 lock_key = %self.lock_key,
336 "distributed lock already held by another instance"
337 );
338 Ok(None)
339 }
340 }
341 }
342}
343
344pub struct LockGuard {
358 release_info: Option<LockReleaseInfo>,
362}
363
364struct LockReleaseInfo {
366 pool: Arc<Pool>,
367 lock_key: String,
368 lock_value: String,
369}
370
371impl LockGuard {
372 pub async fn release(mut self) -> Result<bool> {
385 let info = self
387 .release_info
388 .take()
389 .expect("release_info should always be Some before release");
390
391 Self::do_release(info).await
392 }
393
394 async fn do_release(info: LockReleaseInfo) -> Result<bool> {
396 let mut conn = info.pool.get().await?;
397
398 let script = r#"
401 if redis.call("GET", KEYS[1]) == ARGV[1] then
402 return redis.call("DEL", KEYS[1])
403 else
404 return 0
405 end
406 "#;
407
408 let result: i32 = redis::Script::new(script)
409 .key(&info.lock_key)
410 .arg(&info.lock_value)
411 .invoke_async(&mut conn)
412 .await?;
413
414 if result == 1 {
415 debug!(lock_key = %info.lock_key, "distributed lock released");
416 Ok(true)
417 } else {
418 warn!(
419 lock_key = %info.lock_key,
420 "distributed lock release failed - lock already expired or owned by another instance"
421 );
422 Ok(false)
423 }
424 }
425}
426
427impl Drop for LockGuard {
428 fn drop(&mut self) {
429 if let Some(info) = self.release_info.take() {
431 tokio::spawn(async move {
434 if let Err(e) = LockGuard::do_release(info).await {
435 warn!(error = %e, "failed to release distributed lock in drop, will expire after TTL");
436 }
437 });
438 }
439 }
440}
441
442fn generate_lock_value() -> String {
448 use std::sync::atomic::{AtomicU64, Ordering};
449
450 static LOCK_VALUE_COUNTER: AtomicU64 = AtomicU64::new(0);
451 let process_id = std::process::id();
452 let timestamp = std::time::SystemTime::now()
453 .duration_since(std::time::UNIX_EPOCH)
454 .map(|d| d.as_nanos())
455 .unwrap_or(0);
456 let counter = LOCK_VALUE_COUNTER.fetch_add(1, Ordering::Relaxed);
457
458 format!("{process_id}:{timestamp}:{counter}")
459}
460
461const RELAYER_SYNC_META_KEY: &str = "relayer_sync_meta";
471
472const GLOBAL_INIT_FIELD: &str = "global:init_completed";
474
475const BOOTSTRAP_META_KEY: &str = "bootstrap_meta";
477
478const CONFIG_PROCESSING_STATUS_FIELD: &str = "config_processing:status";
480
481const CONFIG_PROCESSING_STARTED_AT_FIELD: &str = "config_processing:started_at";
483
484const CONFIG_PROCESSING_COMPLETED_AT_FIELD: &str = "config_processing:completed_at";
486
487const CONFIG_PROCESSING_STATUS_IN_PROGRESS: &str = "in_progress";
488const CONFIG_PROCESSING_STATUS_COMPLETED: &str = "completed";
489
490pub async fn set_config_processing_in_progress(pool: &Arc<Pool>, prefix: &str) -> Result<()> {
495 use chrono::Utc;
496 use redis::AsyncCommands;
497
498 let mut conn = pool.get().await?;
499 let hash_key = format!("{prefix}:{BOOTSTRAP_META_KEY}");
500 let timestamp = Utc::now().timestamp();
501
502 conn.hset_multiple::<_, _, _, ()>(
503 &hash_key,
504 &[
505 (
506 CONFIG_PROCESSING_STATUS_FIELD,
507 CONFIG_PROCESSING_STATUS_IN_PROGRESS,
508 ),
509 (CONFIG_PROCESSING_STARTED_AT_FIELD, ×tamp.to_string()),
510 ],
511 )
512 .await
513 .map_err(|e| eyre::eyre!("Failed to set config processing in progress: {}", e))?;
514
515 conn.hdel::<_, _, ()>(&hash_key, CONFIG_PROCESSING_COMPLETED_AT_FIELD)
516 .await
517 .map_err(|e| eyre::eyre!("Failed to clear config processing completion time: {}", e))?;
518
519 debug!(
520 timestamp = %timestamp,
521 "recorded config processing in-progress marker"
522 );
523
524 Ok(())
525}
526
527pub async fn set_config_processing_completed(pool: &Arc<Pool>, prefix: &str) -> Result<()> {
529 use chrono::Utc;
530 use redis::AsyncCommands;
531
532 let mut conn = pool.get().await?;
533 let hash_key = format!("{prefix}:{BOOTSTRAP_META_KEY}");
534 let timestamp = Utc::now().timestamp();
535
536 conn.hset_multiple::<_, _, _, ()>(
537 &hash_key,
538 &[
539 (
540 CONFIG_PROCESSING_STATUS_FIELD,
541 CONFIG_PROCESSING_STATUS_COMPLETED,
542 ),
543 (CONFIG_PROCESSING_COMPLETED_AT_FIELD, ×tamp.to_string()),
544 ],
545 )
546 .await
547 .map_err(|e| eyre::eyre!("Failed to set config processing completed: {}", e))?;
548
549 debug!(
550 timestamp = %timestamp,
551 "recorded config processing completed marker"
552 );
553
554 Ok(())
555}
556
557pub async fn is_config_processing_completed(pool: &Arc<Pool>, prefix: &str) -> Result<bool> {
559 use redis::AsyncCommands;
560
561 let mut conn = pool.get().await?;
562 let hash_key = format!("{prefix}:{BOOTSTRAP_META_KEY}");
563
564 let status: Option<String> = conn
565 .hget(&hash_key, CONFIG_PROCESSING_STATUS_FIELD)
566 .await
567 .map_err(|e| eyre::eyre!("Failed to get config processing status: {}", e))?;
568
569 Ok(matches!(
570 status.as_deref(),
571 Some(CONFIG_PROCESSING_STATUS_COMPLETED)
572 ))
573}
574
575pub async fn is_config_processing_in_progress(pool: &Arc<Pool>, prefix: &str) -> Result<bool> {
577 use redis::AsyncCommands;
578
579 let mut conn = pool.get().await?;
580 let hash_key = format!("{prefix}:{BOOTSTRAP_META_KEY}");
581
582 let status: Option<String> = conn
583 .hget(&hash_key, CONFIG_PROCESSING_STATUS_FIELD)
584 .await
585 .map_err(|e| eyre::eyre!("Failed to get config processing status: {}", e))?;
586
587 Ok(matches!(
588 status.as_deref(),
589 Some(CONFIG_PROCESSING_STATUS_IN_PROGRESS)
590 ))
591}
592
593pub async fn set_relayer_last_sync(pool: &Arc<Pool>, prefix: &str, relayer_id: &str) -> Result<()> {
608 use chrono::Utc;
609 use redis::AsyncCommands;
610
611 let mut conn = pool.get().await?;
612 let hash_key = format!("{prefix}:{RELAYER_SYNC_META_KEY}");
613 let field = format!("{relayer_id}:last_sync");
614 let timestamp = Utc::now().timestamp();
615
616 conn.hset::<_, _, _, ()>(&hash_key, &field, timestamp)
617 .await
618 .map_err(|e| eyre::eyre!("Failed to set relayer last sync: {}", e))?;
619
620 debug!(
621 relayer_id = %relayer_id,
622 timestamp = %timestamp,
623 "recorded relayer last sync time"
624 );
625
626 Ok(())
627}
628
629pub async fn get_relayer_last_sync(
641 pool: &Arc<Pool>,
642 prefix: &str,
643 relayer_id: &str,
644) -> Result<Option<chrono::DateTime<chrono::Utc>>> {
645 use chrono::{TimeZone, Utc};
646 use redis::AsyncCommands;
647
648 let mut conn = pool.get().await?;
649 let hash_key = format!("{prefix}:{RELAYER_SYNC_META_KEY}");
650 let field = format!("{relayer_id}:last_sync");
651
652 let timestamp: Option<i64> = conn
653 .hget(&hash_key, &field)
654 .await
655 .map_err(|e| eyre::eyre!("Failed to get relayer last sync: {}", e))?;
656
657 Ok(timestamp.and_then(|ts| Utc.timestamp_opt(ts, 0).single()))
658}
659
660pub async fn is_relayer_recently_synced(
676 pool: &Arc<Pool>,
677 prefix: &str,
678 relayer_id: &str,
679 threshold_secs: u64,
680) -> Result<bool> {
681 use chrono::Utc;
682
683 let last_sync = get_relayer_last_sync(pool, prefix, relayer_id).await?;
684
685 match last_sync {
686 Some(sync_time) => {
687 let elapsed = Utc::now().signed_duration_since(sync_time);
688 let is_recent = elapsed.num_seconds() < threshold_secs as i64;
689
690 if is_recent {
691 debug!(
692 relayer_id = %relayer_id,
693 last_sync = %sync_time.to_rfc3339(),
694 elapsed_secs = %elapsed.num_seconds(),
695 threshold_secs = %threshold_secs,
696 "relayer was recently synced"
697 );
698 }
699
700 Ok(is_recent)
701 }
702 None => Ok(false),
703 }
704}
705
706pub async fn set_global_init_completed(pool: &Arc<Pool>, prefix: &str) -> Result<()> {
728 use chrono::Utc;
729 use redis::AsyncCommands;
730
731 let mut conn = pool.get().await?;
732 let hash_key = format!("{prefix}:{RELAYER_SYNC_META_KEY}");
733 let timestamp = Utc::now().timestamp();
734
735 conn.hset::<_, _, _, ()>(&hash_key, GLOBAL_INIT_FIELD, timestamp)
736 .await
737 .map_err(|e| eyre::eyre!("Failed to set global init completed: {}", e))?;
738
739 debug!(timestamp = %timestamp, "recorded global initialization completion time");
740 Ok(())
741}
742
743pub async fn is_global_init_recently_completed(
758 pool: &Arc<Pool>,
759 prefix: &str,
760 threshold_secs: u64,
761) -> Result<bool> {
762 use chrono::Utc;
763 use redis::AsyncCommands;
764
765 let mut conn = pool.get().await?;
766 let hash_key = format!("{prefix}:{RELAYER_SYNC_META_KEY}");
767
768 let timestamp: Option<i64> = conn
769 .hget(&hash_key, GLOBAL_INIT_FIELD)
770 .await
771 .map_err(|e| eyre::eyre!("Failed to get global init time: {}", e))?;
772
773 match timestamp {
774 Some(ts) => {
775 let now = Utc::now().timestamp();
776 let elapsed = now - ts;
777 let is_recent = elapsed < threshold_secs as i64;
778
779 if is_recent {
780 debug!(
781 elapsed_secs = %elapsed,
782 threshold_secs = %threshold_secs,
783 "global initialization recently completed"
784 );
785 }
786
787 Ok(is_recent)
788 }
789 None => Ok(false),
790 }
791}
792
793#[cfg(test)]
794mod tests {
795 use super::*;
796
797 #[test]
798 fn test_generate_lock_value_is_unique() {
799 let value1 = generate_lock_value();
800 let value2 = generate_lock_value();
801
802 assert_ne!(value1, value2);
804 }
805
806 #[test]
807 fn test_generate_lock_value_contains_process_id() {
808 let value = generate_lock_value();
809
810 assert!(value.contains(':'));
812
813 let parts: Vec<&str> = value.split(':').collect();
815 assert_eq!(parts.len(), 3);
816
817 assert!(parts[0].parse::<u32>().is_ok());
819 }
820
821 #[test]
822 fn test_should_retain_connection_keeps_young_connection() {
823 let metrics = Metrics::default();
825 assert!(
826 should_retain_connection(&metrics, Duration::from_secs(60)),
827 "young connection should be retained"
828 );
829 }
830
831 #[test]
832 fn test_should_retain_connection_drops_over_age_connection() {
833 let Some(past) = std::time::Instant::now().checked_sub(Duration::from_secs(120)) else {
837 return;
838 };
839 let mut metrics = Metrics::default();
840 metrics.created = past;
841 assert!(
842 !should_retain_connection(&metrics, Duration::from_secs(60)),
843 "over-age connection should be dropped"
844 );
845 }
846
847 #[test]
848 fn test_distributed_lock_key_format() {
849 let prefix = "myrelayer";
851 let lock_name = "transaction_cleanup";
852 let lock_key = format!("{prefix}:lock:{lock_name}");
853 assert_eq!(lock_key, "myrelayer:lock:transaction_cleanup");
854 }
855
856 #[test]
857 fn test_distributed_lock_key_format_with_complex_prefix() {
858 let prefix = "oz-relayer-prod";
860 let lock_name = "transaction_cleanup";
861 let lock_key = format!("{prefix}:lock:{lock_name}");
862 assert_eq!(lock_key, "oz-relayer-prod:lock:transaction_cleanup");
863 }
864
865 #[test]
866 fn test_distributed_lock_key_uses_exact_key() {
867 let lock_key = "myprefix:lock:myoperation";
869 assert_eq!(lock_key, "myprefix:lock:myoperation");
871 }
872
873 mod redis_connections_tests {
878 use super::*;
879
880 #[test]
881 fn test_new_single_pool_returns_same_pool_for_both() {
882 let cfg = Config::from_url("redis://127.0.0.1:6379");
885 let pool = cfg
886 .builder()
887 .expect("Failed to create pool builder")
888 .max_size(16)
889 .runtime(Runtime::Tokio1)
890 .build()
891 .expect("Failed to build pool");
892 let pool = Arc::new(pool);
893
894 let connections = RedisConnections::new_single_pool(pool.clone());
895
896 assert!(Arc::ptr_eq(connections.primary(), &pool));
898 assert!(Arc::ptr_eq(connections.reader(), &pool));
899 assert!(Arc::ptr_eq(connections.primary(), connections.reader()));
900 }
901
902 #[test]
903 fn test_redis_connections_clone() {
904 let cfg = Config::from_url("redis://127.0.0.1:6379");
906 let pool = cfg
907 .builder()
908 .expect("Failed to create pool builder")
909 .max_size(16)
910 .runtime(Runtime::Tokio1)
911 .build()
912 .expect("Failed to build pool");
913 let pool = Arc::new(pool);
914
915 let connections = RedisConnections::new_single_pool(pool);
916 let cloned = connections.clone();
917
918 assert!(Arc::ptr_eq(connections.primary(), cloned.primary()));
920 assert!(Arc::ptr_eq(connections.reader(), cloned.reader()));
921 }
922
923 #[test]
924 fn test_redis_connections_debug() {
925 let cfg = Config::from_url("redis://127.0.0.1:6379");
927 let pool = cfg
928 .builder()
929 .expect("Failed to create pool builder")
930 .max_size(16)
931 .runtime(Runtime::Tokio1)
932 .build()
933 .expect("Failed to build pool");
934 let pool = Arc::new(pool);
935
936 let connections = RedisConnections::new_single_pool(pool);
937 let debug_str = format!("{connections:?}");
938
939 assert!(debug_str.contains("RedisConnections"));
940 }
941 }
942
943 async fn create_test_redis_connection() -> Option<Arc<Pool>> {
951 let cfg = Config::from_url("redis://127.0.0.1:6379");
952 let pool = cfg
953 .builder()
954 .ok()?
955 .max_size(16)
956 .runtime(Runtime::Tokio1)
957 .build()
958 .ok()?;
959 Some(Arc::new(pool))
960 }
961
962 mod integration {
963 use super::*;
964
965 #[tokio::test]
966 #[ignore] async fn test_distributed_lock_acquire_and_release() {
968 let conn = create_test_redis_connection()
969 .await
970 .expect("Redis connection required for this test");
971
972 let lock =
973 DistributedLock::new(conn, "test:lock:acquire_release", Duration::from_secs(60));
974
975 let guard = lock.try_acquire().await.expect("Redis error");
977 assert!(guard.is_some(), "Should acquire lock when not held");
978
979 let released = guard.unwrap().release().await.expect("Redis error");
981 assert!(released, "Should successfully release the lock");
982 }
983
984 #[tokio::test]
985 #[ignore] async fn test_distributed_lock_prevents_double_acquisition() {
987 let conn = create_test_redis_connection()
988 .await
989 .expect("Redis connection required for this test");
990
991 let lock1 = DistributedLock::new(
992 conn.clone(),
993 "test:lock:double_acquire",
994 Duration::from_secs(60),
995 );
996 let lock2 =
997 DistributedLock::new(conn, "test:lock:double_acquire", Duration::from_secs(60));
998
999 let guard1 = lock1.try_acquire().await.expect("Redis error");
1001 assert!(guard1.is_some(), "First acquisition should succeed");
1002
1003 let guard2 = lock2.try_acquire().await.expect("Redis error");
1005 assert!(
1006 guard2.is_none(),
1007 "Second acquisition should fail - lock already held"
1008 );
1009
1010 guard1.unwrap().release().await.expect("Redis error");
1012
1013 let guard2_retry = lock2.try_acquire().await.expect("Redis error");
1015 assert!(guard2_retry.is_some(), "Should acquire lock after release");
1016
1017 guard2_retry.unwrap().release().await.expect("Redis error");
1019 }
1020
1021 #[tokio::test]
1022 #[ignore] async fn test_distributed_lock_expires_after_ttl() {
1024 let conn = create_test_redis_connection()
1025 .await
1026 .expect("Redis connection required for this test");
1027
1028 let lock =
1030 DistributedLock::new(conn.clone(), "test:lock:ttl_expiry", Duration::from_secs(1));
1031
1032 let guard = lock.try_acquire().await.expect("Redis error");
1034 assert!(guard.is_some(), "Should acquire lock");
1035
1036 drop(guard);
1038
1039 tokio::time::sleep(Duration::from_secs(2)).await;
1041
1042 let lock2 = DistributedLock::new(conn, "test:lock:ttl_expiry", Duration::from_secs(60));
1044 let guard2 = lock2.try_acquire().await.expect("Redis error");
1045 assert!(guard2.is_some(), "Should acquire lock after TTL expiry");
1046
1047 if let Some(g) = guard2 {
1049 g.release().await.expect("Redis error");
1050 }
1051 }
1052
1053 #[tokio::test]
1054 #[ignore] async fn test_distributed_lock_release_only_own_lock() {
1056 let conn = create_test_redis_connection()
1057 .await
1058 .expect("Redis connection required for this test");
1059
1060 let lock = DistributedLock::new(
1062 conn.clone(),
1063 "test:lock:release_own",
1064 Duration::from_secs(1),
1065 );
1066
1067 let guard = lock.try_acquire().await.expect("Redis error");
1069 assert!(guard.is_some(), "Should acquire lock");
1070
1071 tokio::time::sleep(Duration::from_secs(2)).await;
1073
1074 let released = guard.unwrap().release().await.expect("Redis error");
1076 assert!(!released, "Should not release expired lock");
1077 }
1078
1079 #[tokio::test]
1080 #[ignore] async fn test_distributed_lock_with_prefix() {
1082 let conn = create_test_redis_connection()
1083 .await
1084 .expect("Redis connection required for this test");
1085
1086 let prefix = "test_prefix";
1089 let lock_name = "cleanup";
1090 let lock_key = format!("{prefix}:lock:{lock_name}");
1091
1092 let lock = DistributedLock::new(conn, &lock_key, Duration::from_secs(60));
1093
1094 let guard = lock.try_acquire().await.expect("Redis error");
1095 assert!(guard.is_some(), "Should acquire prefixed lock");
1096
1097 guard.unwrap().release().await.expect("Redis error");
1099 }
1100
1101 #[tokio::test]
1102 #[ignore] async fn test_distributed_lock_drop_releases_lock() {
1104 let conn = create_test_redis_connection()
1105 .await
1106 .expect("Redis connection required for this test");
1107
1108 let lock_key = "test:lock:drop_release";
1109
1110 {
1112 let lock = DistributedLock::new(conn.clone(), lock_key, Duration::from_secs(60));
1113 let guard = lock.try_acquire().await.expect("Redis error");
1114 assert!(guard.is_some(), "Should acquire lock");
1115
1116 }
1118
1119 tokio::time::sleep(Duration::from_millis(100)).await;
1121
1122 let lock2 = DistributedLock::new(conn, lock_key, Duration::from_secs(60));
1124 let guard2 = lock2.try_acquire().await.expect("Redis error");
1125 assert!(
1126 guard2.is_some(),
1127 "Should acquire lock after previous guard was dropped"
1128 );
1129
1130 if let Some(g) = guard2 {
1132 g.release().await.expect("Redis error");
1133 }
1134 }
1135
1136 #[tokio::test]
1137 #[ignore] async fn test_distributed_lock_explicit_release_prevents_double_release() {
1139 let conn = create_test_redis_connection()
1140 .await
1141 .expect("Redis connection required for this test");
1142
1143 let lock_key = "test:lock:no_double_release";
1144 let lock = DistributedLock::new(conn.clone(), lock_key, Duration::from_secs(60));
1145
1146 let guard = lock.try_acquire().await.expect("Redis error");
1147 assert!(guard.is_some(), "Should acquire lock");
1148
1149 let released = guard.unwrap().release().await.expect("Redis error");
1151 assert!(released, "Should release successfully");
1152
1153 tokio::time::sleep(Duration::from_millis(50)).await;
1156
1157 let lock2 = DistributedLock::new(conn, lock_key, Duration::from_secs(60));
1158 let guard2 = lock2.try_acquire().await.expect("Redis error");
1159 assert!(
1160 guard2.is_some(),
1161 "Should acquire lock - no double-release issue"
1162 );
1163
1164 if let Some(g) = guard2 {
1166 g.release().await.expect("Redis error");
1167 }
1168 }
1169
1170 #[tokio::test]
1171 #[ignore] async fn test_distributed_lock_drop_on_early_return() {
1173 let conn = create_test_redis_connection()
1174 .await
1175 .expect("Redis connection required for this test");
1176
1177 let lock_key = "test:lock:early_return";
1178
1179 async fn simulated_work_with_early_return(
1181 conn: Arc<Pool>,
1182 lock_key: &str,
1183 ) -> Result<(), &'static str> {
1184 let lock = DistributedLock::new(conn, lock_key, Duration::from_secs(60));
1185 let _guard = lock
1186 .try_acquire()
1187 .await
1188 .map_err(|_| "lock error")?
1189 .ok_or("lock held")?;
1190
1191 Err("simulated error")
1193
1194 }
1196
1197 let result = simulated_work_with_early_return(conn.clone(), lock_key).await;
1199 assert!(result.is_err(), "Should have returned early with error");
1200
1201 tokio::time::sleep(Duration::from_millis(100)).await;
1203
1204 let lock2 = DistributedLock::new(conn, lock_key, Duration::from_secs(60));
1206 let guard2 = lock2.try_acquire().await.expect("Redis error");
1207 assert!(
1208 guard2.is_some(),
1209 "Should acquire lock after early return released it"
1210 );
1211
1212 if let Some(g) = guard2 {
1214 g.release().await.expect("Redis error");
1215 }
1216 }
1217
1218 #[tokio::test]
1223 #[ignore] async fn test_redis_connections_single_pool_operations() {
1225 let pool = create_test_redis_connection()
1226 .await
1227 .expect("Redis connection required for this test");
1228
1229 let connections = RedisConnections::new_single_pool(pool);
1230
1231 let mut primary_conn = connections
1233 .primary()
1234 .get()
1235 .await
1236 .expect("Failed to get primary connection");
1237 let mut reader_conn = connections
1238 .reader()
1239 .get()
1240 .await
1241 .expect("Failed to get reader connection");
1242
1243 let _: () = redis::cmd("SET")
1245 .arg("test:connections:key")
1246 .arg("test_value")
1247 .query_async(&mut primary_conn)
1248 .await
1249 .expect("Failed to SET");
1250
1251 let value: String = redis::cmd("GET")
1252 .arg("test:connections:key")
1253 .query_async(&mut reader_conn)
1254 .await
1255 .expect("Failed to GET");
1256
1257 assert_eq!(value, "test_value");
1258
1259 let _: () = redis::cmd("DEL")
1261 .arg("test:connections:key")
1262 .query_async(&mut primary_conn)
1263 .await
1264 .expect("Failed to DEL");
1265 }
1266
1267 #[tokio::test]
1268 #[ignore] async fn test_redis_connections_backward_compatible() {
1270 let pool = create_test_redis_connection()
1272 .await
1273 .expect("Redis connection required for this test");
1274
1275 let connections = Arc::new(RedisConnections::new_single_pool(pool));
1276
1277 let conn1 = connections.clone();
1279 let conn2 = connections.clone();
1280
1281 let _primary1 = conn1.primary().get().await.expect("Failed to get primary");
1283 let _reader1 = conn1.reader().get().await.expect("Failed to get reader");
1284 let _primary2 = conn2.primary().get().await.expect("Failed to get primary");
1285 let _reader2 = conn2.reader().get().await.expect("Failed to get reader");
1286
1287 }
1289 #[tokio::test]
1294 #[ignore] async fn test_set_and_get_relayer_last_sync() {
1296 let conn = create_test_redis_connection()
1297 .await
1298 .expect("Redis connection required for this test");
1299
1300 let prefix = "test_sync";
1301 let relayer_id = "test-relayer-sync";
1302
1303 set_relayer_last_sync(&conn, prefix, relayer_id)
1305 .await
1306 .expect("Should set last sync time");
1307
1308 let last_sync = get_relayer_last_sync(&conn, prefix, relayer_id)
1310 .await
1311 .expect("Should get last sync time");
1312
1313 assert!(last_sync.is_some(), "Should have a last sync time");
1314
1315 let elapsed = chrono::Utc::now()
1317 .signed_duration_since(last_sync.unwrap())
1318 .num_seconds();
1319 assert!(elapsed < 60, "Last sync should be within last minute");
1320
1321 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1323 let hash_key = format!("{prefix}:relayer_sync_meta");
1324 let _: () = redis::AsyncCommands::del(&mut conn_clone, &hash_key)
1325 .await
1326 .expect("Cleanup failed");
1327 }
1328
1329 #[tokio::test]
1330 #[ignore] async fn test_get_relayer_last_sync_returns_none_when_not_set() {
1332 let conn = create_test_redis_connection()
1333 .await
1334 .expect("Redis connection required for this test");
1335
1336 let prefix = "test_sync_none";
1337 let relayer_id = "nonexistent-relayer";
1338
1339 let last_sync = get_relayer_last_sync(&conn, prefix, relayer_id)
1341 .await
1342 .expect("Should not error");
1343
1344 assert!(
1345 last_sync.is_none(),
1346 "Should return None for unsynced relayer"
1347 );
1348 }
1349
1350 #[tokio::test]
1351 #[ignore] async fn test_is_relayer_recently_synced_returns_true_for_recent_sync() {
1353 let conn = create_test_redis_connection()
1354 .await
1355 .expect("Redis connection required for this test");
1356
1357 let prefix = "test_recent_sync";
1358 let relayer_id = "recent-relayer";
1359
1360 set_relayer_last_sync(&conn, prefix, relayer_id)
1362 .await
1363 .expect("Should set last sync time");
1364
1365 let is_recent = is_relayer_recently_synced(&conn, prefix, relayer_id, 300)
1367 .await
1368 .expect("Should check recent sync");
1369
1370 assert!(is_recent, "Should be recently synced");
1371
1372 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1374 let hash_key = format!("{prefix}:relayer_sync_meta");
1375 let _: () = redis::AsyncCommands::del(&mut conn_clone, &hash_key)
1376 .await
1377 .expect("Cleanup failed");
1378 }
1379
1380 #[tokio::test]
1381 #[ignore] async fn test_is_relayer_recently_synced_returns_false_when_not_set() {
1383 let conn = create_test_redis_connection()
1384 .await
1385 .expect("Redis connection required for this test");
1386
1387 let prefix = "test_not_synced";
1388 let relayer_id = "never-synced-relayer";
1389
1390 let is_recent = is_relayer_recently_synced(&conn, prefix, relayer_id, 300)
1392 .await
1393 .expect("Should check recent sync");
1394
1395 assert!(!is_recent, "Should not be recently synced");
1396 }
1397
1398 #[tokio::test]
1399 #[ignore] async fn test_is_relayer_recently_synced_returns_false_for_stale_sync() {
1401 let conn = create_test_redis_connection()
1402 .await
1403 .expect("Redis connection required for this test");
1404
1405 let prefix = "test_stale_sync";
1406 let relayer_id = "stale-relayer";
1407
1408 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1410 let hash_key = format!("{prefix}:relayer_sync_meta");
1411 let field = format!("{relayer_id}:last_sync");
1412 let old_timestamp = chrono::Utc::now().timestamp() - 600; redis::AsyncCommands::hset::<_, _, _, ()>(
1415 &mut conn_clone,
1416 &hash_key,
1417 &field,
1418 old_timestamp,
1419 )
1420 .await
1421 .expect("Should set old timestamp");
1422
1423 let is_recent = is_relayer_recently_synced(&conn, prefix, relayer_id, 300)
1425 .await
1426 .expect("Should check recent sync");
1427
1428 assert!(!is_recent, "Should not be recently synced (stale)");
1429
1430 let _: () = redis::AsyncCommands::del(&mut conn_clone, &hash_key)
1432 .await
1433 .expect("Cleanup failed");
1434 }
1435
1436 #[tokio::test]
1437 #[ignore] async fn test_get_relayer_last_sync_multiple_relayers() {
1439 let conn = create_test_redis_connection()
1440 .await
1441 .expect("Redis connection required for this test");
1442
1443 let prefix = "test_multi_sync";
1444
1445 set_relayer_last_sync(&conn, prefix, "relayer-1")
1447 .await
1448 .expect("Should set sync time");
1449 tokio::time::sleep(Duration::from_millis(10)).await;
1450 set_relayer_last_sync(&conn, prefix, "relayer-2")
1451 .await
1452 .expect("Should set sync time");
1453
1454 let sync1 = get_relayer_last_sync(&conn, prefix, "relayer-1")
1455 .await
1456 .expect("Should get sync time");
1457 let sync2 = get_relayer_last_sync(&conn, prefix, "relayer-2")
1458 .await
1459 .expect("Should get sync time");
1460
1461 assert!(sync1.is_some(), "Relayer 1 should have sync time");
1462 assert!(sync2.is_some(), "Relayer 2 should have sync time");
1463 assert!(
1464 sync2.unwrap() >= sync1.unwrap(),
1465 "Relayer 2 should be synced at same time or after relayer 1"
1466 );
1467
1468 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1470 let hash_key = format!("{prefix}:relayer_sync_meta");
1471 let _: () = redis::AsyncCommands::del(&mut conn_clone, &hash_key)
1472 .await
1473 .expect("Cleanup failed");
1474 }
1475
1476 #[tokio::test]
1477 #[ignore] async fn test_get_relayer_last_sync_update_existing() {
1479 let conn = create_test_redis_connection()
1480 .await
1481 .expect("Redis connection required for this test");
1482
1483 let prefix = "test_update_sync";
1484 let relayer_id = "update-relayer";
1485
1486 set_relayer_last_sync(&conn, prefix, relayer_id)
1488 .await
1489 .expect("Should set sync time");
1490
1491 let first_sync = get_relayer_last_sync(&conn, prefix, relayer_id)
1492 .await
1493 .expect("Should get sync time")
1494 .expect("Should have sync time");
1495
1496 tokio::time::sleep(Duration::from_millis(100)).await;
1498
1499 set_relayer_last_sync(&conn, prefix, relayer_id)
1500 .await
1501 .expect("Should update sync time");
1502
1503 let second_sync = get_relayer_last_sync(&conn, prefix, relayer_id)
1504 .await
1505 .expect("Should get sync time")
1506 .expect("Should have sync time");
1507
1508 assert!(
1509 second_sync >= first_sync,
1510 "Updated sync time should be at the same time or later than first"
1511 );
1512
1513 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1515 let hash_key = format!("{prefix}:relayer_sync_meta");
1516 let _: () = redis::AsyncCommands::del(&mut conn_clone, &hash_key)
1517 .await
1518 .expect("Cleanup failed");
1519 }
1520
1521 #[tokio::test]
1522 #[ignore] async fn test_is_relayer_recently_synced_threshold_boundary() {
1524 let conn = create_test_redis_connection()
1525 .await
1526 .expect("Redis connection required for this test");
1527
1528 let prefix = "test_boundary_sync";
1529 let relayer_id = "boundary-relayer";
1530
1531 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1533 let hash_key = format!("{prefix}:relayer_sync_meta");
1534 let field = format!("{relayer_id}:last_sync");
1535 let threshold_secs: u64 = 60;
1536 let boundary_timestamp = chrono::Utc::now().timestamp() - (threshold_secs as i64);
1537
1538 redis::AsyncCommands::hset::<_, _, _, ()>(
1539 &mut conn_clone,
1540 &hash_key,
1541 &field,
1542 boundary_timestamp,
1543 )
1544 .await
1545 .expect("Should set boundary timestamp");
1546
1547 let is_recent = is_relayer_recently_synced(&conn, prefix, relayer_id, threshold_secs)
1548 .await
1549 .expect("Should check recent sync");
1550
1551 assert!(!is_recent, "Should not be recent at exact threshold");
1552
1553 let _: () = redis::AsyncCommands::del(&mut conn_clone, &hash_key)
1555 .await
1556 .expect("Cleanup failed");
1557 }
1558
1559 #[tokio::test]
1560 #[ignore] async fn test_is_relayer_recently_synced_just_before_threshold() {
1562 let conn = create_test_redis_connection()
1563 .await
1564 .expect("Redis connection required for this test");
1565
1566 let prefix = "test_just_before_sync";
1567 let relayer_id = "just-before-relayer";
1568
1569 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1571 let hash_key = format!("{prefix}:relayer_sync_meta");
1572 let field = format!("{relayer_id}:last_sync");
1573 let threshold_secs: u64 = 60;
1574 let just_before_timestamp =
1575 chrono::Utc::now().timestamp() - (threshold_secs as i64) + 5;
1576
1577 redis::AsyncCommands::hset::<_, _, _, ()>(
1578 &mut conn_clone,
1579 &hash_key,
1580 &field,
1581 just_before_timestamp,
1582 )
1583 .await
1584 .expect("Should set just-before timestamp");
1585
1586 let is_recent = is_relayer_recently_synced(&conn, prefix, relayer_id, threshold_secs)
1587 .await
1588 .expect("Should check recent sync");
1589
1590 assert!(is_recent, "Should be recent just before threshold");
1591
1592 let _: () = redis::AsyncCommands::del(&mut conn_clone, &hash_key)
1594 .await
1595 .expect("Cleanup failed");
1596 }
1597
1598 #[tokio::test]
1599 #[ignore] async fn test_is_relayer_recently_synced_different_prefixes() {
1601 let conn = create_test_redis_connection()
1602 .await
1603 .expect("Redis connection required for this test");
1604
1605 let relayer_id = "shared-relayer";
1606
1607 set_relayer_last_sync(&conn, "prefix1", relayer_id)
1609 .await
1610 .expect("Should set sync time");
1611
1612 let is_recent_prefix1 = is_relayer_recently_synced(&conn, "prefix1", relayer_id, 300)
1613 .await
1614 .expect("Should check sync");
1615 let is_recent_prefix2 = is_relayer_recently_synced(&conn, "prefix2", relayer_id, 300)
1616 .await
1617 .expect("Should check sync");
1618
1619 assert!(is_recent_prefix1, "Should be recent for prefix1");
1620 assert!(!is_recent_prefix2, "Should not be recent for prefix2");
1621
1622 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1624 let hash_key = "prefix1:relayer_sync_meta";
1625 let _: () = redis::AsyncCommands::del(&mut conn_clone, hash_key)
1626 .await
1627 .expect("Cleanup failed");
1628 }
1629
1630 #[tokio::test]
1631 #[ignore] async fn test_is_relayer_recently_synced_zero_threshold() {
1633 let conn = create_test_redis_connection()
1634 .await
1635 .expect("Redis connection required for this test");
1636
1637 let prefix = "test_zero_threshold";
1638 let relayer_id = "zero-threshold-relayer";
1639
1640 set_relayer_last_sync(&conn, prefix, relayer_id)
1641 .await
1642 .expect("Should set sync time");
1643
1644 let is_recent = is_relayer_recently_synced(&conn, prefix, relayer_id, 0)
1646 .await
1647 .expect("Should check sync");
1648
1649 assert!(!is_recent, "Should not be recent with zero threshold");
1650
1651 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1653 let hash_key = format!("{prefix}:relayer_sync_meta");
1654 let _: () = redis::AsyncCommands::del(&mut conn_clone, &hash_key)
1655 .await
1656 .expect("Cleanup failed");
1657 }
1658
1659 #[tokio::test]
1664 #[ignore] async fn test_set_and_check_global_init_completed() {
1666 let conn = create_test_redis_connection()
1667 .await
1668 .expect("Redis connection required for this test");
1669
1670 let prefix = "test_global_init";
1671
1672 set_global_init_completed(&conn, prefix)
1674 .await
1675 .expect("Should set global init completed");
1676
1677 let is_recent = is_global_init_recently_completed(&conn, prefix, 300)
1679 .await
1680 .expect("Should check global init");
1681
1682 assert!(is_recent, "Should be recently completed");
1683
1684 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1686 let hash_key = format!("{prefix}:relayer_sync_meta");
1687 let _: () = redis::AsyncCommands::del(&mut conn_clone, &hash_key)
1688 .await
1689 .expect("Cleanup failed");
1690 }
1691
1692 #[tokio::test]
1693 #[ignore] async fn test_is_global_init_recently_completed_returns_false_when_not_set() {
1695 let conn = create_test_redis_connection()
1696 .await
1697 .expect("Redis connection required for this test");
1698
1699 let prefix = "test_global_init_not_set";
1700
1701 let is_recent = is_global_init_recently_completed(&conn, prefix, 300)
1703 .await
1704 .expect("Should check global init");
1705
1706 assert!(!is_recent, "Should not be recently completed when not set");
1707 }
1708
1709 #[tokio::test]
1710 #[ignore] async fn test_is_global_init_recently_completed_returns_false_when_stale() {
1712 let conn = create_test_redis_connection()
1713 .await
1714 .expect("Redis connection required for this test");
1715
1716 let prefix = "test_global_init_stale";
1717
1718 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1720 let hash_key = format!("{prefix}:relayer_sync_meta");
1721 let old_timestamp = chrono::Utc::now().timestamp() - 600; redis::AsyncCommands::hset::<_, _, _, ()>(
1724 &mut conn_clone,
1725 &hash_key,
1726 "global:init_completed",
1727 old_timestamp,
1728 )
1729 .await
1730 .expect("Should set old timestamp");
1731
1732 let is_recent = is_global_init_recently_completed(&conn, prefix, 300)
1734 .await
1735 .expect("Should check global init");
1736
1737 assert!(!is_recent, "Should not be recent when stale");
1738
1739 let _: () = redis::AsyncCommands::del(&mut conn_clone, &hash_key)
1741 .await
1742 .expect("Cleanup failed");
1743 }
1744
1745 #[tokio::test]
1746 #[ignore] async fn test_global_init_different_prefixes() {
1748 let conn = create_test_redis_connection()
1749 .await
1750 .expect("Redis connection required for this test");
1751
1752 set_global_init_completed(&conn, "global_prefix1")
1754 .await
1755 .expect("Should set global init");
1756
1757 let is_recent_prefix1 = is_global_init_recently_completed(&conn, "global_prefix1", 300)
1758 .await
1759 .expect("Should check global init");
1760 let is_recent_prefix2 = is_global_init_recently_completed(&conn, "global_prefix2", 300)
1761 .await
1762 .expect("Should check global init");
1763
1764 assert!(is_recent_prefix1, "Should be recent for prefix1");
1765 assert!(!is_recent_prefix2, "Should not be recent for prefix2");
1766
1767 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1769 let hash_key = "global_prefix1:relayer_sync_meta";
1770 let _: () = redis::AsyncCommands::del(&mut conn_clone, hash_key)
1771 .await
1772 .expect("Cleanup failed");
1773 }
1774
1775 #[tokio::test]
1776 #[ignore] async fn test_global_init_update_existing() {
1778 let conn = create_test_redis_connection()
1779 .await
1780 .expect("Redis connection required for this test");
1781
1782 let prefix = "test_global_init_update";
1783
1784 set_global_init_completed(&conn, prefix)
1786 .await
1787 .expect("Should set global init");
1788
1789 tokio::time::sleep(Duration::from_millis(100)).await;
1791
1792 set_global_init_completed(&conn, prefix)
1794 .await
1795 .expect("Should update global init");
1796
1797 let is_recent = is_global_init_recently_completed(&conn, prefix, 300)
1799 .await
1800 .expect("Should check global init");
1801
1802 assert!(is_recent, "Should be recent after update");
1803
1804 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1806 let hash_key = format!("{prefix}:relayer_sync_meta");
1807 let _: () = redis::AsyncCommands::del(&mut conn_clone, &hash_key)
1808 .await
1809 .expect("Cleanup failed");
1810 }
1811
1812 #[tokio::test]
1813 #[ignore] async fn test_global_init_coexists_with_relayer_sync() {
1815 let conn = create_test_redis_connection()
1816 .await
1817 .expect("Redis connection required for this test");
1818
1819 let prefix = "test_coexist";
1820 let relayer_id = "test-relayer";
1821
1822 set_global_init_completed(&conn, prefix)
1824 .await
1825 .expect("Should set global init");
1826 set_relayer_last_sync(&conn, prefix, relayer_id)
1827 .await
1828 .expect("Should set relayer sync");
1829
1830 let global_recent = is_global_init_recently_completed(&conn, prefix, 300)
1832 .await
1833 .expect("Should check global init");
1834 let relayer_recent = is_relayer_recently_synced(&conn, prefix, relayer_id, 300)
1835 .await
1836 .expect("Should check relayer sync");
1837
1838 assert!(global_recent, "Global init should be recent");
1839 assert!(relayer_recent, "Relayer sync should be recent");
1840
1841 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1843 let hash_key = format!("{prefix}:relayer_sync_meta");
1844 let _: () = redis::AsyncCommands::del(&mut conn_clone, &hash_key)
1845 .await
1846 .expect("Cleanup failed");
1847 }
1848
1849 #[tokio::test]
1854 #[ignore] async fn test_config_processing_in_progress_is_not_completed() {
1856 let conn = create_test_redis_connection()
1857 .await
1858 .expect("Redis connection required for this test");
1859
1860 let prefix = "test_config_processing_in_progress";
1861
1862 set_config_processing_in_progress(&conn, prefix)
1863 .await
1864 .expect("Should set in-progress marker");
1865
1866 let completed = is_config_processing_completed(&conn, prefix)
1867 .await
1868 .expect("Should read config processing status");
1869
1870 assert!(
1871 !completed,
1872 "In-progress config processing must not be treated as completed"
1873 );
1874
1875 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1876 let hash_key = format!("{prefix}:bootstrap_meta");
1877 let _: () = redis::AsyncCommands::del(&mut conn_clone, &hash_key)
1878 .await
1879 .expect("Cleanup failed");
1880 }
1881
1882 #[tokio::test]
1883 #[ignore] async fn test_config_processing_completed_requires_explicit_completion_marker() {
1885 let conn = create_test_redis_connection()
1886 .await
1887 .expect("Redis connection required for this test");
1888
1889 let prefix = "test_config_processing_completed";
1890
1891 set_config_processing_in_progress(&conn, prefix)
1892 .await
1893 .expect("Should set in-progress marker");
1894 set_config_processing_completed(&conn, prefix)
1895 .await
1896 .expect("Should set completed marker");
1897
1898 let completed = is_config_processing_completed(&conn, prefix)
1899 .await
1900 .expect("Should read config processing status");
1901
1902 assert!(
1903 completed,
1904 "Completed config processing must require the explicit completion marker"
1905 );
1906
1907 let mut conn_clone = conn.get().await.expect("Failed to get connection");
1908 let hash_key = format!("{prefix}:bootstrap_meta");
1909 let _: () = redis::AsyncCommands::del(&mut conn_clone, &hash_key)
1910 .await
1911 .expect("Cleanup failed");
1912 }
1913 }
1914}