1use std::collections::HashMap;
8use std::sync::Arc;
9
10#[derive(Debug)]
12pub enum BatchError {
13 LoadFailed(String),
14 KeyNotFound(String),
15}
16
17impl std::fmt::Display for BatchError {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 match self {
20 BatchError::LoadFailed(msg) => write!(f, "Batch load failed: {}", msg),
21 BatchError::KeyNotFound(key) => write!(f, "Key not found: {}", key),
22 }
23 }
24}
25
26impl std::error::Error for BatchError {}
27
28pub trait BatchLoader: Send + Sync {
30 type Item: Clone + Send;
31
32 fn load_batch(&self, keys: &[String]) -> Result<HashMap<String, Self::Item>, BatchError>;
35}
36
37pub struct BatchResolver<T: Clone + Send> {
42 loader: Arc<dyn BatchLoader<Item = T>>,
43 max_batch_size: usize,
44 cache: HashMap<String, T>,
45 queue: Vec<String>,
46 batches_executed: usize,
47}
48
49impl<T: Clone + Send + 'static> BatchResolver<T> {
50 pub fn new(loader: Arc<dyn BatchLoader<Item = T>>, max_batch_size: usize) -> Self {
55 BatchResolver {
56 loader,
57 max_batch_size,
58 cache: HashMap::new(),
59 queue: Vec::new(),
60 batches_executed: 0,
61 }
62 }
63
64 pub fn queue(&mut self, key: impl Into<String>) {
69 let key = key.into();
70 if !self.cache.contains_key(&key) && !self.queue.contains(&key) {
71 self.queue.push(key);
72 }
73 }
74
75 pub fn resolve_all(&mut self) -> Result<(), BatchError> {
80 if self.queue.is_empty() {
81 return Ok(());
82 }
83
84 let keys: Vec<String> = self.queue.drain(..).collect();
85
86 let batch_size = if self.max_batch_size == 0 {
88 keys.len()
89 } else {
90 self.max_batch_size
91 };
92
93 for chunk in keys.chunks(batch_size) {
94 let loaded = self.loader.load_batch(chunk)?;
95 for (k, v) in loaded {
96 self.cache.insert(k, v);
97 }
98 self.batches_executed += 1;
99 }
100
101 Ok(())
102 }
103
104 pub fn get(&self, key: &str) -> Option<&T> {
108 self.cache.get(key)
109 }
110
111 pub fn cache_size(&self) -> usize {
113 self.cache.len()
114 }
115
116 pub fn clear_cache(&mut self) {
118 self.cache.clear();
119 }
120
121 pub fn pending_count(&self) -> usize {
123 self.queue.len()
124 }
125
126 pub fn batches_executed(&self) -> usize {
128 self.batches_executed
129 }
130}
131
132pub struct UppercaseLoader;
139
140impl BatchLoader for UppercaseLoader {
141 type Item = String;
142
143 fn load_batch(&self, keys: &[String]) -> Result<HashMap<String, String>, BatchError> {
144 for key in keys {
145 if key.starts_with("error:") {
146 return Err(BatchError::LoadFailed(key.clone()));
147 }
148 }
149 let mut map = HashMap::new();
150 for key in keys {
151 if !key.starts_with("missing:") {
152 map.insert(key.clone(), key.to_uppercase());
153 }
154 }
155 Ok(map)
156 }
157}
158
159pub struct CountingLoader {
161 pub call_count: std::sync::atomic::AtomicUsize,
162}
163
164impl Default for CountingLoader {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170impl CountingLoader {
171 pub fn new() -> Self {
172 CountingLoader {
173 call_count: std::sync::atomic::AtomicUsize::new(0),
174 }
175 }
176
177 pub fn calls(&self) -> usize {
178 self.call_count.load(std::sync::atomic::Ordering::SeqCst)
179 }
180}
181
182impl BatchLoader for CountingLoader {
183 type Item = String;
184
185 fn load_batch(&self, keys: &[String]) -> Result<HashMap<String, String>, BatchError> {
186 self.call_count
187 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
188 Ok(keys.iter().map(|k| (k.clone(), k.to_uppercase())).collect())
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 fn uppercase_resolver(max_batch: usize) -> BatchResolver<String> {
197 BatchResolver::new(Arc::new(UppercaseLoader), max_batch)
198 }
199
200 #[test]
203 fn test_single_key_resolve() {
204 let mut r = uppercase_resolver(10);
205 r.queue("hello");
206 r.resolve_all().expect("should succeed");
207 assert_eq!(r.get("hello"), Some(&"HELLO".to_string()));
208 }
209
210 #[test]
211 fn test_single_key_before_resolve_returns_none() {
212 let r = uppercase_resolver(10);
213 assert_eq!(r.get("anything"), None);
214 }
215
216 #[test]
219 fn test_multiple_keys_single_batch() {
220 let mut r = uppercase_resolver(100);
221 r.queue("a");
222 r.queue("b");
223 r.queue("c");
224 r.resolve_all().expect("should succeed");
225 assert_eq!(r.get("a"), Some(&"A".to_string()));
226 assert_eq!(r.get("b"), Some(&"B".to_string()));
227 assert_eq!(r.get("c"), Some(&"C".to_string()));
228 }
229
230 #[test]
233 fn test_batch_splitting_into_two() {
234 let mut r = uppercase_resolver(2);
235 r.queue("a");
236 r.queue("b");
237 r.queue("c");
238 r.queue("d");
239 r.resolve_all().expect("should succeed");
240 assert_eq!(r.batches_executed(), 2);
242 assert_eq!(r.get("a"), Some(&"A".to_string()));
243 assert_eq!(r.get("d"), Some(&"D".to_string()));
244 }
245
246 #[test]
247 fn test_batch_splitting_exact_multiple() {
248 let mut r = uppercase_resolver(3);
249 for k in ["x1", "x2", "x3", "x4", "x5", "x6"] {
250 r.queue(k);
251 }
252 r.resolve_all().expect("should succeed");
253 assert_eq!(r.batches_executed(), 2);
254 }
255
256 #[test]
257 fn test_batch_splitting_uneven() {
258 let mut r = uppercase_resolver(3);
259 for k in ["x1", "x2", "x3", "x4", "x5"] {
260 r.queue(k);
261 }
262 r.resolve_all().expect("should succeed");
263 assert_eq!(r.batches_executed(), 2); }
265
266 #[test]
269 fn test_cache_hit_avoids_re_load() {
270 let loader = Arc::new(CountingLoader::new());
271 let loader_dyn: Arc<dyn BatchLoader<Item = String>> =
272 Arc::clone(&loader) as Arc<dyn BatchLoader<Item = String>>;
273 let mut r: BatchResolver<String> = BatchResolver::new(loader_dyn, 10);
274
275 r.queue("key");
276 r.resolve_all().expect("should succeed");
277 assert_eq!(loader.calls(), 1);
278
279 r.queue("key");
281 r.resolve_all().expect("should succeed");
282 assert_eq!(loader.calls(), 1); }
284
285 #[test]
286 fn test_cache_size_after_resolve() {
287 let mut r = uppercase_resolver(10);
288 r.queue("a");
289 r.queue("b");
290 r.resolve_all().expect("should succeed");
291 assert_eq!(r.cache_size(), 2);
292 }
293
294 #[test]
297 fn test_clear_cache() {
298 let mut r = uppercase_resolver(10);
299 r.queue("a");
300 r.resolve_all().expect("should succeed");
301 assert_eq!(r.cache_size(), 1);
302 r.clear_cache();
303 assert_eq!(r.cache_size(), 0);
304 assert_eq!(r.get("a"), None);
305 }
306
307 #[test]
308 fn test_clear_cache_allows_reload() {
309 let loader = Arc::new(CountingLoader::new());
310 let loader_dyn: Arc<dyn BatchLoader<Item = String>> =
311 Arc::clone(&loader) as Arc<dyn BatchLoader<Item = String>>;
312 let mut r: BatchResolver<String> = BatchResolver::new(loader_dyn, 10);
313
314 r.queue("key");
315 r.resolve_all().expect("should succeed");
316 assert_eq!(loader.calls(), 1);
317
318 r.clear_cache();
319
320 r.queue("key");
321 r.resolve_all().expect("should succeed");
322 assert_eq!(loader.calls(), 2); }
324
325 #[test]
328 fn test_pending_count_before_resolve() {
329 let mut r = uppercase_resolver(10);
330 assert_eq!(r.pending_count(), 0);
331 r.queue("a");
332 r.queue("b");
333 assert_eq!(r.pending_count(), 2);
334 }
335
336 #[test]
337 fn test_pending_count_after_resolve() {
338 let mut r = uppercase_resolver(10);
339 r.queue("a");
340 r.resolve_all().expect("should succeed");
341 assert_eq!(r.pending_count(), 0);
342 }
343
344 #[test]
347 fn test_batches_executed_zero_initially() {
348 let r = uppercase_resolver(10);
349 assert_eq!(r.batches_executed(), 0);
350 }
351
352 #[test]
353 fn test_batches_executed_increments() {
354 let mut r = uppercase_resolver(1);
355 r.queue("a");
356 r.queue("b");
357 r.resolve_all().expect("should succeed");
358 assert_eq!(r.batches_executed(), 2);
359 }
360
361 #[test]
362 fn test_batches_executed_accumulates() {
363 let mut r = uppercase_resolver(10);
364 r.queue("a");
365 r.resolve_all().expect("should succeed");
366 r.queue("b");
367 r.resolve_all().expect("should succeed");
368 assert_eq!(r.batches_executed(), 2);
369 }
370
371 #[test]
374 fn test_error_propagation() {
375 let mut r = uppercase_resolver(10);
376 r.queue("error:bad");
377 let result = r.resolve_all();
378 assert!(result.is_err());
379 match result.unwrap_err() {
380 BatchError::LoadFailed(msg) => assert!(msg.contains("error:bad")),
381 e => panic!("wrong error: {:?}", e),
382 }
383 }
384
385 #[test]
386 fn test_error_in_second_batch() {
387 let mut r = uppercase_resolver(2);
388 r.queue("ok1");
389 r.queue("ok2");
390 r.queue("error:fail");
391 let result = r.resolve_all();
392 assert!(result.is_err());
393 }
394
395 #[test]
398 fn test_dedup_queued_keys() {
399 let mut r = uppercase_resolver(10);
400 r.queue("a");
401 r.queue("a");
402 r.queue("a");
403 assert_eq!(r.pending_count(), 1);
404 }
405
406 #[test]
407 fn test_dedup_does_not_re_queue_cached() {
408 let mut r = uppercase_resolver(10);
409 r.queue("a");
410 r.resolve_all().expect("should succeed");
411 r.queue("a"); assert_eq!(r.pending_count(), 0);
413 }
414
415 #[test]
418 fn test_missing_key_returns_none() {
419 let mut r = uppercase_resolver(10);
420 r.queue("missing:nothere");
421 r.resolve_all().expect("should succeed");
422 assert_eq!(r.get("missing:nothere"), None);
423 }
424
425 #[test]
426 fn test_missing_key_does_not_affect_cache_size() {
427 let mut r = uppercase_resolver(10);
428 r.queue("ok");
429 r.queue("missing:nope");
430 r.resolve_all().expect("should succeed");
431 assert_eq!(r.cache_size(), 1);
432 }
433
434 #[test]
437 fn test_resolve_all_empty_queue_ok() {
438 let mut r = uppercase_resolver(10);
439 let result = r.resolve_all();
440 assert!(result.is_ok());
441 assert_eq!(r.batches_executed(), 0);
442 }
443
444 #[test]
447 fn test_unlimited_batch_size() {
448 let mut r = uppercase_resolver(0);
449 for i in 0..100 {
450 r.queue(format!("key{}", i));
451 }
452 r.resolve_all().expect("should succeed");
453 assert_eq!(r.batches_executed(), 1);
455 assert_eq!(r.cache_size(), 100);
456 }
457
458 #[test]
461 fn test_correct_uppercase_values() {
462 let mut r = uppercase_resolver(10);
463 r.queue("foo");
464 r.queue("bar");
465 r.queue("baz");
466 r.resolve_all().expect("should succeed");
467 assert_eq!(r.get("foo"), Some(&"FOO".to_string()));
468 assert_eq!(r.get("bar"), Some(&"BAR".to_string()));
469 assert_eq!(r.get("baz"), Some(&"BAZ".to_string()));
470 }
471
472 #[test]
475 fn test_two_resolvers_independent_cache() {
476 let loader1: Arc<dyn BatchLoader<Item = String>> = Arc::new(UppercaseLoader);
477 let loader2: Arc<dyn BatchLoader<Item = String>> = Arc::new(UppercaseLoader);
478 let mut r1: BatchResolver<String> = BatchResolver::new(loader1, 10);
479 let mut r2: BatchResolver<String> = BatchResolver::new(loader2, 10);
480
481 r1.queue("hello");
482 r1.resolve_all().expect("should succeed");
483
484 assert_eq!(r2.get("hello"), None);
486 r2.queue("hello");
487 r2.resolve_all().expect("should succeed");
488 assert_eq!(r2.get("hello"), Some(&"HELLO".to_string()));
489 }
490}