Skip to main content

xet_runtime/core/
common.rs

1use std::any::Any;
2use std::collections::HashMap;
3use std::sync::atomic::AtomicU64;
4use std::sync::{Arc, Mutex};
5
6use reqwest::Client;
7use tokio::sync::Semaphore;
8
9use crate::config::XetConfig;
10use crate::utils::adjustable_semaphore::AdjustableSemaphore;
11
12/// Holds shared state that is common across the entire context.
13///
14/// Accessible via `ctx.common` on a [`super::XetContext`].
15pub struct XetCommon {
16    /// Limits the number of files being ingested (cleaned/uploaded) concurrently.
17    pub file_ingestion_semaphore: Arc<Semaphore>,
18
19    /// Limits the number of files being downloaded concurrently.
20    pub file_download_semaphore: Arc<Semaphore>,
21
22    /// Limits total memory used for buffering data during reconstruction downloads.
23    pub reconstruction_download_buffer: Arc<AdjustableSemaphore>,
24
25    /// Tracks the number of currently active file downloads for dynamic buffer scaling.
26    pub active_downloads: Arc<AtomicU64>,
27
28    /// Type-erased cache for runtime-scoped resources. Subsystems store their own
29    /// caches here (keyed by a unique string) instead of using process-global statics,
30    /// so everything is cleaned up when the runtime drops.
31    runtime_cache: Mutex<HashMap<String, Box<dyn Any + Send + Sync>>>,
32}
33
34impl XetCommon {
35    /// Creates a new `XetCommon` instance with the given configuration.
36    pub fn new(config: &XetConfig) -> Self {
37        Self {
38            file_ingestion_semaphore: Arc::new(Semaphore::new(config.data.max_concurrent_file_ingestion)),
39            file_download_semaphore: Arc::new(Semaphore::new(config.data.max_concurrent_file_downloads)),
40            reconstruction_download_buffer: {
41                let base = config.reconstruction.download_buffer_size.as_u64();
42                let limit = config.reconstruction.download_buffer_limit.as_u64();
43                // The semaphore requires floor <= ceiling. XetContext::new normalizes the
44                // config, but this constructor is public and must not panic when handed an
45                // incoherent config directly.
46                let limit = if limit < base {
47                    tracing::warn!(
48                        "download_buffer_limit ({limit}) is below download_buffer_size ({base}); using {base}"
49                    );
50                    base
51                } else {
52                    limit
53                };
54                AdjustableSemaphore::new(base, (base, limit))
55            },
56            active_downloads: Arc::new(AtomicU64::new(0)),
57            runtime_cache: Mutex::new(HashMap::new()),
58        }
59    }
60
61    /// Retrieves a cached value by key, or creates and stores it using `create`.
62    ///
63    /// Values are stored as type-erased `Box<dyn Any>` and recovered via `downcast_ref`.
64    /// Typical stored types are `Arc<Mutex<...>>` or `Arc<RwLock<...>>`, making
65    /// the clone cheap (just an Arc bump).
66    pub fn cache_get_or_create<T, F>(&self, key: &str, create: F) -> T
67    where
68        T: Clone + Send + Sync + 'static,
69        F: FnOnce() -> T,
70    {
71        let mut guard = self.runtime_cache.lock().unwrap();
72        if let Some(existing) = guard.get(key)
73            && let Some(val) = existing.downcast_ref::<T>()
74        {
75            return val.clone();
76        }
77        let value = create();
78        guard.insert(key.to_string(), Box::new(value.clone()));
79        value
80    }
81
82    /// Gets or creates a reqwest client, using a tag to identify the client type.
83    ///
84    /// # Arguments
85    /// * `tag` - A string identifier for the client (e.g., "tcp" for regular, socket path for UDS)
86    /// * `create_client_fn` - A function that creates the client if needed
87    ///
88    /// # Returns
89    /// Returns a clone of the cached client if the tag matches, or creates a new client if the tag differs.
90    pub fn get_or_create_reqwest_client<F>(&self, tag: String, create_client_fn: F) -> crate::error::Result<Client>
91    where
92        F: FnOnce() -> std::result::Result<Client, reqwest::Error>,
93    {
94        let client_cache: Arc<Mutex<Option<(String, Client)>>> =
95            self.cache_get_or_create("global_reqwest_client", || Arc::new(Mutex::new(None)));
96        let mut guard = client_cache.lock()?;
97
98        match guard.as_ref() {
99            Some((cached_tag, cached_client)) if cached_tag == &tag => Ok(cached_client.clone()),
100            _ => {
101                let new_client = create_client_fn()?;
102                *guard = Some((tag, new_client.clone()));
103                Ok(new_client)
104            },
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use std::sync::atomic::{AtomicUsize, Ordering};
112
113    use super::*;
114
115    #[test]
116    fn test_get_or_create_reqwest_client_caches_by_tag() {
117        let common = XetCommon::new(&XetConfig::new());
118        let call_count = AtomicUsize::new(0);
119
120        let _client1 = common
121            .get_or_create_reqwest_client("test-tag".to_string(), || {
122                call_count.fetch_add(1, Ordering::SeqCst);
123                reqwest::Client::builder().build()
124            })
125            .unwrap();
126
127        let _client2 = common
128            .get_or_create_reqwest_client("test-tag".to_string(), || {
129                call_count.fetch_add(1, Ordering::SeqCst);
130                reqwest::Client::builder().build()
131            })
132            .unwrap();
133
134        assert_eq!(call_count.load(Ordering::SeqCst), 1);
135    }
136
137    #[test]
138    fn test_get_or_create_reqwest_client_creates_new_for_different_tag() {
139        let common = XetCommon::new(&XetConfig::new());
140        let call_count = AtomicUsize::new(0);
141
142        let _client1 = common
143            .get_or_create_reqwest_client("tag1".to_string(), || {
144                call_count.fetch_add(1, Ordering::SeqCst);
145                reqwest::Client::builder().user_agent("client1").build()
146            })
147            .unwrap();
148
149        let _client2 = common
150            .get_or_create_reqwest_client("tag2".to_string(), || {
151                call_count.fetch_add(1, Ordering::SeqCst);
152                reqwest::Client::builder().user_agent("client2").build()
153            })
154            .unwrap();
155
156        assert_eq!(call_count.load(Ordering::SeqCst), 2);
157    }
158
159    #[test]
160    fn test_replaces_client_when_tag_changes() {
161        let common = XetCommon::new(&XetConfig::new());
162
163        let _client1 = common
164            .get_or_create_reqwest_client("tcp".to_string(), || {
165                reqwest::Client::builder().user_agent("tcp-client").build()
166            })
167            .unwrap();
168
169        let _client2 = common
170            .get_or_create_reqwest_client("/tmp/socket.sock".to_string(), || {
171                reqwest::Client::builder().user_agent("uds-client").build()
172            })
173            .unwrap();
174
175        // Second call with a different tag should have triggered creation again;
176        // verify by calling with the new tag and confirming no creation happens.
177        let call_count = AtomicUsize::new(0);
178        let _client3 = common
179            .get_or_create_reqwest_client("/tmp/socket.sock".to_string(), || {
180                call_count.fetch_add(1, Ordering::SeqCst);
181                reqwest::Client::builder().build()
182            })
183            .unwrap();
184        assert_eq!(call_count.load(Ordering::SeqCst), 0);
185    }
186
187    #[test]
188    fn test_semaphores_initialized_from_config() {
189        let config = XetConfig::new();
190        let common = XetCommon::new(&config);
191
192        assert_eq!(common.file_ingestion_semaphore.available_permits(), config.data.max_concurrent_file_ingestion);
193        assert_eq!(common.file_download_semaphore.available_permits(), config.data.max_concurrent_file_downloads);
194
195        // Total permits is at least the configured download_buffer_base (may be slightly
196        // larger due to rounding up to a whole number of internal permits).
197        assert!(
198            common.reconstruction_download_buffer.total_permits()
199                >= config.reconstruction.download_buffer_size.as_u64()
200        );
201
202        assert_eq!(common.active_downloads.load(Ordering::Relaxed), 0);
203    }
204
205    #[test]
206    fn test_cache_get_or_create_returns_cached_value() {
207        let common = XetCommon::new(&XetConfig::new());
208        let call_count = AtomicUsize::new(0);
209
210        let v1: Arc<Mutex<Vec<i32>>> = common.cache_get_or_create("my_cache", || {
211            call_count.fetch_add(1, Ordering::SeqCst);
212            Arc::new(Mutex::new(vec![1, 2, 3]))
213        });
214
215        let v2: Arc<Mutex<Vec<i32>>> = common.cache_get_or_create("my_cache", || {
216            call_count.fetch_add(1, Ordering::SeqCst);
217            Arc::new(Mutex::new(vec![4, 5, 6]))
218        });
219
220        assert_eq!(call_count.load(Ordering::SeqCst), 1);
221        assert!(Arc::ptr_eq(&v1, &v2));
222    }
223
224    #[test]
225    fn test_cache_different_keys_are_independent() {
226        let common = XetCommon::new(&XetConfig::new());
227
228        let v1: Arc<String> = common.cache_get_or_create("key_a", || Arc::new("alpha".to_string()));
229        let v2: Arc<String> = common.cache_get_or_create("key_b", || Arc::new("beta".to_string()));
230
231        assert_eq!(v1.as_str(), "alpha");
232        assert_eq!(v2.as_str(), "beta");
233    }
234
235    #[test]
236    fn test_cache_type_mismatch_creates_new_entry() {
237        let common = XetCommon::new(&XetConfig::new());
238
239        let _v1: Arc<String> = common.cache_get_or_create("shared_key", || Arc::new("original".to_string()));
240
241        // Same key but different type -- downcast fails, so create is called and the
242        // entry is replaced with the new type.
243        let v2: Arc<i32> = common.cache_get_or_create("shared_key", || Arc::new(42));
244        assert_eq!(*v2, 42);
245    }
246}