use core::time::Duration;
use std::sync::Arc;
use crate::spec::{Priority, RetryPolicy};
#[derive(Clone, Debug, PartialEq)]
pub struct PriorityConfig {
pub base: [f64; Priority::COUNT],
pub aging_rate: [f64; Priority::COUNT],
pub age_cap: [f64; Priority::COUNT],
}
impl Default for PriorityConfig {
fn default() -> Self {
PriorityConfig {
base: [10_000.0, 1_000.0, 100.0, 10.0],
aging_rate: [2.0; 4],
age_cap: [800.0; 4],
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct UrgencyConfig {
pub max: f64,
pub window: Duration,
pub overdue_rate: f64,
pub overdue_cap: f64,
}
impl Default for UrgencyConfig {
fn default() -> Self {
UrgencyConfig {
max: 5_000.0,
window: Duration::from_secs(5),
overdue_rate: 100.0,
overdue_cap: 3_000.0,
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum OverflowPolicy {
#[default]
Wait,
Reject,
ShedLowest,
ShedOldest,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Bounds {
pub max_pending: Option<usize>,
pub max_pending_per_class: [Option<usize>; Priority::COUNT],
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum UnknownDependency {
#[default]
Reject,
Await {
timeout: Option<Duration>,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct AutoscaleConfig {
pub interval: Duration,
pub target_utilization: f64,
pub drain_time: Duration,
pub slo: [Option<Duration>; Priority::COUNT],
pub reservation_horizon: Duration,
pub stabilization_window: Duration,
pub cooldown: Duration,
pub max_step_up_ratio: f64,
pub max_step_up_abs: u32,
pub max_step_down: u32,
pub ewma_alpha: f64,
pub cost_per_worker_second: f64,
}
impl Default for AutoscaleConfig {
fn default() -> Self {
AutoscaleConfig {
interval: Duration::from_secs(1),
target_utilization: 0.75,
drain_time: Duration::from_secs(30),
slo: [None; 4],
reservation_horizon: Duration::from_secs(60),
stabilization_window: Duration::from_secs(60),
cooldown: Duration::from_secs(30),
max_step_up_ratio: 1.0,
max_step_up_abs: 4,
max_step_down: 1,
ewma_alpha: 0.3,
cost_per_worker_second: 0.0,
}
}
}
#[derive(Clone, Debug)]
pub struct PoolConfig {
pub name: Arc<str>,
pub min_workers: u32,
pub max_workers: u32,
pub initial_workers: u32,
pub autoscale: Option<AutoscaleConfig>,
pub retry: RetryPolicy,
pub bounds: Bounds,
pub overflow: OverflowPolicy,
pub reservable_fraction: f64,
pub default_duration: Duration,
pub max_hold: Duration,
pub lease_ttl: Option<Duration>,
pub unknown_dependency: UnknownDependency,
pub fairness: Option<Fairness>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Fairness {
pub weights: Vec<(Arc<str>, f64)>,
pub default_weight: f64,
}
impl Default for Fairness {
fn default() -> Self {
Fairness {
weights: Vec::new(),
default_weight: 1.0,
}
}
}
impl Fairness {
pub fn weights<K: Into<Arc<str>>>(weights: impl IntoIterator<Item = (K, f64)>) -> Self {
Fairness {
weights: weights.into_iter().map(|(k, w)| (k.into(), w)).collect(),
..Default::default()
}
}
}
impl PoolConfig {
pub fn new(name: impl Into<Arc<str>>) -> Self {
PoolConfig {
name: name.into(),
min_workers: 1,
max_workers: 10,
initial_workers: 1,
autoscale: Some(AutoscaleConfig::default()),
retry: RetryPolicy::default(),
bounds: Bounds::default(),
overflow: OverflowPolicy::Wait,
reservable_fraction: 0.5,
default_duration: Duration::from_secs(1),
max_hold: Duration::from_secs(300),
lease_ttl: None,
unknown_dependency: UnknownDependency::Reject,
fairness: None,
}
}
pub fn fixed(name: impl Into<Arc<str>>, workers: u32) -> Self {
let mut p = Self::new(name);
p.min_workers = workers;
p.max_workers = workers;
p.initial_workers = workers;
p.autoscale = None;
p
}
pub fn workers(mut self, min: u32, initial: u32, max: u32) -> Self {
self.min_workers = min;
self.initial_workers = initial;
self.max_workers = max;
self
}
pub(crate) fn reservation_limit(&self) -> u32 {
((self.max_workers as f64 * self.reservable_fraction).floor() as u32).max(1)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ResourceKind {
Semaphore {
permits: u32,
},
TokenBucket {
rate: f64,
burst: u32,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct ResourceConfig {
pub name: Arc<str>,
pub kind: ResourceKind,
}
impl ResourceConfig {
pub fn semaphore(name: impl Into<Arc<str>>, permits: u32) -> Self {
ResourceConfig {
name: name.into(),
kind: ResourceKind::Semaphore { permits },
}
}
pub fn token_bucket(name: impl Into<Arc<str>>, rate: f64, burst: u32) -> Self {
ResourceConfig {
name: name.into(),
kind: ResourceKind::TokenBucket { rate, burst },
}
}
}
#[derive(Clone, Debug)]
pub struct CoreConfig {
pub pools: Vec<PoolConfig>,
pub resources: Vec<ResourceConfig>,
pub priority: PriorityConfig,
pub urgency: UrgencyConfig,
pub global_bounds: Bounds,
pub retention: Duration,
pub retention_max: usize,
pub persist: bool,
pub epoch_unix_nanos: i64,
pub id_epoch: u16,
pub id_shard: Option<u8>,
pub metrics_window: Duration,
pub seed: u64,
}
impl Default for CoreConfig {
fn default() -> Self {
CoreConfig {
pools: vec![PoolConfig::new("default")],
resources: Vec::new(),
priority: PriorityConfig::default(),
urgency: UrgencyConfig::default(),
global_bounds: Bounds::default(),
retention: Duration::from_secs(3600),
retention_max: 1_000_000,
persist: false,
epoch_unix_nanos: 0,
id_epoch: 0,
id_shard: None,
metrics_window: Duration::from_secs(60),
seed: 0x5eed_c0de,
}
}
}
impl CoreConfig {
pub fn validate(&self) -> Result<(), String> {
if self.pools.is_empty() {
return Err("at least one pool is required".into());
}
let mut names = std::collections::HashSet::new();
for p in &self.pools {
if !names.insert(&p.name) {
return Err(format!("duplicate pool '{}'", p.name));
}
if p.min_workers > p.max_workers {
return Err(format!("pool '{}': min_workers > max_workers", p.name));
}
if !(p.reservable_fraction.is_finite() && p.reservable_fraction > 0.0) {
return Err(format!(
"pool '{}': reservable_fraction must be > 0",
p.name
));
}
if p.retry.max_attempts == 0 {
return Err(format!(
"pool '{}': retry.max_attempts must be >= 1",
p.name
));
}
if let Some(f) = &p.fairness {
let ok = |w: f64| w.is_finite() && w > 0.0;
if !ok(f.default_weight) || f.weights.iter().any(|(_, w)| !ok(*w)) {
return Err(format!(
"pool '{}': tenant weights must be finite and > 0",
p.name
));
}
}
if let Some(a) = &p.autoscale {
if a.interval.is_zero() {
return Err(format!("pool '{}': autoscale interval must be > 0", p.name));
}
if !(a.target_utilization > 0.0 && a.target_utilization <= 1.0) {
return Err(format!(
"pool '{}': target_utilization must be in (0, 1]",
p.name
));
}
if !(a.ewma_alpha > 0.0 && a.ewma_alpha <= 1.0) {
return Err(format!("pool '{}': ewma_alpha must be in (0, 1]", p.name));
}
if !(a.max_step_up_ratio.is_finite() && a.max_step_up_ratio >= 0.0) {
return Err(format!("pool '{}': max_step_up_ratio must be >= 0", p.name));
}
if !(a.cost_per_worker_second.is_finite() && a.cost_per_worker_second >= 0.0) {
return Err(format!(
"pool '{}': cost_per_worker_second must be >= 0",
p.name
));
}
}
}
let mut names = std::collections::HashSet::new();
for r in &self.resources {
if !names.insert(&r.name) {
return Err(format!("duplicate resource '{}'", r.name));
}
match r.kind {
ResourceKind::Semaphore { permits: 0 } => {
return Err(format!("resource '{}': permits must be >= 1", r.name));
}
ResourceKind::TokenBucket { rate, burst } => {
if rate.is_nan() || rate < 0.0 {
return Err(format!("resource '{}': rate must be >= 0", r.name));
}
if burst == 0 {
return Err(format!("resource '{}': burst must be >= 1", r.name));
}
}
ResourceKind::Semaphore { .. } => {}
}
}
let finite = |v: &[f64]| v.iter().all(|x| x.is_finite());
if !finite(&self.priority.base)
|| !finite(&self.priority.aging_rate)
|| !finite(&self.priority.age_cap)
{
return Err("priority weights must be finite".into());
}
let u = &self.urgency;
if !finite(&[u.max, u.overdue_rate, u.overdue_cap]) {
return Err("urgency settings must be finite".into());
}
if self.metrics_window.is_zero() {
return Err("metrics_window must be > 0".into());
}
Ok(())
}
pub(crate) fn sanitized(mut self) -> Self {
const MIN_INTERVAL: Duration = Duration::from_millis(1);
for p in &mut self.pools {
if p.reservable_fraction.is_nan() || p.reservable_fraction <= 0.0 {
p.reservable_fraction = 0.5;
}
if p.retry.max_attempts == 0 {
p.retry.max_attempts = 1;
}
if let Some(a) = &mut p.autoscale {
a.interval = a.interval.max(MIN_INTERVAL);
if !(a.target_utilization > 0.0 && a.target_utilization <= 1.0) {
a.target_utilization = 0.75;
}
if !(a.ewma_alpha > 0.0 && a.ewma_alpha <= 1.0) {
a.ewma_alpha = 0.3;
}
if !(a.max_step_up_ratio.is_finite() && a.max_step_up_ratio >= 0.0) {
a.max_step_up_ratio = 1.0;
}
if !(a.cost_per_worker_second.is_finite() && a.cost_per_worker_second >= 0.0) {
a.cost_per_worker_second = 0.0;
}
}
}
for r in &mut self.resources {
if let ResourceKind::TokenBucket { rate, .. } = &mut r.kind
&& (rate.is_nan() || *rate < 0.0)
{
*rate = 0.0;
}
}
self.metrics_window = self.metrics_window.max(MIN_INTERVAL);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_reports_bad_settings() {
assert!(CoreConfig::default().validate().is_ok());
let mut c = CoreConfig::default();
c.pools[0].autoscale.as_mut().unwrap().interval = Duration::ZERO;
assert!(c.validate().unwrap_err().contains("interval"));
let c = CoreConfig {
resources: vec![ResourceConfig::token_bucket("x", f64::NAN, 1)],
..Default::default()
};
assert!(c.validate().unwrap_err().contains("rate"));
let c = CoreConfig {
pools: vec![PoolConfig::new("a"), PoolConfig::new("a")],
..Default::default()
};
assert!(c.validate().unwrap_err().contains("duplicate"));
let c = CoreConfig {
pools: vec![PoolConfig::new("a").workers(5, 1, 2)],
..Default::default()
};
assert!(c.validate().is_err());
}
}