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