statsig_rust/specs_adapter/
statsig_bootstrap_specs_adapter.rs

1use crate::specs_adapter::{SpecsAdapter, SpecsSource, SpecsUpdate, SpecsUpdateListener};
2use crate::statsig_err::StatsigErr;
3use crate::{log_e, StatsigRuntime};
4use async_trait::async_trait;
5use chrono::Utc;
6
7use std::sync::{Arc, RwLock};
8use std::time::Duration;
9pub struct StatsigBootstrapSpecsAdapter {
10    data: RwLock<String>,
11    listener: RwLock<Option<Arc<dyn SpecsUpdateListener>>>,
12}
13const TAG: &str = stringify!(StatsigBootstrapSpecsAdapter);
14
15impl StatsigBootstrapSpecsAdapter {
16    #[must_use]
17    pub fn new(data: String) -> Self {
18        Self {
19            data: RwLock::new(data),
20            listener: RwLock::new(None),
21        }
22    }
23
24    pub fn set_data(&self, data: String) -> Result<(), StatsigErr> {
25        match self.data.write() {
26            Ok(mut lock) => *lock = data.clone(),
27            Err(e) => return Err(StatsigErr::LockFailure(e.to_string())),
28        };
29
30        self.push_update()
31    }
32
33    fn push_update(&self) -> Result<(), StatsigErr> {
34        let data = match self.data.read() {
35            Ok(lock) => lock.clone(),
36            Err(e) => return Err(StatsigErr::LockFailure(e.to_string())),
37        };
38
39        match &self.listener.read() {
40            Ok(lock) => match lock.as_ref() {
41                Some(listener) => listener.did_receive_specs_update(SpecsUpdate {
42                    data: data.into_bytes(),
43                    source: SpecsSource::Bootstrap,
44                    received_at: Utc::now().timestamp_millis() as u64,
45                    source_api: None,
46                }),
47                None => Err(StatsigErr::UnstartedAdapter("Listener not set".to_string())),
48            },
49            Err(e) => Err(StatsigErr::LockFailure(e.to_string())),
50        }
51    }
52}
53
54#[async_trait]
55impl SpecsAdapter for StatsigBootstrapSpecsAdapter {
56    async fn start(
57        self: Arc<Self>,
58        _statsig_runtime: &Arc<StatsigRuntime>,
59    ) -> Result<(), StatsigErr> {
60        self.push_update()
61    }
62
63    fn initialize(&self, listener: Arc<dyn SpecsUpdateListener>) {
64        match self.listener.write() {
65            Ok(mut lock) => *lock = Some(listener),
66            Err(e) => {
67                log_e!(TAG, "Failed to acquire write lock on listener: {}", e);
68            }
69        }
70    }
71
72    async fn shutdown(
73        &self,
74        _timeout: Duration,
75        _statsig_runtime: &Arc<StatsigRuntime>,
76    ) -> Result<(), StatsigErr> {
77        Ok(())
78    }
79
80    async fn schedule_background_sync(
81        self: Arc<Self>,
82        _statsig_runtime: &Arc<StatsigRuntime>,
83    ) -> Result<(), StatsigErr> {
84        Ok(())
85    }
86
87    fn get_type_name(&self) -> String {
88        stringify!(StatsigBootstrapSpecsAdapter).to_string()
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use crate::SpecsInfo;
95
96    use super::*;
97    use std::sync::Arc;
98
99    struct TestListener {
100        received_update: RwLock<Option<SpecsUpdate>>,
101    }
102
103    impl TestListener {
104        fn new() -> Self {
105            Self {
106                received_update: RwLock::new(None),
107            }
108        }
109    }
110
111    #[async_trait]
112    impl SpecsUpdateListener for TestListener {
113        fn did_receive_specs_update(&self, update: SpecsUpdate) -> Result<(), StatsigErr> {
114            if let Ok(mut lock) = self.received_update.write() {
115                *lock = Some(update);
116            }
117            Ok(())
118        }
119
120        fn get_current_specs_info(&self) -> SpecsInfo {
121            SpecsInfo::empty()
122        }
123    }
124
125    #[tokio::test]
126    async fn test_manually_sync_specs() {
127        let test_data = serde_json::json!({
128            "feature_gates": {},
129            "dynamic_configs": {},
130            "layer_configs": {},
131        })
132        .to_string();
133
134        let adapter = Arc::new(StatsigBootstrapSpecsAdapter::new(test_data.clone()));
135        let listener = Arc::new(TestListener::new());
136
137        let statsig_rt = StatsigRuntime::get_runtime();
138        adapter.initialize(listener.clone());
139        adapter.clone().start(&statsig_rt).await.unwrap();
140
141        if let Ok(lock) = listener.clone().received_update.read() {
142            let update = lock.as_ref().unwrap();
143            assert_eq!(update.source, SpecsSource::Bootstrap);
144            assert_eq!(update.data, test_data.into_bytes());
145        }
146    }
147
148    #[tokio::test]
149    async fn test_set_data() {
150        let statsig_rt = StatsigRuntime::get_runtime();
151
152        let adapter = Arc::new(StatsigBootstrapSpecsAdapter::new(String::new()));
153
154        let listener = Arc::new(TestListener::new());
155        adapter.initialize(listener.clone());
156        adapter.clone().start(&statsig_rt).await.unwrap();
157
158        let test_data = "{\"some\": \"value\"}".to_string();
159        let result = adapter.set_data(test_data.clone());
160        assert!(result.is_ok());
161
162        if let Ok(lock) = listener.clone().received_update.read() {
163            let update = lock.as_ref().unwrap();
164            assert_eq!(update.source, SpecsSource::Bootstrap);
165            assert_eq!(update.data, test_data.into_bytes());
166        }
167    }
168}