Skip to main content

pot_o_validator/
peer_store.rs

1use crate::internal_api::PeerInfo;
2use std::sync::Arc;
3use tokio::sync::RwLock;
4
5pub struct PeerStore {
6    peers: Arc<RwLock<Vec<PeerInfo>>>,
7    path: String,
8}
9
10impl PeerStore {
11    pub fn new(path: String, peers: Arc<RwLock<Vec<PeerInfo>>>) -> Self {
12        Self { peers, path }
13    }
14
15    pub fn load(&self) {
16        if let Ok(content) = std::fs::read_to_string(&self.path) {
17            if let Ok(peers) = serde_json::from_str::<Vec<PeerInfo>>(&content) {
18                let mut current = self.peers.blocking_write();
19                *current = peers;
20            }
21        }
22    }
23
24    pub async fn save(&self) -> Result<(), String> {
25        let peers = self.peers.read().await.clone();
26        let json = serde_json::to_string_pretty(&peers).map_err(|e| e.to_string())?;
27        let tmp = format!("{}.tmp", self.path);
28        std::fs::write(&tmp, &json).map_err(|e| e.to_string())?;
29        std::fs::rename(&tmp, &self.path).map_err(|e| e.to_string())?;
30        Ok(())
31    }
32
33    pub fn spawn_persist(&self) {
34        let peers = self.peers.clone();
35        let path = self.path.clone();
36        tokio::spawn(async move {
37            let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(60));
38            loop {
39                interval.tick().await;
40                let current = peers.read().await.clone();
41                let json = match serde_json::to_string_pretty(&current) {
42                    Ok(j) => j,
43                    Err(_) => continue,
44                };
45                let tmp = format!("{}.tmp", path);
46                let _ = std::fs::write(&tmp, &json);
47                let _ = std::fs::rename(&tmp, &path);
48            }
49        });
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use chrono::Utc;
57
58    #[test]
59    fn test_peer_store_save_and_load() {
60        let dir = tempfile::tempdir().unwrap();
61        let path = dir.path().join("peers.json");
62        let peers = Arc::new(RwLock::new(vec![]));
63
64        let store = PeerStore::new(path.to_str().unwrap().to_string(), peers.clone());
65
66        peers.blocking_write().push(PeerInfo {
67            node_id: "test".to_string(),
68            url: "http://localhost:8900".to_string(),
69            last_seen: Utc::now(),
70        });
71
72        let rt = tokio::runtime::Runtime::new().unwrap();
73        rt.block_on(store.save()).unwrap();
74
75        let peers2 = Arc::new(RwLock::new(vec![]));
76        let store2 = PeerStore::new(path.to_str().unwrap().to_string(), peers2.clone());
77        store2.load();
78
79        assert_eq!(peers2.blocking_read().len(), 1);
80        assert_eq!(peers2.blocking_read()[0].node_id, "test");
81    }
82
83    #[test]
84    fn test_peer_store_load_missing_file() {
85        let dir = tempfile::tempdir().unwrap();
86        let path = dir.path().join("nonexistent.json");
87        let peers = Arc::new(RwLock::new(vec![]));
88
89        let store = PeerStore::new(path.to_str().unwrap().to_string(), peers.clone());
90        store.load();
91
92        assert_eq!(peers.blocking_read().len(), 0);
93    }
94
95    #[test]
96    fn test_peer_store_load_invalid_json() {
97        let dir = tempfile::tempdir().unwrap();
98        let path = dir.path().join("peers.json");
99        std::fs::write(&path, "not valid json").unwrap();
100        let peers = Arc::new(RwLock::new(vec![]));
101
102        let store = PeerStore::new(path.to_str().unwrap().to_string(), peers.clone());
103        store.load();
104
105        assert_eq!(peers.blocking_read().len(), 0);
106    }
107
108    #[test]
109    fn test_peer_store_atomic_write_no_tmp_left() {
110        let dir = tempfile::tempdir().unwrap();
111        let path = dir.path().join("peers.json");
112        let peers = Arc::new(RwLock::new(vec![]));
113
114        let store = PeerStore::new(path.to_str().unwrap().to_string(), peers.clone());
115
116        peers.blocking_write().push(PeerInfo {
117            node_id: "v1".to_string(),
118            url: "http://v1:8900".to_string(),
119            last_seen: Utc::now(),
120        });
121
122        let rt = tokio::runtime::Runtime::new().unwrap();
123        rt.block_on(store.save()).unwrap();
124
125        assert!(path.exists());
126        let tmp_path = format!("{}.tmp", path.display());
127        assert!(!std::path::Path::new(&tmp_path).exists());
128    }
129}