use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use parking_lot::Mutex;
use tokio::sync::Notify;
struct Slot<T> {
result: Mutex<Option<T>>,
ready: Notify,
}
impl<T> Slot<T> {
fn new() -> Self {
Slot {
result: Mutex::new(None),
ready: Notify::new(),
}
}
}
impl<T: Clone> Slot<T> {
fn value(&self) -> Option<T> {
self.result.lock().clone()
}
fn publish(&self, value: T) {
*self.result.lock() = Some(value);
}
}
type SlotMap<T> = Arc<Mutex<HashMap<String, Arc<Slot<T>>>>>;
#[derive(Clone)]
pub struct InflightMap<T> {
map: SlotMap<T>,
}
impl<T> Default for InflightMap<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> InflightMap<T> {
pub fn new() -> Self {
InflightMap {
map: Arc::new(Mutex::new(HashMap::new())),
}
}
}
struct FetchGuard<T> {
map: SlotMap<T>,
key: String,
slot: Arc<Slot<T>>,
registry: &'static str,
}
impl<T> Drop for FetchGuard<T> {
fn drop(&mut self) {
{
let mut map = self.map.lock();
if let Entry::Occupied(occ) = map.entry(self.key.clone()) {
if Arc::ptr_eq(occ.get(), &self.slot) {
occ.remove();
}
}
} self.slot.ready.notify_waiters();
crate::metrics::PROXY_INFLIGHT
.with_label_values(&[self.registry])
.dec();
}
}
impl<T> InflightMap<T>
where
T: Clone,
{
fn elect(&self, key: &str) -> Role<T> {
let mut map = self.map.lock();
match map.entry(key.to_string()) {
Entry::Vacant(v) => {
let slot = Arc::new(Slot::new());
v.insert(Arc::clone(&slot));
Role::Leader(slot)
}
Entry::Occupied(o) => Role::Follower(Arc::clone(o.get())),
}
}
pub async fn coalesced<F, Fut>(
&self,
key: &str,
registry: &'static str,
budget: Duration,
fetch: F,
) -> Option<T>
where
F: FnOnce() -> Fut,
Fut: Future<Output = Option<T>>,
{
debug_assert!(!key.is_empty(), "coalesce key must not be empty");
match self.elect(key) {
Role::Leader(slot) => {
crate::metrics::PROXY_INFLIGHT
.with_label_values(&[registry])
.inc();
let _guard = FetchGuard {
map: Arc::clone(&self.map),
key: key.to_string(),
slot: Arc::clone(&slot),
registry,
};
let out = fetch().await;
if let Some(ref value) = out {
slot.publish(value.clone());
}
out
}
Role::Follower(slot) => {
let notified = slot.ready.notified();
tokio::pin!(notified);
if let Some(value) = slot.value() {
return Some(Self::record_follower(registry, value));
}
let reason = match tokio::time::timeout(budget, &mut notified).await {
Ok(()) => {
if let Some(value) = slot.value() {
return Some(Self::record_follower(registry, value));
}
"leader"
}
Err(_) => {
"budget"
}
};
crate::metrics::PROXY_COALESCE_FALLTHROUGH_TOTAL
.with_label_values(&[registry, reason])
.inc();
fetch().await
}
}
}
pub async fn leased<F, Fut, X>(
&self,
key: &str,
registry: &'static str,
budget: Duration,
fetch: F,
) -> Option<Flight<T, X>>
where
F: FnOnce() -> Fut,
Fut: Future<Output = Option<(T, X)>>,
{
debug_assert!(!key.is_empty(), "coalesce key must not be empty");
let mut fetch = Some(fetch);
loop {
match self.elect(key) {
Role::Leader(slot) => {
let fetch = fetch.take()?;
crate::metrics::PROXY_INFLIGHT
.with_label_values(&[registry])
.inc();
let guard = FetchGuard {
map: Arc::clone(&self.map),
key: key.to_string(),
slot: Arc::clone(&slot),
registry,
};
let (value, own) = fetch().await?; slot.publish(value.clone());
slot.ready.notify_waiters();
return Some(Flight::Leader {
value,
own,
lease: Lease { _guard: guard },
});
}
Role::Follower(slot) => {
let notified = slot.ready.notified();
tokio::pin!(notified);
if let Some(value) = slot.value() {
return Some(Flight::Follower(Self::record_follower(registry, value)));
}
match tokio::time::timeout(budget, &mut notified).await {
Ok(()) => {
if let Some(value) = slot.value() {
return Some(Flight::Follower(Self::record_follower(
registry, value,
)));
}
crate::metrics::PROXY_COALESCE_FALLTHROUGH_TOTAL
.with_label_values(&[registry, "leader"])
.inc();
}
Err(_) => {
crate::metrics::PROXY_COALESCE_FALLTHROUGH_TOTAL
.with_label_values(&[registry, "budget"])
.inc();
let fetch = fetch.take()?;
let (value, own) = fetch().await?;
return Some(Flight::Alone { value, own });
}
}
}
}
}
}
fn record_follower(registry: &'static str, value: T) -> T {
crate::metrics::PROXY_COALESCED_TOTAL
.with_label_values(&[registry])
.inc();
value
}
}
enum Role<T> {
Leader(Arc<Slot<T>>),
Follower(Arc<Slot<T>>),
}
pub enum Flight<T, X> {
Leader { value: T, own: X, lease: Lease<T> },
Follower(T),
Alone { value: T, own: X },
}
pub struct Lease<T> {
_guard: FetchGuard<T>,
}
pub fn follower_budget(proxy_timeout_secs: u64) -> Duration {
Duration::from_secs(2 * proxy_timeout_secs + 3)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::sync::Barrier;
fn budget() -> Duration {
Duration::from_secs(5)
}
#[tokio::test]
async fn coalesces_concurrent_callers_to_single_fetch() {
let map: InflightMap<u64> = InflightMap::new();
let calls = Arc::new(AtomicUsize::new(0));
const M: usize = 32;
let gate = Arc::new(Barrier::new(M));
let mut handles = Vec::new();
for _ in 0..M {
let map = map.clone();
let calls = Arc::clone(&calls);
let gate = Arc::clone(&gate);
handles.push(tokio::spawn(async move {
gate.wait().await;
map.coalesced("pkg", "test", budget(), || async {
calls.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(50)).await;
Some(7u64)
})
.await
}));
}
let results: Vec<_> = futures::future::join_all(handles).await;
for r in results {
assert_eq!(r.unwrap(), Some(7u64), "every caller gets the value");
}
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"exactly one upstream fetch for M concurrent callers"
);
assert!(
map.map.lock().is_empty(),
"slot is removed after the leader finishes"
);
}
#[tokio::test]
async fn distinct_keys_do_not_block() {
let map: InflightMap<u64> = InflightMap::new();
let calls = Arc::new(AtomicUsize::new(0));
let a = {
let map = map.clone();
let calls = Arc::clone(&calls);
tokio::spawn(async move {
map.coalesced("a", "test", budget(), || async {
calls.fetch_add(1, Ordering::SeqCst);
Some(1u64)
})
.await
})
};
let b = {
let map = map.clone();
let calls = Arc::clone(&calls);
tokio::spawn(async move {
map.coalesced("b", "test", budget(), || async {
calls.fetch_add(1, Ordering::SeqCst);
Some(2u64)
})
.await
})
};
assert_eq!(a.await.unwrap(), Some(1));
assert_eq!(b.await.unwrap(), Some(2));
assert_eq!(calls.load(Ordering::SeqCst), 2, "each distinct key fetched");
}
#[tokio::test]
async fn leader_failure_falls_through_to_followers() {
let map: InflightMap<u64> = InflightMap::new();
let calls = Arc::new(AtomicUsize::new(0));
const M: usize = 8;
let gate = Arc::new(Barrier::new(M));
let mut handles = Vec::new();
for _ in 0..M {
let map = map.clone();
let calls = Arc::clone(&calls);
let gate = Arc::clone(&gate);
handles.push(tokio::spawn(async move {
gate.wait().await;
map.coalesced("pkg", "test", budget(), || async {
let n = calls.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(30)).await;
if n == 0 {
None
} else {
Some(99u64)
}
})
.await
}));
}
let results: Vec<_> = futures::future::join_all(handles).await;
let successes = results
.iter()
.filter(|r| r.as_ref().unwrap() == &Some(99))
.count();
assert!(
successes >= 1,
"followers fall through and fetch after the leader fails"
);
assert!(
calls.load(Ordering::SeqCst) >= 2,
"leader failure forces followers to their own fetch"
);
}
#[tokio::test]
async fn leader_cancellation_releases_key() {
let map: InflightMap<u64> = InflightMap::new();
let calls = Arc::new(AtomicUsize::new(0));
let leader = {
let map = map.clone();
let calls = Arc::clone(&calls);
tokio::spawn(async move {
map.coalesced("pkg", "test", budget(), || async {
calls.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(Duration::from_secs(60)).await;
Some(1u64)
})
.await
})
};
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(!map.map.lock().is_empty(), "leader registered its slot");
leader.abort();
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(
map.map.lock().is_empty(),
"cancelled leader's slot is released on drop"
);
let fresh = map
.coalesced("pkg", "test", budget(), || async {
calls.fetch_add(1, Ordering::SeqCst);
Some(2u64)
})
.await;
assert_eq!(fresh, Some(2), "subsequent request fetches with no stall");
assert_eq!(
calls.load(Ordering::SeqCst),
2,
"cancelled leader + one clean refetch"
);
}
fn slots_held<T>(map: &InflightMap<T>) -> usize {
map.map.lock().len()
}
fn leader_value(flight: Option<Flight<u64, ()>>) -> (u64, Lease<u64>) {
match flight {
Some(Flight::Leader { value, lease, .. }) => (value, lease),
_ => panic!("expected to lead"),
}
}
#[tokio::test]
async fn lease_keeps_the_slot_for_late_callers() {
let map: InflightMap<u64> = InflightMap::new();
let calls = Arc::new(AtomicUsize::new(0));
let fetch = |n: u64| {
let calls = Arc::clone(&calls);
move || async move {
calls.fetch_add(1, Ordering::SeqCst);
Some((n, ()))
}
};
let (value, lease) = leader_value(map.leased("blob", "test", budget(), fetch(7)).await);
assert_eq!(value, 7);
tokio::time::sleep(Duration::from_millis(50)).await;
match map.leased("blob", "test", budget(), fetch(8)).await {
Some(Flight::Follower(v)) => assert_eq!(v, 7, "late caller gets the leader's value"),
_ => panic!("a late caller must follow while the lease lives"),
}
assert_eq!(calls.load(Ordering::SeqCst), 1);
drop(lease);
assert!(
slots_held(&map) == 0,
"dropping the lease releases the slot"
);
let (value, _lease) = leader_value(map.leased("blob", "test", budget(), fetch(9)).await);
assert_eq!(
value, 9,
"after release the next caller leads a fresh fetch"
);
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn followers_get_the_value_at_publish_not_at_release() {
let map: InflightMap<u64> = InflightMap::new();
let leader_map = map.clone();
let leader = tokio::spawn({
let map = leader_map;
async move {
let (_, lease) = leader_value(
map.leased("blob", "test", budget(), || async {
tokio::time::sleep(Duration::from_millis(100)).await;
Some((1u64, ()))
})
.await,
);
tokio::time::sleep(Duration::from_secs(30)).await;
drop(lease);
}
});
tokio::time::sleep(Duration::from_millis(20)).await;
let got = tokio::time::timeout(
Duration::from_secs(2),
map.leased("blob", "test", budget(), || async { Some((2u64, ())) }),
)
.await
.expect("follower must not wait for the lease to end");
assert!(matches!(got, Some(Flight::Follower(1))));
leader.abort();
}
#[tokio::test]
async fn lease_released_by_panic_unwind() {
use futures::FutureExt as _;
let map: InflightMap<u64> = InflightMap::new();
let gauge = || {
crate::metrics::PROXY_INFLIGHT
.with_label_values(&["lease-panic"])
.get()
};
let (_, lease) = leader_value(
map.leased("blob", "lease-panic", budget(), || async {
Some((1u64, ()))
})
.await,
);
assert_eq!(gauge(), 1);
let task = async move {
let _lease = lease;
panic!("spool task panicked");
};
let joined = tokio::spawn(std::panic::AssertUnwindSafe(task).catch_unwind()).await;
assert!(joined.unwrap().is_err(), "the task panicked");
assert!(slots_held(&map) == 0, "slot released by the unwind");
assert_eq!(gauge(), 0, "in-flight gauge back to zero");
}
#[tokio::test]
async fn failed_leader_hands_over_to_one_new_leader() {
let map: InflightMap<u64> = InflightMap::new();
let calls = Arc::new(AtomicUsize::new(0));
const M: usize = 8;
let gate = Arc::new(Barrier::new(M));
let mut handles = Vec::new();
for _ in 0..M {
let (map, calls, gate) = (map.clone(), Arc::clone(&calls), Arc::clone(&gate));
handles.push(tokio::spawn(async move {
gate.wait().await;
let flight = map
.leased("blob", "test", budget(), || async {
let n = calls.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(50)).await;
(n > 0).then_some((42u64, ()))
})
.await;
tokio::time::sleep(Duration::from_millis(200)).await;
flight.map(|f| match f {
Flight::Leader { value, .. } | Flight::Follower(value) => value,
Flight::Alone { value, .. } => value + 1000,
})
}));
}
let results: Vec<_> = futures::future::join_all(handles).await;
let values: Vec<_> = results.into_iter().map(|r| r.unwrap()).collect();
assert_eq!(
values.iter().filter(|v| v.is_none()).count(),
1,
"only the failed leader gets None"
);
assert!(
values.iter().flatten().all(|v| *v == 42),
"everyone else shares the second fetch"
);
assert_eq!(
calls.load(Ordering::SeqCst),
2,
"failed leader + ONE new leader"
);
}
#[test]
fn budget_scales_with_timeout() {
assert_eq!(follower_budget(30), Duration::from_secs(63));
assert_eq!(follower_budget(0), Duration::from_secs(3));
}
}