mod config;
mod operations;
mod preloader;
mod stats;
pub use config::{CacheConfig, CacheDependency, InvalidationStrategy};
pub use preloader::{keys, DefaultPreloader};
pub use stats::{CachePreloader, CacheStats, CachedData};
use crate::models::{Area, Project, Task};
use moka::future::Cache;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
pub struct ThingsCache {
tasks: Cache<String, CachedData<Vec<Task>>>,
projects: Cache<String, CachedData<Vec<Project>>>,
areas: Cache<String, CachedData<Vec<Area>>>,
search_results: Cache<String, CachedData<Vec<Task>>>,
stats: Arc<RwLock<CacheStats>>,
config: CacheConfig,
warming_entries: Arc<RwLock<HashMap<String, u32>>>,
preloader: Arc<RwLock<Option<Arc<dyn CachePreloader>>>>,
warming_task: Option<tokio::task::JoinHandle<()>>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{create_mock_areas, create_mock_projects, create_mock_tasks};
use std::time::Duration;
#[test]
fn test_cache_config_default() {
let config = CacheConfig::default();
assert_eq!(config.max_capacity, 1000);
assert_eq!(config.ttl, Duration::from_secs(300));
assert_eq!(config.tti, Duration::from_secs(60));
}
#[test]
fn test_cache_config_custom() {
let config = CacheConfig {
max_capacity: 500,
ttl: Duration::from_secs(600),
tti: Duration::from_secs(120),
invalidation_strategy: InvalidationStrategy::Hybrid,
enable_cache_warming: true,
warming_interval: Duration::from_secs(60),
max_warming_entries: 50,
};
assert_eq!(config.max_capacity, 500);
assert_eq!(config.ttl, Duration::from_secs(600));
assert_eq!(config.tti, Duration::from_secs(120));
}
#[test]
fn test_cached_data_creation() {
let data = vec![1, 2, 3];
let ttl = Duration::from_secs(60);
let cached = CachedData::new(data.clone(), ttl);
assert_eq!(cached.data, data);
assert!(cached.cached_at <= chrono::Utc::now());
assert!(cached.expires_at > cached.cached_at);
assert!(!cached.is_expired());
}
#[test]
fn test_cached_data_expiration() {
let data = vec![1, 2, 3];
let ttl = Duration::from_millis(1);
let cached = CachedData::new(data, ttl);
assert!(!cached.is_expired());
std::thread::sleep(Duration::from_millis(10));
}
#[test]
fn test_cached_data_serialization() {
let data = vec![1, 2, 3];
let ttl = Duration::from_secs(60);
let cached = CachedData::new(data, ttl);
let json = serde_json::to_string(&cached).unwrap();
assert!(json.contains("data"));
assert!(json.contains("cached_at"));
assert!(json.contains("expires_at"));
let deserialized: CachedData<Vec<i32>> = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.data, cached.data);
}
#[test]
fn test_cache_stats_default() {
let stats = CacheStats::default();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
assert_eq!(stats.entries, 0);
assert!((stats.hit_rate - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_cache_stats_calculation() {
let mut stats = CacheStats {
hits: 8,
misses: 2,
entries: 5,
hit_rate: 0.0,
..Default::default()
};
stats.calculate_hit_rate();
assert!((stats.hit_rate - 0.8).abs() < f64::EPSILON);
}
#[test]
fn test_cache_stats_zero_total() {
let mut stats = CacheStats {
hits: 0,
misses: 0,
entries: 0,
hit_rate: 0.0,
..Default::default()
};
stats.calculate_hit_rate();
assert!((stats.hit_rate - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_cache_stats_serialization() {
let stats = CacheStats {
hits: 10,
misses: 5,
entries: 3,
hit_rate: 0.67,
..Default::default()
};
let json = serde_json::to_string(&stats).unwrap();
assert!(json.contains("hits"));
assert!(json.contains("misses"));
assert!(json.contains("entries"));
assert!(json.contains("hit_rate"));
let deserialized: CacheStats = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.hits, stats.hits);
assert_eq!(deserialized.misses, stats.misses);
assert_eq!(deserialized.entries, stats.entries);
assert!((deserialized.hit_rate - stats.hit_rate).abs() < f64::EPSILON);
}
#[test]
fn test_cache_stats_clone() {
let stats = CacheStats {
hits: 5,
misses: 3,
entries: 2,
hit_rate: 0.625,
..Default::default()
};
let cloned = stats.clone();
assert_eq!(cloned.hits, stats.hits);
assert_eq!(cloned.misses, stats.misses);
assert_eq!(cloned.entries, stats.entries);
assert!((cloned.hit_rate - stats.hit_rate).abs() < f64::EPSILON);
}
#[test]
fn test_cache_stats_debug() {
let stats = CacheStats {
hits: 1,
misses: 1,
entries: 1,
hit_rate: 0.5,
..Default::default()
};
let debug_str = format!("{stats:?}");
assert!(debug_str.contains("CacheStats"));
assert!(debug_str.contains("hits"));
assert!(debug_str.contains("misses"));
}
#[tokio::test]
async fn test_cache_new() {
let config = CacheConfig::default();
let _cache = ThingsCache::new(&config);
}
#[tokio::test]
async fn test_cache_new_default() {
let _cache = ThingsCache::new_default();
}
#[tokio::test]
async fn test_cache_basic_operations() {
let cache = ThingsCache::new_default();
let result = cache.get_tasks("test", || async { Ok(vec![]) }).await;
assert!(result.is_ok());
let result = cache.get_tasks("test", || async { Ok(vec![]) }).await;
assert!(result.is_ok());
let stats = cache.get_stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
}
#[tokio::test]
async fn test_cache_tasks_with_data() {
let cache = ThingsCache::new_default();
let mock_tasks = create_mock_tasks();
let result = cache
.get_tasks("tasks", || async { Ok(mock_tasks.clone()) })
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), mock_tasks.len());
let result = cache.get_tasks("tasks", || async { Ok(vec![]) }).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), mock_tasks.len());
let stats = cache.get_stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
}
#[tokio::test]
async fn test_cache_projects() {
let cache = ThingsCache::new_default();
let mock_projects = create_mock_projects();
let result = cache
.get_projects("projects", || async { Ok(mock_projects.clone()) })
.await;
assert!(result.is_ok());
let result = cache
.get_projects("projects", || async { Ok(vec![]) })
.await;
assert!(result.is_ok());
let stats = cache.get_stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
}
#[tokio::test]
async fn test_cache_areas() {
let cache = ThingsCache::new_default();
let mock_areas = create_mock_areas();
let result = cache
.get_areas("areas", || async { Ok(mock_areas.clone()) })
.await;
assert!(result.is_ok());
let result = cache.get_areas("areas", || async { Ok(vec![]) }).await;
assert!(result.is_ok());
let stats = cache.get_stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
}
#[tokio::test]
async fn test_cache_search_results() {
let cache = ThingsCache::new_default();
let mock_tasks = create_mock_tasks();
let result = cache
.get_search_results("search:test", || async { Ok(mock_tasks.clone()) })
.await;
assert!(result.is_ok());
let result = cache
.get_search_results("search:test", || async { Ok(vec![]) })
.await;
assert!(result.is_ok());
let stats = cache.get_stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
}
#[tokio::test]
async fn test_cache_fetcher_error() {
let cache = ThingsCache::new_default();
let result = cache
.get_tasks("error", || async { Err(anyhow::anyhow!("Test error")) })
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Test error"));
let stats = cache.get_stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 1);
}
#[tokio::test]
async fn test_cache_expiration() {
let config = CacheConfig {
max_capacity: 100,
ttl: Duration::from_millis(10),
tti: Duration::from_millis(5),
invalidation_strategy: InvalidationStrategy::Hybrid,
enable_cache_warming: true,
warming_interval: Duration::from_secs(60),
max_warming_entries: 50,
};
let cache = ThingsCache::new(&config);
let _ = cache.get_tasks("test", || async { Ok(vec![]) }).await;
tokio::time::sleep(Duration::from_millis(20)).await;
let _ = cache.get_tasks("test", || async { Ok(vec![]) }).await;
let stats = cache.get_stats();
assert_eq!(stats.misses, 2);
}
#[tokio::test]
async fn test_cache_invalidate_all() {
let cache = ThingsCache::new_default();
let _ = cache.get_tasks("tasks", || async { Ok(vec![]) }).await;
let _ = cache
.get_projects("projects", || async { Ok(vec![]) })
.await;
let _ = cache.get_areas("areas", || async { Ok(vec![]) }).await;
let _ = cache
.get_search_results("search", || async { Ok(vec![]) })
.await;
cache.invalidate_all();
let _ = cache.get_tasks("tasks", || async { Ok(vec![]) }).await;
let _ = cache
.get_projects("projects", || async { Ok(vec![]) })
.await;
let _ = cache.get_areas("areas", || async { Ok(vec![]) }).await;
let _ = cache
.get_search_results("search", || async { Ok(vec![]) })
.await;
let stats = cache.get_stats();
assert_eq!(stats.misses, 8); }
#[tokio::test]
async fn test_cache_invalidate_specific() {
let cache = ThingsCache::new_default();
let _ = cache.get_tasks("key1", || async { Ok(vec![]) }).await;
let _ = cache.get_tasks("key2", || async { Ok(vec![]) }).await;
cache.invalidate("key1").await;
let _ = cache.get_tasks("key1", || async { Ok(vec![]) }).await;
let _ = cache.get_tasks("key2", || async { Ok(vec![]) }).await;
let stats = cache.get_stats();
assert_eq!(stats.hits, 1); assert_eq!(stats.misses, 3); }
#[tokio::test]
async fn test_cache_reset_stats() {
let cache = ThingsCache::new_default();
let _ = cache.get_tasks("test", || async { Ok(vec![]) }).await;
let _ = cache.get_tasks("test", || async { Ok(vec![]) }).await;
let stats_before = cache.get_stats();
assert!(stats_before.hits > 0 || stats_before.misses > 0);
cache.reset_stats();
let stats_after = cache.get_stats();
assert_eq!(stats_after.hits, 0);
assert_eq!(stats_after.misses, 0);
assert!((stats_after.hit_rate - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_cache_keys_inbox() {
assert_eq!(keys::inbox(None), "inbox:all");
assert_eq!(keys::inbox(Some(10)), "inbox:10");
assert_eq!(keys::inbox(Some(0)), "inbox:0");
}
#[test]
fn test_cache_keys_today() {
assert_eq!(keys::today(None), "today:all");
assert_eq!(keys::today(Some(5)), "today:5");
assert_eq!(keys::today(Some(100)), "today:100");
}
#[test]
fn test_cache_keys_projects() {
assert_eq!(keys::projects(None), "projects:all");
assert_eq!(keys::projects(Some("uuid-123")), "projects:uuid-123");
assert_eq!(keys::projects(Some("")), "projects:");
}
#[test]
fn test_cache_keys_areas() {
assert_eq!(keys::areas(), "areas:all");
}
#[test]
fn test_cache_keys_search() {
assert_eq!(keys::search("test query", None), "search:test query:all");
assert_eq!(keys::search("test query", Some(10)), "search:test query:10");
assert_eq!(keys::search("", Some(5)), "search::5");
}
#[tokio::test]
async fn test_cache_multiple_keys() {
let cache = ThingsCache::new_default();
let mock_tasks1 = create_mock_tasks();
let mock_tasks2 = create_mock_tasks();
let _ = cache
.get_tasks("key1", || async { Ok(mock_tasks1.clone()) })
.await;
let _ = cache
.get_tasks("key2", || async { Ok(mock_tasks2.clone()) })
.await;
let result1 = cache
.get_tasks("key1", || async { Ok(vec![]) })
.await
.unwrap();
let result2 = cache
.get_tasks("key2", || async { Ok(vec![]) })
.await
.unwrap();
assert_eq!(result1.len(), mock_tasks1.len());
assert_eq!(result2.len(), mock_tasks2.len());
let stats = cache.get_stats();
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 2);
}
#[tokio::test]
async fn test_cache_entry_count() {
let cache = ThingsCache::new_default();
let stats = cache.get_stats();
assert_eq!(stats.entries, 0);
let _ = cache.get_tasks("tasks", || async { Ok(vec![]) }).await;
let _ = cache
.get_projects("projects", || async { Ok(vec![]) })
.await;
let _ = cache.get_areas("areas", || async { Ok(vec![]) }).await;
let _ = cache
.get_search_results("search", || async { Ok(vec![]) })
.await;
let stats = cache.get_stats();
let _ = stats.entries;
}
#[tokio::test]
async fn test_cache_hit_rate_calculation() {
let cache = ThingsCache::new_default();
let _ = cache.get_tasks("test", || async { Ok(vec![]) }).await; let _ = cache.get_tasks("test", || async { Ok(vec![]) }).await; let _ = cache.get_tasks("test", || async { Ok(vec![]) }).await;
let stats = cache.get_stats();
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 1);
assert!((stats.hit_rate - 2.0 / 3.0).abs() < 0.001);
}
#[test]
fn test_cache_dependency_matches_rules() {
use crate::models::ThingsId;
let id_a = ThingsId::new_v4();
let id_b = ThingsId::new_v4();
let dep_concrete = CacheDependency {
entity_type: "task".to_string(),
entity_id: Some(id_a.clone()),
invalidating_operations: vec!["task_updated".to_string()],
};
let dep_wildcard = CacheDependency {
entity_type: "task".to_string(),
entity_id: None,
invalidating_operations: vec!["task_updated".to_string()],
};
assert!(dep_concrete.matches("task", Some(&id_a)));
assert!(!dep_concrete.matches("task", Some(&id_b)));
assert!(dep_concrete.matches("task", None));
assert!(dep_wildcard.matches("task", Some(&id_a)));
assert!(!dep_concrete.matches("project", Some(&id_a)));
assert!(dep_concrete.matches_operation("task_updated"));
assert!(!dep_concrete.matches_operation("task_deleted"));
}
fn task_with_ids(
uuid: crate::models::ThingsId,
project: Option<crate::models::ThingsId>,
area: Option<crate::models::ThingsId>,
) -> crate::models::Task {
let mut t = create_mock_tasks().into_iter().next().unwrap();
t.uuid = uuid;
t.project_uuid = project;
t.area_uuid = area;
t
}
#[tokio::test]
async fn test_invalidate_by_entity_selective_by_id() {
use crate::models::ThingsId;
let cache = ThingsCache::new_default();
let id_x = ThingsId::new_v4();
let id_y = ThingsId::new_v4();
let id_x2 = id_x.clone();
let id_y2 = id_y.clone();
cache
.get_tasks("key_x", || async {
Ok(vec![task_with_ids(id_x2, None, None)])
})
.await
.unwrap();
cache
.get_tasks("key_y", || async {
Ok(vec![task_with_ids(id_y2, None, None)])
})
.await
.unwrap();
let removed = cache.invalidate_by_entity("task", Some(&id_x)).await;
assert_eq!(removed, 1, "only the entry depending on id_x should evict");
cache.tasks.run_pending_tasks().await;
assert!(cache.tasks.get("key_x").await.is_none());
assert!(cache.tasks.get("key_y").await.is_some());
}
#[tokio::test]
async fn test_invalidate_by_entity_wildcard_id() {
use crate::models::ThingsId;
let cache = ThingsCache::new_default();
let id_x = ThingsId::new_v4();
let id_y = ThingsId::new_v4();
let id_x2 = id_x.clone();
let id_y2 = id_y.clone();
cache
.get_tasks("key_x", || async {
Ok(vec![task_with_ids(id_x2, None, None)])
})
.await
.unwrap();
cache
.get_tasks("key_y", || async {
Ok(vec![task_with_ids(id_y2, None, None)])
})
.await
.unwrap();
let removed = cache.invalidate_by_entity("task", None).await;
assert_eq!(removed, 2);
cache.tasks.run_pending_tasks().await;
assert!(cache.tasks.get("key_x").await.is_none());
assert!(cache.tasks.get("key_y").await.is_none());
}
#[tokio::test]
async fn test_invalidate_by_entity_leaves_unrelated_caches() {
use crate::models::ThingsId;
let cache = ThingsCache::new_default();
let task_id = ThingsId::new_v4();
let project_id = ThingsId::new_v4();
let task_id2 = task_id.clone();
let project_id2 = project_id.clone();
cache
.get_tasks("inbox", || async {
Ok(vec![task_with_ids(task_id2, Some(project_id2), None)])
})
.await
.unwrap();
let mut p = create_mock_projects().into_iter().next().unwrap();
p.uuid = project_id;
cache
.get_projects("projects:all", || async { Ok(vec![p]) })
.await
.unwrap();
let removed = cache.invalidate_by_entity("task", Some(&task_id)).await;
assert_eq!(removed, 1);
cache.tasks.run_pending_tasks().await;
cache.projects.run_pending_tasks().await;
assert!(cache.tasks.get("inbox").await.is_none());
assert!(cache.projects.get("projects:all").await.is_some());
}
#[tokio::test]
async fn test_invalidate_by_operation_selective() {
use crate::models::ThingsId;
let cache = ThingsCache::new_default();
let task_id = ThingsId::new_v4();
let area_id = ThingsId::new_v4();
let task_id2 = task_id.clone();
cache
.get_tasks("inbox", || async {
Ok(vec![task_with_ids(task_id2, None, None)])
})
.await
.unwrap();
let mut a = create_mock_areas().into_iter().next().unwrap();
a.uuid = area_id;
cache
.get_areas("areas:all", || async { Ok(vec![a]) })
.await
.unwrap();
let removed = cache.invalidate_by_operation("task_updated").await;
assert_eq!(removed, 1);
cache.tasks.run_pending_tasks().await;
cache.areas.run_pending_tasks().await;
assert!(cache.tasks.get("inbox").await.is_none());
assert!(cache.areas.get("areas:all").await.is_some());
}
struct RecordingPreloader {
predictions: Arc<RwLock<Vec<(String, u32)>>>,
seen_predict: Arc<RwLock<Vec<String>>>,
seen_warm: Arc<RwLock<Vec<String>>>,
}
impl RecordingPreloader {
fn new(predictions: Vec<(String, u32)>) -> Self {
Self {
predictions: Arc::new(RwLock::new(predictions)),
seen_predict: Arc::new(RwLock::new(Vec::new())),
seen_warm: Arc::new(RwLock::new(Vec::new())),
}
}
}
impl CachePreloader for RecordingPreloader {
fn predict(&self, accessed_key: &str) -> Vec<(String, u32)> {
self.seen_predict.write().push(accessed_key.to_string());
self.predictions.read().clone()
}
fn warm(&self, key: &str) {
self.seen_warm.write().push(key.to_string());
}
}
#[tokio::test]
async fn test_default_preloader_predict_rules() {
let f = tempfile::NamedTempFile::new().unwrap();
crate::test_utils::create_test_database(f.path())
.await
.unwrap();
let db = Arc::new(crate::ThingsDatabase::new(f.path()).await.unwrap());
let cache = Arc::new(ThingsCache::new_default());
let pre = DefaultPreloader::new(&cache, db);
assert_eq!(pre.predict("inbox:all"), vec![("today:all".to_string(), 8)]);
assert_eq!(
pre.predict("today:all"),
vec![("inbox:all".to_string(), 10)]
);
assert_eq!(
pre.predict("areas:all"),
vec![("projects:all".to_string(), 7)]
);
assert!(pre.predict("search:foo").is_empty());
}
#[tokio::test]
async fn test_predict_fires_on_get_tasks_miss_and_hit() {
let cache = ThingsCache::new_default();
let pre = Arc::new(RecordingPreloader::new(vec![]));
cache.set_preloader(pre.clone());
cache
.get_tasks("inbox:all", || async { Ok(vec![]) })
.await
.unwrap();
cache
.get_tasks("inbox:all", || async { Ok(vec![]) })
.await
.unwrap();
let seen = pre.seen_predict.read().clone();
assert_eq!(seen, vec!["inbox:all".to_string(), "inbox:all".to_string()]);
}
#[tokio::test]
async fn test_predict_enqueues_warming() {
let cache = ThingsCache::new_default();
let pre = Arc::new(RecordingPreloader::new(vec![("today:all".to_string(), 5)]));
cache.set_preloader(pre);
cache
.get_tasks("inbox:all", || async { Ok(vec![]) })
.await
.unwrap();
let entries = cache.warming_entries.read();
assert_eq!(entries.get("today:all"), Some(&5));
}
#[tokio::test]
async fn test_no_preloader_is_noop() {
let config = CacheConfig {
warming_interval: Duration::from_millis(20),
..Default::default()
};
let cache = ThingsCache::new(&config);
cache
.get_tasks("inbox:all", || async { Ok(vec![]) })
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(80)).await;
let stats = cache.get_stats();
assert_eq!(stats.warmed_keys, 0);
assert_eq!(stats.warming_runs, 0);
}
#[tokio::test]
async fn test_warming_loop_invokes_warm() {
let config = CacheConfig {
warming_interval: Duration::from_millis(20),
max_warming_entries: 10,
..Default::default()
};
let cache = ThingsCache::new(&config);
let pre = Arc::new(RecordingPreloader::new(vec![]));
cache.set_preloader(pre.clone());
cache.add_to_warming("inbox:all".to_string(), 10);
cache.add_to_warming("today:all".to_string(), 8);
tokio::time::sleep(Duration::from_millis(100)).await;
let warmed = pre.seen_warm.read().clone();
assert!(warmed.contains(&"inbox:all".to_string()));
assert!(warmed.contains(&"today:all".to_string()));
assert!(cache.warming_entries.read().is_empty());
let stats = cache.get_stats();
assert!(stats.warming_runs >= 1);
assert!(stats.warmed_keys >= 2);
}
#[tokio::test]
async fn test_clear_preloader_disables_predict() {
let cache = ThingsCache::new_default();
let pre = Arc::new(RecordingPreloader::new(vec![("today:all".to_string(), 5)]));
cache.set_preloader(pre.clone());
cache
.get_tasks("inbox:all", || async { Ok(vec![]) })
.await
.unwrap();
assert_eq!(pre.seen_predict.read().len(), 1);
cache.clear_preloader();
cache
.get_tasks("inbox:all", || async { Ok(vec![]) })
.await
.unwrap();
assert_eq!(pre.seen_predict.read().len(), 1);
}
#[tokio::test]
async fn test_default_preloader_warms_via_db() {
let f = tempfile::NamedTempFile::new().unwrap();
crate::test_utils::create_test_database(f.path())
.await
.unwrap();
let db = Arc::new(crate::ThingsDatabase::new(f.path()).await.unwrap());
let config = CacheConfig {
warming_interval: Duration::from_millis(20),
..Default::default()
};
let cache = Arc::new(ThingsCache::new(&config));
cache.set_preloader(DefaultPreloader::new(&cache, Arc::clone(&db)));
cache
.get_tasks("inbox:all", || async {
db.get_inbox(None).await.map_err(anyhow::Error::from)
})
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(150)).await;
let result = cache
.get_tasks("today:all", || async {
panic!("today:all should be served from warmed cache, not fetched")
})
.await
.unwrap();
let expected = db.get_today(None).await.unwrap();
assert_eq!(result.len(), expected.len());
}
#[tokio::test]
async fn test_default_preloader_weak_ref_breaks_cycle() {
let f = tempfile::NamedTempFile::new().unwrap();
crate::test_utils::create_test_database(f.path())
.await
.unwrap();
let db = Arc::new(crate::ThingsDatabase::new(f.path()).await.unwrap());
let cache = Arc::new(ThingsCache::new_default());
let preloader = DefaultPreloader::new(&cache, db);
let preloader_dyn: Arc<dyn CachePreloader> = preloader.clone();
drop(cache);
preloader_dyn.warm("inbox:all");
tokio::time::sleep(Duration::from_millis(20)).await;
}
}