use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::Mutex;
use futures::future::BoxFuture;
use nexil::Tool;
use nexil::core::errors::{ConduitError, ErrorKind};
use nexil::tools::context::ToolContext;
use nexil::tools::schema::{ToolHandlerFn, ToolResult};
use serde_json::Value;
pub type NextFn =
Box<dyn Fn(Value, Option<ToolContext>) -> BoxFuture<'static, ToolResult> + Send + Sync>;
pub trait ToolMiddleware: Send + Sync {
fn call(
&self,
tool_name: &str,
args: Value,
ctx: Option<ToolContext>,
next: &NextFn,
) -> BoxFuture<'static, ToolResult>;
}
#[derive(Debug, Clone, Default)]
pub struct CircuitState {
pub consecutive_failures: u32,
pub tripped_at: Option<Instant>,
}
pub struct CircuitBreaker {
max_failures: u32,
reset_timeout: Duration,
states: Arc<Mutex<HashMap<String, CircuitState>>>,
}
impl CircuitBreaker {
pub fn new(max_failures: u32, reset_timeout: Duration) -> Self {
Self {
max_failures,
reset_timeout,
states: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn default_config() -> Self {
Self::new(5, Duration::from_secs(60))
}
fn is_tripped(&self, tool_name: &str) -> bool {
let states = self.states.lock();
if let Some(state) = states.get(tool_name)
&& let Some(tripped_at) = state.tripped_at
{
if tripped_at.elapsed() >= self.reset_timeout {
return false; }
return true;
}
false
}
}
impl ToolMiddleware for CircuitBreaker {
fn call(
&self,
tool_name: &str,
args: Value,
ctx: Option<ToolContext>,
next: &NextFn,
) -> BoxFuture<'static, ToolResult> {
if self.is_tripped(tool_name) {
let msg = format!("Circuit breaker open for tool '{tool_name}'. Try again later.");
return Box::pin(async move { Err(ConduitError::new(ErrorKind::Tool, msg)) });
}
let name = tool_name.to_owned();
let states = Arc::clone(&self.states);
let max_failures = self.max_failures;
let fut = next(args, ctx);
Box::pin(async move {
let result = fut.await;
let mut s = states.lock();
match &result {
Ok(_) => {
s.insert(name, CircuitState::default());
}
Err(_) => {
let state = s.entry(name).or_default();
state.consecutive_failures += 1;
if state.consecutive_failures >= max_failures {
state.tripped_at = Some(Instant::now());
}
}
}
result
})
}
}
#[derive(Debug, Clone, Default)]
pub struct ToolMetrics {
pub call_count: u64,
pub success_count: u64,
pub failure_count: u64,
pub total_duration_ms: u64,
}
pub struct MetricsCollector {
pub stats: Arc<Mutex<HashMap<String, ToolMetrics>>>,
}
impl MetricsCollector {
pub fn new() -> Self {
Self {
stats: Arc::new(Mutex::new(HashMap::new())),
}
}
}
impl Default for MetricsCollector {
fn default() -> Self {
Self::new()
}
}
impl ToolMiddleware for MetricsCollector {
fn call(
&self,
tool_name: &str,
args: Value,
ctx: Option<ToolContext>,
next: &NextFn,
) -> BoxFuture<'static, ToolResult> {
let stats = Arc::clone(&self.stats);
let name = tool_name.to_owned();
let fut = next(args, ctx);
Box::pin(async move {
let start = Instant::now();
let result = fut.await;
let duration_ms = start.elapsed().as_millis() as u64;
let mut map = stats.lock();
let metrics = map.entry(name).or_default();
metrics.call_count += 1;
metrics.total_duration_ms += duration_ms;
match &result {
Ok(_) => metrics.success_count += 1,
Err(_) => metrics.failure_count += 1,
}
result
})
}
}
pub struct MiddlewareChain {
layers: Vec<Arc<dyn ToolMiddleware>>,
}
impl Default for MiddlewareChain {
fn default() -> Self {
Self::new()
}
}
impl MiddlewareChain {
pub fn new() -> Self {
Self { layers: Vec::new() }
}
pub fn push(&mut self, layer: Arc<dyn ToolMiddleware>) -> &mut Self {
self.layers.push(layer);
self
}
pub fn with_defaults() -> Self {
let mut chain = Self::new();
chain.push(Arc::new(CircuitBreaker::default_config()));
chain.push(Arc::new(MetricsCollector::new()));
chain
}
pub fn wrap_handler(&self, tool_name: &str, tool: &Tool) -> Option<Arc<ToolHandlerFn>> {
let handler = tool.handler.as_ref()?.clone();
if self.layers.is_empty() {
return Some(handler);
}
let layers: Vec<Arc<dyn ToolMiddleware>> = self.layers.clone();
let name = tool_name.to_owned();
let wrapped: Arc<ToolHandlerFn> = Arc::new(move |args: Value, ctx: Option<ToolContext>| {
let layers = layers.clone();
let name = name.clone();
let handler = handler.clone();
Box::pin(async move {
let inner: NextFn = Box::new(move |a, c| handler(a, c));
let mut current = inner;
for layer in layers.iter() {
let layer = Arc::clone(layer);
let prev = current;
let n = name.clone();
current = Box::new(move |a, c| {
let layer = Arc::clone(&layer);
let prev = &prev;
let n = n.clone();
layer.call(&n, a, c, prev)
});
}
current(args, ctx).await
}) as BoxFuture<'static, ToolResult>
});
Some(wrapped)
}
pub fn wrap_tools(&self, tools: &[Tool]) -> Vec<Tool> {
if self.layers.is_empty() {
return tools.to_vec();
}
tools
.iter()
.map(|tool| {
let wrapped_handler = self.wrap_handler(&tool.name, tool);
Tool {
name: tool.name.clone(),
description: tool.description.clone(),
parameters: tool.parameters.clone(),
handler: wrapped_handler.or_else(|| tool.handler.clone()),
context: tool.context,
timeout: tool.timeout,
read_only: tool.read_only,
}
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn ok_tool() -> Tool {
Tool::new(
"test_ok",
"always succeeds",
json!({"type": "object"}),
|_args, _ctx| Box::pin(async { Ok(json!({"status": "ok"})) }),
)
}
fn fail_tool() -> Tool {
Tool::new(
"test_fail",
"always fails",
json!({"type": "object"}),
|_args, _ctx| Box::pin(async { Err(ConduitError::new(ErrorKind::Tool, "boom")) }),
)
}
#[tokio::test]
async fn test_metrics_collector_counts() {
let metrics = Arc::new(MetricsCollector::new());
let mut chain = MiddlewareChain::new();
chain.push(Arc::clone(&metrics) as Arc<dyn ToolMiddleware>);
let tools = chain.wrap_tools(&[ok_tool()]);
let tool = &tools[0];
let handler = tool.handler.as_ref().unwrap();
let _ = handler(json!({}), None).await;
let _ = handler(json!({}), None).await;
let stats = metrics.stats.lock();
let m = stats.get("test_ok").unwrap();
assert_eq!(m.call_count, 2);
assert_eq!(m.success_count, 2);
}
#[tokio::test]
async fn test_circuit_breaker_trips() {
let cb = Arc::new(CircuitBreaker::new(2, Duration::from_secs(60)));
let mut chain = MiddlewareChain::new();
chain.push(cb.clone() as Arc<dyn ToolMiddleware>);
let tools = chain.wrap_tools(&[fail_tool()]);
let tool = &tools[0];
let handler = tool.handler.as_ref().unwrap();
let _ = handler(json!({}), None).await;
let _ = handler(json!({}), None).await;
let result = handler(json!({}), None).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.message.contains("Circuit breaker open"));
}
}