use crate::error::{Decision, FlowGuardError};
use crate::limiters::Limiter;
use std::sync::Arc;
use tracing::{debug, info, trace, warn};
#[derive(Clone)]
pub struct DecisionNode {
pub id: String,
pub name: String,
pub limiter: Arc<dyn Limiter>,
pub priority: u16,
pub enabled: bool,
pub short_circuit: bool,
pub cost: u64,
}
impl DecisionNode {
pub fn new(id: String, name: String, limiter: Arc<dyn Limiter>, priority: u16) -> Self {
Self {
id,
name,
limiter,
priority,
enabled: true,
short_circuit: true,
cost: 1,
}
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn with_short_circuit(mut self, short_circuit: bool) -> Self {
self.short_circuit = short_circuit;
self
}
pub fn with_cost(mut self, cost: u64) -> Self {
self.cost = cost;
self
}
async fn check(&self) -> Result<bool, FlowGuardError> {
if !self.enabled {
debug!("DecisionNode {} is disabled, skipping", self.id);
return Ok(true);
}
trace!(
"Checking decision node: {} (cost: {})",
self.name,
self.cost
);
self.limiter.allow(self.cost).await
}
}
#[derive(Clone)]
pub struct DecisionChain {
nodes: Vec<DecisionNode>,
stats: Arc<std::sync::RwLock<ChainStats>>,
}
#[derive(Debug, Clone, Default)]
pub struct ChainStats {
pub total_checks: u64,
pub allowed_count: u64,
pub rejected_count: u64,
pub node_rejections: Vec<(String, u64)>,
}
impl DecisionChain {
pub fn new(nodes: Vec<DecisionNode>) -> Self {
let mut chain = Self {
nodes: Vec::new(),
stats: Arc::new(std::sync::RwLock::new(ChainStats::default())),
};
for node in nodes {
chain.add_node(node);
}
chain
}
pub fn add_node(&mut self, node: DecisionNode) {
let pos = self
.nodes
.binary_search_by(|n| n.priority.cmp(&node.priority).reverse())
.unwrap_or_else(|pos| pos);
self.nodes.insert(pos, node);
}
pub fn remove_node(&mut self, node_id: &str) -> Option<DecisionNode> {
if let Some(pos) = self.nodes.iter().position(|n| n.id == node_id) {
Some(self.nodes.remove(pos))
} else {
None
}
}
pub async fn check(&self) -> Result<Decision, FlowGuardError> {
{
let mut stats = self.stats.write().unwrap();
stats.total_checks += 1;
}
debug!(
"Starting decision chain check with {} nodes",
self.nodes.len()
);
let mut rejected_reason = None;
for node in &self.nodes {
if !node.enabled {
trace!("Skipping disabled node: {}", node.id);
continue;
}
trace!("Checking node: {}", node.name);
match node.check().await {
Ok(true) => {
trace!("Node {} allowed", node.name);
}
Ok(false) => {
warn!("Node {} rejected request", node.name);
{
let mut stats = self.stats.write().unwrap();
stats.rejected_count += 1;
if let Some(pos) = stats
.node_rejections
.iter()
.position(|(id, _)| id == &node.id)
{
stats.node_rejections[pos].1 += 1;
} else {
stats.node_rejections.push((node.id.clone(), 1));
}
}
if rejected_reason.is_none() {
rejected_reason =
Some(format!("Rejected by {}: rate limit exceeded", node.name));
}
if node.short_circuit {
info!("Decision chain short-circuited by node: {}", node.name);
return Ok(Decision::Rejected(rejected_reason.unwrap()));
}
}
Err(e) => {
warn!("Node {} check failed: {:?}", node.name, e);
return Err(e);
}
}
}
if let Some(reason) = rejected_reason {
return Ok(Decision::Rejected(reason));
}
{
let mut stats = self.stats.write().unwrap();
stats.allowed_count += 1;
}
debug!("Decision chain: all nodes allowed");
Ok(Decision::Allowed(None))
}
pub async fn check_all(&self) -> Result<Decision, FlowGuardError> {
{
let mut stats = self.stats.write().unwrap();
stats.total_checks += 1;
}
debug!(
"Starting full decision chain check with {} nodes",
self.nodes.len()
);
let mut rejection_reasons = Vec::new();
for node in &self.nodes {
if !node.enabled {
trace!("Skipping disabled node: {}", node.id);
continue;
}
trace!("Checking node: {}", node.name);
match node.check().await {
Ok(true) => {
trace!("Node {} allowed", node.name);
}
Ok(false) => {
warn!("Node {} rejected request", node.name);
rejection_reasons.push(format!("{}: rate limit exceeded", node.name));
{
let mut stats = self.stats.write().unwrap();
stats.rejected_count += 1;
if let Some(pos) = stats
.node_rejections
.iter()
.position(|(id, _)| id == &node.id)
{
stats.node_rejections[pos].1 += 1;
} else {
stats.node_rejections.push((node.id.clone(), 1));
}
}
}
Err(e) => {
warn!("Node {} check failed: {:?}", node.name, e);
return Err(e);
}
}
}
if rejection_reasons.is_empty() {
{
let mut stats = self.stats.write().unwrap();
stats.allowed_count += 1;
}
debug!("Decision chain: all nodes allowed");
Ok(Decision::Allowed(None))
} else {
let reason = rejection_reasons.join("; ");
info!("Decision chain rejected: {}", reason);
Ok(Decision::Rejected(reason))
}
}
pub fn stats(&self) -> ChainStats {
self.stats.read().unwrap().clone()
}
pub fn reset_stats(&self) {
let mut stats = self.stats.write().unwrap();
*stats = ChainStats::default();
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn enabled_node_count(&self) -> usize {
self.nodes.iter().filter(|n| n.enabled).count()
}
pub fn enable_node(&mut self, node_id: &str) -> bool {
if let Some(node) = self.nodes.iter_mut().find(|n| n.id == node_id) {
node.enabled = true;
info!("Enabled node: {}", node_id);
true
} else {
warn!("Failed to enable node: {} (not found)", node_id);
false
}
}
pub fn disable_node(&mut self, node_id: &str) -> bool {
if let Some(node) = self.nodes.iter_mut().find(|n| n.id == node_id) {
node.enabled = false;
info!("Disabled node: {}", node_id);
true
} else {
warn!("Failed to disable node: {} (not found)", node_id);
false
}
}
pub fn set_short_circuit(&mut self, node_id: &str, short_circuit: bool) -> bool {
if let Some(node) = self.nodes.iter_mut().find(|n| n.id == node_id) {
node.short_circuit = short_circuit;
info!("Set short_circuit={} for node: {}", short_circuit, node_id);
true
} else {
warn!(
"Failed to set short_circuit for node: {} (not found)",
node_id
);
false
}
}
}
pub struct DecisionChainBuilder {
nodes: Vec<DecisionNode>,
}
impl DecisionChainBuilder {
pub fn new() -> Self {
Self { nodes: Vec::new() }
}
pub fn add_node(mut self, node: DecisionNode) -> Self {
self.nodes.push(node);
self
}
pub fn build(self) -> DecisionChain {
DecisionChain::new(self.nodes)
}
}
impl Default for DecisionChainBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::limiters::{
ConcurrencyLimiter, FixedWindowLimiter, SlidingWindowLimiter, TokenBucketLimiter,
};
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
struct MockLimiter {
allowed: Arc<std::sync::atomic::AtomicBool>,
}
impl MockLimiter {
fn new(allowed: bool) -> Self {
Self {
allowed: Arc::new(std::sync::atomic::AtomicBool::new(allowed)),
}
}
fn set_allowed(&self, v: bool) {
self.allowed.store(v, std::sync::atomic::Ordering::SeqCst);
}
}
impl Limiter for MockLimiter {
fn allow(
&self,
_cost: u64,
) -> Pin<Box<dyn Future<Output = Result<bool, FlowGuardError>> + Send + '_>> {
let a = self.allowed.load(std::sync::atomic::Ordering::SeqCst);
Box::pin(async move { Ok(a) })
}
}
struct SpyLimiter {
calls: Arc<std::sync::atomic::AtomicUsize>,
}
impl SpyLimiter {
fn new() -> Self {
Self {
calls: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
}
}
}
impl Limiter for SpyLimiter {
fn allow(
&self,
_cost: u64,
) -> Pin<Box<dyn Future<Output = Result<bool, FlowGuardError>> + Send + '_>> {
self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Box::pin(async move { Ok(true) })
}
}
#[test]
fn test_decision_node_creation() {
let limiter = Arc::new(TokenBucketLimiter::new(100, 10));
let node = DecisionNode::new(
"node1".to_string(),
"Token Bucket".to_string(),
limiter,
100,
);
assert_eq!(node.id, "node1");
assert_eq!(node.name, "Token Bucket");
assert_eq!(node.priority, 100);
assert!(node.enabled);
assert!(node.short_circuit);
assert_eq!(node.cost, 1);
}
#[test]
fn test_decision_node_with_options() {
let limiter = Arc::new(TokenBucketLimiter::new(100, 10));
let node = DecisionNode::new(
"node1".to_string(),
"Token Bucket".to_string(),
limiter,
100,
)
.with_enabled(false)
.with_short_circuit(false)
.with_cost(5);
assert!(!node.enabled);
assert!(!node.short_circuit);
assert_eq!(node.cost, 5);
}
#[tokio::test]
async fn test_decision_chain_empty() {
let chain = DecisionChain::new(vec![]);
let decision = chain.check().await.unwrap();
assert_eq!(decision, Decision::Allowed(None));
}
#[tokio::test]
async fn test_decision_chain_single_node() {
let limiter = Arc::new(TokenBucketLimiter::new(10, 1));
let node = DecisionNode::new(
"node1".to_string(),
"Token Bucket".to_string(),
limiter,
100,
);
let chain = DecisionChain::new(vec![node]);
for _ in 0..10 {
let decision = chain.check().await.unwrap();
assert_eq!(decision, Decision::Allowed(None));
}
let decision = chain.check().await.unwrap();
assert!(matches!(decision, Decision::Rejected(_)));
}
#[tokio::test]
async fn test_decision_chain_multiple_nodes() {
let limiter1 = Arc::new(TokenBucketLimiter::new(5, 1));
let limiter2 = Arc::new(SlidingWindowLimiter::new(Duration::from_secs(1), 10));
let node1 = DecisionNode::new(
"node1".to_string(),
"Token Bucket".to_string(),
limiter1,
100,
);
let node2 = DecisionNode::new(
"node2".to_string(),
"Sliding Window".to_string(),
limiter2,
50,
);
let chain = DecisionChain::new(vec![node1, node2]);
for _ in 0..5 {
let decision = chain.check().await.unwrap();
assert_eq!(decision, Decision::Allowed(None));
}
let decision = chain.check().await.unwrap();
assert!(matches!(decision, Decision::Rejected(_)));
}
#[tokio::test]
async fn test_decision_chain_priority() {
let limiter1 = Arc::new(TokenBucketLimiter::new(10, 1));
let limiter2 = Arc::new(TokenBucketLimiter::new(5, 1));
let node1 = DecisionNode::new(
"node1".to_string(),
"Low Priority".to_string(),
limiter1,
50,
);
let node2 = DecisionNode::new(
"node2".to_string(),
"High Priority".to_string(),
limiter2,
100,
);
let chain = DecisionChain::new(vec![node1, node2]);
for _ in 0..5 {
let decision = chain.check().await.unwrap();
assert_eq!(decision, Decision::Allowed(None));
}
let decision = chain.check().await.unwrap();
assert!(matches!(decision, Decision::Rejected(_)));
if let Decision::Rejected(reason) = decision {
assert!(reason.contains("High Priority"));
}
}
#[tokio::test]
async fn test_decision_chain_disabled_node() {
let limiter1 = Arc::new(TokenBucketLimiter::new(0, 1));
let limiter2 = Arc::new(TokenBucketLimiter::new(10, 1));
let node1 = DecisionNode::new(
"node1".to_string(),
"Disabled Node".to_string(),
limiter1,
100,
)
.with_enabled(false);
let node2 = DecisionNode::new("node2".to_string(), "Active Node".to_string(), limiter2, 50);
let chain = DecisionChain::new(vec![node1, node2]);
let decision = chain.check().await.unwrap();
assert_eq!(decision, Decision::Allowed(None));
}
#[tokio::test]
async fn test_decision_chain_short_circuit() {
let limiter1 = Arc::new(TokenBucketLimiter::new(5, 1));
let limiter2 = Arc::new(TokenBucketLimiter::new(10, 1));
let node1 = DecisionNode::new("node1".to_string(), "First Node".to_string(), limiter1, 100);
let node2 = DecisionNode::new("node2".to_string(), "Second Node".to_string(), limiter2, 50);
let chain = DecisionChain::new(vec![node1, node2]);
for _ in 0..5 {
let decision = chain.check().await.unwrap();
assert_eq!(decision, Decision::Allowed(None));
}
let decision = chain.check().await.unwrap();
assert!(matches!(decision, Decision::Rejected(_)));
}
#[tokio::test]
async fn test_decision_chain_no_short_circuit() {
let limiter1 = Arc::new(TokenBucketLimiter::new(5, 1));
let limiter2 = Arc::new(TokenBucketLimiter::new(3, 1));
let node1 = DecisionNode::new("node1".to_string(), "First Node".to_string(), limiter1, 100)
.with_short_circuit(false);
let node2 = DecisionNode::new("node2".to_string(), "Second Node".to_string(), limiter2, 50);
let chain = DecisionChain::new(vec![node1, node2]);
for _ in 0..3 {
let decision = chain.check().await.unwrap();
assert_eq!(decision, Decision::Allowed(None));
}
let decision = chain.check().await.unwrap();
assert!(matches!(decision, Decision::Rejected(_)));
}
#[tokio::test]
async fn test_decision_chain_check_all() {
let limiter1 = Arc::new(TokenBucketLimiter::new(3, 1));
let limiter2 = Arc::new(TokenBucketLimiter::new(5, 1));
let node1 = DecisionNode::new("node1".to_string(), "First Node".to_string(), limiter1, 100);
let node2 = DecisionNode::new("node2".to_string(), "Second Node".to_string(), limiter2, 50);
let chain = DecisionChain::new(vec![node1, node2]);
for _ in 0..3 {
let decision = chain.check_all().await.unwrap();
assert_eq!(decision, Decision::Allowed(None));
}
let decision = chain.check_all().await.unwrap();
if let Decision::Rejected(reason) = decision {
assert!(reason.contains("First Node"));
}
}
#[tokio::test]
async fn test_decision_chain_stats() {
let limiter = Arc::new(TokenBucketLimiter::new(5, 1));
let node = DecisionNode::new(
"node1".to_string(),
"Token Bucket".to_string(),
limiter,
100,
);
let chain = DecisionChain::new(vec![node]);
for _ in 0..10 {
chain.check().await.unwrap();
}
let stats = chain.stats();
assert_eq!(stats.total_checks, 10);
assert_eq!(stats.allowed_count, 5);
assert_eq!(stats.rejected_count, 5);
}
#[tokio::test]
async fn test_decision_chain_node_rejections() {
let limiter1 = Arc::new(TokenBucketLimiter::new(5, 1));
let limiter2 = Arc::new(TokenBucketLimiter::new(3, 1));
let node1 = DecisionNode::new("node1".to_string(), "First Node".to_string(), limiter1, 100)
.with_short_circuit(false);
let node2 = DecisionNode::new("node2".to_string(), "Second Node".to_string(), limiter2, 50);
let chain = DecisionChain::new(vec![node1, node2]);
for _ in 0..10 {
chain.check_all().await.unwrap();
}
let stats = chain.stats();
assert_eq!(stats.node_rejections.len(), 2);
let node1_rejections = stats
.node_rejections
.iter()
.find(|(id, _)| id == "node1")
.map(|(_, count)| *count)
.unwrap_or(0);
let node2_rejections = stats
.node_rejections
.iter()
.find(|(id, _)| id == "node2")
.map(|(_, count)| *count)
.unwrap_or(0);
assert!(node1_rejections > 0);
assert!(node2_rejections > 0);
}
#[tokio::test]
async fn test_decision_chain_add_remove_node() {
let limiter = Arc::new(TokenBucketLimiter::new(10, 1));
let node = DecisionNode::new(
"node1".to_string(),
"Token Bucket".to_string(),
limiter,
100,
);
let mut chain = DecisionChain::new(vec![]);
assert_eq!(chain.node_count(), 0);
chain.add_node(node);
assert_eq!(chain.node_count(), 1);
chain.remove_node("node1");
assert_eq!(chain.node_count(), 0);
}
#[tokio::test]
async fn test_decision_chain_enable_disable_node() {
let limiter = Arc::new(TokenBucketLimiter::new(0, 1));
let node = DecisionNode::new(
"node1".to_string(),
"Token Bucket".to_string(),
limiter,
100,
);
let mut chain = DecisionChain::new(vec![node]);
chain.disable_node("node1");
assert_eq!(chain.enabled_node_count(), 0);
let decision = chain.check().await.unwrap();
assert_eq!(decision, Decision::Allowed(None));
chain.enable_node("node1");
assert_eq!(chain.enabled_node_count(), 1);
let decision = chain.check().await.unwrap();
assert!(matches!(decision, Decision::Rejected(_)));
}
#[tokio::test]
async fn test_decision_chain_set_short_circuit() {
let limiter1 = Arc::new(MockLimiter::new(true));
let limiter2_spy = Arc::new(SpyLimiter::new());
let limiter2 = limiter2_spy.clone();
let node1 = DecisionNode::new(
"node1".to_string(),
"Mock Node".to_string(),
limiter1.clone(),
100,
)
.with_short_circuit(false);
let node2 = DecisionNode::new("node2".to_string(), "Spy Node".to_string(), limiter2, 50);
let mut chain = DecisionChain::new(vec![node1, node2]);
let decision = chain.check().await.unwrap();
assert_eq!(decision, Decision::Allowed(None));
assert_eq!(
limiter2_spy.calls.load(std::sync::atomic::Ordering::SeqCst),
1
);
limiter1.set_allowed(false);
let decision = chain.check().await.unwrap();
assert!(matches!(decision, Decision::Rejected(_)));
assert_eq!(
limiter2_spy.calls.load(std::sync::atomic::Ordering::SeqCst),
2
);
chain.set_short_circuit("node1", true);
let decision = chain.check().await.unwrap();
assert!(matches!(decision, Decision::Rejected(_)));
assert_eq!(
limiter2_spy.calls.load(std::sync::atomic::Ordering::SeqCst),
2
);
limiter1.set_allowed(true);
let decision = chain.check().await.unwrap();
assert_eq!(decision, Decision::Allowed(None));
assert_eq!(
limiter2_spy.calls.load(std::sync::atomic::Ordering::SeqCst),
3
); }
#[test]
fn test_decision_chain_builder() {
let limiter1 = Arc::new(TokenBucketLimiter::new(100, 10));
let limiter2 = Arc::new(SlidingWindowLimiter::new(Duration::from_secs(1), 100));
let node1 = DecisionNode::new(
"node1".to_string(),
"Token Bucket".to_string(),
limiter1,
100,
);
let node2 = DecisionNode::new(
"node2".to_string(),
"Sliding Window".to_string(),
limiter2,
50,
);
let chain = DecisionChainBuilder::new()
.add_node(node1)
.add_node(node2)
.build();
assert_eq!(chain.node_count(), 2);
}
#[tokio::test]
async fn test_decision_chain_mixed_limiters() {
let limiter1 = Arc::new(TokenBucketLimiter::new(10, 1));
let limiter2 = Arc::new(SlidingWindowLimiter::new(Duration::from_secs(1), 5));
let limiter3 = Arc::new(FixedWindowLimiter::new(Duration::from_secs(1), 3));
let limiter4 = Arc::new(ConcurrencyLimiter::new(2));
let node1 = DecisionNode::new(
"token_bucket".to_string(),
"Token Bucket".to_string(),
limiter1,
100,
);
let node2 = DecisionNode::new(
"sliding_window".to_string(),
"Sliding Window".to_string(),
limiter2,
75,
);
let node3 = DecisionNode::new(
"fixed_window".to_string(),
"Fixed Window".to_string(),
limiter3,
50,
);
let node4 = DecisionNode::new(
"concurrency".to_string(),
"Concurrency".to_string(),
limiter4,
25,
);
let chain = DecisionChain::new(vec![node1, node2, node3, node4]);
let decision = chain.check().await.unwrap();
assert_eq!(decision, Decision::Allowed(None));
let stats = chain.stats();
assert_eq!(stats.total_checks, 1);
assert_eq!(stats.allowed_count, 1);
}
#[tokio::test]
async fn test_decision_chain_cost() {
let limiter = Arc::new(TokenBucketLimiter::new(10, 1));
let node = DecisionNode::new(
"node1".to_string(),
"Token Bucket".to_string(),
limiter,
100,
)
.with_cost(2);
let chain = DecisionChain::new(vec![node]);
for _ in 0..5 {
let decision = chain.check().await.unwrap();
assert_eq!(decision, Decision::Allowed(None));
}
let decision = chain.check().await.unwrap();
assert!(matches!(decision, Decision::Rejected(_)));
}
#[tokio::test]
async fn test_decision_chain_reset_stats() {
let limiter = Arc::new(TokenBucketLimiter::new(10, 1));
let node = DecisionNode::new(
"node1".to_string(),
"Token Bucket".to_string(),
limiter,
100,
);
let chain = DecisionChain::new(vec![node]);
for _ in 0..5 {
chain.check().await.unwrap();
}
let stats = chain.stats();
assert_eq!(stats.total_checks, 5);
chain.reset_stats();
let stats = chain.stats();
assert_eq!(stats.total_checks, 0);
}
#[tokio::test]
async fn test_decision_chain_concurrent_checks() {
let limiter = Arc::new(TokenBucketLimiter::new(100, 10));
let node = DecisionNode::new(
"node1".to_string(),
"Token Bucket".to_string(),
limiter,
100,
);
let chain = Arc::new(DecisionChain::new(vec![node]));
let mut handles = vec![];
for _ in 0..10 {
let chain_clone = Arc::clone(&chain);
handles.push(tokio::spawn(
async move { chain_clone.check().await.unwrap() },
));
}
let mut results = Vec::new();
for handle in handles {
results.push(handle.await.unwrap());
}
for result in results {
assert_eq!(result, Decision::Allowed(None));
}
let stats = chain.stats();
assert_eq!(stats.total_checks, 10);
}
}