use std::collections::HashMap;
use std::sync::Arc;
use arc_swap::ArcSwap;
#[derive(Debug, Clone, Copy)]
pub(crate) struct EnergyRow {
pub(crate) energy_per_op_kwh: f64,
pub(crate) last_update_ms: u64,
}
#[derive(Debug, Default)]
pub(crate) struct AgedEnergyMap {
inner: ArcSwap<HashMap<String, EnergyRow>>,
}
impl AgedEnergyMap {
#[must_use]
pub(crate) fn snapshot(&self, now_ms: u64, staleness_ms: u64) -> HashMap<String, f64> {
let current = self.inner.load_full();
current
.iter()
.filter_map(|(service, energy)| {
let age = now_ms.saturating_sub(energy.last_update_ms);
if age < staleness_ms {
Some((service.clone(), energy.energy_per_op_kwh))
} else {
None
}
})
.collect()
}
pub(crate) fn publish(&self, new_table: HashMap<String, EnergyRow>) {
self.inner.store(Arc::new(new_table));
}
pub(crate) fn current_owned(&self) -> HashMap<String, EnergyRow> {
(*self.inner.load_full()).clone()
}
#[cfg(test)]
pub(crate) fn insert_for_test(
&self,
service: String,
energy_per_op_kwh: f64,
last_update_ms: u64,
) {
let mut current = self.current_owned();
current.insert(
service,
EnergyRow {
energy_per_op_kwh,
last_update_ms,
},
);
self.publish(current);
}
}
macro_rules! impl_energy_state {
(
$(#[$meta:meta])*
$vis:vis struct $Name:ident;
) => {
$(#[$meta])*
$vis struct $Name {
inner: $crate::score::energy_state::AgedEnergyMap,
}
impl $Name {
#[must_use]
$vis fn new() -> std::sync::Arc<Self> {
std::sync::Arc::new(Self::default())
}
#[must_use]
$vis fn snapshot(
&self,
now_ms: u64,
staleness_ms: u64,
) -> std::collections::HashMap<String, f64> {
self.inner.snapshot(now_ms, staleness_ms)
}
pub(super) fn publish(
&self,
new_table: std::collections::HashMap<String, ServiceEnergy>,
) {
self.inner.publish(new_table);
}
pub(super) fn current_owned(
&self,
) -> std::collections::HashMap<String, ServiceEnergy> {
self.inner.current_owned()
}
#[cfg(test)]
pub(crate) fn insert_for_test(
&self,
service: String,
energy_per_op_kwh: f64,
last_update_ms: u64,
) {
self.inner
.insert_for_test(service, energy_per_op_kwh, last_update_ms);
}
}
};
}
pub(crate) use impl_energy_state;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_state_returns_empty_snapshot() {
let state = AgedEnergyMap::default();
assert!(state.snapshot(1000, 5000).is_empty());
}
#[test]
fn fresh_entry_appears_in_snapshot() {
let state = AgedEnergyMap::default();
state.insert_for_test("svc-a".into(), 1e-7, 100);
let snap = state.snapshot(200, 500);
assert_eq!(snap.len(), 1);
assert!((snap["svc-a"] - 1e-7).abs() < 1e-15);
}
#[test]
fn stale_entry_filtered_out() {
let state = AgedEnergyMap::default();
state.insert_for_test("svc-a".into(), 1e-7, 100);
assert!(state.snapshot(700, 500).is_empty());
}
#[test]
fn mixed_fresh_and_stale() {
let state = AgedEnergyMap::default();
state.insert_for_test("fresh".into(), 2e-7, 500);
state.insert_for_test("stale".into(), 3e-7, 100);
let snap = state.snapshot(600, 200);
assert_eq!(snap.len(), 1);
assert!(snap.contains_key("fresh"));
assert!(!snap.contains_key("stale"));
}
#[test]
fn saturating_sub_protects_against_clock_skew() {
let state = AgedEnergyMap::default();
state.insert_for_test("svc".into(), 5e-7, 1000);
let snap = state.snapshot(500, 200);
assert_eq!(snap.len(), 1);
}
#[test]
fn current_owned_returns_independent_copy() {
let state = AgedEnergyMap::default();
state.insert_for_test("svc".into(), 1e-7, 100);
let mut owned = state.current_owned();
owned.clear();
assert_eq!(state.snapshot(200, 500).len(), 1);
}
}