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