firebase_rs_sdk/remote_config/
storage.rs1use std::collections::HashMap;
8use std::fmt;
9use std::fs;
10use std::path::PathBuf;
11use std::sync::{Arc, Mutex};
12
13use crate::remote_config::error::{internal_error, RemoteConfigResult};
14use serde::{Deserialize, Serialize};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
18pub enum FetchStatus {
19 NoFetchYet,
20 Success,
21 Failure,
22 Throttle,
23}
24
25impl Default for FetchStatus {
26 fn default() -> Self {
27 FetchStatus::NoFetchYet
28 }
29}
30
31impl FetchStatus {
32 pub fn as_str(&self) -> &'static str {
33 match self {
34 FetchStatus::NoFetchYet => "no-fetch-yet",
35 FetchStatus::Success => "success",
36 FetchStatus::Failure => "failure",
37 FetchStatus::Throttle => "throttle",
38 }
39 }
40}
41
42pub trait RemoteConfigStorage: Send + Sync {
47 fn get_last_fetch_status(&self) -> RemoteConfigResult<Option<FetchStatus>>;
48 fn set_last_fetch_status(&self, status: FetchStatus) -> RemoteConfigResult<()>;
49
50 fn get_last_successful_fetch_timestamp_millis(&self) -> RemoteConfigResult<Option<u64>>;
51 fn set_last_successful_fetch_timestamp_millis(&self, timestamp: u64) -> RemoteConfigResult<()>;
52
53 fn get_active_config(&self) -> RemoteConfigResult<Option<HashMap<String, String>>>;
54 fn set_active_config(&self, config: HashMap<String, String>) -> RemoteConfigResult<()>;
55
56 fn get_active_config_etag(&self) -> RemoteConfigResult<Option<String>>;
57 fn set_active_config_etag(&self, etag: Option<String>) -> RemoteConfigResult<()>;
58
59 fn get_active_config_template_version(&self) -> RemoteConfigResult<Option<u64>>;
60 fn set_active_config_template_version(
61 &self,
62 template_version: Option<u64>,
63 ) -> RemoteConfigResult<()>;
64}
65
66#[derive(Default)]
68pub struct InMemoryRemoteConfigStorage {
69 inner: Mutex<StorageRecord>,
70}
71
72#[derive(Clone, Debug, Default, Serialize, Deserialize)]
73struct StorageRecord {
74 last_fetch_status: Option<FetchStatus>,
75 last_successful_fetch_timestamp_millis: Option<u64>,
76 active_config: Option<HashMap<String, String>>,
77 active_config_etag: Option<String>,
78 active_config_template_version: Option<u64>,
79}
80
81impl RemoteConfigStorage for InMemoryRemoteConfigStorage {
82 fn get_last_fetch_status(&self) -> RemoteConfigResult<Option<FetchStatus>> {
83 Ok(self.inner.lock().unwrap().last_fetch_status)
84 }
85
86 fn set_last_fetch_status(&self, status: FetchStatus) -> RemoteConfigResult<()> {
87 self.inner.lock().unwrap().last_fetch_status = Some(status);
88 Ok(())
89 }
90
91 fn get_last_successful_fetch_timestamp_millis(&self) -> RemoteConfigResult<Option<u64>> {
92 Ok(self
93 .inner
94 .lock()
95 .unwrap()
96 .last_successful_fetch_timestamp_millis)
97 }
98
99 fn set_last_successful_fetch_timestamp_millis(&self, timestamp: u64) -> RemoteConfigResult<()> {
100 self.inner
101 .lock()
102 .unwrap()
103 .last_successful_fetch_timestamp_millis = Some(timestamp);
104 Ok(())
105 }
106
107 fn get_active_config(&self) -> RemoteConfigResult<Option<HashMap<String, String>>> {
108 Ok(self.inner.lock().unwrap().active_config.clone())
109 }
110
111 fn set_active_config(&self, config: HashMap<String, String>) -> RemoteConfigResult<()> {
112 self.inner.lock().unwrap().active_config = Some(config);
113 Ok(())
114 }
115
116 fn get_active_config_etag(&self) -> RemoteConfigResult<Option<String>> {
117 Ok(self.inner.lock().unwrap().active_config_etag.clone())
118 }
119
120 fn set_active_config_etag(&self, etag: Option<String>) -> RemoteConfigResult<()> {
121 self.inner.lock().unwrap().active_config_etag = etag;
122 Ok(())
123 }
124
125 fn get_active_config_template_version(&self) -> RemoteConfigResult<Option<u64>> {
126 Ok(self.inner.lock().unwrap().active_config_template_version)
127 }
128
129 fn set_active_config_template_version(
130 &self,
131 template_version: Option<u64>,
132 ) -> RemoteConfigResult<()> {
133 self.inner.lock().unwrap().active_config_template_version = template_version;
134 Ok(())
135 }
136}
137
138pub struct RemoteConfigStorageCache {
140 storage: Arc<dyn RemoteConfigStorage>,
141 last_fetch_status: Mutex<FetchStatus>,
142 last_successful_fetch_timestamp_millis: Mutex<Option<u64>>,
143 active_config: Mutex<HashMap<String, String>>,
144 active_config_etag: Mutex<Option<String>>,
145 active_config_template_version: Mutex<Option<u64>>,
146}
147
148pub struct FileRemoteConfigStorage {
150 path: PathBuf,
151 inner: Mutex<StorageRecord>,
152}
153
154impl fmt::Debug for RemoteConfigStorageCache {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 f.debug_struct("RemoteConfigStorageCache")
157 .field("last_fetch_status", &self.last_fetch_status())
158 .field(
159 "last_successful_fetch_timestamp_millis",
160 &self.last_successful_fetch_timestamp_millis(),
161 )
162 .field("active_config_size", &self.active_config().len())
163 .field("active_config_etag", &self.active_config_etag())
164 .finish()
165 }
166}
167
168impl RemoteConfigStorageCache {
169 pub fn new(storage: Arc<dyn RemoteConfigStorage>) -> Self {
170 let cache = Self {
171 storage,
172 last_fetch_status: Mutex::new(FetchStatus::NoFetchYet),
173 last_successful_fetch_timestamp_millis: Mutex::new(None),
174 active_config: Mutex::new(HashMap::new()),
175 active_config_etag: Mutex::new(None),
176 active_config_template_version: Mutex::new(None),
177 };
178 cache.load_from_storage();
179 cache
180 }
181
182 fn load_from_storage(&self) {
183 if let Ok(Some(status)) = self.storage.get_last_fetch_status() {
184 *self.last_fetch_status.lock().unwrap() = status;
185 }
186 if let Ok(Some(timestamp)) = self.storage.get_last_successful_fetch_timestamp_millis() {
187 *self.last_successful_fetch_timestamp_millis.lock().unwrap() = Some(timestamp);
188 }
189 if let Ok(Some(config)) = self.storage.get_active_config() {
190 *self.active_config.lock().unwrap() = config;
191 }
192 if let Ok(Some(etag)) = self.storage.get_active_config_etag() {
193 *self.active_config_etag.lock().unwrap() = Some(etag);
194 }
195 if let Ok(Some(template_version)) = self.storage.get_active_config_template_version() {
196 *self.active_config_template_version.lock().unwrap() = Some(template_version);
197 }
198 }
199
200 pub fn last_fetch_status(&self) -> FetchStatus {
201 *self.last_fetch_status.lock().unwrap()
202 }
203
204 pub fn set_last_fetch_status(&self, status: FetchStatus) -> RemoteConfigResult<()> {
205 self.storage.set_last_fetch_status(status)?;
206 *self.last_fetch_status.lock().unwrap() = status;
207 Ok(())
208 }
209
210 pub fn last_successful_fetch_timestamp_millis(&self) -> Option<u64> {
211 *self.last_successful_fetch_timestamp_millis.lock().unwrap()
212 }
213
214 pub fn set_last_successful_fetch_timestamp_millis(
215 &self,
216 timestamp: u64,
217 ) -> RemoteConfigResult<()> {
218 self.storage
219 .set_last_successful_fetch_timestamp_millis(timestamp)?;
220 *self.last_successful_fetch_timestamp_millis.lock().unwrap() = Some(timestamp);
221 Ok(())
222 }
223
224 pub fn active_config(&self) -> HashMap<String, String> {
225 self.active_config.lock().unwrap().clone()
226 }
227
228 pub fn set_active_config(&self, config: HashMap<String, String>) -> RemoteConfigResult<()> {
229 self.storage.set_active_config(config.clone())?;
230 *self.active_config.lock().unwrap() = config;
231 Ok(())
232 }
233
234 pub fn active_config_etag(&self) -> Option<String> {
235 self.active_config_etag.lock().unwrap().clone()
236 }
237
238 pub fn set_active_config_etag(&self, etag: Option<String>) -> RemoteConfigResult<()> {
239 self.storage.set_active_config_etag(etag.clone())?;
240 *self.active_config_etag.lock().unwrap() = etag;
241 Ok(())
242 }
243
244 pub fn storage(&self) -> Arc<dyn RemoteConfigStorage> {
245 Arc::clone(&self.storage)
246 }
247
248 pub fn active_config_template_version(&self) -> Option<u64> {
249 *self.active_config_template_version.lock().unwrap()
250 }
251
252 pub fn set_active_config_template_version(
253 &self,
254 template_version: Option<u64>,
255 ) -> RemoteConfigResult<()> {
256 self.storage
257 .set_active_config_template_version(template_version)?;
258 *self.active_config_template_version.lock().unwrap() = template_version;
259 Ok(())
260 }
261}
262
263impl FileRemoteConfigStorage {
264 pub fn new(path: PathBuf) -> RemoteConfigResult<Self> {
265 let record = if path.exists() {
266 Self::load_record(&path)?
267 } else {
268 StorageRecord::default()
269 };
270 Ok(Self {
271 path,
272 inner: Mutex::new(record),
273 })
274 }
275
276 fn load_record(path: &PathBuf) -> RemoteConfigResult<StorageRecord> {
277 let data = fs::read(path)
278 .map_err(|err| internal_error(format!("failed to read storage file: {err}")))?;
279 serde_json::from_slice(&data)
280 .map_err(|err| internal_error(format!("failed to parse storage file as JSON: {err}")))
281 }
282
283 fn persist(&self, record: &StorageRecord) -> RemoteConfigResult<()> {
284 if let Some(parent) = self.path.parent() {
285 fs::create_dir_all(parent).map_err(|err| {
286 internal_error(format!("failed to create storage directory: {err}"))
287 })?;
288 }
289 let serialized = serde_json::to_vec_pretty(record)
290 .map_err(|err| internal_error(format!("failed to serialize storage record: {err}")))?;
291 fs::write(&self.path, serialized)
292 .map_err(|err| internal_error(format!("failed to write storage file: {err}")))?;
293 Ok(())
294 }
295}
296
297impl RemoteConfigStorage for FileRemoteConfigStorage {
298 fn get_last_fetch_status(&self) -> RemoteConfigResult<Option<FetchStatus>> {
299 Ok(self.inner.lock().unwrap().last_fetch_status)
300 }
301
302 fn set_last_fetch_status(&self, status: FetchStatus) -> RemoteConfigResult<()> {
303 let mut record = self.inner.lock().unwrap();
304 record.last_fetch_status = Some(status);
305 self.persist(&record)
306 }
307
308 fn get_last_successful_fetch_timestamp_millis(&self) -> RemoteConfigResult<Option<u64>> {
309 Ok(self
310 .inner
311 .lock()
312 .unwrap()
313 .last_successful_fetch_timestamp_millis)
314 }
315
316 fn set_last_successful_fetch_timestamp_millis(&self, timestamp: u64) -> RemoteConfigResult<()> {
317 let mut record = self.inner.lock().unwrap();
318 record.last_successful_fetch_timestamp_millis = Some(timestamp);
319 self.persist(&record)
320 }
321
322 fn get_active_config(&self) -> RemoteConfigResult<Option<HashMap<String, String>>> {
323 Ok(self.inner.lock().unwrap().active_config.clone())
324 }
325
326 fn set_active_config(&self, config: HashMap<String, String>) -> RemoteConfigResult<()> {
327 let mut record = self.inner.lock().unwrap();
328 record.active_config = Some(config);
329 self.persist(&record)
330 }
331
332 fn get_active_config_etag(&self) -> RemoteConfigResult<Option<String>> {
333 Ok(self.inner.lock().unwrap().active_config_etag.clone())
334 }
335
336 fn set_active_config_etag(&self, etag: Option<String>) -> RemoteConfigResult<()> {
337 let mut record = self.inner.lock().unwrap();
338 record.active_config_etag = etag;
339 self.persist(&record)
340 }
341
342 fn get_active_config_template_version(&self) -> RemoteConfigResult<Option<u64>> {
343 Ok(self.inner.lock().unwrap().active_config_template_version)
344 }
345
346 fn set_active_config_template_version(
347 &self,
348 template_version: Option<u64>,
349 ) -> RemoteConfigResult<()> {
350 let mut record = self.inner.lock().unwrap();
351 record.active_config_template_version = template_version;
352 self.persist(&record)
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use std::sync::atomic::{AtomicUsize, Ordering};
360
361 #[test]
362 fn cache_roundtrips_metadata() {
363 let storage: Arc<dyn RemoteConfigStorage> =
364 Arc::new(InMemoryRemoteConfigStorage::default());
365 let cache = RemoteConfigStorageCache::new(storage.clone());
366
367 assert_eq!(cache.last_fetch_status(), FetchStatus::NoFetchYet);
368 assert_eq!(cache.last_successful_fetch_timestamp_millis(), None);
369
370 cache.set_last_fetch_status(FetchStatus::Success).unwrap();
371 cache
372 .set_last_successful_fetch_timestamp_millis(1234)
373 .unwrap();
374 cache
375 .set_active_config(HashMap::from([(
376 String::from("feature"),
377 String::from("on"),
378 )]))
379 .unwrap();
380 cache
381 .set_active_config_etag(Some(String::from("etag")))
382 .unwrap();
383 cache.set_active_config_template_version(Some(42)).unwrap();
384
385 assert_eq!(cache.last_fetch_status(), FetchStatus::Success);
386 assert_eq!(cache.last_successful_fetch_timestamp_millis(), Some(1234));
387 let active = cache.active_config();
388 assert_eq!(active.get("feature"), Some(&String::from("on")));
389 assert_eq!(cache.active_config_etag(), Some(String::from("etag")));
390 assert_eq!(cache.active_config_template_version(), Some(42));
391
392 let cache2 = RemoteConfigStorageCache::new(storage);
394 assert_eq!(cache2.last_fetch_status(), FetchStatus::Success);
395 assert_eq!(cache2.last_successful_fetch_timestamp_millis(), Some(1234));
396 assert_eq!(
397 cache2.active_config().get("feature"),
398 Some(&String::from("on"))
399 );
400 assert_eq!(cache2.active_config_etag(), Some(String::from("etag")));
401 assert_eq!(cache2.active_config_template_version(), Some(42));
402 }
403
404 #[test]
405 fn file_storage_persists_state() {
406 static COUNTER: AtomicUsize = AtomicUsize::new(0);
407 let path = std::env::temp_dir().join(format!(
408 "firebase-remote-config-storage-{}.json",
409 COUNTER.fetch_add(1, Ordering::SeqCst)
410 ));
411
412 let storage = Arc::new(FileRemoteConfigStorage::new(path.clone()).unwrap());
413 let cache = RemoteConfigStorageCache::new(storage.clone());
414
415 cache.set_last_fetch_status(FetchStatus::Success).unwrap();
416 cache
417 .set_last_successful_fetch_timestamp_millis(4321)
418 .unwrap();
419 cache
420 .set_active_config(HashMap::from([(
421 String::from("color"),
422 String::from("blue"),
423 )]))
424 .unwrap();
425 cache
426 .set_active_config_etag(Some(String::from("persist-etag")))
427 .unwrap();
428 cache.set_active_config_template_version(Some(99)).unwrap();
429
430 drop(cache);
431
432 let storage2 = Arc::new(FileRemoteConfigStorage::new(path.clone()).unwrap());
433 let cache2 = RemoteConfigStorageCache::new(storage2);
434 assert_eq!(cache2.last_fetch_status(), FetchStatus::Success);
435 assert_eq!(cache2.last_successful_fetch_timestamp_millis(), Some(4321));
436 assert_eq!(cache2.active_config().get("color"), Some(&"blue".into()));
437 assert_eq!(
438 cache2.active_config_etag(),
439 Some(String::from("persist-etag"))
440 );
441 assert_eq!(cache2.active_config_template_version(), Some(99));
442
443 let _ = fs::remove_file(path);
444 }
445}