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 }