statsig_rust/specs_adapter/
statsig_local_file_specs_adapter.rs1use 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}