use std::collections::HashMap;
use std::sync::Mutex;
use tokio::sync::oneshot;
#[derive(Clone, Debug)]
pub struct StageableAdapter {
pub peer_stage_id: String,
pub peft: metrale_config::PeftAdapterConfig,
}
#[derive(Clone, Debug)]
pub enum PromoteReject {
PoolFull(String),
Peer(String),
}
impl std::fmt::Display for PromoteReject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PromoteReject::PoolFull(m) | PromoteReject::Peer(m) => f.write_str(m),
}
}
}
type PromoteWaiters = Vec<oneshot::Sender<Result<i32, PromoteReject>>>;
#[derive(Default)]
pub struct PromotionManager {
inflight: Mutex<HashMap<String, PromoteWaiters>>,
}
enum Role {
Leader,
Follower(oneshot::Receiver<Result<i32, PromoteReject>>),
}
struct LeaderGuard<'a> {
mgr: &'a PromotionManager,
name: String,
armed: bool,
}
impl LeaderGuard<'_> {
fn disarm(&mut self) {
self.armed = false;
}
}
impl Drop for LeaderGuard<'_> {
fn drop(&mut self) {
if self.armed {
let _ = self.mgr.inflight.lock().unwrap().remove(&self.name);
}
}
}
impl PromotionManager {
pub async fn coalesce<F, Fut>(&self, name: &str, leader: F) -> Result<i32, PromoteReject>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<i32, PromoteReject>>,
{
let role = {
let mut map = self.inflight.lock().unwrap();
match map.get_mut(name) {
Some(waiters) => {
let (tx, rx) = oneshot::channel();
waiters.push(tx);
Role::Follower(rx)
}
None => {
map.insert(name.to_string(), Vec::new());
Role::Leader
}
}
};
match role {
Role::Follower(rx) => rx.await.unwrap_or_else(|_| {
Err(PromoteReject::Peer(
"promotion leader dropped without a result".to_string(),
))
}),
Role::Leader => {
let mut guard = LeaderGuard {
mgr: self,
name: name.to_string(),
armed: true,
};
let result = leader().await;
let waiters = self
.inflight
.lock()
.unwrap()
.remove(name)
.unwrap_or_default();
guard.disarm();
for w in waiters {
let _ = w.send(result.clone());
}
result
}
}
}
#[cfg(test)]
fn inflight_len(&self) -> usize {
self.inflight.lock().unwrap().len()
}
}
pub fn should_attempt_promote(promotion_enabled: bool, is_stageable: bool) -> bool {
promotion_enabled && is_stageable
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn attempt_gate() {
assert!(should_attempt_promote(true, true));
assert!(!should_attempt_promote(false, true));
assert!(!should_attempt_promote(true, false));
assert!(!should_attempt_promote(false, false));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn single_flight_same_name_one_promote() {
let mgr = Arc::new(PromotionManager::default());
let calls = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for _ in 0..100 {
let mgr = Arc::clone(&mgr);
let calls = Arc::clone(&calls);
handles.push(tokio::spawn(async move {
mgr.coalesce("lyra", || async {
calls.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
Ok(5)
})
.await
}));
}
let results: Vec<_> = futures::future::join_all(handles).await;
assert_eq!(calls.load(Ordering::SeqCst), 1, "coalesced to one promote");
for r in results {
assert_eq!(r.unwrap().unwrap(), 5);
}
assert_eq!(mgr.inflight_len(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn single_flight_failure_broadcasts_and_clears() {
let mgr = Arc::new(PromotionManager::default());
let calls = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for _ in 0..50 {
let mgr = Arc::clone(&mgr);
let calls = Arc::clone(&calls);
handles.push(tokio::spawn(async move {
mgr.coalesce("cold", || async {
calls.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
Err::<i32, _>(PromoteReject::PoolFull("all busy".to_string()))
})
.await
}));
}
let results: Vec<_> = futures::future::join_all(handles).await;
assert_eq!(calls.load(Ordering::SeqCst), 1, "one promote attempt");
for r in results {
match r.unwrap() {
Err(PromoteReject::PoolFull(m)) => assert_eq!(m, "all busy"),
other => panic!("expected same PoolFull, got {other:?}"),
}
}
assert_eq!(mgr.inflight_len(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn distinct_names_promote_independently() {
let mgr = Arc::new(PromotionManager::default());
let calls = Arc::new(AtomicUsize::new(0));
let names = ["a", "b", "c"];
let mut handles = Vec::new();
for n in names {
for _ in 0..10 {
let mgr = Arc::clone(&mgr);
let calls = Arc::clone(&calls);
handles.push(tokio::spawn(async move {
mgr.coalesce(n, || async {
calls.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
Ok(7)
})
.await
}));
}
}
let _ = futures::future::join_all(handles).await;
assert_eq!(calls.load(Ordering::SeqCst), names.len());
assert_eq!(mgr.inflight_len(), 0);
}
}