use std::sync::Arc;
use std::time::Duration;
use crate::desired_state::policy::PolicyGeneration;
use super::{ActivePolicy, PolicyRuntime};
#[derive(Debug, Clone)]
pub struct Ceilings(Source);
#[derive(Debug, Clone)]
enum Source {
Fixed(ActivePolicy),
Published(Arc<PolicyRuntime>),
}
impl Ceilings {
pub const fn fixed(policy: ActivePolicy) -> Self {
Self(Source::Fixed(policy))
}
pub fn published(runtime: &Arc<PolicyRuntime>) -> Self {
Self(Source::Published(Arc::clone(runtime)))
}
pub fn active(&self, namespace: &str) -> ActivePolicy {
match &self.0 {
Source::Fixed(policy) => *policy,
Source::Published(runtime) => runtime.active(namespace),
}
}
pub fn enter(&self, generation: Option<PolicyGeneration>) {
if let Source::Published(runtime) = &self.0 {
runtime.enter(generation);
}
}
pub fn exit(&self, generation: Option<PolicyGeneration>) {
if let Source::Published(runtime) = &self.0 {
runtime.exit(generation);
}
}
fn linger(&self, generation: PolicyGeneration, ttl: Duration) {
match &self.0 {
Source::Published(runtime) => runtime.linger(generation, ttl),
Source::Fixed(_) => {}
}
}
}
#[derive(Debug)]
pub struct PolicyHold {
ceilings: Ceilings,
generation: Option<PolicyGeneration>,
kept: bool,
}
impl PolicyHold {
pub fn take(ceilings: &Ceilings, generation: Option<PolicyGeneration>) -> Self {
ceilings.enter(generation);
Self {
ceilings: ceilings.clone(),
generation,
kept: false,
}
}
pub fn kept(mut self) {
self.kept = true;
}
pub fn linger(mut self, ttl: Duration) {
let Some(generation) = self.generation else {
return;
};
self.kept = true;
self.ceilings.linger(generation, ttl);
}
}
impl Drop for PolicyHold {
fn drop(&mut self) {
if !self.kept {
self.ceilings.exit(self.generation);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::desired_state::fixtures::tenant_id;
use crate::desired_state::policy::PolicyScope;
use crate::policy::fixtures::{body, generation};
use crate::policy::view::tests::stateless_config;
#[test]
fn fixed_ceilings_answer_the_same_policy_for_every_namespace_and_drain_nothing() {
let policy = ActivePolicy::default();
let ceilings = Ceilings::fixed(policy);
assert_eq!(ceilings.active("anything"), policy);
let orphan = generation(&body(PolicyScope::Tenant(tenant_id(1)), 1, 10), 1);
ceilings.enter(Some(orphan));
ceilings.exit(Some(orphan));
}
#[test]
fn published_ceilings_account_holds_against_the_runtime_they_read() {
let runtime = Arc::new(PolicyRuntime::bootstrap(&stateless_config()));
let ceilings = Ceilings::published(&runtime);
let held = generation(&body(PolicyScope::Tenant(tenant_id(1)), 1, 10), 1);
ceilings.enter(Some(held));
assert_eq!(runtime.outstanding(held), 1);
ceilings.exit(Some(held));
assert_eq!(runtime.outstanding(held), 0);
}
#[test]
fn a_hold_taken_before_a_store_call_survives_only_the_admission_that_keeps_it() {
let runtime = Arc::new(PolicyRuntime::bootstrap(&stateless_config()));
let ceilings = Ceilings::published(&runtime);
let held = generation(&body(PolicyScope::Tenant(tenant_id(1)), 1, 10), 1);
let denied = PolicyHold::take(&ceilings, Some(held));
assert_eq!(runtime.outstanding(held), 1);
drop(denied);
assert_eq!(runtime.outstanding(held), 0);
let admitted = PolicyHold::take(&ceilings, Some(held));
admitted.kept();
assert_eq!(runtime.outstanding(held), 1);
ceilings.exit(Some(held));
assert_eq!(runtime.outstanding(held), 0);
}
}