use std::collections::HashMap;
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
pub struct MarketAllocator {
auctions: RwLock<HashMap<String, ResourceAuction>>,
budgets: RwLock<HashMap<String, AgentBudget>>,
pricing: PricingStrategy,
allocation_history: RwLock<Vec<AllocationRecord>>,
max_history: usize,
}
pub struct ResourceAuction {
pub resource_id: String,
pub bids: Vec<ResourceBid>,
pub current_holder: Option<CurrentHolder>,
pub auction_start: Instant,
}
#[derive(Debug, Clone)]
pub struct CurrentHolder {
pub agent_id: String,
pub acquired_at: Instant,
pub expected_release: Option<Instant>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceBid {
pub agent_id: String,
pub resource_id: String,
pub base_priority: u8,
pub urgency_multiplier: f32,
pub max_bid: u32,
pub urgency_reason: String,
#[serde(skip, default = "default_duration")]
pub estimated_duration: Duration,
#[serde(skip, default = "Instant::now")]
pub submitted_at: Instant,
}
fn default_duration() -> Duration {
Duration::from_secs(60)
}
impl ResourceBid {
pub fn new(agent_id: impl Into<String>, resource_id: impl Into<String>) -> Self {
Self {
agent_id: agent_id.into(),
resource_id: resource_id.into(),
base_priority: 5,
urgency_multiplier: 1.0,
max_bid: 10,
urgency_reason: String::new(),
estimated_duration: Duration::from_secs(60),
submitted_at: Instant::now(),
}
}
pub fn with_priority(mut self, priority: u8) -> Self {
self.base_priority = priority.min(10);
self
}
pub fn with_urgency(mut self, multiplier: f32, reason: impl Into<String>) -> Self {
self.urgency_multiplier = multiplier.clamp(0.1, 10.0);
self.urgency_reason = reason.into();
self
}
pub fn with_max_bid(mut self, max_bid: u32) -> Self {
self.max_bid = max_bid;
self
}
pub fn with_duration(mut self, duration: Duration) -> Self {
self.estimated_duration = duration;
self
}
pub fn effective_priority(&self) -> f32 {
self.base_priority as f32 * self.urgency_multiplier
}
pub fn score(&self) -> f32 {
let priority_factor = self.effective_priority() / 10.0;
let bid_factor = (self.max_bid as f32 / 100.0).min(1.0);
0.7 * priority_factor + 0.3 * bid_factor
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentBudget {
pub agent_id: String,
pub total_budget: u32,
pub available: u32,
pub replenish_rate: f32,
#[serde(skip, default = "Instant::now")]
pub last_replenish: Instant,
}
impl AgentBudget {
pub fn new(agent_id: impl Into<String>, total_budget: u32) -> Self {
Self {
agent_id: agent_id.into(),
total_budget,
available: total_budget,
replenish_rate: 1.0, last_replenish: Instant::now(),
}
}
pub fn with_replenish_rate(mut self, rate: f32) -> Self {
self.replenish_rate = rate.max(0.0);
self
}
pub fn replenish(&mut self) {
let elapsed = self.last_replenish.elapsed().as_secs_f32();
let replenished = (elapsed * self.replenish_rate) as u32;
self.available = (self.available + replenished).min(self.total_budget);
self.last_replenish = Instant::now();
}
pub fn can_afford(&self, amount: u32) -> bool {
self.available >= amount
}
pub fn spend(&mut self, amount: u32) -> bool {
if self.available >= amount {
self.available -= amount;
true
} else {
false
}
}
pub fn refund(&mut self, amount: u32) {
self.available = (self.available + amount).min(self.total_budget);
}
pub fn availability_percent(&self) -> f32 {
self.available as f32 / self.total_budget as f32 * 100.0
}
}
#[derive(Debug, Clone, Default)]
pub enum PricingStrategy {
FirstPrice,
#[default]
SecondPrice,
FixedPrice(HashMap<String, u32>),
Dynamic {
base_price: u32,
demand_multiplier: f32,
},
Free,
}
#[derive(Debug, Clone)]
pub enum AllocationResult {
Allocated {
agent_id: String,
price: u32,
position: usize,
},
NoBids,
StillHeld {
holder: String,
remaining: Option<Duration>,
},
InsufficientBudget {
agent_id: String,
required: u32,
available: u32,
},
Outbid {
agent_id: String,
winning_agent: String,
winning_score: f32,
},
}
impl AllocationResult {
pub fn is_success(&self) -> bool {
matches!(self, AllocationResult::Allocated { .. })
}
pub fn winning_agent(&self) -> Option<&str> {
match self {
AllocationResult::Allocated { agent_id, .. } => Some(agent_id),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct AllocationRecord {
pub resource_id: String,
pub winner: String,
pub price: u32,
pub competing_bids: usize,
pub allocated_at: Instant,
}
impl MarketAllocator {
pub fn new() -> Self {
Self {
auctions: RwLock::new(HashMap::new()),
budgets: RwLock::new(HashMap::new()),
pricing: PricingStrategy::SecondPrice,
allocation_history: RwLock::new(Vec::new()),
max_history: 1000,
}
}
pub fn with_pricing(pricing: PricingStrategy) -> Self {
Self {
auctions: RwLock::new(HashMap::new()),
budgets: RwLock::new(HashMap::new()),
pricing,
allocation_history: RwLock::new(Vec::new()),
max_history: 1000,
}
}
pub fn with_max_history(mut self, max: usize) -> Self {
self.max_history = max;
self
}
pub async fn register_agent(&self, agent_id: &str, total_budget: u32, replenish_rate: f32) {
self.budgets.write().await.insert(
agent_id.to_string(),
AgentBudget::new(agent_id, total_budget).with_replenish_rate(replenish_rate),
);
}
pub async fn get_budget(&self, agent_id: &str) -> Option<AgentBudget> {
let mut budgets = self.budgets.write().await;
if let Some(budget) = budgets.get_mut(agent_id) {
budget.replenish();
Some(budget.clone())
} else {
None
}
}
pub async fn submit_bid(&self, bid: ResourceBid) -> Result<(), String> {
let mut budgets = self.budgets.write().await;
let budget = budgets
.get_mut(&bid.agent_id)
.ok_or_else(|| "Agent not registered".to_string())?;
budget.replenish();
if !budget.can_afford(bid.max_bid) {
return Err(format!(
"Insufficient budget: have {}, need {}",
budget.available, bid.max_bid
));
}
let mut auctions = self.auctions.write().await;
let auction = auctions
.entry(bid.resource_id.clone())
.or_insert_with(|| ResourceAuction {
resource_id: bid.resource_id.clone(),
bids: Vec::new(),
current_holder: None,
auction_start: Instant::now(),
});
auction.bids.retain(|b| b.agent_id != bid.agent_id);
auction.bids.push(bid);
Ok(())
}
pub async fn cancel_bid(&self, agent_id: &str, resource_id: &str) -> bool {
let mut auctions = self.auctions.write().await;
if let Some(auction) = auctions.get_mut(resource_id) {
let len_before = auction.bids.len();
auction.bids.retain(|b| b.agent_id != agent_id);
return auction.bids.len() < len_before;
}
false
}
pub async fn allocate(&self, resource_id: &str) -> AllocationResult {
let mut auctions = self.auctions.write().await;
let auction = match auctions.get_mut(resource_id) {
Some(a) => a,
None => return AllocationResult::NoBids,
};
if let Some(ref holder) = auction.current_holder {
let remaining = holder
.expected_release
.map(|r| r.saturating_duration_since(Instant::now()));
return AllocationResult::StillHeld {
holder: holder.agent_id.clone(),
remaining,
};
}
if auction.bids.is_empty() {
return AllocationResult::NoBids;
}
auction.bids.sort_by(|a, b| {
b.score()
.partial_cmp(&a.score())
.unwrap_or(std::cmp::Ordering::Equal)
});
let price = self.calculate_price(&auction.bids);
let mut budgets = self.budgets.write().await;
for (position, bid) in auction.bids.iter().enumerate() {
if let Some(budget) = budgets.get_mut(&bid.agent_id) {
budget.replenish();
if budget.spend(price) {
let winner_id = bid.agent_id.clone();
let expected_release = Some(Instant::now() + bid.estimated_duration);
auction.current_holder = Some(CurrentHolder {
agent_id: winner_id.clone(),
acquired_at: Instant::now(),
expected_release,
});
drop(budgets);
let competing_bids = auction.bids.len();
auction.bids.clear();
drop(auctions);
self.record_allocation(resource_id, &winner_id, price, competing_bids)
.await;
return AllocationResult::Allocated {
agent_id: winner_id,
price,
position,
};
} else {
continue;
}
}
}
let first_bid = &auction.bids[0];
AllocationResult::InsufficientBudget {
agent_id: first_bid.agent_id.clone(),
required: price,
available: budgets
.get(&first_bid.agent_id)
.map(|b| b.available)
.unwrap_or(0),
}
}
fn calculate_price(&self, bids: &[ResourceBid]) -> u32 {
match &self.pricing {
PricingStrategy::FirstPrice => bids.first().map(|b| b.max_bid).unwrap_or(0),
PricingStrategy::SecondPrice => {
if bids.len() >= 2 {
bids[1].max_bid.min(bids[0].max_bid)
} else {
1 }
}
PricingStrategy::FixedPrice(prices) => bids
.first()
.and_then(|b| prices.get(&b.resource_id))
.copied()
.unwrap_or(1),
PricingStrategy::Dynamic {
base_price,
demand_multiplier,
} => {
let demand = bids.len() as f32;
(*base_price as f32 * (1.0 + demand * demand_multiplier)) as u32
}
PricingStrategy::Free => 0,
}
}
pub async fn release(&self, resource_id: &str, agent_id: &str) -> bool {
let mut auctions = self.auctions.write().await;
if let Some(auction) = auctions.get_mut(resource_id)
&& let Some(ref holder) = auction.current_holder
&& holder.agent_id == agent_id
{
auction.current_holder = None;
return true;
}
false
}
pub async fn market_status(&self, resource_id: &str) -> Option<MarketStatus> {
let auctions = self.auctions.read().await;
auctions.get(resource_id).map(|a| MarketStatus {
resource_id: resource_id.to_string(),
current_holder: a.current_holder.as_ref().map(|h| h.agent_id.clone()),
pending_bids: a.bids.len(),
highest_score: a.bids.first().map(|b| b.score()),
auction_age: a.auction_start.elapsed(),
})
}
pub async fn list_auctions(&self) -> Vec<MarketStatus> {
let auctions = self.auctions.read().await;
auctions
.iter()
.map(|(resource_id, a)| MarketStatus {
resource_id: resource_id.clone(),
current_holder: a.current_holder.as_ref().map(|h| h.agent_id.clone()),
pending_bids: a.bids.len(),
highest_score: a.bids.first().map(|b| b.score()),
auction_age: a.auction_start.elapsed(),
})
.collect()
}
async fn record_allocation(
&self,
resource_id: &str,
winner: &str,
price: u32,
competing_bids: usize,
) {
let mut history = self.allocation_history.write().await;
history.push(AllocationRecord {
resource_id: resource_id.to_string(),
winner: winner.to_string(),
price,
competing_bids,
allocated_at: Instant::now(),
});
while history.len() > self.max_history {
history.remove(0);
}
}
pub async fn get_history(&self) -> Vec<AllocationRecord> {
self.allocation_history.read().await.clone()
}
pub async fn get_stats(&self) -> MarketStats {
let history = self.allocation_history.read().await;
let auctions = self.auctions.read().await;
let budgets = self.budgets.read().await;
let total_allocations = history.len();
let total_revenue: u32 = history.iter().map(|r| r.price).sum();
let avg_price = if total_allocations > 0 {
total_revenue as f32 / total_allocations as f32
} else {
0.0
};
let avg_competition = if total_allocations > 0 {
history.iter().map(|r| r.competing_bids).sum::<usize>() as f32
/ total_allocations as f32
} else {
0.0
};
MarketStats {
active_auctions: auctions.len(),
total_pending_bids: auctions.values().map(|a| a.bids.len()).sum(),
registered_agents: budgets.len(),
total_allocations,
total_revenue,
avg_price,
avg_competition,
}
}
}
impl Default for MarketAllocator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct MarketStatus {
pub resource_id: String,
pub current_holder: Option<String>,
pub pending_bids: usize,
pub highest_score: Option<f32>,
pub auction_age: Duration,
}
#[derive(Debug, Clone)]
pub struct MarketStats {
pub active_auctions: usize,
pub total_pending_bids: usize,
pub registered_agents: usize,
pub total_allocations: usize,
pub total_revenue: u32,
pub avg_price: f32,
pub avg_competition: f32,
}
pub struct UrgencyCalculator;
impl UrgencyCalculator {
pub fn calculate(context: &UrgencyContext) -> f32 {
let mut multiplier = 1.0;
if context.user_waiting {
multiplier *= 2.0;
}
if let Some(deadline) = context.deadline {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining < Duration::from_secs(60) {
multiplier *= 3.0;
} else if remaining < Duration::from_secs(300) {
multiplier *= 2.0;
} else if remaining < Duration::from_secs(600) {
multiplier *= 1.5;
}
}
if context.critical_path {
multiplier *= 1.5;
}
multiplier *= 1.0 + (context.resources_held as f32 * 0.2);
if let Some(wait_time) = context.wait_time {
let wait_secs = wait_time.as_secs();
if wait_secs > 60 {
multiplier *= 1.0 + (wait_secs as f32 / 120.0).min(2.0);
}
}
multiplier.min(10.0) }
pub fn builder() -> UrgencyContextBuilder {
UrgencyContextBuilder::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct UrgencyContext {
pub user_waiting: bool,
pub deadline: Option<Instant>,
pub critical_path: bool,
pub resources_held: usize,
pub wait_time: Option<Duration>,
}
pub struct UrgencyContextBuilder {
context: UrgencyContext,
}
impl UrgencyContextBuilder {
pub fn new() -> Self {
Self {
context: UrgencyContext::default(),
}
}
pub fn user_waiting(mut self, waiting: bool) -> Self {
self.context.user_waiting = waiting;
self
}
pub fn deadline(mut self, deadline: Instant) -> Self {
self.context.deadline = Some(deadline);
self
}
pub fn deadline_in(mut self, duration: Duration) -> Self {
self.context.deadline = Some(Instant::now() + duration);
self
}
pub fn critical_path(mut self, critical: bool) -> Self {
self.context.critical_path = critical;
self
}
pub fn resources_held(mut self, count: usize) -> Self {
self.context.resources_held = count;
self
}
pub fn wait_time(mut self, duration: Duration) -> Self {
self.context.wait_time = Some(duration);
self
}
pub fn build(self) -> UrgencyContext {
self.context
}
pub fn calculate(self) -> f32 {
UrgencyCalculator::calculate(&self.context)
}
}
impl Default for UrgencyContextBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_agent_registration() {
let allocator = MarketAllocator::new();
allocator.register_agent("agent-1", 100, 1.0).await;
let budget = allocator.get_budget("agent-1").await.unwrap();
assert_eq!(budget.total_budget, 100);
assert_eq!(budget.available, 100);
}
#[tokio::test]
async fn test_submit_bid() {
let allocator = MarketAllocator::new();
allocator.register_agent("agent-1", 100, 1.0).await;
let bid = ResourceBid::new("agent-1", "resource-a")
.with_priority(8)
.with_urgency(1.5, "user waiting")
.with_max_bid(20);
let result = allocator.submit_bid(bid).await;
assert!(result.is_ok());
let status = allocator.market_status("resource-a").await.unwrap();
assert_eq!(status.pending_bids, 1);
}
#[tokio::test]
async fn test_allocation() {
let allocator = MarketAllocator::with_pricing(PricingStrategy::Free);
allocator.register_agent("agent-1", 100, 1.0).await;
allocator.register_agent("agent-2", 100, 1.0).await;
let bid1 = ResourceBid::new("agent-1", "resource-a").with_priority(5);
allocator.submit_bid(bid1).await.unwrap();
let bid2 = ResourceBid::new("agent-2", "resource-a").with_priority(8);
allocator.submit_bid(bid2).await.unwrap();
let result = allocator.allocate("resource-a").await;
match result {
AllocationResult::Allocated { agent_id, .. } => {
assert_eq!(agent_id, "agent-2");
}
_ => panic!("Expected allocation"),
}
}
#[tokio::test]
async fn test_urgency_affects_allocation() {
let allocator = MarketAllocator::with_pricing(PricingStrategy::Free);
allocator.register_agent("agent-1", 100, 1.0).await;
allocator.register_agent("agent-2", 100, 1.0).await;
let bid1 = ResourceBid::new("agent-1", "resource-a")
.with_priority(8)
.with_urgency(1.0, "normal");
allocator.submit_bid(bid1).await.unwrap();
let bid2 = ResourceBid::new("agent-2", "resource-a")
.with_priority(5)
.with_urgency(2.5, "deadline approaching");
allocator.submit_bid(bid2).await.unwrap();
let result = allocator.allocate("resource-a").await;
match result {
AllocationResult::Allocated { agent_id, .. } => {
assert_eq!(agent_id, "agent-2");
}
_ => panic!("Expected allocation"),
}
}
#[tokio::test]
async fn test_second_price_auction() {
let allocator = MarketAllocator::with_pricing(PricingStrategy::SecondPrice);
allocator.register_agent("agent-1", 100, 1.0).await;
allocator.register_agent("agent-2", 100, 1.0).await;
let bid1 = ResourceBid::new("agent-1", "resource-a")
.with_priority(8)
.with_max_bid(30);
allocator.submit_bid(bid1).await.unwrap();
let bid2 = ResourceBid::new("agent-2", "resource-a")
.with_priority(5)
.with_max_bid(20);
allocator.submit_bid(bid2).await.unwrap();
let result = allocator.allocate("resource-a").await;
match result {
AllocationResult::Allocated { price, .. } => {
assert_eq!(price, 20);
}
_ => panic!("Expected allocation"),
}
}
#[tokio::test]
async fn test_insufficient_budget() {
let allocator = MarketAllocator::with_pricing(PricingStrategy::FirstPrice);
allocator.register_agent("agent-1", 10, 1.0).await;
let bid = ResourceBid::new("agent-1", "resource-a").with_max_bid(20);
let result = allocator.submit_bid(bid).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Insufficient budget"));
}
#[tokio::test]
async fn test_release_resource() {
let allocator = MarketAllocator::with_pricing(PricingStrategy::Free);
allocator.register_agent("agent-1", 100, 1.0).await;
let bid = ResourceBid::new("agent-1", "resource-a");
allocator.submit_bid(bid).await.unwrap();
allocator.allocate("resource-a").await;
let status = allocator.market_status("resource-a").await.unwrap();
assert!(status.current_holder.is_some());
let released = allocator.release("resource-a", "agent-1").await;
assert!(released);
let status = allocator.market_status("resource-a").await.unwrap();
assert!(status.current_holder.is_none());
}
#[tokio::test]
async fn test_cannot_allocate_held_resource() {
let allocator = MarketAllocator::with_pricing(PricingStrategy::Free);
allocator.register_agent("agent-1", 100, 1.0).await;
allocator.register_agent("agent-2", 100, 1.0).await;
let bid1 = ResourceBid::new("agent-1", "resource-a");
allocator.submit_bid(bid1).await.unwrap();
allocator.allocate("resource-a").await;
let bid2 = ResourceBid::new("agent-2", "resource-a");
allocator.submit_bid(bid2).await.unwrap();
let result = allocator.allocate("resource-a").await;
match result {
AllocationResult::StillHeld { holder, .. } => {
assert_eq!(holder, "agent-1");
}
_ => panic!("Expected StillHeld result"),
}
}
#[test]
fn test_urgency_calculator() {
let context = UrgencyContext::default();
let urgency = UrgencyCalculator::calculate(&context);
assert!((urgency - 1.0).abs() < 0.01);
let context = UrgencyContext {
user_waiting: true,
..Default::default()
};
let urgency = UrgencyCalculator::calculate(&context);
assert!((urgency - 2.0).abs() < 0.01);
let context = UrgencyContext {
critical_path: true,
..Default::default()
};
let urgency = UrgencyCalculator::calculate(&context);
assert!((urgency - 1.5).abs() < 0.01);
let context = UrgencyContext {
user_waiting: true,
critical_path: true,
..Default::default()
};
let urgency = UrgencyCalculator::calculate(&context);
assert!((urgency - 3.0).abs() < 0.01);
}
#[test]
fn test_urgency_builder() {
let urgency = UrgencyCalculator::builder()
.user_waiting(true)
.critical_path(true)
.resources_held(2)
.calculate();
assert!(urgency > 4.0 && urgency < 4.5);
}
#[tokio::test]
async fn test_budget_replenishment() {
let mut budget = AgentBudget::new("agent-1", 100).with_replenish_rate(10.0);
budget.spend(50);
assert_eq!(budget.available, 50);
budget.last_replenish = Instant::now() - Duration::from_secs(5);
budget.replenish();
assert_eq!(budget.available, 100); }
#[tokio::test]
async fn test_market_stats() {
let allocator = MarketAllocator::with_pricing(PricingStrategy::Free);
allocator.register_agent("agent-1", 100, 1.0).await;
allocator.register_agent("agent-2", 100, 1.0).await;
for i in 0..5 {
let bid = ResourceBid::new("agent-1", format!("resource-{}", i));
allocator.submit_bid(bid).await.unwrap();
allocator.allocate(&format!("resource-{}", i)).await;
}
let stats = allocator.get_stats().await;
assert_eq!(stats.registered_agents, 2);
assert_eq!(stats.total_allocations, 5);
}
#[test]
fn test_bid_scoring() {
let bid1 = ResourceBid::new("agent-1", "resource").with_priority(8);
let bid2 = ResourceBid::new("agent-2", "resource").with_priority(5);
assert!(bid1.score() > bid2.score());
let bid3 = ResourceBid::new("agent-3", "resource")
.with_priority(5)
.with_urgency(2.0, "urgent");
assert!(bid3.effective_priority() > bid2.effective_priority());
}
#[tokio::test]
async fn test_cancel_bid() {
let allocator = MarketAllocator::new();
allocator.register_agent("agent-1", 100, 1.0).await;
let bid = ResourceBid::new("agent-1", "resource-a");
allocator.submit_bid(bid).await.unwrap();
let status = allocator.market_status("resource-a").await.unwrap();
assert_eq!(status.pending_bids, 1);
let cancelled = allocator.cancel_bid("agent-1", "resource-a").await;
assert!(cancelled);
let status = allocator.market_status("resource-a").await.unwrap();
assert_eq!(status.pending_bids, 0);
}
}