sync/src/applied.rs
//! **The downstream's record**: per upstream, the newest generation applied and
//! every invalidation in force here.
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::{Invalidation, SyncError, SyncManifest};
/// What one upstream's syncs did here.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppliedRecord {
pub last_generation: u64,
pub applied_at: i64,
pub applies: u64,
/// Every invalidation from this upstream in force here, by id.
#[serde(default)]
pub invalidations: BTreeMap<String, Invalidation>,
}
/// How a manifest stands against the record.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Admission {
/// A newer generation.
New,
/// The generation already applied, again (a second stick, a retried pull):
/// applied again, which changes nothing, and acknowledged again.
Replay,
}
/// **The downstream's record.** Kept as one JSON file by the server.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Applied {
#[serde(default)]
pub upstreams: BTreeMap<String, AppliedRecord>,
}
impl Applied {
/// May `m` be applied? A generation below the one applied is a rollback.
pub fn admit(&self, m: &SyncManifest) -> Result<Admission, SyncError> {
let last = self
.upstreams
.get(&m.upstream)
.map(|r| r.last_generation)
.unwrap_or(0);
if m.generation < last {
return Err(SyncError::Rollback {
upstream: m.upstream.clone(),
applied: last,
offered: m.generation,
});
}
Ok(if m.generation == last && last > 0 {
Admission::Replay
} else {
Admission::New
})
}
/// Record that `m` was applied: its generation and its invalidations.
pub fn record(&mut self, m: &SyncManifest, now: i64) {
let r = self.upstreams.entry(m.upstream.clone()).or_default();
r.last_generation = r.last_generation.max(m.generation);
r.applied_at = now;
r.applies += 1;
for i in &m.invalidations {
r.invalidations
.entry(i.id.clone())
.or_insert_with(|| i.clone());
}
}
/// The ids of `upstream`'s invalidations in force here.
pub fn ids(&self, upstream: &str) -> Vec<String> {
self.upstreams
.get(upstream)
.map(|r| r.invalidations.keys().cloned().collect())
.unwrap_or_default()
}
/// The invalidation in force here that names this package, from any upstream.
pub fn blocking(&self, coordinate: &str, sha256: &str) -> Option<&Invalidation> {
self.upstreams
.values()
.flat_map(|r| r.invalidations.values())
.find(|i| i.matches(coordinate, sha256))
}
pub fn last_generation(&self, upstream: &str) -> u64 {
self.upstreams
.get(upstream)
.map(|r| r.last_generation)
.unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Reason, MANIFEST_SCHEMA};
fn m(generation: u64, invs: Vec<Invalidation>) -> SyncManifest {
SyncManifest {
schema: MANIFEST_SCHEMA.into(),
upstream: "dmz".into(),
generation,
issued_at: 0,
carrier: "usb".into(),
added: vec![],
invalidations: invs,
}
}
#[test]
fn a_rollback_is_refused_and_a_replay_is_admitted() {
let mut a = Applied::default();
assert_eq!(a.admit(&m(3, vec![])).unwrap(), Admission::New);
a.record(&m(3, vec![]), 1);
assert_eq!(a.admit(&m(3, vec![])).unwrap(), Admission::Replay);
assert_eq!(a.admit(&m(4, vec![])).unwrap(), Admission::New);
let e = a.admit(&m(2, vec![])).unwrap_err();
assert_eq!(e.number(), 4, "{e}");
}
#[test]
fn invalidations_stay_in_force_and_block_by_bytes() {
let mut a = Applied::default();
let i = Invalidation::new("x@1", Some("ab".into()), Reason::Malware, "", 2, 0, "");
a.record(&m(2, vec![i.clone()]), 1);
a.record(&m(3, vec![i.clone()]), 2);
assert_eq!(a.ids("dmz"), vec![i.id.clone()]);
assert_eq!(a.blocking("other-name", "AB"), Some(&i));
assert_eq!(a.blocking("x@1", "cd"), None);
}
}