1use std::collections::{HashMap, HashSet, VecDeque};
5use std::sync::{Arc, Mutex, OnceLock, Weak};
6use std::time::{Duration, Instant};
7
8use serde_json::Value;
9use sofie_core::{Result, Shown, Uuid, unpoisoned};
10use sofie_db::{Announced, Db, Reach, Reply, ReplyData, Topic, Topics};
11use tokio::sync::broadcast::Receiver;
12use tokio::sync::broadcast::error::{RecvError, TryRecvError};
13
14use crate::context::Matched;
15use crate::target::Target;
16use crate::{Area, PageRoute};
17
18const MAX_ENTRIES: usize = 10_000;
20const MAX_TIME: Duration = Duration::from_hours(1);
21
22impl<A: Area> PageRoute<'_, A> {
23 pub fn cached(&mut self, time: Duration) -> &mut Self {
27 self.rule(|rules| rules.cached = Some(time.min(MAX_TIME)))
28 }
29}
30
31#[derive(PartialEq, Eq, Hash)]
34struct Key {
35 target: String,
36 reach: Reach,
37 principal: Option<Uuid>,
38}
39
40struct Entry {
42 reply: Arc<Reply>,
43 topics: Topics,
44 until: Instant,
45}
46
47const RECENT_CHANGES: usize = 256;
49
50#[derive(Default)]
51struct Kept {
52 entries: HashMap<Arc<Key>, Entry>,
53 by_topic: HashMap<Topic, HashSet<Arc<Key>>>,
54 recent: VecDeque<Announced>,
55 heard: u64,
56 changes: Option<Receiver<Announced>>,
57}
58
59#[derive(Default)]
60pub(crate) struct ScreenCache {
61 kept: Arc<Mutex<Kept>>,
62 listening: OnceLock<()>,
63}
64
65impl ScreenCache {
66 pub(crate) async fn read(
67 &self,
68 db: &Db,
69 target: &Target,
70 time: Duration,
71 matched: Matched,
72 asked: &http::Uri,
73 ) -> (Result<Reply>, Topics) {
74 self.listen(db);
75 let key = Key {
76 target: asked.to_string(),
77 reach: matched.reach,
78 principal: matched.session.principal(),
79 };
80 let hearing = sofie_db::hears_other_instances(db);
81 let before = match unpoisoned::lock(&self.kept).hit(&key, hearing) {
82 Ok((reply, topics)) => return (Ok(shared(reply)), topics),
83 Err(heard) => heard,
84 };
85 let load = (target.endpoint)(db.clone(), matched);
86 let (loaded, topics) = sofie_db::track_topics(load).await;
87 let Ok(reply) = loaded else {
88 return (loaded, topics);
89 };
90 let reply = Arc::new(reply);
91 if topics != Topics::Every {
92 let entry = Entry {
93 reply: Arc::clone(&reply),
94 topics: topics.clone(),
95 until: Instant::now() + time,
96 };
97 let hearing = sofie_db::hears_other_instances(db);
98 unpoisoned::lock(&self.kept).keep(key, entry, before, hearing);
99 }
100 (Ok(shared(reply)), topics)
101 }
102
103 fn listen(&self, db: &Db) {
104 self.listening.get_or_init(|| {
105 unpoisoned::lock(&self.kept).changes = Some(sofie_db::committed_changes(db));
106 let woken = sofie_db::committed_changes(db);
107 tokio::spawn(drain_on_change(Arc::downgrade(&self.kept), woken));
108 });
109 }
110}
111
112impl Kept {
113 fn catch_up(&mut self, hearing: bool) {
116 self.drain();
117 if !hearing {
118 self.hear(Announced::MaybeMissed);
119 }
120 }
121
122 fn drain(&mut self) {
123 while let Some(changes) = &mut self.changes {
124 let announced = match changes.try_recv() {
125 Ok(announced) => announced,
126 Err(TryRecvError::Lagged(_)) => Announced::MaybeMissed,
127 Err(TryRecvError::Empty | TryRecvError::Closed) => return,
128 };
129 self.hear(announced);
130 }
131 }
132
133 fn hit(&mut self, key: &Key, hearing: bool) -> Result<(Arc<Reply>, Topics), u64> {
135 self.catch_up(hearing);
136 self.entries
137 .get(key)
138 .filter(|entry| entry.until > Instant::now())
139 .map(|entry| (Arc::clone(&entry.reply), entry.topics.clone()))
140 .ok_or(self.heard)
141 }
142
143 fn keep(&mut self, key: Key, entry: Entry, before: u64, hearing: bool) {
145 self.catch_up(hearing);
146 if self.changed_since(before, &entry.topics, key.reach) {
147 return;
148 }
149 if self.entries.len() >= MAX_ENTRIES {
150 self.make_room(Instant::now());
151 }
152 let key = Arc::new(key);
153 self.remove(&key);
154 for topic in read(&entry.topics) {
155 self.by_topic
156 .entry(*topic)
157 .or_default()
158 .insert(Arc::clone(&key));
159 }
160 self.entries.insert(key, entry);
161 }
162
163 fn changed_since(&self, before: u64, topics: &Topics, reach: Reach) -> bool {
164 let Ok(missed) = usize::try_from(self.heard - before) else {
165 return true;
166 };
167 if missed > self.recent.len() {
168 return true;
169 }
170 self.recent
171 .iter()
172 .rev()
173 .take(missed)
174 .any(|announced| announced.concerns(topics, reach))
175 }
176
177 fn hear(&mut self, announced: Announced) {
178 self.heard += 1;
179 match &announced {
180 Announced::Changed(changes) => {
181 for change in changes.iter() {
182 let concerned: Vec<Arc<Key>> = self
183 .by_topic
184 .get(&change.topic)
185 .into_iter()
186 .flatten()
187 .filter(|key| change.reaches(key.reach))
188 .cloned()
189 .collect();
190 for key in concerned {
191 self.remove(&key);
192 }
193 }
194 }
195 Announced::MaybeMissed => {
196 self.entries.clear();
197 self.by_topic.clear();
198 }
199 }
200 if self.recent.len() == RECENT_CHANGES {
201 self.recent.pop_front();
202 }
203 self.recent.push_back(announced);
204 }
205
206 fn remove(&mut self, key: &Key) {
207 let Some(entry) = self.entries.remove(key) else {
208 return;
209 };
210 for topic in read(&entry.topics) {
211 if let Some(keys) = self.by_topic.get_mut(topic) {
212 keys.remove(key);
213 if keys.is_empty() {
214 self.by_topic.remove(topic);
215 }
216 }
217 }
218 }
219
220 fn make_room(&mut self, now: Instant) {
223 self.drop_until(now);
224 if self.entries.len() < MAX_ENTRIES {
225 return;
226 }
227 let mut untils: Vec<Instant> = self.entries.values().map(|entry| entry.until).collect();
228 let (_, cutoff, _) = untils.select_nth_unstable(MAX_ENTRIES / 16);
229 self.drop_until(*cutoff);
230 }
231
232 fn drop_until(&mut self, cutoff: Instant) {
233 let dropped: Vec<Arc<Key>> = self
234 .entries
235 .iter()
236 .filter(|(_, entry)| entry.until <= cutoff)
237 .map(|(key, _)| Arc::clone(key))
238 .collect();
239 for key in dropped {
240 self.remove(&key);
241 }
242 }
243}
244
245fn read(topics: &Topics) -> impl Iterator<Item = &Topic> {
246 match topics {
247 Topics::Of(read) => Some(read),
248 Topics::Every => None,
249 }
250 .into_iter()
251 .flatten()
252}
253
254async fn drain_on_change(kept: Weak<Mutex<Kept>>, mut woken: Receiver<Announced>) {
257 loop {
258 if let Err(RecvError::Closed) = woken.recv().await {
259 return;
260 }
261 while !matches!(
262 woken.try_recv(),
263 Err(TryRecvError::Empty | TryRecvError::Closed)
264 ) {}
265 let Some(kept) = kept.upgrade() else {
266 return;
267 };
268 unpoisoned::lock(&kept).drain();
269 }
270}
271
272fn shared(reply: Arc<Reply>) -> Reply {
273 Reply::Data(Box::new(Shared(reply)))
274}
275
276struct Shared(Arc<Reply>);
278
279impl ReplyData for Shared {
280 fn write_json(&self, out: &mut Vec<u8>) -> serde_json::Result<()> {
281 self.0.write_json(out).map_err(json_error)
282 }
283
284 fn to_json(&self) -> serde_json::Result<Value> {
285 self.0.to_json().map_err(json_error)
286 }
287
288 fn type_name(&self) -> &'static str {
289 match self.0.as_ref() {
290 Reply::Data(data) => data.type_name(),
291 _ => std::any::type_name::<Reply>(),
292 }
293 }
294
295 fn shown(&self) -> Option<Shown<'_>> {
296 self.0.shown()
297 }
298}
299
300fn json_error(error: sofie_core::Error) -> serde_json::Error {
301 serde_json::Error::io(std::io::Error::other(error))
302}
303
304#[cfg(test)]
305mod tests {
306 use std::collections::BTreeSet;
307
308 use sofie_db::{Readers, TableChange};
309 use tokio::sync::broadcast;
310
311 use super::*;
312
313 fn key(target: &str) -> Key {
314 Key {
315 target: target.to_owned(),
316 reach: Reach::All,
317 principal: None,
318 }
319 }
320
321 fn entry(tables: &[&str], time: Duration) -> Entry {
322 let topics = tables
323 .iter()
324 .map(|table| Topic::named("shop", table))
325 .collect::<BTreeSet<_>>();
326 Entry {
327 reply: Arc::new(Reply::Empty),
328 topics: Topics::Of(topics),
329 until: Instant::now() + time,
330 }
331 }
332
333 fn changed(table: &str) -> Announced {
334 Announced::Changed(Arc::from([TableChange {
335 topic: Topic::named("shop", table),
336 readers: Readers::Anyone,
337 }]))
338 }
339
340 #[test]
341 fn a_hit_hears_a_change_already_sent_and_nothing_is_kept_while_others_go_unheard() {
342 let (sender, receiver) = broadcast::channel(16);
343 let mut kept = Kept {
344 changes: Some(receiver),
345 ..Kept::default()
346 };
347 kept.keep(key("/orders"), entry(&["order"], MAX_TIME), 0, true);
348 sender.send(changed("order")).unwrap();
349
350 assert!(kept.hit(&key("/orders"), true).is_err());
351
352 let before = kept.heard;
353 kept.keep(key("/orders"), entry(&["order"], MAX_TIME), before, true);
354 assert!(kept.hit(&key("/orders"), false).is_err());
355 let before = kept.heard;
356 kept.keep(key("/orders"), entry(&["order"], MAX_TIME), before, false);
357 assert!(kept.hit(&key("/orders"), true).is_err());
358 }
359
360 #[test]
361 fn a_reply_loaded_across_a_change_is_kept_unless_the_change_concerns_it() {
362 let mut kept = Kept::default();
363 let before = kept.heard;
364 kept.hear(changed("customer"));
365
366 kept.keep(key("/orders"), entry(&["order"], MAX_TIME), before, true);
367 assert!(kept.hit(&key("/orders"), true).is_ok());
368
369 kept.hear(changed("order"));
370 kept.keep(
371 key("/orders?page=2"),
372 entry(&["order"], MAX_TIME),
373 before,
374 true,
375 );
376 assert!(kept.hit(&key("/orders?page=2"), true).is_err());
377
378 kept.hear(Announced::MaybeMissed);
379 kept.keep(
380 key("/customers"),
381 entry(&["customer"], MAX_TIME),
382 before + 2,
383 true,
384 );
385 assert!(kept.hit(&key("/customers"), true).is_err());
386 }
387
388 #[test]
389 fn a_change_forgets_only_the_replies_that_read_its_table() {
390 let mut kept = Kept::default();
391 kept.keep(
392 key("/orders"),
393 entry(&["order", "customer"], MAX_TIME),
394 0,
395 true,
396 );
397 kept.keep(key("/products"), entry(&["product"], MAX_TIME), 0, true);
398
399 kept.hear(changed("customer"));
400
401 assert!(kept.hit(&key("/orders"), true).is_err());
402 assert!(kept.hit(&key("/products"), true).is_ok());
403 assert!(!kept.by_topic.contains_key(&Topic::named("shop", "order")));
404 }
405
406 #[test]
407 fn a_full_cache_forgets_the_entries_closest_to_expiring() {
408 let mut kept = Kept::default();
409 let page = |index: usize| key(&format!("/orders?page={index}"));
410 for index in 0..MAX_ENTRIES {
411 let time = Duration::from_millis(u64::try_from(index).unwrap() + 1);
412 kept.keep(page(index), entry(&["order"], time), 0, true);
413 }
414
415 kept.keep(page(MAX_ENTRIES), entry(&["order"], MAX_TIME), 0, true);
416
417 assert!(kept.hit(&page(MAX_ENTRIES), true).is_ok());
418 assert!(kept.hit(&page(0), true).is_err());
419 assert!(kept.hit(&page(MAX_ENTRIES - 1), true).is_ok());
420 }
421}