batata_client/cache/
file_cache.rs

1use std::fs;
2use std::io::{Read, Write};
3use std::path::{Path, PathBuf};
4
5use tracing::{debug, warn};
6
7use crate::api::config::ConfigInfo;
8use crate::api::naming::Service;
9use crate::common::{build_config_key, build_service_key, md5_hash};
10
11/// File-based cache for failover
12pub struct FileCache {
13    cache_dir: PathBuf,
14}
15
16impl FileCache {
17    /// Create a new file cache
18    pub fn new(cache_dir: impl AsRef<Path>) -> std::io::Result<Self> {
19        let cache_dir = cache_dir.as_ref().to_path_buf();
20        fs::create_dir_all(&cache_dir)?;
21        fs::create_dir_all(cache_dir.join("config"))?;
22        fs::create_dir_all(cache_dir.join("naming"))?;
23        Ok(Self { cache_dir })
24    }
25
26    /// Get the config cache directory
27    fn config_dir(&self) -> PathBuf {
28        self.cache_dir.join("config")
29    }
30
31    /// Get the naming cache directory
32    fn naming_dir(&self) -> PathBuf {
33        self.cache_dir.join("naming")
34    }
35
36    /// Encode a key for use as a filename
37    fn encode_key(key: &str) -> String {
38        // Use MD5 hash to avoid issues with special characters
39        md5_hash(key)
40    }
41
42    // ==================== Config Cache ====================
43
44    /// Save configuration to file cache
45    pub fn save_config(&self, config: &ConfigInfo) -> std::io::Result<()> {
46        let key = build_config_key(&config.data_id, &config.group, &config.tenant);
47        let filename = Self::encode_key(&key);
48        let path = self.config_dir().join(&filename);
49
50        let json = serde_json::to_string(&ConfigCacheEntry {
51            data_id: config.data_id.clone(),
52            group: config.group.clone(),
53            tenant: config.tenant.clone(),
54            content: config.content.clone(),
55            md5: config.md5.clone(),
56            last_modified: config.last_modified,
57            content_type: config.content_type.clone(),
58        })
59        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
60
61        let mut file = fs::File::create(&path)?;
62        file.write_all(json.as_bytes())?;
63        debug!("Saved config to cache: {}", key);
64        Ok(())
65    }
66
67    /// Load configuration from file cache
68    pub fn load_config(&self, data_id: &str, group: &str, tenant: &str) -> Option<ConfigInfo> {
69        let key = build_config_key(data_id, group, tenant);
70        let filename = Self::encode_key(&key);
71        let path = self.config_dir().join(&filename);
72
73        if !path.exists() {
74            return None;
75        }
76
77        match fs::File::open(&path) {
78            Ok(mut file) => {
79                let mut contents = String::new();
80                if file.read_to_string(&mut contents).is_err() {
81                    return None;
82                }
83
84                match serde_json::from_str::<ConfigCacheEntry>(&contents) {
85                    Ok(entry) => {
86                        debug!("Loaded config from cache: {}", key);
87                        Some(ConfigInfo {
88                            data_id: entry.data_id,
89                            group: entry.group,
90                            tenant: entry.tenant,
91                            content: entry.content,
92                            md5: entry.md5,
93                            last_modified: entry.last_modified,
94                            content_type: entry.content_type,
95                        })
96                    }
97                    Err(e) => {
98                        warn!("Failed to parse cached config {}: {}", key, e);
99                        None
100                    }
101                }
102            }
103            Err(e) => {
104                warn!("Failed to read cached config {}: {}", key, e);
105                None
106            }
107        }
108    }
109
110    /// Remove configuration from file cache
111    pub fn remove_config(&self, data_id: &str, group: &str, tenant: &str) -> std::io::Result<()> {
112        let key = build_config_key(data_id, group, tenant);
113        let filename = Self::encode_key(&key);
114        let path = self.config_dir().join(&filename);
115
116        if path.exists() {
117            fs::remove_file(&path)?;
118            debug!("Removed config from cache: {}", key);
119        }
120        Ok(())
121    }
122
123    // ==================== Naming Cache ====================
124
125    /// Save service info to file cache
126    pub fn save_service(&self, namespace: &str, service: &Service) -> std::io::Result<()> {
127        let key = build_service_key(&service.name, &service.group_name, namespace);
128        let filename = Self::encode_key(&key);
129        let path = self.naming_dir().join(&filename);
130
131        let json = serde_json::to_string(service)
132            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
133
134        let mut file = fs::File::create(&path)?;
135        file.write_all(json.as_bytes())?;
136        debug!("Saved service to cache: {}", key);
137        Ok(())
138    }
139
140    /// Load service info from file cache
141    pub fn load_service(
142        &self,
143        namespace: &str,
144        group_name: &str,
145        service_name: &str,
146    ) -> Option<Service> {
147        let key = build_service_key(service_name, group_name, namespace);
148        let filename = Self::encode_key(&key);
149        let path = self.naming_dir().join(&filename);
150
151        if !path.exists() {
152            return None;
153        }
154
155        match fs::File::open(&path) {
156            Ok(mut file) => {
157                let mut contents = String::new();
158                if file.read_to_string(&mut contents).is_err() {
159                    return None;
160                }
161
162                match serde_json::from_str::<Service>(&contents) {
163                    Ok(service) => {
164                        debug!("Loaded service from cache: {}", key);
165                        Some(service)
166                    }
167                    Err(e) => {
168                        warn!("Failed to parse cached service {}: {}", key, e);
169                        None
170                    }
171                }
172            }
173            Err(e) => {
174                warn!("Failed to read cached service {}: {}", key, e);
175                None
176            }
177        }
178    }
179
180    /// Remove service info from file cache
181    pub fn remove_service(
182        &self,
183        namespace: &str,
184        group_name: &str,
185        service_name: &str,
186    ) -> std::io::Result<()> {
187        let key = build_service_key(service_name, group_name, namespace);
188        let filename = Self::encode_key(&key);
189        let path = self.naming_dir().join(&filename);
190
191        if path.exists() {
192            fs::remove_file(&path)?;
193            debug!("Removed service from cache: {}", key);
194        }
195        Ok(())
196    }
197
198    /// Clear all cached data
199    pub fn clear(&self) -> std::io::Result<()> {
200        // Clear config cache
201        for entry in fs::read_dir(self.config_dir())?.flatten() {
202            fs::remove_file(entry.path())?;
203        }
204
205        // Clear naming cache
206        for entry in fs::read_dir(self.naming_dir())?.flatten() {
207            fs::remove_file(entry.path())?;
208        }
209
210        debug!("Cleared file cache");
211        Ok(())
212    }
213}
214
215/// Cache entry for configuration
216#[derive(serde::Serialize, serde::Deserialize)]
217struct ConfigCacheEntry {
218    data_id: String,
219    group: String,
220    tenant: String,
221    content: String,
222    md5: String,
223    last_modified: i64,
224    content_type: String,
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use tempfile::tempdir;
231
232    #[test]
233    fn test_config_cache() {
234        let dir = tempdir().unwrap();
235        let cache = FileCache::new(dir.path()).unwrap();
236
237        let mut config = ConfigInfo::new("test-data-id", "test-group", "test-tenant");
238        config.content = "test content".to_string();
239        config.md5 = md5_hash(&config.content);
240
241        // Save
242        cache.save_config(&config).unwrap();
243
244        // Load
245        let loaded = cache
246            .load_config("test-data-id", "test-group", "test-tenant")
247            .unwrap();
248        assert_eq!(loaded.content, "test content");
249
250        // Remove
251        cache
252            .remove_config("test-data-id", "test-group", "test-tenant")
253            .unwrap();
254        assert!(cache
255            .load_config("test-data-id", "test-group", "test-tenant")
256            .is_none());
257    }
258
259    #[test]
260    fn test_service_cache() {
261        let dir = tempdir().unwrap();
262        let cache = FileCache::new(dir.path()).unwrap();
263
264        let service = Service::new("test-service", "test-group");
265
266        // Save
267        cache.save_service("test-namespace", &service).unwrap();
268
269        // Load
270        let loaded = cache
271            .load_service("test-namespace", "test-group", "test-service")
272            .unwrap();
273        assert_eq!(loaded.name, "test-service");
274
275        // Remove
276        cache
277            .remove_service("test-namespace", "test-group", "test-service")
278            .unwrap();
279        assert!(cache
280            .load_service("test-namespace", "test-group", "test-service")
281            .is_none());
282    }
283}