1use anyhow::{anyhow, Result};
7use async_trait::async_trait;
8use std::collections::HashMap;
9use std::hash::Hash;
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12use tokio::sync::{Mutex, RwLock};
13use tokio::time::sleep;
14
15#[async_trait]
17pub trait BatchLoadFn<K, V>: Send + Sync
18where
19 K: Send + Sync + Clone + Hash + Eq,
20 V: Send + Sync + Clone,
21{
22 async fn load(&self, keys: Vec<K>) -> Result<HashMap<K, V>>;
23}
24
25#[derive(Debug, Clone)]
27pub struct DataLoaderConfig {
28 pub max_batch_size: usize,
30 pub batch_delay: Duration,
32 pub cache_ttl: Duration,
34 pub max_cache_size: usize,
36 pub enable_cache: bool,
38}
39
40impl Default for DataLoaderConfig {
41 fn default() -> Self {
42 Self {
43 max_batch_size: 100,
44 batch_delay: Duration::from_millis(10),
45 cache_ttl: Duration::from_secs(300), max_cache_size: 1000,
47 enable_cache: true,
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
54struct CachedEntry<V> {
55 value: V,
56 created_at: Instant,
57 ttl: Duration,
58}
59
60impl<V> CachedEntry<V> {
61 fn new(value: V, ttl: Duration) -> Self {
62 Self {
63 value,
64 created_at: Instant::now(),
65 ttl,
66 }
67 }
68
69 fn is_expired(&self) -> bool {
70 self.created_at.elapsed() > self.ttl
71 }
72}
73
74#[derive(Debug)]
76struct PendingBatch<K, V>
77where
78 K: Send + Sync + Clone + Hash + Eq,
79 V: Send + Sync + Clone,
80{
81 keys: Vec<K>,
82 resolvers: Vec<tokio::sync::oneshot::Sender<Result<Option<V>>>>,
83 created_at: Instant,
84}
85
86impl<K, V> PendingBatch<K, V>
87where
88 K: Send + Sync + Clone + Hash + Eq,
89 V: Send + Sync + Clone,
90{
91 fn new() -> Self {
92 Self {
93 keys: Vec::new(),
94 resolvers: Vec::new(),
95 created_at: Instant::now(),
96 }
97 }
98
99 fn add_request(&mut self, key: K, resolver: tokio::sync::oneshot::Sender<Result<Option<V>>>) {
100 self.keys.push(key);
101 self.resolvers.push(resolver);
102 }
103
104 fn should_dispatch(&self, config: &DataLoaderConfig) -> bool {
105 self.keys.len() >= config.max_batch_size || self.created_at.elapsed() >= config.batch_delay
106 }
107}
108
109pub struct DataLoader<K, V>
111where
112 K: Send + Sync + Clone + Hash + Eq + 'static,
113 V: Send + Sync + Clone + 'static,
114{
115 batch_fn: Arc<dyn BatchLoadFn<K, V>>,
116 config: DataLoaderConfig,
117 cache: Arc<RwLock<HashMap<K, CachedEntry<V>>>>,
118 pending_batch: Arc<Mutex<Option<PendingBatch<K, V>>>>,
119 stats: Arc<RwLock<DataLoaderStats>>,
120 dispatcher_handle: Option<tokio::task::JoinHandle<()>>,
124}
125
126#[derive(Debug, Default, Clone)]
128pub struct DataLoaderStats {
129 pub requests_total: u64,
130 pub cache_hits: u64,
131 pub cache_misses: u64,
132 pub batches_dispatched: u64,
133 pub avg_batch_size: f64,
134 pub total_load_time: Duration,
135}
136
137impl DataLoaderStats {
138 pub fn cache_hit_ratio(&self) -> f64 {
139 if self.requests_total == 0 {
140 0.0
141 } else {
142 self.cache_hits as f64 / self.requests_total as f64
143 }
144 }
145
146 pub fn avg_load_time(&self) -> Duration {
147 if self.batches_dispatched == 0 {
148 Duration::from_secs(0)
149 } else {
150 self.total_load_time / self.batches_dispatched as u32
151 }
152 }
153}
154
155impl<K, V> DataLoader<K, V>
156where
157 K: Send + Sync + Clone + Hash + Eq + 'static,
158 V: Send + Sync + Clone + 'static,
159{
160 pub fn new(batch_fn: Arc<dyn BatchLoadFn<K, V>>) -> Self {
162 Self::with_config(batch_fn, DataLoaderConfig::default())
163 }
164
165 pub fn with_config(batch_fn: Arc<dyn BatchLoadFn<K, V>>, config: DataLoaderConfig) -> Self {
167 let mut loader = Self {
168 batch_fn,
169 config,
170 cache: Arc::new(RwLock::new(HashMap::new())),
171 pending_batch: Arc::new(Mutex::new(None)),
172 stats: Arc::new(RwLock::new(DataLoaderStats::default())),
173 dispatcher_handle: None,
174 };
175
176 loader.dispatcher_handle = Some(loader.start_batch_dispatcher());
178 loader
179 }
180
181 pub fn shutdown(&self) {
186 if let Some(handle) = &self.dispatcher_handle {
187 handle.abort();
188 }
189 }
190
191 pub async fn load(&self, key: K) -> Result<Option<V>> {
193 self.update_stats_request().await;
194
195 if self.config.enable_cache {
197 if let Some(cached_value) = self.get_from_cache(&key).await {
198 self.update_stats_cache_hit().await;
199 return Ok(Some(cached_value));
200 }
201 }
202
203 self.update_stats_cache_miss().await;
204
205 let (tx, rx) = tokio::sync::oneshot::channel();
207
208 self.add_to_batch(key, tx).await;
210
211 rx.await.map_err(|_| anyhow!("DataLoader batch failed"))?
213 }
214
215 pub async fn load_many(&self, keys: Vec<K>) -> Result<HashMap<K, V>> {
217 if keys.is_empty() {
218 return Ok(HashMap::new());
219 }
220
221 let mut results = HashMap::new();
223 let mut missing_keys = Vec::new();
224
225 if self.config.enable_cache {
226 for key in keys {
227 match self.get_from_cache(&key).await {
228 Some(cached_value) => {
229 results.insert(key, cached_value);
230 self.update_stats_cache_hit().await;
231 }
232 _ => {
233 missing_keys.push(key);
234 self.update_stats_cache_miss().await;
235 }
236 }
237 self.update_stats_request().await;
238 }
239 } else {
240 missing_keys = keys;
241 for _ in &missing_keys {
242 self.update_stats_request().await;
243 self.update_stats_cache_miss().await;
244 }
245 }
246
247 if missing_keys.is_empty() {
248 return Ok(results);
249 }
250
251 let batch_results = self.load_batch_direct(missing_keys).await?;
253 results.extend(batch_results);
254
255 Ok(results)
256 }
257
258 pub async fn prime(&self, key: K, value: V) {
260 if self.config.enable_cache {
261 self.set_in_cache(key, value).await;
262 }
263 }
264
265 pub async fn clear_cache(&self) {
267 if self.config.enable_cache {
268 let mut cache = self.cache.write().await;
269 cache.clear();
270 }
271 }
272
273 pub async fn get_stats(&self) -> DataLoaderStats {
275 self.stats.read().await.clone()
276 }
277
278 pub async fn clear_stats(&self) {
280 let mut stats = self.stats.write().await;
281 *stats = DataLoaderStats::default();
282 }
283
284 async fn get_from_cache(&self, key: &K) -> Option<V> {
285 let cache = self.cache.read().await;
286 if let Some(entry) = cache.get(key) {
287 if !entry.is_expired() {
288 return Some(entry.value.clone());
289 }
290 }
291 None
292 }
293
294 async fn set_in_cache(&self, key: K, value: V) {
295 let mut cache = self.cache.write().await;
296
297 self.evict_expired_entries(&mut cache);
299
300 if cache.len() >= self.config.max_cache_size {
302 if let Some(first_key) = cache.keys().next().cloned() {
304 cache.remove(&first_key);
305 }
306 }
307
308 cache.insert(key, CachedEntry::new(value, self.config.cache_ttl));
309 }
310
311 fn evict_expired_entries(&self, cache: &mut HashMap<K, CachedEntry<V>>) {
312 let expired_keys: Vec<K> = cache
313 .iter()
314 .filter(|(_, entry)| entry.is_expired())
315 .map(|(key, _)| key.clone())
316 .collect();
317
318 for key in expired_keys {
319 cache.remove(&key);
320 }
321 }
322
323 async fn add_to_batch(
324 &self,
325 key: K,
326 resolver: tokio::sync::oneshot::Sender<Result<Option<V>>>,
327 ) {
328 let mut pending = self.pending_batch.lock().await;
329 if pending.is_none() {
330 *pending = Some(PendingBatch::new());
331 }
332
333 if let Some(batch) = pending.as_mut() {
334 batch.add_request(key, resolver);
335 }
336 }
337
338 async fn load_batch_direct(&self, keys: Vec<K>) -> Result<HashMap<K, V>> {
339 let start_time = Instant::now();
340 let result = self.batch_fn.load(keys).await;
341 let load_time = start_time.elapsed();
342
343 self.update_stats_batch_dispatched(load_time).await;
345
346 match result {
347 Ok(loaded_values) => {
348 if self.config.enable_cache {
350 for (key, value) in &loaded_values {
351 self.set_in_cache(key.clone(), value.clone()).await;
352 }
353 }
354 Ok(loaded_values)
355 }
356 Err(e) => Err(e),
357 }
358 }
359
360 fn start_batch_dispatcher(&self) -> tokio::task::JoinHandle<()> {
361 let pending_batch = Arc::clone(&self.pending_batch);
362 let batch_fn = Arc::clone(&self.batch_fn);
363 let config = self.config.clone();
364 let cache = Arc::clone(&self.cache);
365 let stats = Arc::clone(&self.stats);
366
367 tokio::spawn(async move {
368 loop {
369 sleep(config.batch_delay).await;
370
371 let batch_to_dispatch = {
372 let mut pending = pending_batch.lock().await;
373 if let Some(batch) = pending.as_ref() {
374 if batch.should_dispatch(&config) {
375 pending.take()
376 } else {
377 None
378 }
379 } else {
380 None
381 }
382 };
383
384 if let Some(batch) = batch_to_dispatch {
385 Self::dispatch_batch(batch, &batch_fn, &config, &cache, &stats).await;
386 }
387 }
388 })
389 }
390
391 async fn dispatch_batch(
392 batch: PendingBatch<K, V>,
393 batch_fn: &Arc<dyn BatchLoadFn<K, V>>,
394 config: &DataLoaderConfig,
395 cache: &Arc<RwLock<HashMap<K, CachedEntry<V>>>>,
396 stats: &Arc<RwLock<DataLoaderStats>>,
397 ) {
398 let start_time = Instant::now();
399 let batch_size = batch.keys.len();
400
401 {
403 let mut stats = stats.write().await;
404 stats.batches_dispatched += 1;
405 stats.avg_batch_size =
406 (stats.avg_batch_size + batch_size as f64) / stats.batches_dispatched as f64;
407 }
408
409 match batch_fn.load(batch.keys.clone()).await {
410 Ok(loaded_values) => {
411 let load_time = start_time.elapsed();
412
413 {
415 let mut stats = stats.write().await;
416 stats.total_load_time += load_time;
417 }
418
419 if config.enable_cache {
421 let mut cache = cache.write().await;
422 for (key, value) in &loaded_values {
423 cache.insert(
424 key.clone(),
425 CachedEntry::new(value.clone(), config.cache_ttl),
426 );
427 }
428 }
429
430 for (key, resolver) in batch.keys.into_iter().zip(batch.resolvers) {
432 let result = loaded_values.get(&key).cloned();
433 let _ = resolver.send(Ok(result));
434 }
435 }
436 Err(e) => {
437 for resolver in batch.resolvers {
439 let _ = resolver.send(Err(anyhow!("Batch load failed: {}", e)));
440 }
441 }
442 }
443 }
444
445 async fn update_stats_request(&self) {
446 let mut stats = self.stats.write().await;
447 stats.requests_total += 1;
448 }
449
450 async fn update_stats_cache_hit(&self) {
451 let mut stats = self.stats.write().await;
452 stats.cache_hits += 1;
453 }
454
455 async fn update_stats_cache_miss(&self) {
456 let mut stats = self.stats.write().await;
457 stats.cache_misses += 1;
458 }
459
460 async fn update_stats_batch_dispatched(&self, load_time: Duration) {
461 let mut stats = self.stats.write().await;
462 stats.batches_dispatched += 1;
463 stats.total_load_time += load_time;
464 }
465}
466
467impl<K, V> Drop for DataLoader<K, V>
468where
469 K: Send + Sync + Clone + Hash + Eq + 'static,
470 V: Send + Sync + Clone + 'static,
471{
472 fn drop(&mut self) {
473 if let Some(handle) = &self.dispatcher_handle {
477 handle.abort();
478 }
479 }
480}
481
482pub struct DataLoaderFactory {
484 default_config: DataLoaderConfig,
485}
486
487impl DataLoaderFactory {
488 pub fn new() -> Self {
489 Self {
490 default_config: DataLoaderConfig::default(),
491 }
492 }
493
494 pub fn with_config(config: DataLoaderConfig) -> Self {
495 Self {
496 default_config: config,
497 }
498 }
499
500 pub fn create_subject_loader(
502 &self,
503 store: Arc<crate::RdfStore>,
504 ) -> DataLoader<String, serde_json::Value> {
505 let batch_fn = Arc::new(SubjectBatchLoader { store });
506 DataLoader::with_config(batch_fn, self.default_config.clone())
507 }
508
509 pub fn create_predicate_loader(
511 &self,
512 store: Arc<crate::RdfStore>,
513 ) -> DataLoader<String, Vec<String>> {
514 let batch_fn = Arc::new(PredicateBatchLoader { store });
515 DataLoader::with_config(batch_fn, self.default_config.clone())
516 }
517}
518
519impl Default for DataLoaderFactory {
520 fn default() -> Self {
521 Self::new()
522 }
523}
524
525struct SubjectBatchLoader {
527 store: Arc<crate::RdfStore>,
528}
529
530#[async_trait]
531impl BatchLoadFn<String, serde_json::Value> for SubjectBatchLoader {
532 async fn load(&self, keys: Vec<String>) -> Result<HashMap<String, serde_json::Value>> {
533 let mut results = HashMap::new();
534
535 for key in keys {
536 let query = format!("SELECT ?p ?o WHERE {{ <{key}> ?p ?o }}");
538
539 match self.store.query(&query) {
540 Ok(crate::QueryResults::Solutions(solutions)) => {
541 let mut triples = Vec::new();
542 for solution in solutions {
543 if let (Some(p), Some(o)) = (
544 solution.get(
545 &oxirs_core::model::Variable::new("p")
546 .expect("parse should succeed for valid input"),
547 ),
548 solution.get(
549 &oxirs_core::model::Variable::new("o")
550 .expect("parse should succeed for valid input"),
551 ),
552 ) {
553 triples.push(serde_json::json!({
554 "predicate": p.to_string(),
555 "object": o.to_string()
556 }));
557 }
558 }
559 results.insert(key, serde_json::json!(triples));
560 }
561 _ => {
562 results.insert(key, serde_json::json!([]));
564 }
565 }
566 }
567
568 Ok(results)
569 }
570}
571
572struct PredicateBatchLoader {
574 store: Arc<crate::RdfStore>,
575}
576
577#[async_trait]
578impl BatchLoadFn<String, Vec<String>> for PredicateBatchLoader {
579 async fn load(&self, keys: Vec<String>) -> Result<HashMap<String, Vec<String>>> {
580 let mut results = HashMap::new();
581
582 for key in keys {
583 let query = format!("SELECT DISTINCT ?s WHERE {{ ?s <{key}> ?o }}");
585
586 match self.store.query(&query) {
587 Ok(crate::QueryResults::Solutions(solutions)) => {
588 let mut subjects = Vec::new();
589 for solution in solutions {
590 if let Some(s) = solution.get(
591 &oxirs_core::model::Variable::new("s")
592 .expect("parse should succeed for valid input"),
593 ) {
594 subjects.push(s.to_string());
595 }
596 }
597 results.insert(key, subjects);
598 }
599 _ => {
600 results.insert(key, Vec::new());
601 }
602 }
603 }
604
605 Ok(results)
606 }
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612
613 struct TestBatchLoader;
614
615 #[async_trait]
616 impl BatchLoadFn<i32, String> for TestBatchLoader {
617 async fn load(&self, keys: Vec<i32>) -> Result<HashMap<i32, String>> {
618 sleep(Duration::from_millis(10)).await;
620
621 let mut results = HashMap::new();
622 for key in keys {
623 results.insert(key, format!("value_{key}"));
624 }
625 Ok(results)
626 }
627 }
628
629 #[tokio::test]
630 async fn test_dataloader_basic() {
631 let batch_fn = Arc::new(TestBatchLoader);
632 let loader = DataLoader::new(batch_fn);
633
634 let result = loader.load(1).await.expect("should succeed");
635 assert_eq!(result, Some("value_1".to_string()));
636
637 let stats = loader.get_stats().await;
638 assert_eq!(stats.requests_total, 1);
639 }
640
641 #[tokio::test]
642 async fn test_dataloader_caching() {
643 let batch_fn = Arc::new(TestBatchLoader);
644 let loader = DataLoader::new(batch_fn);
645
646 let result1 = loader.load(1).await.expect("should succeed");
648 assert_eq!(result1, Some("value_1".to_string()));
649
650 let result2 = loader.load(1).await.expect("should succeed");
652 assert_eq!(result2, Some("value_1".to_string()));
653
654 let stats = loader.get_stats().await;
655 assert_eq!(stats.requests_total, 2);
656 assert_eq!(stats.cache_hits, 1);
657 assert_eq!(stats.cache_misses, 1);
658 }
659
660 #[tokio::test]
661 async fn test_dataloader_load_many() {
662 let batch_fn = Arc::new(TestBatchLoader);
663 let loader = DataLoader::new(batch_fn);
664
665 let keys = vec![1, 2, 3];
666 let results = loader.load_many(keys).await.expect("should succeed");
667
668 assert_eq!(results.len(), 3);
669 assert_eq!(results.get(&1), Some(&"value_1".to_string()));
670 assert_eq!(results.get(&2), Some(&"value_2".to_string()));
671 assert_eq!(results.get(&3), Some(&"value_3".to_string()));
672 }
673
674 #[tokio::test]
675 async fn regression_dispatcher_task_aborted_on_shutdown() {
676 let batch_fn = Arc::new(TestBatchLoader);
677 let loader = DataLoader::new(batch_fn);
678
679 assert!(loader.dispatcher_handle.is_some());
681
682 loader.shutdown();
683 for _ in 0..20 {
685 if loader
686 .dispatcher_handle
687 .as_ref()
688 .map(|h| h.is_finished())
689 .unwrap_or(false)
690 {
691 break;
692 }
693 tokio::time::sleep(Duration::from_millis(10)).await;
694 }
695 assert!(
696 loader
697 .dispatcher_handle
698 .as_ref()
699 .expect("handle")
700 .is_finished(),
701 "dispatcher task must be aborted after shutdown"
702 );
703 }
704
705 #[tokio::test]
706 async fn regression_drop_runs_without_panic() {
707 {
710 let loader = DataLoader::new(Arc::new(TestBatchLoader));
711 let _ = loader.load(1).await.expect("load ok");
712 }
713 tokio::task::yield_now().await;
716 }
717}