1use crate::expiring_cache::ExpiringCache;
7use aws_smithy_async::future::timeout::Timeout;
8use aws_smithy_async::rt::sleep::{AsyncSleep, SharedAsyncSleep};
9use aws_smithy_async::time::{SharedTimeSource, TimeSource};
10use aws_smithy_runtime_api::box_error::BoxError;
11use aws_smithy_runtime_api::client::identity::{
12 Identity, IdentityCachePartition, IdentityFuture, ResolveCachedIdentity, ResolveIdentity,
13 SharedIdentityCache, SharedIdentityResolver,
14};
15use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
16use aws_smithy_runtime_api::shared::IntoShared;
17use aws_smithy_types::config_bag::ConfigBag;
18use aws_smithy_types::retry::RetryConfig;
19use aws_smithy_types::timeout::TimeoutConfig;
20use aws_smithy_types::DateTime;
21use std::collections::HashMap;
22use std::fmt;
23use std::sync::RwLock;
24use std::time::Duration;
25use tracing::Instrument;
26
27const DEFAULT_EXPIRATION: Duration = Duration::from_secs(15 * 60);
28const DEFAULT_BUFFER_TIME: Duration = Duration::from_secs(10);
29const DEFAULT_BUFFER_TIME_JITTER_FRACTION: fn() -> f64 = || fastrand::f64() * 0.5;
30const DEFAULT_MAX_PARTITIONS: usize = 64;
31
32fn pessimistic_load_timeout(config_bag: &ConfigBag) -> Duration {
43 let retry_config = config_bag
44 .load::<RetryConfig>()
45 .cloned()
46 .unwrap_or_else(RetryConfig::standard);
47 let timeout_config = config_bag
48 .load::<TimeoutConfig>()
49 .cloned()
50 .unwrap_or_else(TimeoutConfig::disabled);
51
52 let attempts = retry_config.max_attempts();
53 let initial_backoff = retry_config.initial_backoff().as_secs_f64();
54 let max_backoff = retry_config.max_backoff().as_secs_f64();
55
56 let total_backoff: f64 = (0..attempts.saturating_sub(1))
58 .map(|i| (initial_backoff * 2.0_f64.powi(i as i32)).min(max_backoff))
59 .sum();
60
61 let connect = timeout_config
78 .connect_timeout()
79 .unwrap_or(crate::client::defaults::DEFAULT_CONNECT_TIMEOUT)
80 .max(crate::client::defaults::DEFAULT_CONNECT_TIMEOUT)
81 .as_secs_f64();
82 let attempt_ceiling = timeout_config
83 .operation_attempt_timeout()
84 .map(|d| d.as_secs_f64())
85 .unwrap_or(0.0);
86 let per_attempt = (connect * 2.0).max(attempt_ceiling);
87 let total_attempts = attempts as f64 * per_attempt;
88
89 let computed = total_backoff + total_attempts;
91 Duration::from_secs_f64(computed.max(per_attempt))
92}
93
94#[derive(Default, Debug)]
96pub struct LazyCacheBuilder {
97 time_source: Option<SharedTimeSource>,
98 sleep_impl: Option<SharedAsyncSleep>,
99 load_timeout: Option<Duration>,
100 buffer_time: Option<Duration>,
101 buffer_time_jitter_fraction: Option<fn() -> f64>,
102 default_expiration: Option<Duration>,
103 max_partitions: Option<usize>,
104}
105
106impl LazyCacheBuilder {
107 pub fn new() -> Self {
109 Default::default()
110 }
111
112 pub fn time_source(mut self, time_source: impl TimeSource + 'static) -> Self {
114 self.set_time_source(time_source.into_shared());
115 self
116 }
117 pub fn set_time_source(&mut self, time_source: SharedTimeSource) -> &mut Self {
119 self.time_source = Some(time_source.into_shared());
120 self
121 }
122
123 pub fn sleep_impl(mut self, sleep_impl: impl AsyncSleep + 'static) -> Self {
125 self.set_sleep_impl(sleep_impl.into_shared());
126 self
127 }
128 pub fn set_sleep_impl(&mut self, sleep_impl: SharedAsyncSleep) -> &mut Self {
130 self.sleep_impl = Some(sleep_impl);
131 self
132 }
133
134 pub fn load_timeout(mut self, timeout: Duration) -> Self {
143 self.set_load_timeout(Some(timeout));
144 self
145 }
146
147 pub fn set_load_timeout(&mut self, timeout: Option<Duration>) -> &mut Self {
156 self.load_timeout = timeout;
157 self
158 }
159
160 pub fn buffer_time(mut self, buffer_time: Duration) -> Self {
169 self.set_buffer_time(Some(buffer_time));
170 self
171 }
172
173 pub fn set_buffer_time(&mut self, buffer_time: Option<Duration>) -> &mut Self {
182 self.buffer_time = buffer_time;
183 self
184 }
185
186 #[allow(unused)]
194 #[cfg(test)]
195 fn buffer_time_jitter_fraction(mut self, buffer_time_jitter_fraction: fn() -> f64) -> Self {
196 self.set_buffer_time_jitter_fraction(Some(buffer_time_jitter_fraction));
197 self
198 }
199
200 #[allow(unused)]
208 #[cfg(test)]
209 fn set_buffer_time_jitter_fraction(
210 &mut self,
211 buffer_time_jitter_fraction: Option<fn() -> f64>,
212 ) -> &mut Self {
213 self.buffer_time_jitter_fraction = buffer_time_jitter_fraction;
214 self
215 }
216
217 pub fn default_expiration(mut self, duration: Duration) -> Self {
224 self.set_default_expiration(Some(duration));
225 self
226 }
227
228 pub fn set_default_expiration(&mut self, duration: Option<Duration>) -> &mut Self {
235 self.default_expiration = duration;
236 self
237 }
238
239 pub fn max_partitions(mut self, max: usize) -> Self {
251 self.set_max_partitions(Some(max));
252 self
253 }
254
255 pub fn set_max_partitions(&mut self, max: Option<usize>) -> &mut Self {
267 if let Some(0) = max {
268 panic!("max_partitions must be greater than 0");
269 }
270 self.max_partitions = max;
271 self
272 }
273
274 pub fn build(self) -> SharedIdentityCache {
280 let default_expiration = self.default_expiration.unwrap_or(DEFAULT_EXPIRATION);
281 assert!(
282 default_expiration >= DEFAULT_EXPIRATION,
283 "default_expiration must be at least 15 minutes"
284 );
285 LazyCache::new(
286 self.load_timeout,
287 self.buffer_time.unwrap_or(DEFAULT_BUFFER_TIME),
288 self.buffer_time_jitter_fraction
289 .unwrap_or(DEFAULT_BUFFER_TIME_JITTER_FRACTION),
290 default_expiration,
291 self.max_partitions.unwrap_or(DEFAULT_MAX_PARTITIONS),
292 )
293 .into_shared()
294 }
295}
296
297#[derive(Debug)]
298struct CachePartitions {
299 partitions: RwLock<HashMap<IdentityCachePartition, ExpiringCache<Identity, BoxError>>>,
300 buffer_time: Duration,
301 max_partitions: usize,
302}
303
304impl CachePartitions {
305 fn new(buffer_time: Duration, max_partitions: usize) -> Self {
306 Self {
307 partitions: RwLock::new(HashMap::new()),
308 buffer_time,
309 max_partitions,
310 }
311 }
312
313 fn partition(&self, key: IdentityCachePartition) -> ExpiringCache<Identity, BoxError> {
314 if let Some(partition) = self.partitions.read().unwrap().get(&key).cloned() {
316 return partition;
317 }
318 let mut partitions = self.partitions.write().unwrap();
320 if let Some(partition) = partitions.get(&key).cloned() {
322 return partition;
323 }
324 if partitions.len() >= self.max_partitions {
328 if let Some(&evict_key) = partitions.keys().next() {
329 partitions.remove(&evict_key);
330 }
331 }
332 let partition = ExpiringCache::new(self.buffer_time);
333 partitions.insert(key, partition.clone());
334 tracing::debug!(
335 partition_count = partitions.len(),
336 "identity cache partition created"
337 );
338 partition
339 }
340}
341
342#[derive(Debug)]
343struct LazyCache {
344 partitions: CachePartitions,
345 load_timeout: Option<Duration>,
348 buffer_time: Duration,
349 buffer_time_jitter_fraction: fn() -> f64,
350 default_expiration: Duration,
351}
352
353impl LazyCache {
354 fn new(
355 load_timeout: Option<Duration>,
356 buffer_time: Duration,
357 buffer_time_jitter_fraction: fn() -> f64,
358 default_expiration: Duration,
359 max_partitions: usize,
360 ) -> Self {
361 Self {
362 partitions: CachePartitions::new(buffer_time, max_partitions),
363 load_timeout,
364 buffer_time,
365 buffer_time_jitter_fraction,
366 default_expiration,
367 }
368 }
369}
370
371macro_rules! required_err {
372 ($thing:literal, $how:literal) => {
373 BoxError::from(concat!(
374 "Lazy identity caching requires ",
375 $thing,
376 " to be configured. ",
377 $how,
378 " If this isn't possible, then disable identity caching by calling ",
379 "the `identity_cache` method on config with `IdentityCache::no_cache()`",
380 ))
381 };
382}
383macro_rules! validate_components {
384 ($components:ident) => {
385 let _ = $components.time_source().ok_or_else(|| {
386 required_err!(
387 "a time source",
388 "Set a time source using the `time_source` method on config."
389 )
390 })?;
391 let _ = $components.sleep_impl().ok_or_else(|| {
392 required_err!(
393 "an async sleep implementation",
394 "Set a sleep impl using the `sleep_impl` method on config."
395 )
396 })?;
397 };
398}
399
400impl ResolveCachedIdentity for LazyCache {
401 fn validate_base_client_config(
402 &self,
403 runtime_components: &aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder,
404 _cfg: &ConfigBag,
405 ) -> Result<(), BoxError> {
406 validate_components!(runtime_components);
407 Ok(())
408 }
409
410 fn validate_final_config(
411 &self,
412 runtime_components: &RuntimeComponents,
413 _cfg: &ConfigBag,
414 ) -> Result<(), BoxError> {
415 validate_components!(runtime_components);
416 Ok(())
417 }
418
419 fn resolve_cached_identity<'a>(
420 &'a self,
421 resolver: SharedIdentityResolver,
422 runtime_components: &'a RuntimeComponents,
423 config_bag: &'a ConfigBag,
424 ) -> IdentityFuture<'a> {
425 let (time_source, sleep_impl) = (
426 runtime_components.time_source().expect("validated"),
427 runtime_components.sleep_impl().expect("validated"),
428 );
429
430 let now = time_source.now();
431 let load_timeout = self
432 .load_timeout
433 .unwrap_or_else(|| pessimistic_load_timeout(config_bag));
434 tracing::debug!(
435 load_timeout=?load_timeout,
436 explicitly_configured=self.load_timeout.is_some(),
437 "identity cache load timeout"
438 );
439 let timeout_future = sleep_impl.sleep(load_timeout);
440 let partition = resolver.cache_partition();
441 let cache = self.partitions.partition(partition);
442 let default_expiration = self.default_expiration;
443
444 IdentityFuture::new(async move {
445 if let Some(identity) = cache.yield_or_clear_if_expired(now).await {
447 tracing::debug!(
448 buffer_time=?self.buffer_time,
449 cached_expiration=?identity.expiration(),
450 now=?now,
451 "loaded identity from cache"
452 );
453 Ok(identity)
454 } else {
455 let start_time = time_source.now();
460 let result = cache
461 .get_or_load(|| {
462 let span = tracing::debug_span!("lazy_load_identity");
463 async move {
464 let fut = Timeout::new(
465 resolver.resolve_identity(runtime_components, config_bag),
466 timeout_future,
467 );
468 let identity = match fut.await {
469 Ok(result) => result?,
470 Err(_err) => match resolver.fallback_on_interrupt() {
471 Some(identity) => identity,
472 None => {
473 return Err(BoxError::from(TimedOutError(load_timeout)))
474 }
475 },
476 };
477 let expiration =
479 identity.expiration().unwrap_or(now + default_expiration);
480
481 let jitter = self
482 .buffer_time
483 .mul_f64((self.buffer_time_jitter_fraction)());
484
485 let printable = DateTime::from(expiration);
490 tracing::debug!(
491 new_expiration=%printable,
492 valid_for=?expiration.duration_since(time_source.now()).unwrap_or_default(),
493 partition=?partition,
494 "identity cache miss occurred; added new identity (took {:?})",
495 time_source.now().duration_since(start_time).unwrap_or_default()
496 );
497
498 Ok((identity, expiration + jitter))
499 }
500 .instrument(span)
503 })
504 .await;
505 tracing::debug!("loaded identity");
506 result
507 }
508 })
509 }
510}
511
512#[derive(Debug)]
513struct TimedOutError(Duration);
514
515impl std::error::Error for TimedOutError {}
516
517impl fmt::Display for TimedOutError {
518 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519 write!(f, "identity resolver timed out after {:?}", self.0)
520 }
521}
522
523#[cfg(all(test, feature = "client", feature = "http-auth"))]
524mod tests {
525 use super::*;
526 use aws_smithy_async::rt::sleep::TokioSleep;
527 use aws_smithy_async::test_util::{instant_time_and_sleep, ManualTimeSource};
528 use aws_smithy_async::time::TimeSource;
529 use aws_smithy_runtime_api::client::identity::http::Token;
530 use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
531 use std::sync::atomic::{AtomicUsize, Ordering};
532 use std::sync::{Arc, Mutex};
533 use std::time::{Duration, SystemTime, UNIX_EPOCH};
534 use tracing::info;
535
536 const LOAD_TIMEOUT_FOR_TESTS: Duration = Duration::from_secs(5);
537
538 const BUFFER_TIME_NO_JITTER: fn() -> f64 = || 0_f64;
539
540 struct ResolverFn<F>(F);
541 impl<F> fmt::Debug for ResolverFn<F> {
542 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
543 f.write_str("ResolverFn")
544 }
545 }
546 impl<F> ResolveIdentity for ResolverFn<F>
547 where
548 F: Fn() -> IdentityFuture<'static> + Send + Sync,
549 {
550 fn resolve_identity<'a>(
551 &'a self,
552 _: &'a RuntimeComponents,
553 _config_bag: &'a ConfigBag,
554 ) -> IdentityFuture<'a> {
555 (self.0)()
556 }
557 }
558
559 fn resolver_fn<F>(f: F) -> SharedIdentityResolver
560 where
561 F: Fn() -> IdentityFuture<'static> + Send + Sync + 'static,
562 {
563 SharedIdentityResolver::new(ResolverFn(f))
564 }
565
566 fn test_cache(
567 buffer_time_jitter_fraction: fn() -> f64,
568 load_list: Vec<Result<Identity, BoxError>>,
569 ) -> (LazyCache, SharedIdentityResolver) {
570 #[derive(Debug)]
571 struct Resolver(Mutex<Vec<Result<Identity, BoxError>>>);
572 impl ResolveIdentity for Resolver {
573 fn resolve_identity<'a>(
574 &'a self,
575 _: &'a RuntimeComponents,
576 _config_bag: &'a ConfigBag,
577 ) -> IdentityFuture<'a> {
578 let mut list = self.0.lock().unwrap();
579 if list.len() > 0 {
580 let next = list.remove(0);
581 info!("refreshing the identity to {:?}", next);
582 IdentityFuture::ready(next)
583 } else {
584 drop(list);
585 panic!("no more identities")
586 }
587 }
588 }
589
590 let identity_resolver = SharedIdentityResolver::new(Resolver(Mutex::new(load_list)));
591 let cache = LazyCache::new(
592 Some(LOAD_TIMEOUT_FOR_TESTS),
593 DEFAULT_BUFFER_TIME,
594 buffer_time_jitter_fraction,
595 DEFAULT_EXPIRATION,
596 DEFAULT_MAX_PARTITIONS,
597 );
598 (cache, identity_resolver)
599 }
600
601 fn epoch_secs(secs: u64) -> SystemTime {
602 SystemTime::UNIX_EPOCH + Duration::from_secs(secs)
603 }
604
605 fn test_identity(expired_secs: u64) -> Identity {
606 let expiration = Some(epoch_secs(expired_secs));
607 Identity::new(Token::new("test", expiration), expiration)
608 }
609
610 async fn expect_identity(
611 expired_secs: u64,
612 cache: &LazyCache,
613 components: &RuntimeComponents,
614 resolver: SharedIdentityResolver,
615 ) {
616 let config_bag = ConfigBag::base();
617 let identity = cache
618 .resolve_cached_identity(resolver, components, &config_bag)
619 .await
620 .expect("expected identity");
621 assert_eq!(Some(epoch_secs(expired_secs)), identity.expiration());
622 }
623
624 #[tokio::test]
625 async fn initial_populate_test_identity() {
626 let time = ManualTimeSource::new(UNIX_EPOCH);
627 let components = RuntimeComponentsBuilder::for_tests()
628 .with_time_source(Some(time.clone()))
629 .with_sleep_impl(Some(TokioSleep::new()))
630 .build()
631 .unwrap();
632 let config_bag = ConfigBag::base();
633 let resolver = SharedIdentityResolver::new(resolver_fn(|| {
634 info!("refreshing the test_identity");
635 IdentityFuture::ready(Ok(test_identity(1000)))
636 }));
637 let cache = LazyCache::new(
638 Some(LOAD_TIMEOUT_FOR_TESTS),
639 DEFAULT_BUFFER_TIME,
640 BUFFER_TIME_NO_JITTER,
641 DEFAULT_EXPIRATION,
642 DEFAULT_MAX_PARTITIONS,
643 );
644 assert_eq!(
645 epoch_secs(1000),
646 cache
647 .resolve_cached_identity(resolver, &components, &config_bag)
648 .await
649 .unwrap()
650 .expiration()
651 .unwrap()
652 );
653 }
654
655 #[tokio::test]
656 async fn reload_expired_test_identity() {
657 let time = ManualTimeSource::new(epoch_secs(100));
658 let components = RuntimeComponentsBuilder::for_tests()
659 .with_time_source(Some(time.clone()))
660 .with_sleep_impl(Some(TokioSleep::new()))
661 .build()
662 .unwrap();
663 let (cache, resolver) = test_cache(
664 BUFFER_TIME_NO_JITTER,
665 vec![
666 Ok(test_identity(1000)),
667 Ok(test_identity(2000)),
668 Ok(test_identity(3000)),
669 ],
670 );
671
672 expect_identity(1000, &cache, &components, resolver.clone()).await;
673 expect_identity(1000, &cache, &components, resolver.clone()).await;
674 time.set_time(epoch_secs(1500));
675 expect_identity(2000, &cache, &components, resolver.clone()).await;
676 expect_identity(2000, &cache, &components, resolver.clone()).await;
677 time.set_time(epoch_secs(2500));
678 expect_identity(3000, &cache, &components, resolver.clone()).await;
679 expect_identity(3000, &cache, &components, resolver.clone()).await;
680 }
681
682 #[tokio::test]
683 async fn load_failed_error() {
684 let config_bag = ConfigBag::base();
685 let time = ManualTimeSource::new(epoch_secs(100));
686 let components = RuntimeComponentsBuilder::for_tests()
687 .with_time_source(Some(time.clone()))
688 .with_sleep_impl(Some(TokioSleep::new()))
689 .build()
690 .unwrap();
691 let (cache, resolver) = test_cache(
692 BUFFER_TIME_NO_JITTER,
693 vec![Ok(test_identity(1000)), Err("failed".into())],
694 );
695
696 expect_identity(1000, &cache, &components, resolver.clone()).await;
697 time.set_time(epoch_secs(1500));
698 assert!(cache
699 .resolve_cached_identity(resolver.clone(), &components, &config_bag)
700 .await
701 .is_err());
702 }
703
704 #[test]
705 fn load_contention() {
706 let rt = tokio::runtime::Builder::new_multi_thread()
707 .enable_time()
708 .worker_threads(16)
709 .build()
710 .unwrap();
711
712 let time = ManualTimeSource::new(epoch_secs(0));
713 let components = RuntimeComponentsBuilder::for_tests()
714 .with_time_source(Some(time.clone()))
715 .with_sleep_impl(Some(TokioSleep::new()))
716 .build()
717 .unwrap();
718 let (cache, resolver) = test_cache(
719 BUFFER_TIME_NO_JITTER,
720 vec![
721 Ok(test_identity(500)),
722 Ok(test_identity(1500)),
723 Ok(test_identity(2500)),
724 Ok(test_identity(3500)),
725 Ok(test_identity(4500)),
726 ],
727 );
728 let cache: SharedIdentityCache = cache.into_shared();
729
730 for _ in 0..4 {
733 let mut tasks = Vec::new();
734 for _ in 0..50 {
735 let resolver = resolver.clone();
736 let cache = cache.clone();
737 let time = time.clone();
738 let components = components.clone();
739 tasks.push(rt.spawn(async move {
740 let now = time.advance(Duration::from_secs(22));
741
742 let config_bag = ConfigBag::base();
743 let identity = cache
744 .resolve_cached_identity(resolver, &components, &config_bag)
745 .await
746 .unwrap();
747 assert!(
748 identity.expiration().unwrap() >= now,
749 "{:?} >= {:?}",
750 identity.expiration(),
751 now
752 );
753 }));
754 }
755 for task in tasks {
756 rt.block_on(task).unwrap();
757 }
758 }
759 }
760
761 #[tokio::test]
762 async fn load_timeout() {
763 let config_bag = ConfigBag::base();
764 let (time, sleep) = instant_time_and_sleep(epoch_secs(100));
765 let components = RuntimeComponentsBuilder::for_tests()
766 .with_time_source(Some(time.clone()))
767 .with_sleep_impl(Some(sleep))
768 .build()
769 .unwrap();
770 let resolver = SharedIdentityResolver::new(resolver_fn(|| {
771 IdentityFuture::new(async {
772 aws_smithy_async::future::never::Never::new().await;
773 Ok(test_identity(1000))
774 })
775 }));
776 let cache = LazyCache::new(
777 Some(Duration::from_secs(5)),
778 DEFAULT_BUFFER_TIME,
779 BUFFER_TIME_NO_JITTER,
780 DEFAULT_EXPIRATION,
781 DEFAULT_MAX_PARTITIONS,
782 );
783
784 let err: BoxError = cache
785 .resolve_cached_identity(resolver, &components, &config_bag)
786 .await
787 .expect_err("it should return an error");
788 let downcasted = err.downcast_ref::<TimedOutError>();
789 assert!(
790 downcasted.is_some(),
791 "expected a BoxError of TimedOutError, but was {err:?}"
792 );
793 assert_eq!(time.now(), epoch_secs(105));
794 }
795
796 #[tokio::test]
797 async fn buffer_time_jitter() {
798 let time = ManualTimeSource::new(epoch_secs(100));
799 let components = RuntimeComponentsBuilder::for_tests()
800 .with_time_source(Some(time.clone()))
801 .with_sleep_impl(Some(TokioSleep::new()))
802 .build()
803 .unwrap();
804 let buffer_time_jitter_fraction = || 0.5_f64;
805 let (cache, resolver) = test_cache(
806 buffer_time_jitter_fraction,
807 vec![Ok(test_identity(1000)), Ok(test_identity(2000))],
808 );
809
810 expect_identity(1000, &cache, &components, resolver.clone()).await;
811 let buffer_time_with_jitter =
812 (DEFAULT_BUFFER_TIME.as_secs_f64() * buffer_time_jitter_fraction()) as u64;
813 assert_eq!(buffer_time_with_jitter, 5);
814 let almost_expired_secs = 1000 - buffer_time_with_jitter - 1;
816 time.set_time(epoch_secs(almost_expired_secs));
817 expect_identity(1000, &cache, &components, resolver.clone()).await;
819 let expired_secs = almost_expired_secs + 1;
821 time.set_time(epoch_secs(expired_secs));
822 expect_identity(2000, &cache, &components, resolver.clone()).await;
824 }
825
826 #[tokio::test]
827 async fn cache_partitioning() {
828 let time = ManualTimeSource::new(epoch_secs(0));
829 let components = RuntimeComponentsBuilder::for_tests()
830 .with_time_source(Some(time.clone()))
831 .with_sleep_impl(Some(TokioSleep::new()))
832 .build()
833 .unwrap();
834 let (cache, _) = test_cache(BUFFER_TIME_NO_JITTER, Vec::new());
835
836 #[allow(clippy::disallowed_methods)]
837 let far_future = SystemTime::now() + Duration::from_secs(10_000);
838
839 let resolver_a_calls = Arc::new(AtomicUsize::new(0));
842 let resolver_b_calls = Arc::new(AtomicUsize::new(0));
843 let resolver_a = resolver_fn({
844 let calls = resolver_a_calls.clone();
845 move || {
846 calls.fetch_add(1, Ordering::Relaxed);
847 IdentityFuture::ready(Ok(Identity::new(
848 Token::new("A", Some(far_future)),
849 Some(far_future),
850 )))
851 }
852 });
853 let resolver_b = resolver_fn({
854 let calls = resolver_b_calls.clone();
855 move || {
856 calls.fetch_add(1, Ordering::Relaxed);
857 IdentityFuture::ready(Ok(Identity::new(
858 Token::new("B", Some(far_future)),
859 Some(far_future),
860 )))
861 }
862 });
863 assert_ne!(
864 resolver_a.cache_partition(),
865 resolver_b.cache_partition(),
866 "pre-condition: they should have different partition keys"
867 );
868
869 let config_bag = ConfigBag::base();
870
871 let identity = cache
874 .resolve_cached_identity(resolver_a.clone(), &components, &config_bag)
875 .await
876 .unwrap();
877 assert_eq!("A", identity.data::<Token>().unwrap().token());
878 let identity = cache
879 .resolve_cached_identity(resolver_a.clone(), &components, &config_bag)
880 .await
881 .unwrap();
882 assert_eq!("A", identity.data::<Token>().unwrap().token());
883 assert_eq!(1, resolver_a_calls.load(Ordering::Relaxed));
884
885 let identity = cache
888 .resolve_cached_identity(resolver_b.clone(), &components, &config_bag)
889 .await
890 .unwrap();
891 assert_eq!("B", identity.data::<Token>().unwrap().token());
892 let identity = cache
893 .resolve_cached_identity(resolver_b.clone(), &components, &config_bag)
894 .await
895 .unwrap();
896 assert_eq!("B", identity.data::<Token>().unwrap().token());
897 assert_eq!(1, resolver_a_calls.load(Ordering::Relaxed));
898 assert_eq!(1, resolver_b_calls.load(Ordering::Relaxed));
899
900 let identity = cache
902 .resolve_cached_identity(resolver_a.clone(), &components, &config_bag)
903 .await
904 .unwrap();
905 assert_eq!("A", identity.data::<Token>().unwrap().token());
906 assert_eq!(1, resolver_a_calls.load(Ordering::Relaxed));
907 assert_eq!(1, resolver_b_calls.load(Ordering::Relaxed));
908 }
909
910 #[tokio::test]
911 async fn eviction_when_at_capacity() {
912 let time = ManualTimeSource::new(epoch_secs(0));
913 let components = RuntimeComponentsBuilder::for_tests()
914 .with_time_source(Some(time.clone()))
915 .with_sleep_impl(Some(TokioSleep::new()))
916 .build()
917 .unwrap();
918 let cache = LazyCache::new(
920 Some(LOAD_TIMEOUT_FOR_TESTS),
921 DEFAULT_BUFFER_TIME,
922 BUFFER_TIME_NO_JITTER,
923 DEFAULT_EXPIRATION,
924 2,
925 );
926
927 #[allow(clippy::disallowed_methods)]
928 let far_future = SystemTime::now() + Duration::from_secs(10_000);
929
930 let resolver_a_calls = Arc::new(AtomicUsize::new(0));
931 let resolver_b_calls = Arc::new(AtomicUsize::new(0));
932 let resolver_c_calls = Arc::new(AtomicUsize::new(0));
933
934 let resolver_a = resolver_fn({
935 let calls = resolver_a_calls.clone();
936 move || {
937 calls.fetch_add(1, Ordering::Relaxed);
938 IdentityFuture::ready(Ok(Identity::new(
939 Token::new("A", Some(far_future)),
940 Some(far_future),
941 )))
942 }
943 });
944 let resolver_b = resolver_fn({
945 let calls = resolver_b_calls.clone();
946 move || {
947 calls.fetch_add(1, Ordering::Relaxed);
948 IdentityFuture::ready(Ok(Identity::new(
949 Token::new("B", Some(far_future)),
950 Some(far_future),
951 )))
952 }
953 });
954 let resolver_c = resolver_fn({
955 let calls = resolver_c_calls.clone();
956 move || {
957 calls.fetch_add(1, Ordering::Relaxed);
958 IdentityFuture::ready(Ok(Identity::new(
959 Token::new("C", Some(far_future)),
960 Some(far_future),
961 )))
962 }
963 });
964
965 let config_bag = ConfigBag::base();
966
967 cache
969 .resolve_cached_identity(resolver_a.clone(), &components, &config_bag)
970 .await
971 .unwrap();
972 cache
973 .resolve_cached_identity(resolver_b.clone(), &components, &config_bag)
974 .await
975 .unwrap();
976 assert_eq!(1, resolver_a_calls.load(Ordering::Relaxed));
977 assert_eq!(1, resolver_b_calls.load(Ordering::Relaxed));
978
979 cache
981 .resolve_cached_identity(resolver_c.clone(), &components, &config_bag)
982 .await
983 .unwrap();
984 assert_eq!(1, resolver_c_calls.load(Ordering::Relaxed));
985
986 cache
990 .resolve_cached_identity(resolver_a.clone(), &components, &config_bag)
991 .await
992 .unwrap();
993 cache
994 .resolve_cached_identity(resolver_b.clone(), &components, &config_bag)
995 .await
996 .unwrap();
997 let total_calls = resolver_a_calls.load(Ordering::Relaxed)
998 + resolver_b_calls.load(Ordering::Relaxed)
999 + resolver_c_calls.load(Ordering::Relaxed);
1000 assert!(
1003 (4..=5).contains(&total_calls),
1004 "expected 4 or 5 total calls (3 initial + 1 or 2 re-resolutions), got {total_calls}"
1005 );
1006 }
1007
1008 #[tokio::test]
1009 async fn single_partition_cache() {
1010 let time = ManualTimeSource::new(epoch_secs(0));
1011 let components = RuntimeComponentsBuilder::for_tests()
1012 .with_time_source(Some(time.clone()))
1013 .with_sleep_impl(Some(TokioSleep::new()))
1014 .build()
1015 .unwrap();
1016 let cache = LazyCache::new(
1018 Some(LOAD_TIMEOUT_FOR_TESTS),
1019 DEFAULT_BUFFER_TIME,
1020 BUFFER_TIME_NO_JITTER,
1021 DEFAULT_EXPIRATION,
1022 1,
1023 );
1024
1025 #[allow(clippy::disallowed_methods)]
1026 let far_future = SystemTime::now() + Duration::from_secs(10_000);
1027
1028 let resolver_a_calls = Arc::new(AtomicUsize::new(0));
1029 let resolver_b_calls = Arc::new(AtomicUsize::new(0));
1030
1031 let resolver_a = resolver_fn({
1032 let calls = resolver_a_calls.clone();
1033 move || {
1034 calls.fetch_add(1, Ordering::Relaxed);
1035 IdentityFuture::ready(Ok(Identity::new(
1036 Token::new("A", Some(far_future)),
1037 Some(far_future),
1038 )))
1039 }
1040 });
1041 let resolver_b = resolver_fn({
1042 let calls = resolver_b_calls.clone();
1043 move || {
1044 calls.fetch_add(1, Ordering::Relaxed);
1045 IdentityFuture::ready(Ok(Identity::new(
1046 Token::new("B", Some(far_future)),
1047 Some(far_future),
1048 )))
1049 }
1050 });
1051
1052 let config_bag = ConfigBag::base();
1053
1054 let identity = cache
1056 .resolve_cached_identity(resolver_a.clone(), &components, &config_bag)
1057 .await
1058 .unwrap();
1059 assert_eq!("A", identity.data::<Token>().unwrap().token());
1060 assert_eq!(1, resolver_a_calls.load(Ordering::Relaxed));
1061
1062 let identity = cache
1064 .resolve_cached_identity(resolver_a.clone(), &components, &config_bag)
1065 .await
1066 .unwrap();
1067 assert_eq!("A", identity.data::<Token>().unwrap().token());
1068 assert_eq!(1, resolver_a_calls.load(Ordering::Relaxed));
1069
1070 let identity = cache
1072 .resolve_cached_identity(resolver_b.clone(), &components, &config_bag)
1073 .await
1074 .unwrap();
1075 assert_eq!("B", identity.data::<Token>().unwrap().token());
1076 assert_eq!(1, resolver_b_calls.load(Ordering::Relaxed));
1077
1078 let identity = cache
1080 .resolve_cached_identity(resolver_a.clone(), &components, &config_bag)
1081 .await
1082 .unwrap();
1083 assert_eq!("A", identity.data::<Token>().unwrap().token());
1084 assert_eq!(2, resolver_a_calls.load(Ordering::Relaxed));
1085 }
1086
1087 #[test]
1088 #[should_panic(expected = "max_partitions must be greater than 0")]
1089 fn max_partitions_zero_panics() {
1090 LazyCacheBuilder::new().max_partitions(0);
1091 }
1092
1093 #[test]
1094 fn pessimistic_load_timeout_default_config() {
1095 let mut config_bag = ConfigBag::base();
1097 config_bag
1098 .interceptor_state()
1099 .store_put(RetryConfig::standard());
1100 config_bag.interceptor_state().store_put(
1101 TimeoutConfig::builder()
1102 .connect_timeout(crate::client::defaults::DEFAULT_CONNECT_TIMEOUT)
1103 .build(),
1104 );
1105 let timeout = pessimistic_load_timeout(&config_bag);
1106 assert!(
1108 timeout > Duration::from_secs(21) && timeout < Duration::from_secs(22),
1109 "expected ~21.6s, got {:?}",
1110 timeout
1111 );
1112 }
1113
1114 #[test]
1115 fn pessimistic_load_timeout_five_attempts() {
1116 let mut config_bag = ConfigBag::base();
1117 config_bag
1118 .interceptor_state()
1119 .store_put(RetryConfig::standard().with_max_attempts(5));
1120 config_bag.interceptor_state().store_put(
1121 TimeoutConfig::builder()
1122 .connect_timeout(crate::client::defaults::DEFAULT_CONNECT_TIMEOUT)
1123 .build(),
1124 );
1125 let timeout = pessimistic_load_timeout(&config_bag);
1126 assert!(
1128 timeout > Duration::from_secs(45) && timeout < Duration::from_secs(47),
1129 "expected ~46s, got {:?}",
1130 timeout
1131 );
1132 }
1133
1134 #[test]
1135 fn pessimistic_load_timeout_zero_attempts_has_floor() {
1136 let mut config_bag = ConfigBag::base();
1137 config_bag
1138 .interceptor_state()
1139 .store_put(RetryConfig::standard().with_max_attempts(0));
1140 config_bag.interceptor_state().store_put(
1141 TimeoutConfig::builder()
1142 .connect_timeout(crate::client::defaults::DEFAULT_CONNECT_TIMEOUT)
1143 .build(),
1144 );
1145 let timeout = pessimistic_load_timeout(&config_bag);
1146 assert!(
1148 timeout >= Duration::from_secs(6) && timeout < Duration::from_secs(7),
1149 "expected ~6.2s floor, got {:?}",
1150 timeout
1151 );
1152 }
1153
1154 #[test]
1155 fn pessimistic_load_timeout_no_config_in_bag_uses_defaults() {
1156 let config_bag = ConfigBag::base();
1158 let timeout = pessimistic_load_timeout(&config_bag);
1159 assert!(
1162 timeout > Duration::from_secs(21) && timeout < Duration::from_secs(22),
1163 "expected ~21.6s, got {:?}",
1164 timeout
1165 );
1166 }
1167
1168 #[test]
1169 fn pessimistic_load_timeout_low_connect_is_floored_at_default() {
1170 let mut config_bag = ConfigBag::base();
1173 config_bag
1174 .interceptor_state()
1175 .store_put(RetryConfig::standard());
1176 config_bag.interceptor_state().store_put(
1177 TimeoutConfig::builder()
1178 .connect_timeout(Duration::from_secs(1))
1179 .build(),
1180 );
1181 let timeout = pessimistic_load_timeout(&config_bag);
1182 assert!(
1184 timeout > Duration::from_secs(21) && timeout < Duration::from_secs(22),
1185 "expected ~21.6s (floored), got {:?}",
1186 timeout
1187 );
1188 }
1189
1190 #[test]
1191 fn pessimistic_load_timeout_uses_operation_attempt_timeout_when_larger() {
1192 let mut config_bag = ConfigBag::base();
1196 config_bag
1197 .interceptor_state()
1198 .store_put(RetryConfig::standard());
1199 config_bag.interceptor_state().store_put(
1200 TimeoutConfig::builder()
1201 .connect_timeout(crate::client::defaults::DEFAULT_CONNECT_TIMEOUT)
1202 .operation_attempt_timeout(Duration::from_secs(30))
1203 .build(),
1204 );
1205 let timeout = pessimistic_load_timeout(&config_bag);
1206 assert!(
1208 timeout > Duration::from_secs(92) && timeout < Duration::from_secs(94),
1209 "expected ~93s, got {:?}",
1210 timeout
1211 );
1212 }
1213
1214 #[test]
1215 fn pessimistic_load_timeout_ignores_operation_attempt_timeout_when_smaller() {
1216 let mut config_bag = ConfigBag::base();
1219 config_bag
1220 .interceptor_state()
1221 .store_put(RetryConfig::standard());
1222 config_bag.interceptor_state().store_put(
1223 TimeoutConfig::builder()
1224 .connect_timeout(crate::client::defaults::DEFAULT_CONNECT_TIMEOUT)
1225 .operation_attempt_timeout(Duration::from_secs(1))
1226 .build(),
1227 );
1228 let timeout = pessimistic_load_timeout(&config_bag);
1229 assert!(
1231 timeout > Duration::from_secs(21) && timeout < Duration::from_secs(22),
1232 "expected ~21.6s, got {:?}",
1233 timeout
1234 );
1235 }
1236}