use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use crate::score::cloud_energy::table::lookup_instance_power;
const MS_PER_HOUR: f64 = 3_600_000.0;
const MAX_BILLABLE_MS: u64 = 3_600_000;
const MIN_BILLABLE_MS: u64 = 1_000;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StaticBrokerConfig {
pub nodes: u32,
pub instance_type: String,
pub provider: String,
pub region: Option<String>,
}
impl StaticBrokerConfig {
#[must_use]
pub fn cluster_watts(&self) -> f64 {
let (_idle, max_watts) = lookup_instance_power(&self.instance_type, &self.provider);
f64::from(self.nodes) * max_watts
}
}
#[derive(Debug)]
pub struct StaticBrokerState {
last_ms: AtomicU64,
watts: f64,
billed_during_outage: AtomicBool,
}
impl StaticBrokerState {
#[must_use]
pub fn new(now_ms: u64, cfg: &StaticBrokerConfig) -> Self {
Self {
last_ms: AtomicU64::new(now_ms),
watts: cfg.cluster_watts(),
billed_during_outage: AtomicBool::new(false),
}
}
pub fn mark_outage_billed(&self) {
self.billed_during_outage.store(true, Ordering::SeqCst);
}
pub fn outage_billed(&self) -> bool {
self.billed_during_outage.load(Ordering::SeqCst)
}
pub fn clear_outage_billed(&self) -> bool {
self.billed_during_outage.swap(false, Ordering::SeqCst)
}
pub fn take_window_kwh(&self, now_ms: u64) -> Option<f64> {
if !self.watts.is_finite() || self.watts <= 0.0 {
return None;
}
let last = self.last_ms.load(Ordering::SeqCst);
let elapsed = now_ms.saturating_sub(last);
if elapsed < MIN_BILLABLE_MS {
return None;
}
if self
.last_ms
.compare_exchange(last, now_ms, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
return None;
}
let kwh = self.watts * elapsed.min(MAX_BILLABLE_MS) as f64 / MS_PER_HOUR / 1000.0;
kwh.is_finite().then_some(kwh)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg(nodes: u32) -> StaticBrokerConfig {
StaticBrokerConfig {
nodes,
instance_type: "m5.2xlarge".to_string(),
provider: "aws".to_string(),
region: None,
}
}
#[test]
fn cluster_watts_scales_with_node_count() {
let one = cfg(1).cluster_watts();
let three = cfg(3).cluster_watts();
assert!(one > 0.0);
assert!((three - one * 3.0).abs() < 1e-9);
}
#[test]
fn unknown_instance_type_falls_back_to_a_provider_default() {
let unknown = StaticBrokerConfig {
instance_type: "not-a-real-type".to_string(),
..cfg(1)
};
assert!(unknown.cluster_watts() > 0.0);
}
fn state_at(now_ms: u64) -> (StaticBrokerState, f64) {
let c = cfg(3);
let watts = c.cluster_watts();
(StaticBrokerState::new(now_ms, &c), watts)
}
#[test]
fn window_energy_matches_watts_times_elapsed() {
let (state, watts) = state_at(0);
let kwh = state.take_window_kwh(3_600_000).expect("energy");
assert!((kwh - watts / 1000.0).abs() < 1e-9);
}
#[test]
fn a_second_take_bills_only_the_new_elapsed_time() {
let (state, watts) = state_at(0);
state.take_window_kwh(1_800_000).expect("first");
let second = state.take_window_kwh(3_600_000).expect("second");
assert!((second - watts / 2000.0).abs() < 1e-9);
}
#[test]
fn no_elapsed_time_yields_nothing() {
let (state, _) = state_at(1_000);
assert!(state.take_window_kwh(1_000).is_none());
}
#[test]
fn a_sub_second_tick_accrues_instead_of_billing() {
let (state, watts) = state_at(0);
assert!(state.take_window_kwh(300).is_none());
assert!(state.take_window_kwh(600).is_none());
assert!(state.take_window_kwh(900).is_none());
let kwh = state.take_window_kwh(1_200).expect("energy");
assert!((kwh - watts * 1_200.0 / MS_PER_HOUR / 1000.0).abs() < 1e-12);
}
#[test]
fn a_long_outage_is_capped() {
let (state, watts) = state_at(0);
let kwh = state.take_window_kwh(36_000_000).expect("energy");
assert!((kwh - watts / 1000.0).abs() < 1e-9);
}
#[test]
fn a_backwards_clock_yields_nothing() {
let (state, _) = state_at(5_000);
assert!(state.take_window_kwh(1_000).is_none());
}
}