keep.rsannotatedkeep.rssource70 lines · 3.4 KB · raw

Keeping the service's copy of the parents' log as long as the household says: the same setting a device applies to its own (HouseholdConfig::log_retention_days; none, the default, keeps everything). The lines older than that are emptied (never removed: the counts devices sync by are made of the lines' numbers), the pictures only they showed are deleted, and a Cleared line is written to the log as the device service, which every device pulls like any other line, so nothing leaves the log unannounced.

7use log::{error, info, warn};
8use whiskers_core::{Entry, Event, Log};
9use whiskers_ports::Hub;
10use whiskers_store::{SqliteHouseholdHub, SqliteLog, Store};

The name the service writes its own lines under.

13pub const SERVICE_DEVICE: &str = "service";

Clears what the household's setting says is too old, as of now_ms. Returns how many lines were emptied.

16pub fn apply_retention(store: &Store, household: &SqliteHouseholdHub, now_ms: u64) -> usize {
17    let Ok(doc) = whiskers_ports::run_ready(household.current()) else {
18        error!("retention: the household's choices cannot be read");
19        return 0;
20    };
21    let Some(days) = doc.config().log_retention_days else { return 0 };
22    let cutoff = now_ms.saturating_sub(u64::from(days) * 86_400_000);
23    let log = SqliteLog::new(store.clone(), SERVICE_DEVICE);
24    match log.expire_before(cutoff) {
25        Ok(0) => 0,
26        Ok(n) => {
27            info!("retention: {n} line(s) older than {days} day(s) were emptied");
28            if let Err(e) = log.clone().append(&Entry::new(now_ms, Event::Cleared { lines: n, older_than_ms: cutoff })) {
29                warn!("retention: {n} line(s) were emptied but the record of it could not be written: {}", e.0);
30            }
31            n
32        }
33        Err(e) => {
34            error!("retention: the log could not be cleared: {}", e.0);
35            0
36        }
37    }
38}
40#[cfg(test)]
41mod tests {
42    use super::*;
43    use whiskers_core::{HouseholdConfig, TimeKeeper};
44    use whiskers_store::SqliteHousehold;
45
46    const DAY: u64 = 86_400_000;
47
48    #[test]
49    fn the_service_clears_its_copy_only_when_the_household_says_so_and_says_it_did() {
50        let store = Store::open_in_memory().unwrap();
51        let hub = SqliteHouseholdHub::open(store.clone());
52        let mut phone = SqliteLog::new(store.clone(), "phone");
53        phone.append(&Entry::new(10 * DAY, Event::Heard { text: "old".into(), pictures: vec![] })).unwrap();
54        phone.append(&Entry::new(99 * DAY, Event::Heard { text: "new".into(), pictures: vec![] })).unwrap();
55        let now = 100 * DAY;
56
57        assert_eq!(apply_retention(&store, &hub, now), 0, "no setting keeps everything");
58
59        let mut keeper = TimeKeeper::new(Box::new(SqliteHousehold::new(Store::open_in_memory().unwrap())), "phone");
60        keeper.set_config(HouseholdConfig { log_retention_days: Some(30), ..HouseholdConfig::default() }, 5);
61        whiskers_ports::run_ready(hub.merge(keeper.household())).unwrap();
62        assert_eq!(apply_retention(&store, &hub, now), 1);
63        assert_eq!(apply_retention(&store, &hub, now), 0, "once");
64        let phone_lines = phone.own_entries().unwrap();
65        assert_eq!(phone_lines[0].event, Event::Expired);
66        assert!(matches!(&phone_lines[1].event, Event::Heard { text, .. } if text == "new"));
67        let said = SqliteLog::new(store, SERVICE_DEVICE).own_entries().unwrap();
68        assert!(matches!(said[0].event, Event::Cleared { lines: 1, .. }), "{said:?}");
69    }
70}