lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

summary.rs (6802B)


      1 use radroots_transport::{
      2     outcome::{FetchTargetOutcome, FetchTargetState},
      3     target::{TargetFingerprint, TargetSet},
      4 };
      5 
      6 /// Bounded evidence for one requested target across all returned pull pages.
      7 ///
      8 /// Counts describe returned, validated pages only. A source failure returning no
      9 /// page adds no target observation; the pull termination retains that failure.
     10 /// No diagnostic messages, event bodies or per-page history are retained here.
     11 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
     12 #[derive(Clone, Debug, Eq, PartialEq)]
     13 pub struct PullTargetSummary {
     14     target: TargetFingerprint,
     15     pages_observed: u16,
     16     incomplete_pages: u16,
     17     missing_outcome_pages: u16,
     18     last_incomplete: Option<FetchTargetState>,
     19 }
     20 
     21 impl PullTargetSummary {
     22     /// Exact requested target identity.
     23     pub const fn target(&self) -> &TargetFingerprint {
     24         &self.target
     25     }
     26 
     27     /// Number of validated pages returned during this pull.
     28     pub const fn pages_observed(&self) -> u16 {
     29         self.pages_observed
     30     }
     31 
     32     /// Pages that supplied an explicit non-complete outcome for this target.
     33     pub const fn incomplete_pages(&self) -> u16 {
     34         self.incomplete_pages
     35     }
     36 
     37     /// Pages that supplied no outcome for this requested target.
     38     pub const fn missing_outcome_pages(&self) -> u16 {
     39         self.missing_outcome_pages
     40     }
     41 
     42     /// Last actual non-complete state, preserved across later success or omission.
     43     pub const fn last_incomplete(&self) -> Option<FetchTargetState> {
     44         self.last_incomplete
     45     }
     46 
     47     /// Whether every returned page positively reported this target complete.
     48     ///
     49     /// False when no page returned. The caller must also inspect pull termination
     50     /// and request bounds; this is not a claim about complete global history.
     51     pub const fn all_pages_complete(&self) -> bool {
     52         self.pages_observed > 0 && self.incomplete_pages == 0 && self.missing_outcome_pages == 0
     53     }
     54 }
     55 
     56 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
     57 #[cfg_attr(feature = "serde", serde(transparent))]
     58 #[derive(Clone, Debug, Eq, PartialEq)]
     59 pub(super) struct PullTargetSummaries(Vec<PullTargetSummary>);
     60 
     61 impl PullTargetSummaries {
     62     pub(super) fn new(targets: &TargetSet) -> Self {
     63         Self(
     64             targets
     65                 .targets()
     66                 .iter()
     67                 .map(|target| PullTargetSummary {
     68                     target: target.fingerprint().clone(),
     69                     pages_observed: 0,
     70                     incomplete_pages: 0,
     71                     missing_outcome_pages: 0,
     72                     last_incomplete: None,
     73                 })
     74                 .collect(),
     75         )
     76     }
     77 
     78     pub(super) fn as_slice(&self) -> &[PullTargetSummary] {
     79         &self.0
     80     }
     81 
     82     pub(super) fn observe(&mut self, outcomes: &[FetchTargetOutcome]) {
     83         // Only the validated pull loop calls this, at most PULL_MAX_PAGES times.
     84         for summary in &mut self.0 {
     85             summary.pages_observed += 1;
     86             match outcomes
     87                 .iter()
     88                 .find(|outcome| outcome.target() == summary.target())
     89             {
     90                 Some(outcome) if outcome.state() != FetchTargetState::Complete => {
     91                     summary.incomplete_pages += 1;
     92                     summary.last_incomplete = Some(outcome.state());
     93                 }
     94                 Some(_) => {}
     95                 None => summary.missing_outcome_pages += 1,
     96             }
     97         }
     98     }
     99 }
    100 
    101 #[cfg(feature = "serde")]
    102 impl<'de> serde::Deserialize<'de> for PullTargetSummary {
    103     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    104     where
    105         D: serde::Deserializer<'de>,
    106     {
    107         #[derive(serde::Deserialize)]
    108         #[serde(deny_unknown_fields)]
    109         struct Wire {
    110             target: TargetFingerprint,
    111             pages_observed: u16,
    112             incomplete_pages: u16,
    113             missing_outcome_pages: u16,
    114             last_incomplete: Option<FetchTargetState>,
    115         }
    116         let wire = Wire::deserialize(deserializer)?;
    117         if wire.pages_observed > super::PULL_MAX_PAGES
    118             || u32::from(wire.incomplete_pages) + u32::from(wire.missing_outcome_pages)
    119                 > u32::from(wire.pages_observed)
    120             || (wire.incomplete_pages > 0) != wire.last_incomplete.is_some()
    121             || wire.last_incomplete == Some(FetchTargetState::Complete)
    122         {
    123             return Err(serde::de::Error::custom(
    124                 "invalid pull target summary counts or state",
    125             ));
    126         }
    127         Ok(Self {
    128             target: wire.target,
    129             pages_observed: wire.pages_observed,
    130             incomplete_pages: wire.incomplete_pages,
    131             missing_outcome_pages: wire.missing_outcome_pages,
    132             last_incomplete: wire.last_incomplete,
    133         })
    134     }
    135 }
    136 
    137 #[cfg(feature = "serde")]
    138 impl<'de> serde::Deserialize<'de> for PullTargetSummaries {
    139     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    140     where
    141         D: serde::Deserializer<'de>,
    142     {
    143         struct BoundedSummaries;
    144         impl<'de> serde::de::Visitor<'de> for BoundedSummaries {
    145             type Value = PullTargetSummaries;
    146 
    147             fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
    148                 formatter.write_str("a nonempty bounded list of unique pull target summaries")
    149             }
    150 
    151             fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
    152             where
    153                 A: serde::de::SeqAccess<'de>,
    154             {
    155                 let mut summaries: Vec<PullTargetSummary> = Vec::new();
    156                 while summaries.len() < radroots_transport::target::TARGET_SET_MAX_ITEMS {
    157                     let Some(summary) = sequence.next_element::<PullTargetSummary>()? else {
    158                         if summaries.is_empty() {
    159                             return Err(serde::de::Error::custom("empty pull target summaries"));
    160                         }
    161                         return Ok(PullTargetSummaries(summaries));
    162                     };
    163                     if summaries
    164                         .iter()
    165                         .any(|prior| prior.target() == summary.target())
    166                     {
    167                         return Err(serde::de::Error::custom("duplicate pull summary target"));
    168                     }
    169                     summaries.push(summary);
    170                 }
    171                 // Skip an extra item without deserializing/retaining its fields.
    172                 if sequence.next_element::<serde::de::IgnoredAny>()?.is_some() {
    173                     return Err(serde::de::Error::custom("too many pull target summaries"));
    174                 }
    175                 Ok(PullTargetSummaries(summaries))
    176             }
    177         }
    178         deserializer.deserialize_seq(BoundedSummaries)
    179     }
    180 }