aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 01:10:40 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 01:10:40 +0100
commit0c5d2791ab8d310bc201a3b0212c1004343abf82 (patch)
tree5aac11e9377995caf83b2174a318ed74437af6b2
parentevents: never join or move voice just to play TTS (diff)
downloadsenator-sperma-0c5d2791ab8d310bc201a3b0212c1004343abf82.tar.gz
senator-sperma-0c5d2791ab8d310bc201a3b0212c1004343abf82.zip
autocomplete: bound the in-memory caches
-rw-r--r--src/main.rs30
1 files changed, 24 insertions, 6 deletions
diff --git a/src/main.rs b/src/main.rs
index 8d11d76..56f81cb 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -25,6 +25,10 @@ use database::DbPool;
25/// How long a search cache entry stays fresh (10 seconds). 25/// How long a search cache entry stays fresh (10 seconds).
26const AC_SEARCH_CACHE_TTL: Duration = Duration::from_secs(10); 26const AC_SEARCH_CACHE_TTL: Duration = Duration::from_secs(10);
27 27
28/// Hard caps so autocomplete traffic cannot grow memory without bound.
29const AC_CACHE_MAX_ENTRIES: usize = 2_000;
30const AC_SEARCH_CACHE_MAX_ENTRIES: usize = 256;
31
28/// Per-user autocomplete stash: (UserId, filename) → SearchHit. 32/// Per-user autocomplete stash: (UserId, filename) → SearchHit.
29type AcCache = tokio::sync::Mutex<HashMap<(serenity::UserId, String), copyparty::SearchHit>>; 33type AcCache = tokio::sync::Mutex<HashMap<(serenity::UserId, String), copyparty::SearchHit>>;
30 34
@@ -70,7 +74,13 @@ impl Data {
70 key: String, 74 key: String,
71 hit: copyparty::SearchHit, 75 hit: copyparty::SearchHit,
72 ) { 76 ) {
73 self.ac_cache.lock().await.insert((user_id, key), hit); 77 let mut cache = self.ac_cache.lock().await;
78 if cache.len() >= AC_CACHE_MAX_ENTRIES {
79 // Entries are only useful for the command invocation that
80 // triggered the autocomplete, so dropping all of them is safe.
81 cache.clear();
82 }
83 cache.insert((user_id, key), hit);
74 } 84 }
75 85
76 pub async fn ac_lookup( 86 pub async fn ac_lookup(
@@ -88,9 +98,10 @@ impl Data {
88 /// Look up search cache for an autocomplete query. Returns `None` if 98 /// Look up search cache for an autocomplete query. Returns `None` if
89 /// uncached or stale. 99 /// uncached or stale.
90 pub async fn ac_search_cache_lookup(&self, query: &str) -> Option<Vec<copyparty::SearchHit>> { 100 pub async fn ac_search_cache_lookup(&self, query: &str) -> Option<Vec<copyparty::SearchHit>> {
91 let cache = self.ac_search_cache.lock().await; 101 let mut cache = self.ac_search_cache.lock().await;
92 let (hits, cached_at) = cache.get(query)?; 102 let (hits, cached_at) = cache.get(query)?;
93 if cached_at.elapsed() > AC_SEARCH_CACHE_TTL { 103 if cached_at.elapsed() > AC_SEARCH_CACHE_TTL {
104 cache.remove(query);
94 None 105 None
95 } else { 106 } else {
96 Some(hits.clone()) 107 Some(hits.clone())
@@ -99,10 +110,17 @@ impl Data {
99 110
100 /// Store search results into the shared cache. 111 /// Store search results into the shared cache.
101 pub async fn ac_search_cache_store(&self, query: String, hits: Vec<copyparty::SearchHit>) { 112 pub async fn ac_search_cache_store(&self, query: String, hits: Vec<copyparty::SearchHit>) {
102 self.ac_search_cache 113 let mut cache = self.ac_search_cache.lock().await;
103 .lock() 114 cache.retain(|_, (_, cached_at)| cached_at.elapsed() <= AC_SEARCH_CACHE_TTL);
104 .await 115 if cache.len() >= AC_SEARCH_CACHE_MAX_ENTRIES
105 .insert(query, (hits, Instant::now())); 116 && let Some(oldest) = cache
117 .iter()
118 .min_by_key(|(_, (_, cached_at))| *cached_at)
119 .map(|(query, _)| query.clone())
120 {
121 cache.remove(&oldest);
122 }
123 cache.insert(query, (hits, Instant::now()));
106 } 124 }
107} 125}
108 126