apollo_rust_client/
lib.rs

1use cache::Cache;
2use client_config::ClientConfig;
3use futures::executor::block_on;
4use log::trace;
5use std::{
6    collections::HashMap,
7    sync::{Arc, RwLock},
8    thread::{self, JoinHandle},
9    time::Duration,
10};
11
12pub mod cache;
13pub mod client_config;
14
15/// Different types of errors that can occur when using the client.
16#[derive(Debug, thiserror::Error)]
17pub enum Error {
18    #[error("Client is already running")]
19    AlreadyRunning,
20}
21
22/// Apollo client.
23pub struct Client {
24    client_config: ClientConfig,
25    namespaces: Arc<RwLock<HashMap<String, Arc<Cache>>>>,
26    handle: Option<JoinHandle<()>>,
27    running: Arc<RwLock<bool>>,
28}
29
30impl Client {
31    /// Create a new Apollo client.
32    ///
33    /// # Arguments
34    ///
35    /// * `client_config` - The configuration for the Apollo client.
36    ///
37    /// # Returns
38    ///
39    /// A new Apollo client.
40    pub fn new(client_config: ClientConfig) -> Self {
41        Self {
42            client_config,
43            namespaces: Arc::new(RwLock::new(HashMap::new())),
44            handle: None,
45            running: Arc::new(RwLock::new(false)),
46        }
47    }
48
49    /// Get a cache for a given namespace.
50    ///
51    /// # Arguments
52    ///
53    /// * `namespace` - The namespace to get the cache for.
54    ///
55    /// # Returns
56    ///
57    /// A cache for the given namespace.
58    pub fn namespace(&self, namespace: &str) -> Arc<Cache> {
59        let mut namespaces = self.namespaces.write().unwrap();
60        let cache = namespaces.entry(namespace.to_string()).or_insert_with(|| {
61            trace!("Cache miss, creating cache for namespace {}", namespace);
62            Arc::new(Cache::new(self.client_config.clone(), namespace))
63        });
64        cache.clone()
65    }
66
67    pub fn start(&mut self) -> Result<(), Error> {
68        let mut running = self.running.write().unwrap();
69        if *running {
70            return Err(Error::AlreadyRunning);
71        }
72
73        *running = true;
74
75        let running = self.running.clone();
76        let namespaces = self.namespaces.clone();
77        // Spawn a background thread to refresh caches
78        let handle = thread::spawn(move || {
79            loop {
80                let running = running.read().unwrap();
81                if !*running {
82                    break;
83                }
84
85                let namespaces = namespaces.read().unwrap();
86                // Refresh each namespace's cache
87                for (namespace, cache) in namespaces.iter() {
88                    if let Err(err) = block_on(cache.refresh()) {
89                        log::error!(
90                            "Failed to refresh cache for namespace {}: {:?}",
91                            namespace,
92                            err
93                        );
94                    } else {
95                        log::debug!("Successfully refreshed cache for namespace {}", namespace);
96                    }
97                }
98
99                // Sleep for 30 seconds before the next refresh
100                thread::sleep(Duration::from_secs(30));
101            }
102        });
103
104        self.handle = Some(handle);
105
106        Ok(())
107    }
108
109    pub fn stop(&mut self) {
110        let mut running = self.running.write().unwrap();
111        *running = false;
112        if let Some(handle) = self.handle.take() {
113            handle.join().unwrap();
114        }
115    }
116}
117
118impl Drop for Client {
119    fn drop(&mut self) {
120        self.stop();
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use lazy_static::lazy_static;
128    use std::path::PathBuf;
129
130    lazy_static! {
131        static ref CLIENT_NO_SECRET: Client = {
132            let config = ClientConfig {
133                app_id: String::from("101010101"),
134                cluster: String::from("default"),
135                config_server: String::from("http://81.68.181.139:8080"),
136                label: None,
137                secret: None,
138                cache_dir: Some(PathBuf::from("/tmp/apollo")),
139                ip: None,
140            };
141            Client::new(config)
142        };
143        static ref CLIENT_WITH_SECRET: Client = {
144            let config = ClientConfig {
145                app_id: String::from("101010102"),
146                cluster: String::from("default"),
147                config_server: String::from("http://81.68.181.139:8080"),
148                label: None,
149                secret: Some(String::from("53bf47631db540ac9700f0020d2192c8")),
150                cache_dir: Some(PathBuf::from("/tmp/apollo")),
151                ip: None,
152            };
153            Client::new(config)
154        };
155        static ref CLIENT_WITH_GRAYSCALE_IP: Client = {
156            let config = ClientConfig {
157                app_id: String::from("101010101"),
158                cluster: String::from("default"),
159                config_server: String::from("http://81.68.181.139:8080"),
160                label: None,
161                secret: None,
162                cache_dir: Some(PathBuf::from("/tmp/apollo")),
163                ip: Some(String::from("1.2.3.4")),
164            };
165            Client::new(config)
166        };
167        static ref CLIENT_WITH_GRAYSCALE_LABEL: Client = {
168            let config = ClientConfig {
169                app_id: String::from("101010101"),
170                cluster: String::from("default"),
171                config_server: String::from("http://81.68.181.139:8080"),
172                label: Some(String::from("GrayScale")),
173                secret: None,
174                cache_dir: Some(PathBuf::from("/tmp/apollo")),
175                ip: None,
176            };
177            Client::new(config)
178        };
179    }
180
181    pub(crate) fn setup() {
182        let _ = env_logger::builder().is_test(true).try_init();
183    }
184
185    #[tokio::test]
186    async fn test_missing_value() {
187        setup();
188        let cache = CLIENT_NO_SECRET.namespace("application");
189        assert_eq!(cache.get_property::<String>("missingValue").await, None);
190    }
191
192    #[tokio::test]
193    async fn test_string_value() {
194        setup();
195        let cache = CLIENT_NO_SECRET.namespace("application");
196        assert_eq!(
197            cache.get_property::<String>("stringValue").await,
198            Some("string value".to_string())
199        );
200    }
201
202    #[tokio::test]
203    async fn test_string_value_with_secret() {
204        setup();
205        let cache = CLIENT_WITH_SECRET.namespace("application");
206        assert_eq!(
207            cache.get_property::<String>("stringValue").await,
208            Some("string value".to_string())
209        );
210    }
211
212    #[tokio::test]
213    async fn test_int_value() {
214        setup();
215        let cache = CLIENT_NO_SECRET.namespace("application");
216        assert_eq!(cache.get_property::<i32>("intValue").await, Some(42));
217    }
218
219    #[tokio::test]
220    async fn test_int_value_with_secret() {
221        setup();
222        let cache = CLIENT_WITH_SECRET.namespace("application");
223        assert_eq!(cache.get_property::<i32>("intValue").await, Some(42));
224    }
225
226    #[tokio::test]
227    async fn test_float_value() {
228        setup();
229        let cache = CLIENT_NO_SECRET.namespace("application");
230        assert_eq!(cache.get_property::<f64>("floatValue").await, Some(4.20));
231    }
232
233    #[tokio::test]
234    async fn test_float_value_with_secret() {
235        setup();
236        let cache = CLIENT_WITH_SECRET.namespace("application");
237        assert_eq!(cache.get_property::<f64>("floatValue").await, Some(4.20));
238    }
239
240    #[tokio::test]
241    async fn test_bool_value() {
242        setup();
243        let cache = CLIENT_NO_SECRET.namespace("application");
244        assert_eq!(cache.get_property::<bool>("boolValue").await, Some(false));
245    }
246
247    #[tokio::test]
248    async fn test_bool_value_with_secret() {
249        setup();
250        let cache = CLIENT_WITH_SECRET.namespace("application");
251        assert_eq!(cache.get_property::<bool>("boolValue").await, Some(false));
252    }
253
254    #[tokio::test]
255    async fn test_bool_value_with_grayscale_ip() {
256        setup();
257        let cache = CLIENT_WITH_GRAYSCALE_IP.namespace("application");
258        assert_eq!(
259            cache.get_property::<bool>("grayScaleValue").await,
260            Some(true)
261        );
262        let cache = CLIENT_NO_SECRET.namespace("application");
263        assert_eq!(
264            cache.get_property::<bool>("grayScaleValue").await,
265            Some(false)
266        );
267    }
268
269    #[tokio::test]
270    async fn test_bool_value_with_grayscale_label() {
271        setup();
272        let cache = CLIENT_WITH_GRAYSCALE_LABEL.namespace("application");
273        assert_eq!(
274            cache.get_property::<bool>("grayScaleValue").await,
275            Some(true)
276        );
277        let cache = CLIENT_NO_SECRET.namespace("application");
278        assert_eq!(
279            cache.get_property::<bool>("grayScaleValue").await,
280            Some(false)
281        );
282    }
283}