use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Lane {
Main,
Delegate,
Cron,
}
impl std::fmt::Display for Lane {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Lane::Main => write!(f, "main"),
Lane::Delegate => write!(f, "delegate"),
Lane::Cron => write!(f, "cron"),
}
}
}
#[derive(Debug, Clone)]
pub struct LaneConfig {
pub max_concurrent: usize,
pub priority: u8,
}
impl Default for LaneConfig {
fn default() -> Self {
Self {
max_concurrent: 1,
priority: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct ExecutionSlot {
pub slot_id: String,
pub agent_id: String,
pub lane: Lane,
pub description: String,
pub started_at: DateTime<Utc>,
pub waiting_on: Option<String>,
}
pub struct ConcurrencyScheduler {
lane_configs: HashMap<Lane, LaneConfig>,
active_slots: Arc<RwLock<Vec<ExecutionSlot>>>,
wait_graph: Arc<RwLock<HashMap<String, String>>>,
}
impl Default for ConcurrencyScheduler {
fn default() -> Self {
Self::new()
}
}
impl ConcurrencyScheduler {
pub fn new() -> Self {
let mut configs = HashMap::new();
configs.insert(
Lane::Main,
LaneConfig {
max_concurrent: 3,
priority: 2,
},
);
configs.insert(
Lane::Delegate,
LaneConfig {
max_concurrent: 5,
priority: 1,
},
);
configs.insert(
Lane::Cron,
LaneConfig {
max_concurrent: 2,
priority: 0,
},
);
Self {
lane_configs: configs,
active_slots: Arc::new(RwLock::new(Vec::new())),
wait_graph: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn configure_lane(&mut self, lane: Lane, config: LaneConfig) {
self.lane_configs.insert(lane, config);
}
pub async fn acquire_slot(
&self,
agent_id: &str,
lane: Lane,
description: &str,
) -> Option<String> {
let config = self.lane_configs.get(&lane)?;
let mut slots = self.active_slots.write().await;
let lane_count = slots.iter().filter(|s| s.lane == lane).count();
if lane_count >= config.max_concurrent {
return None;
}
let slot_id = uuid::Uuid::new_v4().to_string();
slots.push(ExecutionSlot {
slot_id: slot_id.clone(),
agent_id: agent_id.to_string(),
lane,
description: description.to_string(),
started_at: Utc::now(),
waiting_on: None,
});
Some(slot_id)
}
pub async fn release_slot(&self, slot_id: &str) {
let mut slots = self.active_slots.write().await;
slots.retain(|s| s.slot_id != slot_id);
let mut graph = self.wait_graph.write().await;
graph.retain(|_, v| v != slot_id);
}
pub async fn set_waiting(&self, slot_id: &str, waiting_on_agent: &str) {
let mut slots = self.active_slots.write().await;
if let Some(slot) = slots.iter_mut().find(|s| s.slot_id == slot_id) {
slot.waiting_on = Some(waiting_on_agent.to_string());
}
let mut graph = self.wait_graph.write().await;
graph.insert(slot_id.to_string(), waiting_on_agent.to_string());
}
pub async fn clear_waiting(&self, slot_id: &str) {
let mut slots = self.active_slots.write().await;
if let Some(slot) = slots.iter_mut().find(|s| s.slot_id == slot_id) {
slot.waiting_on = None;
}
let mut graph = self.wait_graph.write().await;
graph.remove(slot_id);
}
pub async fn detect_deadlocks(&self) -> Vec<Vec<String>> {
let slots = self.active_slots.read().await;
let wait_map: HashMap<String, String> = slots
.iter()
.filter_map(|slot| {
slot.waiting_on
.as_ref()
.map(|waiting_on| (slot.agent_id.clone(), waiting_on.clone()))
})
.collect();
find_cycles(&wait_map)
}
pub async fn get_status(&self) -> SchedulerStatus {
let slots = self.active_slots.read().await;
let mut lane_usage = HashMap::new();
for (lane, config) in &self.lane_configs {
let active = slots.iter().filter(|s| s.lane == *lane).count();
lane_usage.insert(*lane, (active, config.max_concurrent));
}
drop(slots); let deadlocks = self.detect_deadlocks().await;
SchedulerStatus {
lane_usage,
active_slots: self.active_slots.read().await.clone(),
deadlocks,
}
}
pub async fn lane_count(&self, lane: Lane) -> usize {
let slots = self.active_slots.read().await;
slots.iter().filter(|s| s.lane == lane).count()
}
}
fn find_cycles(wait_map: &HashMap<String, String>) -> Vec<Vec<String>> {
let mut cycles = Vec::new();
let mut visited = std::collections::HashSet::new();
for start in wait_map.keys() {
if visited.contains(start) {
continue;
}
let mut path = vec![start.clone()];
let mut current = start.clone();
let mut path_set = std::collections::HashSet::new();
path_set.insert(start.clone());
while let Some(next) = wait_map.get(¤t) {
if path_set.contains(next) {
let cycle_start = path.iter().position(|p| p == next).unwrap();
cycles.push(path[cycle_start..].to_vec());
break;
}
if visited.contains(next) {
break;
}
path.push(next.clone());
path_set.insert(next.clone());
current = next.clone();
}
for p in &path {
visited.insert(p.clone());
}
}
cycles
}
#[derive(Debug)]
pub struct SchedulerStatus {
pub lane_usage: HashMap<Lane, (usize, usize)>, pub active_slots: Vec<ExecutionSlot>,
pub deadlocks: Vec<Vec<String>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_cycles_no_cycles() {
let mut wait_map = HashMap::new();
wait_map.insert("A".to_string(), "B".to_string());
wait_map.insert("B".to_string(), "C".to_string());
let cycles = find_cycles(&wait_map);
assert!(cycles.is_empty());
}
#[test]
fn test_find_cycles_single_cycle() {
let mut wait_map = HashMap::new();
wait_map.insert("A".to_string(), "B".to_string());
wait_map.insert("B".to_string(), "C".to_string());
wait_map.insert("C".to_string(), "A".to_string());
let cycles = find_cycles(&wait_map);
assert_eq!(cycles.len(), 1);
let cycle = &cycles[0];
assert_eq!(cycle.len(), 3);
assert!(cycle.contains(&"A".to_string()));
assert!(cycle.contains(&"B".to_string()));
assert!(cycle.contains(&"C".to_string()));
}
#[test]
fn test_find_cycles_disconnected_cycles() {
let mut wait_map = HashMap::new();
wait_map.insert("A".to_string(), "B".to_string());
wait_map.insert("B".to_string(), "A".to_string());
wait_map.insert("C".to_string(), "D".to_string());
wait_map.insert("D".to_string(), "E".to_string());
wait_map.insert("E".to_string(), "C".to_string());
let cycles = find_cycles(&wait_map);
assert_eq!(cycles.len(), 2);
}
}