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::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, 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}