pot_o_validator/
peer_store.rs1use 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(¤t) {
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}