statsig_rust/specs_adapter/
statsig_local_file_specs_adapter.rs

1use crate::hashing::djb2;
2use crate::specs_adapter::{SpecsAdapter, SpecsSource, SpecsUpdate, SpecsUpdateListener};
3use crate::specs_response::spec_types::SpecsResponseFull;
4use crate::specs_response::spec_types_encoded::DecodedSpecsResponse;
5use crate::statsig_err::StatsigErr;
6use crate::{log_w, StatsigRuntime};
7use async_trait::async_trait;
8use chrono::Utc;
9
10use std::sync::{Arc, RwLock};
11use std::time::Duration;
12
13use super::{SpecsInfo, StatsigHttpSpecsAdapter};
14
15const TAG: &str = stringify!(StatsigLocalFileSpecsAdapter);
16
17pub struct StatsigLocalFileSpecsAdapter {
18    file_path: String,
19    listener: RwLock<Option<Arc<dyn SpecsUpdateListener>>>,
20    http_adapter: StatsigHttpSpecsAdapter,
21}
22
23impl StatsigLocalFileSpecsAdapter {
24    #[must_use]
25    pub fn new(
26        sdk_key: &str,
27        output_directory: &str,
28        specs_url: Option<String>,
29        fallback_to_statsig_api: bool,
30        disable_network: bool,
31    ) -> Self {
32        let hashed_key = djb2(sdk_key);
33        let file_path = format!("{output_directory}/{hashed_key}_specs.json");
34
35        Self {
36            file_path,
37            listener: RwLock::new(None),
38            http_adapter: StatsigHttpSpecsAdapter::new(
39                sdk_key,
40                specs_url.as_ref(),
41                fallback_to_statsig_api,
42                None,
43                Some(disable_network),
44            ),
45        }
46    }
47
48    pub async fn fetch_and_write_to_file(&self) -> Result<(), StatsigErr> {
49        let specs_info = match self.read_specs_from_file() {
50            Ok(Some(specs)) => SpecsInfo {
51                lcut: Some(specs.time),
52                checksum: specs.checksum,
53                zstd_dict_id: None, // For backwards compatibility, we only store uncompressed specs
54                source: SpecsSource::Adapter("FileBased".to_owned()),
55            },
56            _ => SpecsInfo::empty(),
57        };
58
59        let data = match self.http_adapter.fetch_specs_from_network(specs_info).await {
60            Ok(data) => String::from_utf8(data)
61                .map_err(|e| StatsigErr::SerializationError(e.to_string()))?,
62            Err(e) => {
63                return Err(StatsigErr::NetworkError(
64                    e,
65                    Some("No data received".to_string()),
66                ));
67            }
68        };
69
70        if let Some(response) = self.parse_specs_data_to_full_response(&data) {
71            if response.has_updates {
72                self.write_specs_to_file(&data);
73            }
74        }
75
76        Ok(())
77    }
78
79    pub fn resync_from_file(&self) -> Result<(), StatsigErr> {
80        let data = match std::fs::read_to_string(&self.file_path) {
81            Ok(data) => data,
82            Err(e) => {
83                return Err(StatsigErr::FileError(e.to_string()));
84            }
85        };
86
87        match &self.listener.read() {
88            Ok(lock) => match lock.as_ref() {
89                Some(listener) => listener.did_receive_specs_update(SpecsUpdate {
90                    data: data.into_bytes(),
91                    source: SpecsSource::Adapter("FileBased".to_owned()),
92                    received_at: Utc::now().timestamp_millis() as u64,
93                }),
94                None => Err(StatsigErr::UnstartedAdapter("Listener not set".to_string())),
95            },
96            Err(e) => Err(StatsigErr::LockFailure(e.to_string())),
97        }
98    }
99
100    fn read_specs_from_file(&self) -> Result<Option<SpecsResponseFull>, StatsigErr> {
101        if !std::path::Path::new(&self.file_path).exists() {
102            return Ok(None);
103        }
104
105        let data = match std::fs::read_to_string(&self.file_path) {
106            Ok(data) => data,
107            Err(e) => {
108                return Err(StatsigErr::FileError(e.to_string()));
109            }
110        };
111
112        Ok(self.parse_specs_data_to_full_response(&data))
113    }
114
115    fn parse_specs_data_to_full_response(&self, data: &str) -> Option<SpecsResponseFull> {
116        let mut place = SpecsResponseFull::default();
117        match DecodedSpecsResponse::from_slice(data.as_bytes(), &mut place, None) {
118            Ok(_) => Some(place),
119            Err(e) => {
120                log_w!(TAG, "Failed to parse specs data: {}", e);
121                None
122            }
123        }
124    }
125
126    fn write_specs_to_file(&self, data: &str) {
127        match std::fs::write(&self.file_path, data) {
128            Ok(()) => (),
129            Err(e) => log_w!(TAG, "Failed to write specs to file: {}", e),
130        }
131    }
132}
133
134#[async_trait]
135impl SpecsAdapter for StatsigLocalFileSpecsAdapter {
136    async fn start(
137        self: Arc<Self>,
138        _statsig_runtime: &Arc<StatsigRuntime>,
139    ) -> Result<(), StatsigErr> {
140        self.resync_from_file()
141    }
142
143    fn initialize(&self, listener: Arc<dyn SpecsUpdateListener>) {
144        if let Ok(mut mut_listener) = self.listener.write() {
145            *mut_listener = Some(listener);
146        }
147    }
148
149    async fn shutdown(
150        &self,
151        _timeout: Duration,
152        _statsig_runtime: &Arc<StatsigRuntime>,
153    ) -> Result<(), StatsigErr> {
154        Ok(())
155    }
156
157    async fn schedule_background_sync(
158        self: Arc<Self>,
159        _statsig_runtime: &Arc<StatsigRuntime>,
160    ) -> Result<(), StatsigErr> {
161        Ok(())
162    }
163
164    fn get_type_name(&self) -> String {
165        stringify!(StatsigLocalFileSpecsAdapter).to_string()
166    }
167}