use std::sync::Arc;
use crate::adapter::net::behavior::fold::capability::CapabilityFold;
use crate::adapter::net::behavior::fold::reservation::ReservationFold;
use crate::adapter::net::behavior::fold::{Fold, FoldKind, NodeState};
use crate::adapter::net::subnet::SubnetId;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SummaryAnnouncement {
pub source_subnet: SubnetId,
pub fold_kind: u16,
pub generation: u64,
pub buckets: Vec<(String, u64)>,
}
pub trait Summarizer: Send + Sync {
fn fold_kind(&self) -> u16;
fn summarize(&self, ctx: &SummarizerContext<'_>) -> Vec<SummaryAnnouncement>;
}
pub struct SummarizerContext<'a> {
pub source_subnet: SubnetId,
pub generation: u64,
pub fold: &'a dyn FoldHandle,
}
pub trait FoldHandle: Send + Sync {
fn capability_fold(&self) -> Option<&Fold<CapabilityFold>> {
None
}
fn reservation_fold(&self) -> Option<&Fold<ReservationFold>> {
None
}
}
pub struct CapabilityFoldSummarizer;
impl Summarizer for CapabilityFoldSummarizer {
fn fold_kind(&self) -> u16 {
CapabilityFold::KIND_ID
}
fn summarize(&self, ctx: &SummarizerContext<'_>) -> Vec<SummaryAnnouncement> {
let Some(fold) = ctx.fold.capability_fold() else {
return Vec::new();
};
let (idle, busy, reserved, faulty) = fold.with_state(|state| {
let mut idle = 0u64;
let mut busy = 0u64;
let mut reserved = 0u64;
let mut faulty = 0u64;
for entry in state.entries.values() {
match entry.payload.state {
NodeState::Idle => idle += 1,
NodeState::Busy => busy += 1,
NodeState::Reserved => reserved += 1,
NodeState::Faulty => faulty += 1,
}
}
(idle, busy, reserved, faulty)
});
vec![SummaryAnnouncement {
source_subnet: ctx.source_subnet,
fold_kind: CapabilityFold::KIND_ID,
generation: ctx.generation,
buckets: vec![
("busy".to_string(), busy),
("faulty".to_string(), faulty),
("idle".to_string(), idle),
("reserved".to_string(), reserved),
],
}]
}
}
pub struct ReservationFoldSummarizer;
impl Summarizer for ReservationFoldSummarizer {
fn fold_kind(&self) -> u16 {
ReservationFold::KIND_ID
}
fn summarize(&self, ctx: &SummarizerContext<'_>) -> Vec<SummaryAnnouncement> {
use crate::adapter::net::behavior::fold::reservation::ReservationState;
let Some(fold) = ctx.fold.reservation_fold() else {
return Vec::new();
};
let (free, reserved, active) = fold.with_state(|state| {
let mut free = 0u64;
let mut reserved = 0u64;
let mut active = 0u64;
for entry in state.entries.values() {
match entry.payload.state {
ReservationState::Free => free += 1,
ReservationState::Reserved { .. } => reserved += 1,
ReservationState::Active { .. } => active += 1,
}
}
(free, reserved, active)
});
vec![SummaryAnnouncement {
source_subnet: ctx.source_subnet,
fold_kind: ReservationFold::KIND_ID,
generation: ctx.generation,
buckets: vec![
("active".to_string(), active),
("free".to_string(), free),
("reserved".to_string(), reserved),
],
}]
}
}
pub fn resolve_summarizer(
fold_kind: u16,
custom: &std::collections::HashMap<u16, Arc<dyn Summarizer>>,
) -> Option<Arc<dyn Summarizer>> {
if let Some(custom) = custom.get(&fold_kind) {
return Some(custom.clone());
}
if fold_kind == CapabilityFold::KIND_ID {
return Some(Arc::new(CapabilityFoldSummarizer));
}
if fold_kind == ReservationFold::KIND_ID {
return Some(Arc::new(ReservationFoldSummarizer));
}
None
}
pub struct CapabilityFoldHandle<'a>(pub &'a Fold<CapabilityFold>);
impl FoldHandle for CapabilityFoldHandle<'_> {
fn capability_fold(&self) -> Option<&Fold<CapabilityFold>> {
Some(self.0)
}
}
pub struct ReservationFoldHandle<'a>(pub &'a Fold<ReservationFold>);
impl FoldHandle for ReservationFoldHandle<'_> {
fn reservation_fold(&self) -> Option<&Fold<ReservationFold>> {
Some(self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::adapter::net::behavior::fold::capability::CapabilityMembership;
use crate::adapter::net::behavior::fold::wire::{EnvelopeMeta, SignedAnnouncement};
use crate::adapter::net::identity::EntityKeypair;
use std::collections::BTreeMap;
use std::time::Duration;
fn sign_cap(
kp: &EntityKeypair,
publisher: u64,
class: u64,
state: NodeState,
) -> SignedAnnouncement<CapabilityMembership> {
SignedAnnouncement::sign(
kp,
CapabilityFold::KIND_ID,
class,
publisher,
1,
EnvelopeMeta::default(),
CapabilityMembership {
class_hash: class,
tags: Vec::new(),
hardware: None,
state,
region: None,
price_quote: None,
reflex_addr: None,
allowed_nodes: Vec::new(),
allowed_subnets: Vec::new(),
allowed_groups: Vec::new(),
metadata: BTreeMap::new(),
},
)
.expect("sign")
}
#[test]
fn capability_summarizer_buckets_by_state_with_lex_sorted_keys() {
let fold: Fold<CapabilityFold> = Fold::with_sweep_interval(Duration::ZERO);
let kp = EntityKeypair::generate();
fold.apply(sign_cap(&kp, 0xA, 1, NodeState::Idle)).unwrap();
fold.apply(sign_cap(&kp, 0xB, 2, NodeState::Idle)).unwrap();
fold.apply(sign_cap(&kp, 0xC, 3, NodeState::Busy)).unwrap();
fold.apply(sign_cap(&kp, 0xD, 4, NodeState::Faulty))
.unwrap();
let handle = CapabilityFoldHandle(&fold);
let ctx = SummarizerContext {
source_subnet: SubnetId::new(&[3, 7]),
generation: 42,
fold: &handle,
};
let out = CapabilityFoldSummarizer.summarize(&ctx);
assert_eq!(out.len(), 1);
let summary = &out[0];
assert_eq!(summary.source_subnet, SubnetId::new(&[3, 7]));
assert_eq!(summary.fold_kind, CapabilityFold::KIND_ID);
assert_eq!(summary.generation, 42);
let bucket_names: Vec<&str> = summary.buckets.iter().map(|(n, _)| n.as_str()).collect();
assert_eq!(bucket_names, vec!["busy", "faulty", "idle", "reserved"]);
let bucket_counts: Vec<u64> = summary.buckets.iter().map(|(_, c)| *c).collect();
assert_eq!(bucket_counts, vec![1, 1, 2, 0]);
}
#[test]
fn resolve_summarizer_returns_builtin_for_known_kinds() {
let custom: std::collections::HashMap<u16, Arc<dyn Summarizer>> =
std::collections::HashMap::new();
let s = resolve_summarizer(CapabilityFold::KIND_ID, &custom).expect("builtin capability");
assert_eq!(s.fold_kind(), CapabilityFold::KIND_ID);
let s = resolve_summarizer(ReservationFold::KIND_ID, &custom).expect("builtin reservation");
assert_eq!(s.fold_kind(), ReservationFold::KIND_ID);
assert!(resolve_summarizer(0xDEAD, &custom).is_none());
}
#[test]
fn resolve_summarizer_custom_overrides_builtin() {
struct StubSummarizer;
impl Summarizer for StubSummarizer {
fn fold_kind(&self) -> u16 {
CapabilityFold::KIND_ID
}
fn summarize(&self, _ctx: &SummarizerContext<'_>) -> Vec<SummaryAnnouncement> {
vec![SummaryAnnouncement {
source_subnet: SubnetId::GLOBAL,
fold_kind: CapabilityFold::KIND_ID,
generation: 1,
buckets: vec![("custom".into(), 1)],
}]
}
}
let mut custom: std::collections::HashMap<u16, Arc<dyn Summarizer>> =
std::collections::HashMap::new();
custom.insert(CapabilityFold::KIND_ID, Arc::new(StubSummarizer));
let s = resolve_summarizer(CapabilityFold::KIND_ID, &custom).expect("custom");
assert_eq!(s.fold_kind(), CapabilityFold::KIND_ID);
let handle_fold: Fold<CapabilityFold> = Fold::with_sweep_interval(Duration::ZERO);
let handle = CapabilityFoldHandle(&handle_fold);
let ctx = SummarizerContext {
source_subnet: SubnetId::GLOBAL,
generation: 1,
fold: &handle,
};
let out = s.summarize(&ctx);
assert_eq!(out[0].buckets[0].0, "custom");
}
}