statsig_rust/specs_adapter/
statsig_local_file_specs_adapter.rs

1use crate::hashing::djb2;
2use crate::specs_adapter::statsig_http_specs_adapter::SpecsSyncTrigger;
3use crate::specs_adapter::{SpecsAdapter, SpecsSource, SpecsUpdate, SpecsUpdateListener};
4use crate::specs_response::spec_types::SpecsResponseFull;
5use crate::statsig_err::StatsigErr;
6use crate::{log_e, log_w, StatsigOptions, StatsigRuntime};
7use async_trait::async_trait;
8use chrono::Utc;
9use parking_lot::RwLock;
10use std::sync::Arc;
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        let options = StatsigOptions {
36            specs_url,
37            disable_network: Some(disable_network),
38            fallback_to_statsig_api: Some(fallback_to_statsig_api),
39            ..Default::default()
40        };
41
42        Self {
43            file_path,
44            listener: RwLock::new(None),
45            http_adapter: StatsigHttpSpecsAdapter::new(sdk_key, Some(&options), None),
46        }
47    }
48
49    pub async fn fetch_and_write_to_file(&self) -> Result<(), StatsigErr> {
50        let specs_info = match self.read_specs_from_file() {
51            Ok(Some(specs)) => SpecsInfo {
52                lcut: Some(specs.time),
53                checksum: specs.checksum,
54                source: SpecsSource::Adapter("FileBased".to_owned()),
55                source_api: None,
56            },
57            _ => SpecsInfo::empty(),
58        };
59
60        let data = match self
61            .http_adapter
62            .fetch_specs_from_network(specs_info, SpecsSyncTrigger::Manual)
63            .await
64        {
65            Ok(response) => match String::from_utf8(response.data) {
66                Ok(data) => data,
67                Err(e) => {
68                    return Err(StatsigErr::SerializationError(e.to_string()));
69                }
70            },
71            Err(e) => {
72                return Err(StatsigErr::NetworkError(e));
73            }
74        };
75
76        if let Some(response) = self.parse_specs_data_to_full_response(&data) {
77            if response.has_updates {
78                self.write_specs_to_file(&data);
79            }
80        }
81
82        Ok(())
83    }
84
85    pub fn resync_from_file(&self) -> Result<(), StatsigErr> {
86        let data = match std::fs::read_to_string(&self.file_path) {
87            Ok(data) => data,
88            Err(e) => {
89                return Err(StatsigErr::FileError(e.to_string()));
90            }
91        };
92
93        match &self
94            .listener
95            .try_read_for(std::time::Duration::from_secs(5))
96        {
97            Some(lock) => match lock.as_ref() {
98                Some(listener) => listener.did_receive_specs_update(SpecsUpdate {
99                    data: data.into_bytes(),
100                    source: SpecsSource::Adapter("FileBased".to_owned()),
101                    received_at: Utc::now().timestamp_millis() as u64,
102                    source_api: None,
103                }),
104                None => Err(StatsigErr::UnstartedAdapter("Listener not set".to_string())),
105            },
106            None => Err(StatsigErr::LockFailure(
107                "Failed to acquire read lock on listener".to_string(),
108            )),
109        }
110    }
111
112    fn read_specs_from_file(&self) -> Result<Option<SpecsResponseFull>, StatsigErr> {
113        if !std::path::Path::new(&self.file_path).exists() {
114            return Ok(None);
115        }
116
117        let data = match std::fs::read_to_string(&self.file_path) {
118            Ok(data) => data,
119            Err(e) => {
120                return Err(StatsigErr::FileError(e.to_string()));
121            }
122        };
123
124        Ok(self.parse_specs_data_to_full_response(&data))
125    }
126
127    fn parse_specs_data_to_full_response(&self, data: &str) -> Option<SpecsResponseFull> {
128        match serde_json::from_slice::<SpecsResponseFull>(data.as_bytes()) {
129            Ok(response) => Some(response),
130            Err(e) => {
131                log_w!(TAG, "Failed to parse specs data: {}", e);
132                None
133            }
134        }
135    }
136
137    fn write_specs_to_file(&self, data: &str) {
138        match std::fs::write(&self.file_path, data) {
139            Ok(()) => (),
140            Err(e) => log_w!(TAG, "Failed to write specs to file: {}", e),
141        }
142    }
143}
144
145#[async_trait]
146impl SpecsAdapter for StatsigLocalFileSpecsAdapter {
147    async fn start(
148        self: Arc<Self>,
149        _statsig_runtime: &Arc<StatsigRuntime>,
150    ) -> Result<(), StatsigErr> {
151        self.resync_from_file()
152    }
153
154    fn initialize(&self, listener: Arc<dyn SpecsUpdateListener>) {
155        match self
156            .listener
157            .try_write_for(std::time::Duration::from_secs(5))
158        {
159            Some(mut lock) => *lock = Some(listener),
160            None => {
161                log_e!(TAG, "Failed to acquire write lock on listener");
162            }
163        }
164    }
165
166    async fn shutdown(
167        &self,
168        _timeout: Duration,
169        _statsig_runtime: &Arc<StatsigRuntime>,
170    ) -> Result<(), StatsigErr> {
171        Ok(())
172    }
173
174    async fn schedule_background_sync(
175        self: Arc<Self>,
176        _statsig_runtime: &Arc<StatsigRuntime>,
177    ) -> Result<(), StatsigErr> {
178        Ok(())
179    }
180
181    fn get_type_name(&self) -> String {
182        stringify!(StatsigLocalFileSpecsAdapter).to_string()
183    }
184}