blob: c9b610d339b36f63aa6799268ad547f9e9bf13d6 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use std::{
collections::{BTreeMap, VecDeque, hash_map::RandomState},
hash::BuildHasher,
sync::{Arc, Mutex},
time::{Duration, Instant},
};
use tokio::sync::Semaphore;
const COOLDOWN: Duration = Duration::from_secs(5 * 60);
const MAX_FAILURES: usize = 64;
/// Runtime-wide disposable transport memory. Route fingerprints keep cooldown
/// bounded without retaining credentials or unbounded endpoint strings.
pub struct Shared {
pub cache: Arc<Semaphore>,
routes: RandomState,
failures: Mutex<VecDeque<(u64, Instant)>>,
}
impl Default for Shared {
fn default() -> Self {
Self {
cache: Arc::new(Semaphore::new(super::cache::CACHE_LIMIT as usize)),
routes: RandomState::new(),
failures: Mutex::new(VecDeque::new()),
}
}
}
impl Shared {
pub fn route(&self, url: &str, headers: &BTreeMap<String, String>, network: u64) -> u64 {
self.routes.hash_one((url, headers, network))
}
pub fn deferred(&self, route: u64, now: Instant) -> bool {
let mut failures = self.failures.lock().unwrap();
failures.retain(|(_, until)| *until > now);
failures.iter().any(|(key, _)| *key == route)
}
pub fn defer(&self, route: u64, now: Instant) {
let mut failures = self.failures.lock().unwrap();
failures.retain(|(key, until)| *key != route && *until > now);
if failures.len() == MAX_FAILURES {
failures.pop_front();
}
failures.push_back((route, now + COOLDOWN));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cooldown_is_route_scoped_expires_and_retains_only_bounded_recent_failures() {
let shared = Shared::default();
let now = Instant::now();
let direct = 1;
let headers = BTreeMap::from([("authorization".into(), "Bearer first".into())]);
let route = shared.route("https://api.example/responses", &headers, direct);
shared.defer(route, now);
assert!(shared.deferred(route, now + COOLDOWN - Duration::from_millis(1)));
let changed_headers = BTreeMap::from([("authorization".into(), "Bearer second".into())]);
assert!(!shared.deferred(
shared.route("https://api.example/responses", &changed_headers, direct),
now
));
assert!(!shared.deferred(
shared.route("https://other.example/responses", &headers, direct),
now
));
let proxy = 2;
assert!(!shared.deferred(
shared.route("https://api.example/responses", &headers, proxy),
now
));
assert!(!shared.deferred(route, now + COOLDOWN));
assert!(shared.failures.lock().unwrap().is_empty());
for key in 0..100 {
shared.defer(key, now);
}
assert_eq!(shared.failures.lock().unwrap().len(), MAX_FAILURES);
assert!(!shared.deferred(0, now));
assert!(shared.deferred(99, now));
shared.defer(99, now + Duration::from_secs(1));
assert_eq!(shared.failures.lock().unwrap().len(), MAX_FAILURES);
}
}