use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub enum RateLimitPolicy {
#[default]
Reject,
Queue,
Backoff {
base_delay: Duration,
max_delay: Duration,
multiplier: f64,
},
Throttle {
reduced_rate: f64,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitResult {
pub allowed: bool,
pub wait_time: Duration,
pub remaining_tokens: f64,
pub cost_remaining: f64,
pub reason: Option<String>,
}
impl RateLimitResult {
pub fn allowed(remaining_tokens: f64, cost_remaining: f64) -> Self {
Self {
allowed: true,
wait_time: Duration::ZERO,
remaining_tokens,
cost_remaining,
reason: None,
}
}
pub fn denied(
wait_time: Duration,
remaining_tokens: f64,
cost_remaining: f64,
reason: impl Into<String>,
) -> Self {
Self {
allowed: false,
wait_time,
remaining_tokens,
cost_remaining,
reason: Some(reason.into()),
}
}
}
pub struct TokenBucket {
capacity: f64,
refill_rate: f64,
available: Mutex<f64>,
last_refill: Mutex<Instant>,
}
impl TokenBucket {
pub fn new(capacity: f64, refill_rate: f64) -> Self {
Self {
capacity,
refill_rate,
available: Mutex::new(capacity),
last_refill: Mutex::new(Instant::now()),
}
}
fn refill(&self) {
let mut last = self.last_refill.lock().unwrap();
let mut avail = self.available.lock().unwrap();
let now = Instant::now();
let elapsed = now.duration_since(*last).as_secs_f64();
*avail = (*avail + elapsed * self.refill_rate).min(self.capacity);
*last = now;
}
pub fn try_acquire(&self, n: f64) -> RateLimitResult {
self.refill();
let mut avail = self.available.lock().unwrap();
if *avail >= n {
*avail -= n;
RateLimitResult::allowed(*avail, f64::INFINITY)
} else {
let deficit = n - *avail;
let wait = if self.refill_rate > 0.0 {
Duration::from_secs_f64(deficit / self.refill_rate)
} else {
Duration::from_secs(u64::MAX / 2)
};
RateLimitResult::denied(wait, *avail, f64::INFINITY, "token bucket exhausted")
}
}
pub fn force_acquire(&self, n: f64) {
self.refill();
let mut avail = self.available.lock().unwrap();
*avail -= n;
}
pub fn available(&self) -> f64 {
self.refill();
*self.available.lock().unwrap()
}
pub fn capacity(&self) -> f64 {
self.capacity
}
pub fn refill_rate(&self) -> f64 {
self.refill_rate
}
pub fn wait_time_for(&self, n: f64) -> Duration {
self.refill();
let avail = *self.available.lock().unwrap();
if avail >= n {
return Duration::ZERO;
}
let deficit = n - avail;
if self.refill_rate <= 0.0 {
return Duration::from_secs(u64::MAX / 2);
}
Duration::from_secs_f64(deficit / self.refill_rate)
}
pub fn reset(&self) {
*self.available.lock().unwrap() = self.capacity;
*self.last_refill.lock().unwrap() = Instant::now();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TimeWindow {
PerSecond,
PerMinute,
PerHour,
}
impl TimeWindow {
pub fn duration(&self) -> Duration {
match self {
Self::PerSecond => Duration::from_secs(1),
Self::PerMinute => Duration::from_secs(60),
Self::PerHour => Duration::from_secs(3600),
}
}
}
struct WindowState {
limit: u64,
timestamps: Vec<Instant>,
}
pub struct SlidingWindowLimiter {
windows: Mutex<Vec<(TimeWindow, WindowState)>>,
policy: RateLimitPolicy,
}
impl SlidingWindowLimiter {
pub fn new(policy: RateLimitPolicy) -> Self {
Self {
windows: Mutex::new(Vec::new()),
policy,
}
}
pub fn add_window(&self, window: TimeWindow, limit: u64) {
let mut windows = self.windows.lock().unwrap();
windows.push((
window,
WindowState {
limit,
timestamps: Vec::new(),
},
));
}
fn prune(windows: &mut [(TimeWindow, WindowState)]) {
let now = Instant::now();
for (tw, state) in windows.iter_mut() {
let cutoff = now.checked_sub(tw.duration()).unwrap_or(now);
state.timestamps.retain(|t| *t > cutoff);
}
}
pub fn check_and_record(&self) -> RateLimitResult {
let mut windows = self.windows.lock().unwrap();
Self::prune(&mut windows);
let mut worst_wait = Duration::ZERO;
let mut min_remaining: u64 = u64::MAX;
let mut denied_reason: Option<String> = None;
for (tw, state) in windows.iter() {
let count = state.timestamps.len() as u64;
let remaining = state.limit.saturating_sub(count);
if remaining < min_remaining {
min_remaining = remaining;
}
if count >= state.limit {
if let Some(oldest) = state.timestamps.first() {
let elapsed = oldest.elapsed();
let window_dur = tw.duration();
if elapsed < window_dur {
let wait = window_dur - elapsed;
if wait > worst_wait {
worst_wait = wait;
}
}
}
denied_reason = Some(format!("{:?} limit of {} exceeded", tw, state.limit));
}
}
if let Some(reason) = denied_reason {
return RateLimitResult::denied(
worst_wait,
min_remaining as f64,
f64::INFINITY,
reason,
);
}
let now = Instant::now();
for (_tw, state) in windows.iter_mut() {
state.timestamps.push(now);
}
RateLimitResult::allowed(min_remaining.saturating_sub(1) as f64, f64::INFINITY)
}
pub fn count_for(&self, window: TimeWindow) -> u64 {
let mut windows = self.windows.lock().unwrap();
Self::prune(&mut windows);
for (tw, state) in windows.iter() {
if *tw == window {
return state.timestamps.len() as u64;
}
}
0
}
pub fn policy(&self) -> &RateLimitPolicy {
&self.policy
}
pub fn reset(&self) {
let mut windows = self.windows.lock().unwrap();
for (_tw, state) in windows.iter_mut() {
state.timestamps.clear();
}
}
}
pub struct CostBasedLimiter {
budget: f64,
spent: Mutex<f64>,
model_costs: Mutex<HashMap<String, f64>>,
default_cost: f64,
policy: RateLimitPolicy,
}
impl CostBasedLimiter {
pub fn new(budget: f64, default_cost: f64, policy: RateLimitPolicy) -> Self {
Self {
budget,
spent: Mutex::new(0.0),
model_costs: Mutex::new(HashMap::new()),
default_cost,
policy,
}
}
pub fn set_model_cost(&self, model: impl Into<String>, cost: f64) {
self.model_costs.lock().unwrap().insert(model.into(), cost);
}
pub fn cost_for_model(&self, model: &str) -> f64 {
self.model_costs
.lock()
.unwrap()
.get(model)
.copied()
.unwrap_or(self.default_cost)
}
pub fn check(&self, cost: f64) -> RateLimitResult {
let spent = *self.spent.lock().unwrap();
let remaining = self.budget - spent;
if remaining >= cost {
RateLimitResult::allowed(f64::INFINITY, remaining - cost)
} else {
RateLimitResult::denied(
Duration::ZERO,
f64::INFINITY,
remaining,
format!(
"budget exceeded: spent ${:.4} of ${:.4}",
spent, self.budget
),
)
}
}
pub fn record_cost(&self, cost: f64) {
let mut spent = self.spent.lock().unwrap();
*spent += cost;
}
pub fn check_and_record(&self, cost: f64) -> RateLimitResult {
let mut spent = self.spent.lock().unwrap();
let remaining = self.budget - *spent;
if remaining >= cost {
*spent += cost;
RateLimitResult::allowed(f64::INFINITY, remaining - cost)
} else {
RateLimitResult::denied(
Duration::ZERO,
f64::INFINITY,
remaining,
format!(
"budget exceeded: spent ${:.4} of ${:.4}",
*spent, self.budget
),
)
}
}
pub fn total_spent(&self) -> f64 {
*self.spent.lock().unwrap()
}
pub fn remaining_budget(&self) -> f64 {
self.budget - *self.spent.lock().unwrap()
}
pub fn budget(&self) -> f64 {
self.budget
}
pub fn policy(&self) -> &RateLimitPolicy {
&self.policy
}
pub fn reset(&self) {
*self.spent.lock().unwrap() = 0.0;
}
}
pub struct CompositeLimiter {
limiters: Vec<Box<dyn Limiter + Send + Sync>>,
}
pub trait Limiter: Send + Sync {
fn check(&self) -> RateLimitResult;
fn record(&self);
fn reset(&self);
fn name(&self) -> &str;
}
impl Limiter for TokenBucket {
fn check(&self) -> RateLimitResult {
self.refill();
let avail = *self.available.lock().unwrap();
if avail >= 1.0 {
RateLimitResult::allowed(avail, f64::INFINITY)
} else {
let wait = self.wait_time_for(1.0);
RateLimitResult::denied(wait, avail, f64::INFINITY, "token bucket exhausted")
}
}
fn record(&self) {
self.try_acquire(1.0);
}
fn reset(&self) {
TokenBucket::reset(self);
}
fn name(&self) -> &str {
"token_bucket"
}
}
impl Limiter for SlidingWindowLimiter {
fn check(&self) -> RateLimitResult {
let mut windows = self.windows.lock().unwrap();
SlidingWindowLimiter::prune(&mut windows);
let mut min_remaining: u64 = u64::MAX;
let mut worst_wait = Duration::ZERO;
let mut denied_reason: Option<String> = None;
for (tw, state) in windows.iter() {
let count = state.timestamps.len() as u64;
let remaining = state.limit.saturating_sub(count);
if remaining < min_remaining {
min_remaining = remaining;
}
if count >= state.limit {
if let Some(oldest) = state.timestamps.first() {
let elapsed = oldest.elapsed();
let window_dur = tw.duration();
if elapsed < window_dur {
let wait = window_dur - elapsed;
if wait > worst_wait {
worst_wait = wait;
}
}
}
denied_reason = Some(format!("{:?} limit exceeded", tw));
}
}
if let Some(reason) = denied_reason {
RateLimitResult::denied(worst_wait, min_remaining as f64, f64::INFINITY, reason)
} else {
RateLimitResult::allowed(min_remaining as f64, f64::INFINITY)
}
}
fn record(&self) {
let mut windows = self.windows.lock().unwrap();
let now = Instant::now();
for (_tw, state) in windows.iter_mut() {
state.timestamps.push(now);
}
}
fn reset(&self) {
SlidingWindowLimiter::reset(self);
}
fn name(&self) -> &str {
"sliding_window"
}
}
impl Limiter for CostBasedLimiter {
fn check(&self) -> RateLimitResult {
CostBasedLimiter::check(self, self.default_cost)
}
fn record(&self) {
self.record_cost(self.default_cost);
}
fn reset(&self) {
CostBasedLimiter::reset(self);
}
fn name(&self) -> &str {
"cost_based"
}
}
impl CompositeLimiter {
pub fn new() -> Self {
Self {
limiters: Vec::new(),
}
}
pub fn add_limiter(&mut self, limiter: Box<dyn Limiter + Send + Sync>) {
self.limiters.push(limiter);
}
pub fn check_all(&self) -> RateLimitResult {
let mut worst_wait = Duration::ZERO;
let mut min_tokens = f64::INFINITY;
let mut min_cost = f64::INFINITY;
let mut denied_reasons = Vec::new();
for limiter in &self.limiters {
let result = limiter.check();
if result.remaining_tokens < min_tokens {
min_tokens = result.remaining_tokens;
}
if result.cost_remaining < min_cost {
min_cost = result.cost_remaining;
}
if !result.allowed {
if result.wait_time > worst_wait {
worst_wait = result.wait_time;
}
if let Some(reason) = &result.reason {
denied_reasons.push(format!("{}: {}", limiter.name(), reason));
}
}
}
if denied_reasons.is_empty() {
RateLimitResult::allowed(min_tokens, min_cost)
} else {
RateLimitResult::denied(worst_wait, min_tokens, min_cost, denied_reasons.join("; "))
}
}
pub fn check_and_record(&self) -> RateLimitResult {
let result = self.check_all();
if result.allowed {
for limiter in &self.limiters {
limiter.record();
}
}
result
}
pub fn reset_all(&self) {
for limiter in &self.limiters {
limiter.reset();
}
}
pub fn len(&self) -> usize {
self.limiters.len()
}
pub fn is_empty(&self) -> bool {
self.limiters.is_empty()
}
}
impl Default for CompositeLimiter {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageRecord {
pub elapsed: Duration,
pub request_count: u64,
pub tokens_used: u64,
pub cost: f64,
pub label: Option<String>,
}
pub struct UsageTracker {
start: Instant,
records: Mutex<Vec<UsageRecord>>,
total_requests: Mutex<u64>,
total_tokens: Mutex<u64>,
total_cost: Mutex<f64>,
peak_requests_per_second: Mutex<f64>,
label_costs: Mutex<HashMap<String, f64>>,
label_requests: Mutex<HashMap<String, u64>>,
}
impl UsageTracker {
pub fn new() -> Self {
Self {
start: Instant::now(),
records: Mutex::new(Vec::new()),
total_requests: Mutex::new(0),
total_tokens: Mutex::new(0),
total_cost: Mutex::new(0.0),
peak_requests_per_second: Mutex::new(0.0),
label_costs: Mutex::new(HashMap::new()),
label_requests: Mutex::new(HashMap::new()),
}
}
pub fn record(&self, tokens: u64, cost: f64, label: Option<&str>) {
let elapsed = self.start.elapsed();
{
let mut total_req = self.total_requests.lock().unwrap();
*total_req += 1;
let secs = elapsed.as_secs_f64().max(0.001);
let rps = *total_req as f64 / secs;
let mut peak = self.peak_requests_per_second.lock().unwrap();
if rps > *peak {
*peak = rps;
}
}
{
let mut total_tok = self.total_tokens.lock().unwrap();
*total_tok += tokens;
}
{
let mut total_c = self.total_cost.lock().unwrap();
*total_c += cost;
}
if let Some(lbl) = label {
let mut lc = self.label_costs.lock().unwrap();
*lc.entry(lbl.to_string()).or_insert(0.0) += cost;
let mut lr = self.label_requests.lock().unwrap();
*lr.entry(lbl.to_string()).or_insert(0) += 1;
}
let record = UsageRecord {
elapsed,
request_count: 1,
tokens_used: tokens,
cost,
label: label.map(|s| s.to_string()),
};
self.records.lock().unwrap().push(record);
}
pub fn total_requests(&self) -> u64 {
*self.total_requests.lock().unwrap()
}
pub fn total_tokens(&self) -> u64 {
*self.total_tokens.lock().unwrap()
}
pub fn total_cost(&self) -> f64 {
*self.total_cost.lock().unwrap()
}
pub fn report(&self) -> UsageReport {
let elapsed = self.start.elapsed();
let total_requests = *self.total_requests.lock().unwrap();
let total_tokens = *self.total_tokens.lock().unwrap();
let total_cost = *self.total_cost.lock().unwrap();
let peak_rps = *self.peak_requests_per_second.lock().unwrap();
let cost_breakdown = self.label_costs.lock().unwrap().clone();
let request_breakdown = self.label_requests.lock().unwrap().clone();
let avg_rps = if elapsed.as_secs_f64() > 0.0 {
total_requests as f64 / elapsed.as_secs_f64()
} else {
0.0
};
let avg_tokens_per_request = if total_requests > 0 {
total_tokens as f64 / total_requests as f64
} else {
0.0
};
let avg_cost_per_request = if total_requests > 0 {
total_cost / total_requests as f64
} else {
0.0
};
UsageReport {
period: elapsed,
total_requests,
total_tokens,
total_cost,
average_rps: avg_rps,
peak_rps,
average_tokens_per_request: avg_tokens_per_request,
average_cost_per_request: avg_cost_per_request,
cost_breakdown,
request_breakdown,
}
}
pub fn reset(&mut self) {
self.start = Instant::now();
self.records.lock().unwrap().clear();
*self.total_requests.lock().unwrap() = 0;
*self.total_tokens.lock().unwrap() = 0;
*self.total_cost.lock().unwrap() = 0.0;
*self.peak_requests_per_second.lock().unwrap() = 0.0;
self.label_costs.lock().unwrap().clear();
self.label_requests.lock().unwrap().clear();
}
}
impl Default for UsageTracker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageReport {
pub period: Duration,
pub total_requests: u64,
pub total_tokens: u64,
pub total_cost: f64,
pub average_rps: f64,
pub peak_rps: f64,
pub average_tokens_per_request: f64,
pub average_cost_per_request: f64,
pub cost_breakdown: HashMap<String, f64>,
pub request_breakdown: HashMap<String, u64>,
}
struct QuotaEntry {
max_requests: u64,
current: u64,
max_cost: f64,
current_cost: f64,
window: Duration,
window_start: Instant,
}
impl QuotaEntry {
fn maybe_reset(&mut self) {
if self.window_start.elapsed() >= self.window {
self.current = 0;
self.current_cost = 0.0;
self.window_start = Instant::now();
}
}
}
pub struct QuotaManager {
quotas: Mutex<HashMap<String, QuotaEntry>>,
default_window: Duration,
}
impl QuotaManager {
pub fn new(default_window: Duration) -> Self {
Self {
quotas: Mutex::new(HashMap::new()),
default_window,
}
}
pub fn set_quota(&self, key: impl Into<String>, max_requests: u64, max_cost: f64) {
let mut quotas = self.quotas.lock().unwrap();
quotas.insert(
key.into(),
QuotaEntry {
max_requests,
current: 0,
max_cost,
current_cost: 0.0,
window: self.default_window,
window_start: Instant::now(),
},
);
}
pub fn set_quota_with_window(
&self,
key: impl Into<String>,
max_requests: u64,
max_cost: f64,
window: Duration,
) {
let mut quotas = self.quotas.lock().unwrap();
quotas.insert(
key.into(),
QuotaEntry {
max_requests,
current: 0,
max_cost,
current_cost: 0.0,
window,
window_start: Instant::now(),
},
);
}
pub fn check(&self, key: &str) -> RateLimitResult {
let mut quotas = self.quotas.lock().unwrap();
if let Some(entry) = quotas.get_mut(key) {
entry.maybe_reset();
if entry.current >= entry.max_requests {
let remaining_window = entry
.window
.checked_sub(entry.window_start.elapsed())
.unwrap_or(Duration::ZERO);
return RateLimitResult::denied(
remaining_window,
(entry.max_requests - entry.current) as f64,
entry.max_cost - entry.current_cost,
format!("quota exceeded for '{}'", key),
);
}
if entry.current_cost >= entry.max_cost {
return RateLimitResult::denied(
Duration::ZERO,
(entry.max_requests - entry.current) as f64,
entry.max_cost - entry.current_cost,
format!("cost quota exceeded for '{}'", key),
);
}
RateLimitResult::allowed(
(entry.max_requests - entry.current) as f64,
entry.max_cost - entry.current_cost,
)
} else {
RateLimitResult::allowed(f64::INFINITY, f64::INFINITY)
}
}
pub fn record(&self, key: &str, cost: f64) {
let mut quotas = self.quotas.lock().unwrap();
if let Some(entry) = quotas.get_mut(key) {
entry.maybe_reset();
entry.current += 1;
entry.current_cost += cost;
}
}
pub fn check_and_record(&self, key: &str, cost: f64) -> RateLimitResult {
let mut quotas = self.quotas.lock().unwrap();
if let Some(entry) = quotas.get_mut(key) {
entry.maybe_reset();
if entry.current >= entry.max_requests {
let remaining_window = entry
.window
.checked_sub(entry.window_start.elapsed())
.unwrap_or(Duration::ZERO);
return RateLimitResult::denied(
remaining_window,
0.0,
entry.max_cost - entry.current_cost,
format!("quota exceeded for '{}'", key),
);
}
if entry.current_cost + cost > entry.max_cost {
return RateLimitResult::denied(
Duration::ZERO,
(entry.max_requests - entry.current) as f64,
entry.max_cost - entry.current_cost,
format!("cost quota exceeded for '{}'", key),
);
}
entry.current += 1;
entry.current_cost += cost;
RateLimitResult::allowed(
(entry.max_requests - entry.current) as f64,
entry.max_cost - entry.current_cost,
)
} else {
RateLimitResult::allowed(f64::INFINITY, f64::INFINITY)
}
}
pub fn usage(&self, key: &str) -> Option<(u64, f64)> {
let mut quotas = self.quotas.lock().unwrap();
if let Some(entry) = quotas.get_mut(key) {
entry.maybe_reset();
Some((entry.current, entry.current_cost))
} else {
None
}
}
pub fn reset(&self, key: &str) {
let mut quotas = self.quotas.lock().unwrap();
if let Some(entry) = quotas.get_mut(key) {
entry.current = 0;
entry.current_cost = 0.0;
entry.window_start = Instant::now();
}
}
pub fn reset_all(&self) {
let mut quotas = self.quotas.lock().unwrap();
for entry in quotas.values_mut() {
entry.current = 0;
entry.current_cost = 0.0;
entry.window_start = Instant::now();
}
}
pub fn keys(&self) -> Vec<String> {
self.quotas.lock().unwrap().keys().cloned().collect()
}
}
pub trait RateLimitMiddleware: Send + Sync {
fn check_model_call(&self, model: &str) -> RateLimitResult;
fn check_tool_call(&self, tool_name: &str) -> RateLimitResult;
fn record_model_usage(&self, model: &str, tokens: u64, cost: f64);
fn record_tool_usage(&self, tool_name: &str);
fn usage_report(&self) -> UsageReport;
fn reset(&self);
}
pub struct DefaultRateLimitMiddleware {
bucket: TokenBucket,
quotas: QuotaManager,
tracker: UsageTracker,
tool_bucket: TokenBucket,
}
impl DefaultRateLimitMiddleware {
pub fn new(
model_capacity: f64,
model_refill_rate: f64,
tool_capacity: f64,
tool_refill_rate: f64,
) -> Self {
Self {
bucket: TokenBucket::new(model_capacity, model_refill_rate),
quotas: QuotaManager::new(Duration::from_secs(60)),
tracker: UsageTracker::new(),
tool_bucket: TokenBucket::new(tool_capacity, tool_refill_rate),
}
}
pub fn quota_manager(&self) -> &QuotaManager {
&self.quotas
}
}
impl RateLimitMiddleware for DefaultRateLimitMiddleware {
fn check_model_call(&self, model: &str) -> RateLimitResult {
let bucket_result = self.bucket.try_acquire(1.0);
if !bucket_result.allowed {
return bucket_result;
}
self.quotas.check(model)
}
fn check_tool_call(&self, _tool_name: &str) -> RateLimitResult {
self.tool_bucket.try_acquire(1.0)
}
fn record_model_usage(&self, model: &str, tokens: u64, cost: f64) {
self.quotas.record(model, cost);
self.tracker.record(tokens, cost, Some(model));
}
fn record_tool_usage(&self, _tool_name: &str) {
}
fn usage_report(&self) -> UsageReport {
self.tracker.report()
}
fn reset(&self) {
self.bucket.reset();
self.tool_bucket.reset();
self.quotas.reset_all();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_policy_default_is_reject() {
let policy = RateLimitPolicy::default();
assert_eq!(policy, RateLimitPolicy::Reject);
}
#[test]
fn test_policy_backoff_variant() {
let policy = RateLimitPolicy::Backoff {
base_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(10),
multiplier: 2.0,
};
match policy {
RateLimitPolicy::Backoff {
base_delay,
max_delay,
multiplier,
} => {
assert_eq!(base_delay, Duration::from_millis(100));
assert_eq!(max_delay, Duration::from_secs(10));
assert!((multiplier - 2.0).abs() < f64::EPSILON);
}
_ => panic!("expected Backoff"),
}
}
#[test]
fn test_policy_throttle_variant() {
let policy = RateLimitPolicy::Throttle { reduced_rate: 5.0 };
match policy {
RateLimitPolicy::Throttle { reduced_rate } => {
assert!((reduced_rate - 5.0).abs() < f64::EPSILON);
}
_ => panic!("expected Throttle"),
}
}
#[test]
fn test_policy_queue_variant() {
let policy = RateLimitPolicy::Queue;
assert_eq!(policy, RateLimitPolicy::Queue);
}
#[test]
fn test_policy_serialization() {
let policy = RateLimitPolicy::Reject;
let json = serde_json::to_string(&policy).unwrap();
let deserialized: RateLimitPolicy = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, RateLimitPolicy::Reject);
}
#[test]
fn test_result_allowed() {
let r = RateLimitResult::allowed(10.0, 5.0);
assert!(r.allowed);
assert_eq!(r.wait_time, Duration::ZERO);
assert!((r.remaining_tokens - 10.0).abs() < f64::EPSILON);
assert!((r.cost_remaining - 5.0).abs() < f64::EPSILON);
assert!(r.reason.is_none());
}
#[test]
fn test_result_denied() {
let r = RateLimitResult::denied(Duration::from_secs(5), 0.0, 1.0, "over limit");
assert!(!r.allowed);
assert_eq!(r.wait_time, Duration::from_secs(5));
assert!((r.remaining_tokens - 0.0).abs() < f64::EPSILON);
assert!((r.cost_remaining - 1.0).abs() < f64::EPSILON);
assert_eq!(r.reason.as_deref(), Some("over limit"));
}
#[test]
fn test_result_serialization() {
let r = RateLimitResult::allowed(42.0, 10.0);
let json = serde_json::to_string(&r).unwrap();
let r2: RateLimitResult = serde_json::from_str(&json).unwrap();
assert!(r2.allowed);
assert!((r2.remaining_tokens - 42.0).abs() < f64::EPSILON);
}
#[test]
fn test_token_bucket_creation() {
let bucket = TokenBucket::new(100.0, 10.0);
assert!((bucket.capacity() - 100.0).abs() < f64::EPSILON);
assert!((bucket.refill_rate() - 10.0).abs() < f64::EPSILON);
assert!((bucket.available() - 100.0).abs() < 1.0);
}
#[test]
fn test_token_bucket_acquire_success() {
let bucket = TokenBucket::new(10.0, 1.0);
let result = bucket.try_acquire(5.0);
assert!(result.allowed);
assert!(bucket.available() >= 4.0 && bucket.available() <= 5.5);
}
#[test]
fn test_token_bucket_acquire_failure() {
let bucket = TokenBucket::new(5.0, 1.0);
let r1 = bucket.try_acquire(5.0);
assert!(r1.allowed);
let r2 = bucket.try_acquire(5.0);
assert!(!r2.allowed);
assert!(r2.wait_time > Duration::ZERO);
assert_eq!(r2.reason.as_deref(), Some("token bucket exhausted"));
}
#[test]
fn test_token_bucket_partial_acquire() {
let bucket = TokenBucket::new(10.0, 1.0);
assert!(bucket.try_acquire(3.0).allowed);
assert!(bucket.try_acquire(3.0).allowed);
assert!(bucket.try_acquire(3.0).allowed);
assert!(!bucket.try_acquire(3.0).allowed);
}
#[test]
fn test_token_bucket_refill() {
let bucket = TokenBucket::new(10.0, 10000.0); assert!(bucket.try_acquire(10.0).allowed);
assert!(!bucket.try_acquire(1.0).allowed);
thread::sleep(Duration::from_millis(10));
assert!(bucket.try_acquire(1.0).allowed);
}
#[test]
fn test_token_bucket_wait_time() {
let bucket = TokenBucket::new(10.0, 2.0);
assert!(bucket.try_acquire(10.0).allowed);
let wait = bucket.wait_time_for(4.0);
assert!(wait >= Duration::from_millis(1500));
assert!(wait <= Duration::from_millis(2500));
}
#[test]
fn test_token_bucket_wait_time_zero_when_available() {
let bucket = TokenBucket::new(10.0, 1.0);
let wait = bucket.wait_time_for(5.0);
assert_eq!(wait, Duration::ZERO);
}
#[test]
fn test_token_bucket_force_acquire() {
let bucket = TokenBucket::new(5.0, 1.0);
bucket.force_acquire(10.0);
assert!(bucket.available() < 0.0);
}
#[test]
fn test_token_bucket_reset() {
let bucket = TokenBucket::new(10.0, 1.0);
bucket.try_acquire(10.0);
assert!(bucket.available() < 1.0);
bucket.reset();
assert!((bucket.available() - 10.0).abs() < 1.0);
}
#[test]
fn test_token_bucket_zero_refill_rate() {
let bucket = TokenBucket::new(5.0, 0.0);
assert!(bucket.try_acquire(5.0).allowed);
let result = bucket.try_acquire(1.0);
assert!(!result.allowed);
assert!(result.wait_time > Duration::from_secs(1000));
}
#[test]
fn test_sliding_window_single_window() {
let limiter = SlidingWindowLimiter::new(RateLimitPolicy::Reject);
limiter.add_window(TimeWindow::PerSecond, 3);
assert!(limiter.check_and_record().allowed);
assert!(limiter.check_and_record().allowed);
assert!(limiter.check_and_record().allowed);
assert!(!limiter.check_and_record().allowed);
}
#[test]
fn test_sliding_window_multiple_windows() {
let limiter = SlidingWindowLimiter::new(RateLimitPolicy::Reject);
limiter.add_window(TimeWindow::PerSecond, 10);
limiter.add_window(TimeWindow::PerMinute, 3);
assert!(limiter.check_and_record().allowed);
assert!(limiter.check_and_record().allowed);
assert!(limiter.check_and_record().allowed);
let result = limiter.check_and_record();
assert!(!result.allowed);
assert!(result.reason.as_deref().unwrap().contains("PerMinute"));
}
#[test]
fn test_sliding_window_count() {
let limiter = SlidingWindowLimiter::new(RateLimitPolicy::Reject);
limiter.add_window(TimeWindow::PerMinute, 100);
limiter.check_and_record();
limiter.check_and_record();
limiter.check_and_record();
assert_eq!(limiter.count_for(TimeWindow::PerMinute), 3);
assert_eq!(limiter.count_for(TimeWindow::PerHour), 0); }
#[test]
fn test_sliding_window_reset() {
let limiter = SlidingWindowLimiter::new(RateLimitPolicy::Reject);
limiter.add_window(TimeWindow::PerSecond, 2);
limiter.check_and_record();
limiter.check_and_record();
assert!(!limiter.check_and_record().allowed);
limiter.reset();
assert!(limiter.check_and_record().allowed);
}
#[test]
fn test_sliding_window_policy() {
let limiter = SlidingWindowLimiter::new(RateLimitPolicy::Queue);
assert_eq!(*limiter.policy(), RateLimitPolicy::Queue);
}
#[test]
fn test_sliding_window_remaining_decreases() {
let limiter = SlidingWindowLimiter::new(RateLimitPolicy::Reject);
limiter.add_window(TimeWindow::PerMinute, 5);
let r1 = limiter.check_and_record();
assert!(r1.allowed);
assert!((r1.remaining_tokens - 4.0).abs() < f64::EPSILON);
let r2 = limiter.check_and_record();
assert!(r2.allowed);
assert!((r2.remaining_tokens - 3.0).abs() < f64::EPSILON);
}
#[test]
fn test_time_window_durations() {
assert_eq!(TimeWindow::PerSecond.duration(), Duration::from_secs(1));
assert_eq!(TimeWindow::PerMinute.duration(), Duration::from_secs(60));
assert_eq!(TimeWindow::PerHour.duration(), Duration::from_secs(3600));
}
#[test]
fn test_time_window_serialization() {
let tw = TimeWindow::PerMinute;
let json = serde_json::to_string(&tw).unwrap();
let tw2: TimeWindow = serde_json::from_str(&json).unwrap();
assert_eq!(tw2, TimeWindow::PerMinute);
}
#[test]
fn test_cost_limiter_allows_within_budget() {
let limiter = CostBasedLimiter::new(10.0, 0.01, RateLimitPolicy::Reject);
let result = limiter.check(1.0);
assert!(result.allowed);
assert!((result.cost_remaining - 9.0).abs() < f64::EPSILON);
}
#[test]
fn test_cost_limiter_denies_over_budget() {
let limiter = CostBasedLimiter::new(1.0, 0.01, RateLimitPolicy::Reject);
limiter.record_cost(0.8);
let result = limiter.check(0.5);
assert!(!result.allowed);
assert!(result
.reason
.as_deref()
.unwrap()
.contains("budget exceeded"));
}
#[test]
fn test_cost_limiter_check_and_record() {
let limiter = CostBasedLimiter::new(1.0, 0.01, RateLimitPolicy::Reject);
let r1 = limiter.check_and_record(0.4);
assert!(r1.allowed);
assert!((limiter.total_spent() - 0.4).abs() < f64::EPSILON);
let r2 = limiter.check_and_record(0.4);
assert!(r2.allowed);
let r3 = limiter.check_and_record(0.4);
assert!(!r3.allowed);
assert!((limiter.total_spent() - 0.8).abs() < f64::EPSILON);
}
#[test]
fn test_cost_limiter_model_costs() {
let limiter = CostBasedLimiter::new(10.0, 0.01, RateLimitPolicy::Reject);
limiter.set_model_cost("gpt-4", 0.03);
limiter.set_model_cost("gpt-3.5", 0.002);
assert!((limiter.cost_for_model("gpt-4") - 0.03).abs() < f64::EPSILON);
assert!((limiter.cost_for_model("gpt-3.5") - 0.002).abs() < f64::EPSILON);
assert!((limiter.cost_for_model("unknown") - 0.01).abs() < f64::EPSILON);
}
#[test]
fn test_cost_limiter_remaining_budget() {
let limiter = CostBasedLimiter::new(5.0, 0.01, RateLimitPolicy::Reject);
limiter.record_cost(2.0);
assert!((limiter.remaining_budget() - 3.0).abs() < f64::EPSILON);
assert!((limiter.budget() - 5.0).abs() < f64::EPSILON);
}
#[test]
fn test_cost_limiter_reset() {
let limiter = CostBasedLimiter::new(5.0, 0.01, RateLimitPolicy::Reject);
limiter.record_cost(4.0);
limiter.reset();
assert!((limiter.total_spent() - 0.0).abs() < f64::EPSILON);
assert!((limiter.remaining_budget() - 5.0).abs() < f64::EPSILON);
}
#[test]
fn test_cost_limiter_policy() {
let limiter = CostBasedLimiter::new(5.0, 0.01, RateLimitPolicy::Queue);
assert_eq!(*limiter.policy(), RateLimitPolicy::Queue);
}
#[test]
fn test_composite_empty_allows() {
let composite = CompositeLimiter::new();
let result = composite.check_all();
assert!(result.allowed);
}
#[test]
fn test_composite_all_pass() {
let mut composite = CompositeLimiter::new();
composite.add_limiter(Box::new(TokenBucket::new(10.0, 1.0)));
composite.add_limiter(Box::new(CostBasedLimiter::new(
100.0,
0.01,
RateLimitPolicy::Reject,
)));
let result = composite.check_all();
assert!(result.allowed);
}
#[test]
fn test_composite_one_fails() {
let mut composite = CompositeLimiter::new();
let bucket = TokenBucket::new(1.0, 0.0);
bucket.try_acquire(1.0); composite.add_limiter(Box::new(bucket));
composite.add_limiter(Box::new(CostBasedLimiter::new(
100.0,
0.01,
RateLimitPolicy::Reject,
)));
let result = composite.check_all();
assert!(!result.allowed);
assert!(result.reason.as_deref().unwrap().contains("token_bucket"));
}
#[test]
fn test_composite_check_and_record() {
let mut composite = CompositeLimiter::new();
composite.add_limiter(Box::new(TokenBucket::new(3.0, 0.0)));
assert!(composite.check_and_record().allowed);
assert!(composite.check_and_record().allowed);
assert!(composite.check_and_record().allowed);
assert!(!composite.check_and_record().allowed);
}
#[test]
fn test_composite_reset_all() {
let mut composite = CompositeLimiter::new();
composite.add_limiter(Box::new(TokenBucket::new(1.0, 0.0)));
assert!(composite.check_and_record().allowed);
assert!(!composite.check_and_record().allowed);
composite.reset_all();
assert!(composite.check_and_record().allowed);
}
#[test]
fn test_composite_len_and_empty() {
let mut composite = CompositeLimiter::new();
assert!(composite.is_empty());
assert_eq!(composite.len(), 0);
composite.add_limiter(Box::new(TokenBucket::new(10.0, 1.0)));
assert!(!composite.is_empty());
assert_eq!(composite.len(), 1);
}
#[test]
fn test_composite_default() {
let composite = CompositeLimiter::default();
assert!(composite.is_empty());
}
#[test]
fn test_usage_tracker_basic() {
let tracker = UsageTracker::new();
tracker.record(100, 0.01, Some("gpt-4"));
tracker.record(200, 0.02, Some("gpt-4"));
tracker.record(50, 0.005, Some("claude"));
assert_eq!(tracker.total_requests(), 3);
assert_eq!(tracker.total_tokens(), 350);
assert!((tracker.total_cost() - 0.035).abs() < 1e-9);
}
#[test]
fn test_usage_tracker_report() {
let tracker = UsageTracker::new();
tracker.record(100, 0.01, Some("gpt-4"));
tracker.record(200, 0.02, Some("gpt-4"));
tracker.record(50, 0.005, Some("claude"));
let report = tracker.report();
assert_eq!(report.total_requests, 3);
assert_eq!(report.total_tokens, 350);
assert!((report.total_cost - 0.035).abs() < 1e-9);
assert!((report.average_tokens_per_request - 116.666).abs() < 1.0);
assert!(report.cost_breakdown.contains_key("gpt-4"));
assert!(report.cost_breakdown.contains_key("claude"));
assert!((report.cost_breakdown["gpt-4"] - 0.03).abs() < 1e-9);
assert!((report.cost_breakdown["claude"] - 0.005).abs() < 1e-9);
assert_eq!(report.request_breakdown["gpt-4"], 2);
assert_eq!(report.request_breakdown["claude"], 1);
}
#[test]
fn test_usage_tracker_no_label() {
let tracker = UsageTracker::new();
tracker.record(100, 0.01, None);
assert_eq!(tracker.total_requests(), 1);
let report = tracker.report();
assert!(report.cost_breakdown.is_empty());
}
#[test]
fn test_usage_tracker_reset() {
let mut tracker = UsageTracker::new();
tracker.record(100, 0.01, Some("test"));
tracker.reset();
assert_eq!(tracker.total_requests(), 0);
assert_eq!(tracker.total_tokens(), 0);
assert!((tracker.total_cost() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_usage_tracker_default() {
let tracker = UsageTracker::default();
assert_eq!(tracker.total_requests(), 0);
}
#[test]
fn test_usage_report_serialization() {
let tracker = UsageTracker::new();
tracker.record(100, 0.01, Some("gpt-4"));
let report = tracker.report();
let json = serde_json::to_string(&report).unwrap();
let r2: UsageReport = serde_json::from_str(&json).unwrap();
assert_eq!(r2.total_requests, 1);
}
#[test]
fn test_quota_manager_no_quota_allows() {
let qm = QuotaManager::new(Duration::from_secs(60));
let result = qm.check("unknown-model");
assert!(result.allowed);
}
#[test]
fn test_quota_manager_set_and_check() {
let qm = QuotaManager::new(Duration::from_secs(60));
qm.set_quota("gpt-4", 3, 1.0);
let r1 = qm.check_and_record("gpt-4", 0.1);
assert!(r1.allowed);
let r2 = qm.check_and_record("gpt-4", 0.1);
assert!(r2.allowed);
let r3 = qm.check_and_record("gpt-4", 0.1);
assert!(r3.allowed);
let r4 = qm.check_and_record("gpt-4", 0.1);
assert!(!r4.allowed);
assert!(r4.reason.as_deref().unwrap().contains("quota exceeded"));
}
#[test]
fn test_quota_manager_cost_quota() {
let qm = QuotaManager::new(Duration::from_secs(60));
qm.set_quota("gpt-4", 100, 0.5);
let r1 = qm.check_and_record("gpt-4", 0.3);
assert!(r1.allowed);
let r2 = qm.check_and_record("gpt-4", 0.3);
assert!(!r2.allowed);
assert!(r2
.reason
.as_deref()
.unwrap()
.contains("cost quota exceeded"));
}
#[test]
fn test_quota_manager_usage() {
let qm = QuotaManager::new(Duration::from_secs(60));
qm.set_quota("model-a", 10, 5.0);
qm.record("model-a", 0.5);
qm.record("model-a", 0.3);
let (count, cost) = qm.usage("model-a").unwrap();
assert_eq!(count, 2);
assert!((cost - 0.8).abs() < 1e-9);
}
#[test]
fn test_quota_manager_usage_none() {
let qm = QuotaManager::new(Duration::from_secs(60));
assert!(qm.usage("nonexistent").is_none());
}
#[test]
fn test_quota_manager_reset_specific() {
let qm = QuotaManager::new(Duration::from_secs(60));
qm.set_quota("model-a", 3, 5.0);
qm.record("model-a", 1.0);
qm.record("model-a", 1.0);
qm.reset("model-a");
let (count, cost) = qm.usage("model-a").unwrap();
assert_eq!(count, 0);
assert!((cost - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_quota_manager_reset_all() {
let qm = QuotaManager::new(Duration::from_secs(60));
qm.set_quota("a", 10, 5.0);
qm.set_quota("b", 10, 5.0);
qm.record("a", 1.0);
qm.record("b", 2.0);
qm.reset_all();
let (a_count, _) = qm.usage("a").unwrap();
let (b_count, _) = qm.usage("b").unwrap();
assert_eq!(a_count, 0);
assert_eq!(b_count, 0);
}
#[test]
fn test_quota_manager_keys() {
let qm = QuotaManager::new(Duration::from_secs(60));
qm.set_quota("alpha", 10, 5.0);
qm.set_quota("beta", 10, 5.0);
let mut keys = qm.keys();
keys.sort();
assert_eq!(keys, vec!["alpha", "beta"]);
}
#[test]
fn test_quota_manager_custom_window() {
let qm = QuotaManager::new(Duration::from_secs(60));
qm.set_quota_with_window("fast", 2, 10.0, Duration::from_millis(50));
let r1 = qm.check_and_record("fast", 0.1);
assert!(r1.allowed);
let r2 = qm.check_and_record("fast", 0.1);
assert!(r2.allowed);
let r3 = qm.check_and_record("fast", 0.1);
assert!(!r3.allowed);
thread::sleep(Duration::from_millis(60));
let r4 = qm.check_and_record("fast", 0.1);
assert!(r4.allowed);
}
#[test]
fn test_limiter_trait_token_bucket() {
let bucket = TokenBucket::new(3.0, 0.0);
let limiter: &dyn Limiter = &bucket;
assert!(limiter.check().allowed);
limiter.record();
limiter.record();
limiter.record();
assert!(!limiter.check().allowed);
limiter.reset();
assert!(limiter.check().allowed);
assert_eq!(limiter.name(), "token_bucket");
}
#[test]
fn test_limiter_trait_cost_based() {
let cost_limiter = CostBasedLimiter::new(0.05, 0.01, RateLimitPolicy::Reject);
let limiter: &dyn Limiter = &cost_limiter;
assert!(limiter.check().allowed);
limiter.record(); limiter.record(); limiter.record(); limiter.record(); limiter.record(); assert!(!limiter.check().allowed);
limiter.reset();
assert!(limiter.check().allowed);
assert_eq!(limiter.name(), "cost_based");
}
#[test]
fn test_limiter_trait_sliding_window() {
let sw = SlidingWindowLimiter::new(RateLimitPolicy::Reject);
sw.add_window(TimeWindow::PerMinute, 2);
let limiter: &dyn Limiter = &sw;
assert!(limiter.check().allowed);
limiter.record();
limiter.record();
assert!(!limiter.check().allowed);
limiter.reset();
assert!(limiter.check().allowed);
assert_eq!(limiter.name(), "sliding_window");
}
#[test]
fn test_default_middleware_model_check() {
let mw = DefaultRateLimitMiddleware::new(3.0, 0.0, 10.0, 1.0);
assert!(mw.check_model_call("gpt-4").allowed);
assert!(mw.check_model_call("gpt-4").allowed);
assert!(mw.check_model_call("gpt-4").allowed);
assert!(!mw.check_model_call("gpt-4").allowed);
}
#[test]
fn test_default_middleware_tool_check() {
let mw = DefaultRateLimitMiddleware::new(10.0, 1.0, 2.0, 0.0);
assert!(mw.check_tool_call("search").allowed);
assert!(mw.check_tool_call("search").allowed);
assert!(!mw.check_tool_call("search").allowed);
}
#[test]
fn test_default_middleware_record_usage() {
let mw = DefaultRateLimitMiddleware::new(10.0, 1.0, 10.0, 1.0);
mw.record_model_usage("gpt-4", 500, 0.05);
mw.record_model_usage("claude", 300, 0.03);
let report = mw.usage_report();
assert_eq!(report.total_requests, 2);
assert_eq!(report.total_tokens, 800);
assert!((report.total_cost - 0.08).abs() < 1e-9);
}
#[test]
fn test_default_middleware_with_quotas() {
let mw = DefaultRateLimitMiddleware::new(100.0, 10.0, 100.0, 10.0);
mw.quota_manager().set_quota("gpt-4", 2, 1.0);
assert!(mw.check_model_call("gpt-4").allowed);
mw.record_model_usage("gpt-4", 100, 0.1);
assert!(mw.check_model_call("gpt-4").allowed);
mw.record_model_usage("gpt-4", 100, 0.1);
let result = mw.check_model_call("gpt-4");
assert!(!result.allowed);
}
#[test]
fn test_default_middleware_reset() {
let mw = DefaultRateLimitMiddleware::new(2.0, 0.0, 2.0, 0.0);
mw.check_model_call("test");
mw.check_model_call("test");
assert!(!mw.check_model_call("test").allowed);
mw.reset();
assert!(mw.check_model_call("test").allowed);
}
#[test]
fn test_usage_record_serialization() {
let record = UsageRecord {
elapsed: Duration::from_secs(5),
request_count: 1,
tokens_used: 100,
cost: 0.01,
label: Some("gpt-4".to_string()),
};
let json = serde_json::to_string(&record).unwrap();
let r2: UsageRecord = serde_json::from_str(&json).unwrap();
assert_eq!(r2.tokens_used, 100);
assert_eq!(r2.label.as_deref(), Some("gpt-4"));
}
#[test]
fn test_usage_record_no_label() {
let record = UsageRecord {
elapsed: Duration::from_secs(1),
request_count: 1,
tokens_used: 50,
cost: 0.005,
label: None,
};
assert!(record.label.is_none());
}
#[test]
fn test_token_bucket_fractional_tokens() {
let bucket = TokenBucket::new(1.5, 0.0);
assert!(bucket.try_acquire(1.0).allowed);
assert!(!bucket.try_acquire(1.0).allowed);
assert!(bucket.try_acquire(0.5).allowed);
}
#[test]
fn test_sliding_window_expiry() {
let limiter = SlidingWindowLimiter::new(RateLimitPolicy::Reject);
limiter.add_window(TimeWindow::PerSecond, 2);
assert!(limiter.check_and_record().allowed);
assert!(limiter.check_and_record().allowed);
assert!(!limiter.check_and_record().allowed);
thread::sleep(Duration::from_millis(1100));
assert!(limiter.check_and_record().allowed);
}
#[test]
fn test_composite_multiple_failures_aggregated() {
let mut composite = CompositeLimiter::new();
let b1 = TokenBucket::new(0.0, 0.0); let b2 = CostBasedLimiter::new(0.0, 0.01, RateLimitPolicy::Reject); composite.add_limiter(Box::new(b1));
composite.add_limiter(Box::new(b2));
let result = composite.check_all();
assert!(!result.allowed);
let reason = result.reason.unwrap();
assert!(reason.contains("token_bucket"));
assert!(reason.contains("cost_based"));
}
#[test]
fn test_usage_tracker_peak_rps() {
let tracker = UsageTracker::new();
for _ in 0..10 {
tracker.record(10, 0.001, None);
}
let report = tracker.report();
assert!(report.peak_rps > 0.0);
}
#[test]
fn test_quota_manager_record_without_quota() {
let qm = QuotaManager::new(Duration::from_secs(60));
qm.record("unknown", 1.0);
assert!(qm.usage("unknown").is_none());
}
#[test]
fn test_cost_limiter_exact_budget() {
let limiter = CostBasedLimiter::new(1.0, 0.01, RateLimitPolicy::Reject);
let result = limiter.check_and_record(1.0);
assert!(result.allowed);
assert!((limiter.remaining_budget() - 0.0).abs() < f64::EPSILON);
let result2 = limiter.check_and_record(0.01);
assert!(!result2.allowed);
}
}