lib

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

policy.rs (9169B)


      1 //! Transport-neutral delivery satisfaction policy.
      2 
      3 use crate::{
      4     Error,
      5     outcome::DeliveryOutcome,
      6     target::{TargetFingerprint, TargetSet},
      7 };
      8 use alloc::{collections::BTreeMap, vec::Vec};
      9 
     10 /// Current result of evaluating delivery evidence against one exact policy.
     11 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     12 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
     13 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     14 pub enum SatisfactionState {
     15     /// The evidence already satisfies the policy.
     16     Satisfied,
     17     /// The policy is not satisfied, but unattempted or retryable work can satisfy it.
     18     Pending,
     19     /// The available evidence proves that the policy can no longer be satisfied.
     20     Exhausted,
     21 }
     22 
     23 /// Success level a caller requires from selected targets.
     24 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     25 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
     26 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     27 pub enum SatisfactionClass {
     28     /// The target accepted responsibility for the event.
     29     Accepted,
     30     /// The target confirmed final delivery.
     31     Delivered,
     32 }
     33 
     34 /// Which requested targets must reach the satisfaction class.
     35 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
     36 #[cfg_attr(feature = "serde", serde(transparent))]
     37 #[derive(Clone, Debug, Eq, PartialEq)]
     38 pub struct TargetPolicy {
     39     kind: TargetPolicyKind,
     40 }
     41 
     42 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     43 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
     44 #[derive(Clone, Debug, Eq, PartialEq)]
     45 enum TargetPolicyKind {
     46     Any,
     47     All,
     48     Quorum(u16),
     49     Required(Vec<TargetFingerprint>),
     50 }
     51 
     52 impl TargetPolicy {
     53     /// Any one requested target must satisfy the class.
     54     pub const fn any() -> Self {
     55         Self {
     56             kind: TargetPolicyKind::Any,
     57         }
     58     }
     59 
     60     /// Every requested target must satisfy the class.
     61     pub const fn all() -> Self {
     62         Self {
     63             kind: TargetPolicyKind::All,
     64         }
     65     }
     66 
     67     /// At least this non-zero count of requested targets must satisfy the class.
     68     pub const fn quorum(threshold: u16) -> Result<Self, Error> {
     69         if threshold == 0 {
     70             return Err(Error::InvalidSatisfactionPolicy);
     71         }
     72         Ok(Self {
     73             kind: TargetPolicyKind::Quorum(threshold),
     74         })
     75     }
     76 
     77     /// These exact, unique target fingerprints must satisfy the class.
     78     pub fn required(mut targets: Vec<TargetFingerprint>) -> Result<Self, Error> {
     79         if targets.is_empty() {
     80             return Err(Error::EmptyRequiredTargetSet);
     81         }
     82         targets.sort();
     83         if targets.windows(2).any(|pair| pair[0] == pair[1]) {
     84             return Err(Error::DuplicateRequiredTargetFingerprint);
     85         }
     86         Ok(Self {
     87             kind: TargetPolicyKind::Required(targets),
     88         })
     89     }
     90 
     91     /// Returns exact required fingerprints for a required-target policy.
     92     pub fn required_targets(&self) -> Option<&[TargetFingerprint]> {
     93         match &self.kind {
     94             TargetPolicyKind::Required(targets) => Some(targets.as_slice()),
     95             TargetPolicyKind::Any | TargetPolicyKind::All | TargetPolicyKind::Quorum(_) => None,
     96         }
     97     }
     98 
     99     /// Returns whether any requested target may satisfy this policy.
    100     pub const fn is_any(&self) -> bool {
    101         matches!(self.kind, TargetPolicyKind::Any)
    102     }
    103 
    104     /// Returns whether every requested target must satisfy this policy.
    105     pub const fn is_all(&self) -> bool {
    106         matches!(self.kind, TargetPolicyKind::All)
    107     }
    108 
    109     /// Returns the threshold for a quorum policy.
    110     pub const fn quorum_threshold(&self) -> Option<u16> {
    111         match self.kind {
    112             TargetPolicyKind::Quorum(threshold) => Some(threshold),
    113             TargetPolicyKind::Any | TargetPolicyKind::All | TargetPolicyKind::Required(_) => None,
    114         }
    115     }
    116 
    117     pub(crate) fn validate_for(&self, targets: &TargetSet) -> Result<(), Error> {
    118         match &self.kind {
    119             TargetPolicyKind::Any | TargetPolicyKind::All => Ok(()),
    120             TargetPolicyKind::Quorum(threshold) => {
    121                 if usize::from(*threshold) > targets.len() {
    122                     Err(Error::InvalidSatisfactionPolicy)
    123                 } else {
    124                     Ok(())
    125                 }
    126             }
    127             TargetPolicyKind::Required(required) => {
    128                 if required
    129                     .iter()
    130                     .any(|fingerprint| !targets.contains(fingerprint))
    131                 {
    132                     Err(Error::RequiredTargetNotRequested)
    133                 } else {
    134                     Ok(())
    135                 }
    136             }
    137         }
    138     }
    139 
    140     fn is_satisfied(&self, total_targets: usize, satisfied: usize, fingerprints: &[&str]) -> bool {
    141         match &self.kind {
    142             TargetPolicyKind::Any => satisfied != 0,
    143             TargetPolicyKind::All => satisfied == total_targets,
    144             TargetPolicyKind::Quorum(threshold) => satisfied >= usize::from(*threshold),
    145             TargetPolicyKind::Required(required) => required
    146                 .iter()
    147                 .all(|fingerprint| fingerprints.contains(&fingerprint.as_str())),
    148         }
    149     }
    150 }
    151 
    152 /// Required success level and target selection for one delivery request.
    153 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    154 #[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
    155 #[derive(Clone, Debug, Eq, PartialEq)]
    156 pub struct SatisfactionPolicy {
    157     class: SatisfactionClass,
    158     targets: TargetPolicy,
    159 }
    160 
    161 impl SatisfactionPolicy {
    162     /// Creates an explicit satisfaction policy.
    163     pub const fn new(class: SatisfactionClass, targets: TargetPolicy) -> Self {
    164         Self { class, targets }
    165     }
    166 
    167     /// Returns the accepted or delivered success level.
    168     pub const fn class(&self) -> SatisfactionClass {
    169         self.class
    170     }
    171 
    172     /// Returns the selected target policy.
    173     pub const fn targets(&self) -> &TargetPolicy {
    174         &self.targets
    175     }
    176 
    177     /// Validates this policy against an exact bounded target set.
    178     ///
    179     /// This permits higher-level composition layers to reject impossible
    180     /// quorum and required-target profiles before constructing a delivery
    181     /// request, without reproducing transport policy law.
    182     pub fn validate_for(&self, targets: &TargetSet) -> Result<(), Error> {
    183         self.targets.validate_for(targets)
    184     }
    185 }
    186 
    187 /// Evaluates target evidence using the transport-owned satisfaction law.
    188 ///
    189 /// Evidence is ordered from oldest to newest when a target occurs more than
    190 /// once. A prior success remains authoritative; otherwise the newest outcome
    191 /// determines whether the target can be retried. Targets without evidence are
    192 /// pending. Evidence for a target outside `targets` is rejected.
    193 pub fn evaluate_satisfaction<'a, I>(
    194     policy: &SatisfactionPolicy,
    195     targets: &TargetSet,
    196     evidence: I,
    197 ) -> Result<SatisfactionState, Error>
    198 where
    199     I: IntoIterator<Item = (&'a TargetFingerprint, &'a DeliveryOutcome)>,
    200 {
    201     policy.validate_for(targets)?;
    202     let mut states: BTreeMap<&str, (bool, bool)> = targets
    203         .targets()
    204         .iter()
    205         .map(|target| (target.fingerprint().as_str(), (false, true)))
    206         .collect();
    207 
    208     for (target, outcome) in evidence {
    209         outcome.validate()?;
    210         let Some((satisfied, retryable)) = states.get_mut(target.as_str()) else {
    211             return Err(Error::UnexpectedDeliveryTargetReceipt);
    212         };
    213         if outcome.satisfies(policy.class()) {
    214             *satisfied = true;
    215             *retryable = false;
    216         } else if !*satisfied {
    217             *retryable = outcome.is_retryable();
    218         }
    219     }
    220 
    221     let satisfied_targets: Vec<&str> = states
    222         .iter()
    223         .filter_map(|(target, (satisfied, _))| satisfied.then_some(*target))
    224         .collect();
    225     if policy.targets.is_satisfied(
    226         targets.len(),
    227         satisfied_targets.len(),
    228         satisfied_targets.as_slice(),
    229     ) {
    230         return Ok(SatisfactionState::Satisfied);
    231     }
    232 
    233     let possible_targets: Vec<&str> = states
    234         .iter()
    235         .filter_map(|(target, (satisfied, retryable))| {
    236             (*satisfied || *retryable).then_some(*target)
    237         })
    238         .collect();
    239     if policy.targets.is_satisfied(
    240         targets.len(),
    241         possible_targets.len(),
    242         possible_targets.as_slice(),
    243     ) {
    244         Ok(SatisfactionState::Pending)
    245     } else {
    246         Ok(SatisfactionState::Exhausted)
    247     }
    248 }
    249 
    250 #[cfg(feature = "serde")]
    251 impl<'de> serde::Deserialize<'de> for TargetPolicy {
    252     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    253     where
    254         D: serde::Deserializer<'de>,
    255     {
    256         match TargetPolicyKind::deserialize(deserializer)? {
    257             TargetPolicyKind::Any => Ok(Self::any()),
    258             TargetPolicyKind::All => Ok(Self::all()),
    259             TargetPolicyKind::Quorum(threshold) => {
    260                 Self::quorum(threshold).map_err(serde::de::Error::custom)
    261             }
    262             TargetPolicyKind::Required(targets) => {
    263                 Self::required(targets).map_err(serde::de::Error::custom)
    264             }
    265         }
    266     }
    267 }