1use std::any::Any;
48use std::collections::HashMap;
49use std::sync::Arc;
50
51use dashmap::DashMap;
52use tonic::{Response, Status};
53
54use crate::cache::{CacheConfig, ResponseCache};
55use crate::coalesce::{DefaultKeyFn, KeyFn, Singleflight};
56
57#[derive(Clone, Default)]
59pub struct ClientConfig {
60 pub coalesce: bool,
62 pub caches: HashMap<String, CacheConfig>,
64}
65
66impl ClientConfig {
67 pub fn new() -> Self {
68 Self {
69 coalesce: true,
70 caches: HashMap::new(),
71 }
72 }
73}
74
75pub struct CoalescingClientBuilder<C> {
77 inner: C,
78 coalesce: bool,
79 caches: HashMap<String, CacheConfig>,
80 key_fn: Arc<dyn KeyFn>,
81}
82
83impl<C> CoalescingClientBuilder<C> {
84 fn new(inner: C) -> Self {
85 Self {
86 inner,
87 coalesce: true,
88 caches: HashMap::new(),
89 key_fn: Arc::new(DefaultKeyFn),
90 }
91 }
92
93 pub fn coalesce(mut self, enabled: bool) -> Self {
95 self.coalesce = enabled;
96 self
97 }
98
99 pub fn cache(mut self, method: impl Into<String>, cfg: CacheConfig) -> Self {
101 self.caches.insert(method.into(), cfg);
102 self
103 }
104
105 pub fn key_fn(mut self, f: impl KeyFn) -> Self {
109 self.key_fn = Arc::new(f);
110 self
111 }
112
113 pub fn build(self) -> CoalescingClient<C> {
114 CoalescingClient {
115 inner: self.inner,
116 coalesce: self.coalesce,
117 cache_configs: self.caches,
118 key_fn: self.key_fn,
119 singleflights: DashMap::new(),
120 response_caches: DashMap::new(),
121 }
122 }
123}
124
125pub struct CoalescingClient<C> {
131 pub inner: C,
133 coalesce: bool,
134 cache_configs: HashMap<String, CacheConfig>,
135 key_fn: Arc<dyn KeyFn>,
136 singleflights: DashMap<String, Arc<dyn Any + Send + Sync>>,
140 response_caches: DashMap<String, Arc<dyn Any + Send + Sync>>,
142}
143
144impl<C> CoalescingClient<C> {
145 pub fn new(inner: C) -> Self {
147 CoalescingClientBuilder::new(inner).build()
148 }
149
150 pub fn builder(inner: C) -> CoalescingClientBuilder<C> {
152 CoalescingClientBuilder::new(inner)
153 }
154
155 pub async fn call<R, F, Fut>(
169 &self,
170 service: &str,
171 method: &str,
172 req_bytes: Vec<u8>,
173 upstream: F,
174 ) -> Result<Response<R>, Status>
175 where
176 R: Clone + Send + Sync + 'static,
177 F: FnOnce() -> Fut + Send + 'static,
178 Fut: std::future::Future<Output = Result<Response<R>, Status>> + Send + 'static,
179 {
180 let key = self.key_fn.derive(service, method, &req_bytes);
181
182 if let Some(cache) = self.get_cache::<R>(method)
184 && let Some(hit) = cache.get(&key)
185 {
186 tracing::debug!(method, "coalesce: cache hit");
187 return Ok(Response::new(hit));
188 }
189
190 let result: Result<R, Status> = if self.coalesce {
192 let sf = self.get_or_create_sf::<R>(method);
193 sf.run(key.clone(), service, method, async move {
194 upstream().await.map(Response::into_inner)
195 })
196 .await
197 } else {
198 upstream().await.map(Response::into_inner)
199 };
200
201 if let Ok(ref value) = result
203 && let Some(cache) = self.get_cache::<R>(method)
204 {
205 cache.insert(&key, value.clone());
206 }
207
208 result.map(Response::new)
209 }
210
211 fn get_or_create_sf<R: Clone + Send + Sync + 'static>(
219 &self,
220 method: &str,
221 ) -> Arc<Singleflight<R>> {
222 let entry = self
223 .singleflights
224 .entry(method.to_string())
225 .or_insert_with(|| Arc::new(Singleflight::<R>::new()) as Arc<dyn Any + Send + Sync>);
226
227 entry
228 .value()
229 .clone()
230 .downcast::<Singleflight<R>>()
231 .expect("method always maps to the same response type")
232 }
233
234 fn get_cache<R: Clone + Send + Sync + 'static>(
237 &self,
238 method: &str,
239 ) -> Option<Arc<ResponseCache<R>>> {
240 let cfg = self.cache_configs.get(method)?;
241
242 let entry = self
243 .response_caches
244 .entry(method.to_string())
245 .or_insert_with(|| {
246 Arc::new(ResponseCache::<R>::new(cfg.clone())) as Arc<dyn Any + Send + Sync>
247 });
248
249 entry.value().clone().downcast::<ResponseCache<R>>().ok()
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use crate::cache::CacheConfig;
257 use std::sync::Arc;
258 use std::sync::atomic::{AtomicU32, Ordering};
259 use std::time::Duration;
260 use tonic::{Response, Status};
261
262 #[derive(Clone)]
265 struct FakeClient {
266 counter: Arc<AtomicU32>,
267 fail: bool,
268 }
269
270 impl FakeClient {
271 fn new() -> Self {
272 Self {
273 counter: Arc::new(AtomicU32::new(0)),
274 fail: false,
275 }
276 }
277 fn failing() -> Self {
278 Self {
279 counter: Arc::new(AtomicU32::new(0)),
280 fail: true,
281 }
282 }
283 async fn check(&self, _: &str) -> Result<Response<String>, Status> {
284 self.counter.fetch_add(1, Ordering::SeqCst);
285 tokio::time::sleep(Duration::from_millis(20)).await;
286 if self.fail {
287 Err(Status::internal("injected failure"))
288 } else {
289 Ok(Response::new("ok".into()))
290 }
291 }
292 }
293
294 #[tokio::test]
295 async fn concurrent_identical_calls_share_one_upstream() {
296 let fake = FakeClient::new();
297 let counter = fake.counter.clone();
298 let client = CoalescingClient::new(fake.clone());
299
300 let client = Arc::new(client);
301 let mut handles = Vec::new();
302 for _ in 0..6 {
303 let c = client.inner.clone();
304 let cl = Arc::clone(&client);
305 handles.push(tokio::spawn(async move {
306 cl.call::<String, _, _>("svc", "Check", b"request".to_vec(), move || {
307 let c = c.clone();
308 async move { c.check("input").await }
309 })
310 .await
311 }));
312 }
313
314 let results = futures_util::future::join_all(handles).await;
315 assert_eq!(counter.load(Ordering::SeqCst), 1);
316 for r in results {
317 assert_eq!(r.unwrap().unwrap().into_inner(), "ok");
318 }
319 }
320
321 #[tokio::test]
322 async fn different_request_bytes_not_coalesced() {
323 let fake = FakeClient::new();
324 let counter = fake.counter.clone();
325 let client = Arc::new(CoalescingClient::new(fake.clone()));
326
327 let mut handles = Vec::new();
328 for i in 0u8..4 {
329 let c = fake.clone();
330 let cl = Arc::clone(&client);
331 handles.push(tokio::spawn(async move {
332 cl.call::<String, _, _>("svc", "Check", vec![i], move || {
333 let c = c.clone();
334 async move { c.check("x").await }
335 })
336 .await
337 }));
338 }
339 futures_util::future::join_all(handles).await;
340 assert_eq!(counter.load(Ordering::SeqCst), 4);
341 }
342
343 #[tokio::test]
344 async fn error_shared_not_cached_next_call_retries() {
345 let fake = FakeClient::failing();
346 let counter = fake.counter.clone();
347 let client = Arc::new(CoalescingClient::new(fake.clone()));
348
349 let mut handles = Vec::new();
351 for _ in 0..3 {
352 let c = fake.clone();
353 let cl = Arc::clone(&client);
354 handles.push(tokio::spawn(async move {
355 cl.call::<String, _, _>("svc", "Check", b"k".to_vec(), move || {
356 let c = c.clone();
357 async move { c.check("x").await }
358 })
359 .await
360 }));
361 }
362 let results = futures_util::future::join_all(handles).await;
363 assert_eq!(counter.load(Ordering::SeqCst), 1);
364 for r in results {
365 assert!(r.unwrap().is_err());
366 }
367
368 let ok_fake = FakeClient::new();
371 let ok_counter = ok_fake.counter.clone();
372 let result = client
373 .call::<String, _, _>("svc", "Check", b"k".to_vec(), move || {
374 let c = ok_fake.clone();
375 async move { c.check("x").await }
376 })
377 .await;
378 assert_eq!(ok_counter.load(Ordering::SeqCst), 1);
379 assert!(result.is_ok());
380 }
381
382 #[tokio::test]
383 async fn ttl_cache_hit_within_ttl_no_upstream() {
384 let fake = FakeClient::new();
385 let counter = fake.counter.clone();
386 let client = CoalescingClient::builder(fake.clone())
387 .cache("Check", CacheConfig::new(Duration::from_secs(10), 100))
388 .build();
389
390 let c = fake.clone();
392 client
393 .call::<String, _, _>("svc", "Check", b"k".to_vec(), move || {
394 let c = c.clone();
395 async move { c.check("x").await }
396 })
397 .await
398 .unwrap();
399 assert_eq!(counter.load(Ordering::SeqCst), 1);
400
401 let c = fake.clone();
403 let result = client
404 .call::<String, _, _>("svc", "Check", b"k".to_vec(), move || {
405 let c = c.clone();
406 async move { c.check("x").await }
407 })
408 .await
409 .unwrap();
410 assert_eq!(counter.load(Ordering::SeqCst), 1); assert_eq!(result.into_inner(), "ok");
412 }
413
414 #[tokio::test]
415 async fn ttl_cache_miss_after_expiry() {
416 let fake = FakeClient::new();
417 let counter = fake.counter.clone();
418 let client = CoalescingClient::builder(fake.clone())
419 .cache("Check", CacheConfig::new(Duration::from_millis(10), 100))
420 .build();
421
422 let c = fake.clone();
423 client
424 .call::<String, _, _>("svc", "Check", b"k".to_vec(), move || {
425 let c = c.clone();
426 async move { c.check("x").await }
427 })
428 .await
429 .unwrap();
430
431 tokio::time::sleep(Duration::from_millis(30)).await;
432
433 let c = fake.clone();
434 client
435 .call::<String, _, _>("svc", "Check", b"k".to_vec(), move || {
436 let c = c.clone();
437 async move { c.check("x").await }
438 })
439 .await
440 .unwrap();
441 assert_eq!(counter.load(Ordering::SeqCst), 2);
442 }
443
444 #[tokio::test]
445 async fn coalescing_disabled_passes_through() {
446 let fake = FakeClient::new();
447 let counter = fake.counter.clone();
448 let client = Arc::new(
449 CoalescingClient::builder(fake.clone())
450 .coalesce(false)
451 .build(),
452 );
453
454 let mut handles = Vec::new();
455 for _ in 0..4 {
456 let c = fake.clone();
457 let cl = Arc::clone(&client);
458 handles.push(tokio::spawn(async move {
459 cl.call::<String, _, _>("svc", "Check", b"k".to_vec(), move || {
460 let c = c.clone();
461 async move { c.check("x").await }
462 })
463 .await
464 }));
465 }
466 futures_util::future::join_all(handles).await;
467 assert_eq!(counter.load(Ordering::SeqCst), 4);
469 }
470}