mod activation;
mod source;
pub(crate) mod ungoverned;
pub(crate) mod view;
pub use activation::{Activation, ActivationRefusal, BackendSupport};
pub use source::{Ceilings, PolicyHold};
pub(crate) use ungoverned::{Unenforceable, denied};
pub use view::{ActivePolicy, BudgetCaps, ConcurrencyCaps, PolicyView};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::time::Instant;
use arc_swap::ArcSwap;
use crate::config::Config;
use crate::desired_state::policy::PolicyGeneration;
#[derive(Debug)]
pub struct PolicyRuntime {
support: BackendSupport,
view: ArcSwap<PolicyView>,
holds: Mutex<HashMap<PolicyGeneration, u64>>,
lingering: Mutex<HashMap<PolicyGeneration, Lingering>>,
}
#[derive(Debug)]
struct Lingering {
held: u64,
until: Instant,
}
impl PolicyRuntime {
pub fn bootstrap(config: &Config) -> Self {
Self {
support: BackendSupport::of(config),
view: ArcSwap::from_pointee(PolicyView::of(config)),
holds: Mutex::new(HashMap::new()),
lingering: Mutex::new(HashMap::new()),
}
}
pub fn active(&self, namespace: &str) -> ActivePolicy {
self.view.load().policy(namespace)
}
pub fn plan(&self, candidate: &PolicyView) -> Result<Activation, ActivationRefusal> {
activation::plan(&self.view.load(), candidate, self.support)
}
pub fn install(&self, candidate: PolicyView) -> Activation {
let activation = match self.plan(&candidate) {
Ok(activation) => activation,
Err(refusal) => {
tracing::error!(
%refusal,
"a policy view that the compile gate admitted was refused at installation; \
installing it anyway to keep the snapshot and the policy it is served under \
consistent"
);
Activation::forced()
}
};
self.view.store(std::sync::Arc::new(candidate));
activation.log(&self.draining());
activation
}
pub fn enter(&self, generation: Option<PolicyGeneration>) {
let Some(generation) = generation else { return };
*self
.holds
.lock()
.expect("not poisoned")
.entry(generation)
.or_insert(0) += 1;
}
pub fn exit(&self, generation: Option<PolicyGeneration>) {
let Some(generation) = generation else { return };
let mut holds = self.holds.lock().expect("not poisoned");
if let Some(count) = holds.get_mut(&generation) {
*count = count.saturating_sub(1);
if *count == 0 {
holds.remove(&generation);
}
}
}
pub fn linger(self: &Arc<Self>, generation: PolicyGeneration, ttl: Duration) {
let until = Instant::now() + ttl;
let mut lingering = self.lingering.lock().expect("not poisoned");
if let Some(waiting) = lingering.get_mut(&generation) {
waiting.held += 1;
waiting.until = waiting.until.max(until);
return;
}
lingering.insert(generation, Lingering { held: 1, until });
drop(lingering);
let Ok(handle) = tokio::runtime::Handle::try_current() else {
self.release_lingering(generation);
return;
};
let runtime = Arc::clone(self);
handle.spawn(async move {
while let Some(until) = runtime.release_lingering_if_expired(generation) {
tokio::time::sleep_until(until).await;
}
});
}
fn release_lingering_if_expired(&self, generation: PolicyGeneration) -> Option<Instant> {
let held = {
let mut lingering = self.lingering.lock().expect("not poisoned");
let waiting = lingering.get(&generation)?;
if Instant::now() < waiting.until {
return Some(waiting.until);
}
lingering.remove(&generation)?.held
};
for _ in 0..held {
self.exit(Some(generation));
}
None
}
fn release_lingering(&self, generation: PolicyGeneration) {
let Some(waiting) = self
.lingering
.lock()
.expect("not poisoned")
.remove(&generation)
else {
return;
};
for _ in 0..waiting.held {
self.exit(Some(generation));
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn outstanding(&self, generation: PolicyGeneration) -> u64 {
self.holds
.lock()
.expect("not poisoned")
.get(&generation)
.copied()
.unwrap_or_default()
}
pub fn draining(&self) -> Vec<(PolicyGeneration, u64)> {
let view = self.view.load();
let mut draining: Vec<(PolicyGeneration, u64)> = self
.holds
.lock()
.expect("not poisoned")
.iter()
.filter(|(generation, _)| !view.enforces(**generation))
.map(|(generation, count)| (*generation, *count))
.collect();
draining.sort_by_key(|(generation, _)| {
(
generation.scope(),
generation.epoch().get(),
generation.content().to_string(),
)
});
draining
}
}
#[cfg(test)]
pub(crate) mod fixtures {
use crate::desired_state::fixtures::revision_id;
use crate::desired_state::policy::{
BudgetPolicy, ConcurrencyPolicy, PolicyBody, PolicyEpoch, PolicyGeneration, PolicyScope,
RevocationPolicy,
};
pub(crate) fn body(scope: PolicyScope, epoch: u64, subject_limit: u64) -> PolicyBody {
detailed(scope, epoch, subject_limit, None, 300, 8, 60, 0)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn detailed(
scope: PolicyScope,
epoch: u64,
subject_limit: u64,
namespace_limit: Option<u64>,
reservation_ttl_seconds: u64,
max_in_flight: u64,
lease_ttl_seconds: u64,
minimum_token_epoch: u64,
) -> PolicyBody {
PolicyBody::new(
scope,
PolicyEpoch::new(epoch).expect("a positive epoch"),
BudgetPolicy::new(subject_limit, namespace_limit, reservation_ttl_seconds)
.expect("a positive reservation ttl"),
ConcurrencyPolicy::new(max_in_flight, lease_ttl_seconds)
.expect("a positive concurrency policy"),
RevocationPolicy::new(minimum_token_epoch),
)
}
pub(crate) fn stored_zero_cap(
scope: PolicyScope,
epoch: u64,
subject_limit: u64,
namespace_limit: Option<u64>,
) -> PolicyBody {
PolicyBody::new(
scope,
PolicyEpoch::new(epoch).expect("a positive epoch"),
BudgetPolicy::stored(subject_limit, namespace_limit, 300)
.expect("a stored cap of zero reads back"),
ConcurrencyPolicy::new(8, 60).expect("a positive concurrency policy"),
RevocationPolicy::new(0),
)
}
pub(crate) fn generation(body: &PolicyBody, revision: u64) -> PolicyGeneration {
body.generation(revision_id(revision))
}
}
#[cfg(test)]
mod tests {
use super::fixtures::{body, generation};
use super::*;
use crate::desired_state::fixtures::tenant_id;
use crate::desired_state::policy::PolicyScope;
fn scope() -> PolicyScope {
PolicyScope::Tenant(tenant_id(1))
}
fn runtime() -> PolicyRuntime {
PolicyRuntime::bootstrap(&crate::policy::view::tests::stateless_config())
}
#[test]
fn a_hold_is_outstanding_until_it_exits_and_only_then_stops_draining() {
let runtime = runtime();
let first = generation(&body(scope(), 1, 1_000), 1);
runtime.enter(Some(first));
runtime.enter(Some(first));
assert_eq!(runtime.outstanding(first), 2);
runtime.exit(Some(first));
assert_eq!(runtime.outstanding(first), 1);
runtime.exit(Some(first));
assert_eq!(runtime.outstanding(first), 0);
assert!(runtime.draining().is_empty());
}
#[test]
fn a_hold_survives_the_installation_that_supersedes_its_generation() {
use crate::config::NamespacePolicy;
use crate::policy::view::tests::governed;
let old = body(
PolicyScope::Project {
tenant: tenant_id(1),
project: crate::desired_state::fixtures::project_id(1),
},
1,
10_000,
);
let held = generation(&old, 1);
let runtime = PolicyRuntime::bootstrap(&governed(
"acme/core",
NamespacePolicy {
body: old,
generation: held,
},
));
runtime.enter(Some(held));
let rolled_back =
crate::policy::fixtures::detailed(old.scope(), 2, 1_000, None, 300, 8, 60, 0);
let next = generation(&rolled_back, 2);
runtime.install(PolicyView::of(&governed(
"acme/core",
NamespacePolicy {
body: rolled_back,
generation: next,
},
)));
let active = runtime.active("acme/core");
assert_eq!(active.budget.expect("governed").subject_microdollars, 1_000);
assert_eq!(active.generation, Some(next));
assert_eq!(runtime.outstanding(held), 1);
assert_eq!(runtime.draining(), vec![(held, 1)]);
runtime.exit(Some(held));
assert!(runtime.draining().is_empty());
}
#[tokio::test(start_paused = true)]
async fn holds_lingering_under_one_generation_share_a_single_deadline() {
let runtime = std::sync::Arc::new(runtime());
let held = generation(&body(scope(), 1, 1_000), 1);
let ttl = Duration::from_secs(300);
for _ in 0..1_000 {
runtime.enter(Some(held));
runtime.linger(held, ttl);
}
assert_eq!(runtime.outstanding(held), 1_000);
assert_eq!(
runtime.lingering.lock().expect("not poisoned").len(),
1,
"a thousand failed reserves waited on a thousand timers"
);
tokio::time::sleep(ttl - Duration::from_secs(1)).await;
runtime.enter(Some(held));
runtime.linger(held, ttl);
tokio::time::sleep(Duration::from_secs(2)).await;
assert_eq!(runtime.outstanding(held), 1_001);
tokio::time::sleep(ttl).await;
assert_eq!(runtime.outstanding(held), 0);
assert!(runtime.lingering.lock().expect("not poisoned").is_empty());
}
#[tokio::test(start_paused = true)]
async fn a_hold_lingering_at_the_deadline_still_waits_out_its_own_ttl() {
let runtime = std::sync::Arc::new(runtime());
let held = generation(&body(scope(), 1, 1_000), 1);
let ttl = Duration::from_secs(300);
runtime.enter(Some(held));
runtime.linger(held, ttl);
tokio::time::sleep(ttl).await;
runtime.enter(Some(held));
runtime.linger(held, ttl);
tokio::time::sleep(ttl - Duration::from_secs(1)).await;
assert!(
runtime.outstanding(held) >= 1,
"a hold taken at the deadline was released with the batch before it"
);
tokio::time::sleep(Duration::from_secs(2)).await;
assert_eq!(runtime.outstanding(held), 0);
}
#[test]
fn a_bootstrap_hold_is_not_counted() {
let runtime = runtime();
runtime.enter(None);
runtime.exit(None);
assert!(runtime.draining().is_empty());
}
}