#![allow(dead_code)]
use google_cloud_gax::error::rpc::Code;
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::RwLock;
use std::time::Duration;
use std::time::Instant;
const DEFAULT_INITIAL_COOLDOWN: Duration = Duration::from_secs(5);
const DEFAULT_MAX_COOLDOWN: Duration = Duration::from_secs(60);
const DEFAULT_RESET_AFTER: Duration = Duration::from_secs(600);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct EndpointCooldownState {
consecutive_failures: usize,
cooldown_until: Instant,
last_failure_at: Instant,
}
#[derive(Debug)]
pub(crate) struct EndpointCooldownTracker {
initial_cooldown: Duration,
max_cooldown: Duration,
reset_after: Duration,
state: RwLock<HashMap<String, EndpointCooldownState>>,
}
impl Default for EndpointCooldownTracker {
fn default() -> Self {
Self::new()
}
}
impl EndpointCooldownTracker {
pub(crate) fn new() -> Self {
Self::with_options(
DEFAULT_INITIAL_COOLDOWN,
DEFAULT_MAX_COOLDOWN,
DEFAULT_RESET_AFTER,
)
}
pub(crate) fn with_options(
initial_cooldown: Duration,
max_cooldown: Duration,
reset_after: Duration,
) -> Self {
Self {
initial_cooldown,
max_cooldown,
reset_after,
state: RwLock::new(HashMap::new()),
}
}
pub(crate) fn is_cooling_down(&self, endpoint: &str) -> bool {
self.is_cooling_down_at(endpoint, Instant::now())
}
pub(crate) fn is_cooling_down_at(&self, endpoint: &str, now: Instant) -> bool {
let guard = self
.state
.read()
.expect("EndpointCooldownTracker read lock poisoned");
guard
.get(endpoint)
.is_some_and(|entry| now < entry.cooldown_until)
}
pub(crate) fn record_error(&self, endpoint: &str, status_code: Code) -> Option<Duration> {
if status_code != Code::ResourceExhausted {
return None;
}
Some(self.record_failure(endpoint))
}
pub(crate) fn record_failure(&self, endpoint: &str) -> Duration {
self.record_failure_at(endpoint, Instant::now())
}
pub(crate) fn record_failure_at(&self, endpoint: &str, now: Instant) -> Duration {
let mut guard = self
.state
.write()
.expect("EndpointCooldownTracker write lock poisoned");
let consecutive_failures = match guard.get(endpoint) {
Some(entry)
if now.saturating_duration_since(entry.last_failure_at) < self.reset_after =>
{
entry.consecutive_failures.saturating_add(1)
}
_ => 1,
};
let shift = consecutive_failures.saturating_sub(1).min(31) as u32;
let base_cooldown = self.initial_cooldown.saturating_mul(1u32 << shift);
let capped_cooldown = base_cooldown.min(self.max_cooldown);
let millis = u64::try_from(capped_cooldown.as_millis()).unwrap_or(u64::MAX);
let jittered_millis = if millis == 0 {
0
} else {
let floor_millis = (millis / 2).max(1);
rand::random_range(floor_millis..=millis)
};
let jittered_cooldown = Duration::from_millis(jittered_millis);
let new_state = EndpointCooldownState {
consecutive_failures,
cooldown_until: now
.checked_add(jittered_cooldown)
.or_else(|| now.checked_add(Duration::from_secs(86400 * 30)))
.unwrap_or(now),
last_failure_at: now,
};
match guard.get_mut(endpoint) {
Some(existing) => {
*existing = new_state;
}
None => {
guard.insert(endpoint.to_string(), new_state);
}
}
jittered_cooldown
}
pub(crate) fn clear_expired(&self) {
self.clear_expired_at(Instant::now());
}
pub(crate) fn clear_expired_at(&self, now: Instant) {
{
let guard = self
.state
.read()
.expect("EndpointCooldownTracker read lock poisoned");
let has_expired = guard.values().any(|entry| {
now >= entry.cooldown_until
&& now.saturating_duration_since(entry.last_failure_at) >= self.reset_after
});
if !has_expired {
return;
}
}
let mut guard = self
.state
.write()
.expect("EndpointCooldownTracker write lock poisoned");
guard.retain(|_, entry| {
now < entry.cooldown_until
|| now.saturating_duration_since(entry.last_failure_at) < self.reset_after
});
}
pub(crate) fn clear(&self) {
let mut guard = self
.state
.write()
.expect("EndpointCooldownTracker write lock poisoned");
guard.clear();
}
pub(crate) fn len(&self) -> usize {
let guard = self
.state
.read()
.expect("EndpointCooldownTracker read lock poisoned");
guard.len()
}
pub(crate) fn is_empty(&self) -> bool {
let guard = self
.state
.read()
.expect("EndpointCooldownTracker read lock poisoned");
guard.is_empty()
}
}
#[derive(Debug, Default, Clone)]
pub(crate) struct EndpointExclusionList {
excluded: HashSet<String>,
}
impl EndpointExclusionList {
pub(crate) fn new() -> Self {
Self {
excluded: HashSet::new(),
}
}
pub(crate) fn exclude(&mut self, endpoint: impl Into<String>) {
self.excluded.insert(endpoint.into());
}
pub(crate) fn is_excluded(&self, endpoint: &str) -> bool {
self.excluded.contains(endpoint)
}
pub(crate) fn is_excluded_or_cooling_down(
&self,
endpoint: &str,
cooldown_tracker: &EndpointCooldownTracker,
) -> bool {
if self.is_excluded(endpoint) {
return true;
}
cooldown_tracker.is_cooling_down(endpoint)
}
pub(crate) fn clear(&mut self) {
self.excluded.clear();
}
pub(crate) fn len(&self) -> usize {
self.excluded.len()
}
pub(crate) fn is_empty(&self) -> bool {
self.excluded.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn traits() {
static_assertions::assert_impl_all!(EndpointCooldownTracker: Send, Sync, std::fmt::Debug);
static_assertions::assert_impl_all!(
EndpointExclusionList: Send,
Sync,
std::fmt::Debug,
Clone
);
}
#[test]
fn cooldown_tracker_default_construction() {
let tracker = EndpointCooldownTracker::new();
assert!(tracker.is_empty(), "tracker should be empty initially");
assert_eq!(tracker.len(), 0, "tracker length should be 0");
assert!(
!tracker.is_cooling_down("10.0.0.1:15000"),
"untracked endpoint should not be cooling down"
);
}
#[test]
fn cooldown_tracker_record_failure_increases_consecutive_failures() {
let now = Instant::now();
let tracker = EndpointCooldownTracker::with_options(
Duration::from_millis(100),
Duration::from_millis(1000),
Duration::from_secs(60),
);
let duration1 = tracker.record_failure_at("ep-1", now);
assert!(
duration1 <= Duration::from_millis(100),
"duration1 {duration1:?} <= 100ms"
);
assert!(
duration1 >= Duration::from_millis(50),
"duration1 {duration1:?} >= 50ms (jitter floor)"
);
let duration2 = tracker.record_failure_at("ep-1", now + Duration::from_millis(10));
assert!(
duration2 <= Duration::from_millis(200),
"duration2 {duration2:?} <= 200ms"
);
assert!(
duration2 >= Duration::from_millis(100),
"duration2 {duration2:?} >= 100ms (jitter floor)"
);
let duration3 = tracker.record_failure_at("ep-1", now + Duration::from_millis(20));
assert!(
duration3 <= Duration::from_millis(400),
"duration3 {duration3:?} <= 400ms"
);
assert!(
duration3 >= Duration::from_millis(200),
"duration3 {duration3:?} >= 200ms (jitter floor)"
);
assert_eq!(tracker.len(), 1, "should have 1 tracked endpoint");
}
#[test]
fn cooldown_tracker_exponential_backoff_and_max_cap() {
let now = Instant::now();
let tracker = EndpointCooldownTracker::with_options(
Duration::from_millis(100),
Duration::from_millis(500),
Duration::from_secs(60),
);
for i in 0..10 {
let duration = tracker.record_failure_at("ep-cap", now + Duration::from_millis(i));
assert!(
duration <= Duration::from_millis(500),
"duration {duration:?} exceeded max cap 500ms on attempt {i}"
);
if i >= 3 {
assert!(
duration >= Duration::from_millis(250),
"duration {duration:?} under capped floor 250ms on attempt {i}"
);
}
}
}
#[test]
fn cooldown_tracker_reset_after_window() {
let now = Instant::now();
let reset_after = Duration::from_secs(10);
let tracker = EndpointCooldownTracker::with_options(
Duration::from_secs(5),
Duration::from_secs(60),
reset_after,
);
tracker.record_failure_at("ep-reset", now);
let duration =
tracker.record_failure_at("ep-reset", now + reset_after + Duration::from_millis(1));
assert!(
duration <= Duration::from_secs(5),
"after reset window, backoff should reset to base cooldown <= 5s"
);
assert!(
duration >= Duration::from_millis(2500),
"after reset window, backoff should reset to base cooldown >= 2.5s"
);
}
#[test]
fn cooldown_tracker_only_triggers_on_resource_exhausted() {
let tracker = EndpointCooldownTracker::new();
assert_eq!(
tracker.record_error("ep-1", Code::Unavailable),
None,
"UNAVAILABLE should not trigger cooldown"
);
assert_eq!(
tracker.record_error("ep-1", Code::Internal),
None,
"INTERNAL should not trigger cooldown"
);
assert_eq!(
tracker.record_error("ep-1", Code::DeadlineExceeded),
None,
"DEADLINE_EXCEEDED should not trigger cooldown"
);
assert!(tracker.is_empty(), "tracker should remain empty");
let res = tracker.record_error("ep-1", Code::ResourceExhausted);
assert!(res.is_some(), "RESOURCE_EXHAUSTED should trigger cooldown");
assert_eq!(tracker.len(), 1, "tracker should have 1 entry");
}
#[test]
fn cooldown_tracker_clear_expired_removes_old_entries() {
let now = Instant::now();
let tracker = EndpointCooldownTracker::with_options(
Duration::from_millis(10),
Duration::from_millis(50),
Duration::from_millis(100),
);
tracker.record_failure_at("ep-old", now);
assert_eq!(tracker.len(), 1, "tracker should have 1 entry");
let future = now + Duration::from_millis(200);
tracker.clear_expired_at(future);
assert!(
tracker.is_empty(),
"expired entry should be cleaned up by clear_expired_at"
);
}
#[test]
fn cooldown_tracker_clear_removes_all_entries() {
let tracker = EndpointCooldownTracker::new();
tracker.record_failure("ep-1");
tracker.record_failure("ep-2");
assert_eq!(tracker.len(), 2, "tracker should have 2 entries");
tracker.clear();
assert!(tracker.is_empty(), "tracker should be empty after clear");
}
#[test]
fn cooldown_tracker_is_cooling_down_at_time() {
let now = Instant::now();
let tracker = EndpointCooldownTracker::with_options(
Duration::from_secs(5),
Duration::from_secs(5),
Duration::from_secs(60),
);
tracker.record_failure_at("ep-time", now);
assert!(
tracker.is_cooling_down_at("ep-time", now),
"endpoint should be cooling down at time of failure"
);
assert!(
!tracker.is_cooling_down_at("ep-time", now + Duration::from_secs(10)),
"endpoint should not be cooling down after cooldown expires"
);
}
#[test]
fn cooldown_tracker_concurrent_access() {
let tracker = EndpointCooldownTracker::new();
thread::scope(|s| {
for i in 0..10 {
let t = &tracker;
s.spawn(move || {
let ep = format!("ep-{}", i % 3);
t.record_failure(&ep);
let _ = t.is_cooling_down(&ep);
let _ = t.len();
});
}
});
assert!(
tracker.len() <= 3,
"tracker length should be at most 3 distinct endpoints"
);
}
#[test]
fn exclusion_list_basic_operations() {
let mut list = EndpointExclusionList::new();
assert!(list.is_empty(), "exclusion list should be empty initially");
assert_eq!(list.len(), 0, "exclusion list length should be 0");
list.exclude("10.0.0.1:15000");
assert!(!list.is_empty(), "exclusion list should not be empty");
assert_eq!(list.len(), 1, "exclusion list length should be 1");
assert!(
list.is_excluded("10.0.0.1:15000"),
"10.0.0.1:15000 should be excluded"
);
assert!(
!list.is_excluded("10.0.0.2:15000"),
"10.0.0.2:15000 should not be excluded"
);
list.exclude("10.0.0.2:15000");
assert_eq!(list.len(), 2, "exclusion list length should be 2");
list.clear();
assert!(
list.is_empty(),
"exclusion list should be empty after clear"
);
assert!(
!list.is_excluded("10.0.0.1:15000"),
"cleared list should not contain excluded endpoints"
);
}
#[test]
fn exclusion_list_is_excluded_or_cooling_down_short_circuit() {
let now = Instant::now();
let tracker = EndpointCooldownTracker::with_options(
Duration::from_secs(10),
Duration::from_secs(10),
Duration::from_secs(60),
);
let mut list = EndpointExclusionList::new();
assert!(
!list.is_excluded_or_cooling_down("ep-1", &tracker),
"ep-1 is neither excluded nor on cooldown"
);
list.exclude("ep-2");
assert!(
list.is_excluded_or_cooling_down("ep-2", &tracker),
"ep-2 is in request exclusion list"
);
tracker.record_failure_at("ep-3", now);
assert!(
tracker.is_cooling_down_at("ep-3", now),
"ep-3 must be on cooldown at now"
);
assert!(
list.is_excluded_or_cooling_down("ep-3", &tracker),
"ep-3 must be recognized as cooling down"
);
}
#[test]
fn exclusion_list_clear() {
let mut list = EndpointExclusionList::new();
list.exclude("ep-a");
list.exclude("ep-b");
assert_eq!(list.len(), 2, "should have 2 items");
list.clear();
assert_eq!(list.len(), 0, "should have 0 items after clear");
assert!(!list.is_excluded("ep-a"), "ep-a should not be excluded");
}
#[test]
fn cooldown_tracker_and_exclusion_list_default_traits() {
let tracker = EndpointCooldownTracker::default();
assert!(tracker.is_empty(), "default tracker should be empty");
let list = EndpointExclusionList::default();
assert!(list.is_empty(), "default exclusion list should be empty");
}
#[test]
fn cooldown_tracker_zero_duration() {
let tracker = EndpointCooldownTracker::with_options(
Duration::ZERO,
Duration::ZERO,
Duration::from_secs(60),
);
let now = Instant::now();
let duration = tracker.record_failure_at("ep-zero", now);
assert_eq!(duration, Duration::ZERO, "cooldown duration should be zero");
assert!(
!tracker.is_cooling_down_at("ep-zero", now + Duration::from_millis(1)),
"should not be cooling down past now"
);
}
#[test]
fn cooldown_tracker_clear_expired_selective() {
let now = Instant::now();
let tracker = EndpointCooldownTracker::with_options(
Duration::from_millis(50),
Duration::from_millis(50),
Duration::from_millis(100),
);
tracker.record_failure_at("ep-old", now);
tracker.record_failure_at("ep-new", now + Duration::from_millis(200));
assert_eq!(tracker.len(), 2, "tracker should have 2 entries");
tracker.clear_expired_at(now + Duration::from_millis(150));
assert_eq!(tracker.len(), 1, "tracker should have 1 entry left");
assert!(
!tracker.is_cooling_down_at("ep-old", now + Duration::from_millis(150)),
"ep-old should no longer be cooling down"
);
}
#[test]
fn cooldown_tracker_clear_expired_no_op_when_none_expired() {
let now = Instant::now();
let tracker = EndpointCooldownTracker::with_options(
Duration::from_secs(10),
Duration::from_secs(10),
Duration::from_secs(60),
);
tracker.record_failure_at("ep-1", now);
tracker.clear_expired_at(now);
assert_eq!(tracker.len(), 1, "tracker should retain non-expired entry");
}
#[test]
fn cooldown_tracker_atomic_consecutive_failures_no_lost_updates() {
let tracker = EndpointCooldownTracker::new();
let num_threads = 10;
let iterations_per_thread = 100;
let total_expected = num_threads * iterations_per_thread;
let barrier = std::sync::Barrier::new(num_threads);
thread::scope(|s| {
for _ in 0..num_threads {
let t = &tracker;
let b = &barrier;
s.spawn(move || {
b.wait();
for _ in 0..iterations_per_thread {
t.record_failure("ep-atomic");
}
});
}
});
let failures = tracker
.state
.read()
.expect("read lock poisoned")
.get("ep-atomic")
.map_or(0, |entry| entry.consecutive_failures);
assert_eq!(
failures, total_expected,
"all concurrent failures must be recorded atomically without lost updates"
);
}
#[test]
fn cooldown_tracker_large_duration_no_wrapping_cast() {
let tracker = EndpointCooldownTracker::with_options(
Duration::MAX,
Duration::MAX,
Duration::from_secs(600),
);
let now = Instant::now();
let duration = tracker.record_failure_at("ep-huge", now);
assert!(
duration <= Duration::from_millis(u64::MAX),
"duration should not overflow u64 millis"
);
assert_eq!(tracker.len(), 1, "tracker should have 1 entry");
}
}