use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use dashmap::DashMap;
use tonic::{Response, Status};
use crate::cache::{CacheConfig, ResponseCache};
use crate::coalesce::{DefaultKeyFn, KeyFn, Singleflight};
#[derive(Clone, Default)]
pub struct ClientConfig {
pub coalesce: bool,
pub caches: HashMap<String, CacheConfig>,
}
impl ClientConfig {
pub fn new() -> Self {
Self {
coalesce: true,
caches: HashMap::new(),
}
}
}
pub struct CoalescingClientBuilder<C> {
inner: C,
coalesce: bool,
caches: HashMap<String, CacheConfig>,
key_fn: Arc<dyn KeyFn>,
}
impl<C> CoalescingClientBuilder<C> {
fn new(inner: C) -> Self {
Self {
inner,
coalesce: true,
caches: HashMap::new(),
key_fn: Arc::new(DefaultKeyFn),
}
}
pub fn coalesce(mut self, enabled: bool) -> Self {
self.coalesce = enabled;
self
}
pub fn cache(mut self, method: impl Into<String>, cfg: CacheConfig) -> Self {
self.caches.insert(method.into(), cfg);
self
}
pub fn key_fn(mut self, f: impl KeyFn) -> Self {
self.key_fn = Arc::new(f);
self
}
pub fn build(self) -> CoalescingClient<C> {
CoalescingClient {
inner: self.inner,
coalesce: self.coalesce,
cache_configs: self.caches,
key_fn: self.key_fn,
singleflights: DashMap::new(),
response_caches: DashMap::new(),
}
}
}
pub struct CoalescingClient<C> {
pub inner: C,
coalesce: bool,
cache_configs: HashMap<String, CacheConfig>,
key_fn: Arc<dyn KeyFn>,
singleflights: DashMap<String, Arc<dyn Any + Send + Sync>>,
response_caches: DashMap<String, Arc<dyn Any + Send + Sync>>,
}
impl<C> CoalescingClient<C> {
pub fn new(inner: C) -> Self {
CoalescingClientBuilder::new(inner).build()
}
pub fn builder(inner: C) -> CoalescingClientBuilder<C> {
CoalescingClientBuilder::new(inner)
}
pub async fn call<R, F, Fut>(
&self,
service: &str,
method: &str,
req_bytes: Vec<u8>,
upstream: F,
) -> Result<Response<R>, Status>
where
R: Clone + Send + Sync + 'static,
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<Response<R>, Status>> + Send + 'static,
{
let key = self.key_fn.derive(service, method, &req_bytes);
if let Some(cache) = self.get_cache::<R>(method)
&& let Some(hit) = cache.get(&key)
{
tracing::debug!(method, "coalesce: cache hit");
return Ok(Response::new(hit));
}
let result: Result<R, Status> = if self.coalesce {
let sf = self.get_or_create_sf::<R>(method);
sf.run(key.clone(), service, method, async move {
upstream().await.map(Response::into_inner)
})
.await
} else {
upstream().await.map(Response::into_inner)
};
if let Ok(ref value) = result
&& let Some(cache) = self.get_cache::<R>(method)
{
cache.insert(&key, value.clone());
}
result.map(Response::new)
}
fn get_or_create_sf<R: Clone + Send + Sync + 'static>(
&self,
method: &str,
) -> Arc<Singleflight<R>> {
let entry = self
.singleflights
.entry(method.to_string())
.or_insert_with(|| Arc::new(Singleflight::<R>::new()) as Arc<dyn Any + Send + Sync>);
entry
.value()
.clone()
.downcast::<Singleflight<R>>()
.expect("method always maps to the same response type")
}
fn get_cache<R: Clone + Send + Sync + 'static>(
&self,
method: &str,
) -> Option<Arc<ResponseCache<R>>> {
let cfg = self.cache_configs.get(method)?;
let entry = self
.response_caches
.entry(method.to_string())
.or_insert_with(|| {
Arc::new(ResponseCache::<R>::new(cfg.clone())) as Arc<dyn Any + Send + Sync>
});
entry.value().clone().downcast::<ResponseCache<R>>().ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::CacheConfig;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use tonic::{Response, Status};
#[derive(Clone)]
struct FakeClient {
counter: Arc<AtomicU32>,
fail: bool,
}
impl FakeClient {
fn new() -> Self {
Self {
counter: Arc::new(AtomicU32::new(0)),
fail: false,
}
}
fn failing() -> Self {
Self {
counter: Arc::new(AtomicU32::new(0)),
fail: true,
}
}
async fn check(&self, _: &str) -> Result<Response<String>, Status> {
self.counter.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(20)).await;
if self.fail {
Err(Status::internal("injected failure"))
} else {
Ok(Response::new("ok".into()))
}
}
}
#[tokio::test]
async fn concurrent_identical_calls_share_one_upstream() {
let fake = FakeClient::new();
let counter = fake.counter.clone();
let client = CoalescingClient::new(fake.clone());
let client = Arc::new(client);
let mut handles = Vec::new();
for _ in 0..6 {
let c = client.inner.clone();
let cl = Arc::clone(&client);
handles.push(tokio::spawn(async move {
cl.call::<String, _, _>("svc", "Check", b"request".to_vec(), move || {
let c = c.clone();
async move { c.check("input").await }
})
.await
}));
}
let results = futures_util::future::join_all(handles).await;
assert_eq!(counter.load(Ordering::SeqCst), 1);
for r in results {
assert_eq!(r.unwrap().unwrap().into_inner(), "ok");
}
}
#[tokio::test]
async fn different_request_bytes_not_coalesced() {
let fake = FakeClient::new();
let counter = fake.counter.clone();
let client = Arc::new(CoalescingClient::new(fake.clone()));
let mut handles = Vec::new();
for i in 0u8..4 {
let c = fake.clone();
let cl = Arc::clone(&client);
handles.push(tokio::spawn(async move {
cl.call::<String, _, _>("svc", "Check", vec![i], move || {
let c = c.clone();
async move { c.check("x").await }
})
.await
}));
}
futures_util::future::join_all(handles).await;
assert_eq!(counter.load(Ordering::SeqCst), 4);
}
#[tokio::test]
async fn error_shared_not_cached_next_call_retries() {
let fake = FakeClient::failing();
let counter = fake.counter.clone();
let client = Arc::new(CoalescingClient::new(fake.clone()));
let mut handles = Vec::new();
for _ in 0..3 {
let c = fake.clone();
let cl = Arc::clone(&client);
handles.push(tokio::spawn(async move {
cl.call::<String, _, _>("svc", "Check", b"k".to_vec(), move || {
let c = c.clone();
async move { c.check("x").await }
})
.await
}));
}
let results = futures_util::future::join_all(handles).await;
assert_eq!(counter.load(Ordering::SeqCst), 1);
for r in results {
assert!(r.unwrap().is_err());
}
let ok_fake = FakeClient::new();
let ok_counter = ok_fake.counter.clone();
let result = client
.call::<String, _, _>("svc", "Check", b"k".to_vec(), move || {
let c = ok_fake.clone();
async move { c.check("x").await }
})
.await;
assert_eq!(ok_counter.load(Ordering::SeqCst), 1);
assert!(result.is_ok());
}
#[tokio::test]
async fn ttl_cache_hit_within_ttl_no_upstream() {
let fake = FakeClient::new();
let counter = fake.counter.clone();
let client = CoalescingClient::builder(fake.clone())
.cache("Check", CacheConfig::new(Duration::from_secs(10), 100))
.build();
let c = fake.clone();
client
.call::<String, _, _>("svc", "Check", b"k".to_vec(), move || {
let c = c.clone();
async move { c.check("x").await }
})
.await
.unwrap();
assert_eq!(counter.load(Ordering::SeqCst), 1);
let c = fake.clone();
let result = client
.call::<String, _, _>("svc", "Check", b"k".to_vec(), move || {
let c = c.clone();
async move { c.check("x").await }
})
.await
.unwrap();
assert_eq!(counter.load(Ordering::SeqCst), 1); assert_eq!(result.into_inner(), "ok");
}
#[tokio::test]
async fn ttl_cache_miss_after_expiry() {
let fake = FakeClient::new();
let counter = fake.counter.clone();
let client = CoalescingClient::builder(fake.clone())
.cache("Check", CacheConfig::new(Duration::from_millis(10), 100))
.build();
let c = fake.clone();
client
.call::<String, _, _>("svc", "Check", b"k".to_vec(), move || {
let c = c.clone();
async move { c.check("x").await }
})
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(30)).await;
let c = fake.clone();
client
.call::<String, _, _>("svc", "Check", b"k".to_vec(), move || {
let c = c.clone();
async move { c.check("x").await }
})
.await
.unwrap();
assert_eq!(counter.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn coalescing_disabled_passes_through() {
let fake = FakeClient::new();
let counter = fake.counter.clone();
let client = Arc::new(
CoalescingClient::builder(fake.clone())
.coalesce(false)
.build(),
);
let mut handles = Vec::new();
for _ in 0..4 {
let c = fake.clone();
let cl = Arc::clone(&client);
handles.push(tokio::spawn(async move {
cl.call::<String, _, _>("svc", "Check", b"k".to_vec(), move || {
let c = c.clone();
async move { c.check("x").await }
})
.await
}));
}
futures_util::future::join_all(handles).await;
assert_eq!(counter.load(Ordering::SeqCst), 4);
}
}