1use std::collections::HashMap;
13use std::fmt;
14use std::sync::Arc;
15use std::time::Duration;
16
17#[cfg(test)]
18use mock_instant::thread_local::{SystemTime, UNIX_EPOCH};
19
20#[cfg(not(test))]
21use std::time::{SystemTime, UNIX_EPOCH};
22
23use async_trait::async_trait;
24use lance_namespace::LanceNamespace;
25use lance_namespace::models::DescribeTableRequest;
26use tokio::sync::RwLock;
27
28use crate::{Error, Result};
29
30pub const EXPIRES_AT_MILLIS_KEY: &str = "expires_at_millis";
32
33pub const REFRESH_OFFSET_MILLIS_KEY: &str = "refresh_offset_millis";
35
36const DEFAULT_REFRESH_OFFSET_MILLIS: u64 = 60_000;
38
39#[async_trait]
72pub trait StorageOptionsProvider: Send + Sync + fmt::Debug {
73 async fn fetch_storage_options(&self) -> Result<Option<HashMap<String, String>>>;
81
82 async fn force_fetch_storage_options(&self) -> Result<Option<HashMap<String, String>>> {
88 self.fetch_storage_options().await
89 }
90
91 fn provider_id(&self) -> String;
102}
103
104pub struct LanceNamespaceStorageOptionsProvider {
106 namespace_client: Arc<dyn LanceNamespace>,
107 table_id: Vec<String>,
108}
109
110impl fmt::Debug for LanceNamespaceStorageOptionsProvider {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 write!(f, "{}", self.provider_id())
113 }
114}
115
116impl fmt::Display for LanceNamespaceStorageOptionsProvider {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 write!(f, "{}", self.provider_id())
119 }
120}
121
122impl LanceNamespaceStorageOptionsProvider {
123 pub fn new(namespace_client: Arc<dyn LanceNamespace>, table_id: Vec<String>) -> Self {
129 Self {
130 namespace_client,
131 table_id,
132 }
133 }
134}
135
136#[async_trait]
137impl StorageOptionsProvider for LanceNamespaceStorageOptionsProvider {
138 async fn fetch_storage_options(&self) -> Result<Option<HashMap<String, String>>> {
139 let request = DescribeTableRequest {
140 id: Some(self.table_id.clone()),
141 vend_credentials: Some(true),
143 ..Default::default()
144 };
145
146 let response = self
147 .namespace_client
148 .describe_table(request)
149 .await
150 .map_err(|e| {
151 Error::io_source(Box::new(std::io::Error::other(format!(
152 "Failed to fetch storage options: {}",
153 e
154 ))))
155 })?;
156
157 Ok(response.storage_options)
158 }
159
160 fn provider_id(&self) -> String {
161 format!(
162 "LanceNamespaceStorageOptionsProvider {{ namespace_client: {}, table_id: {:?} }}",
163 self.namespace_client.namespace_id(),
164 self.table_id
165 )
166 }
167}
168
169pub const BASE_SCOPED_OPTION_PREFIX: &str = "base_";
176
177pub fn parse_base_scoped_key(key: &str) -> Option<(u32, &str)> {
184 let rest = key.strip_prefix(BASE_SCOPED_OPTION_PREFIX)?;
185 let (id_str, scoped_key) = rest.split_once('.')?;
186 if scoped_key.is_empty() || id_str.is_empty() || !id_str.bytes().all(|b| b.is_ascii_digit()) {
187 return None;
188 }
189 let id = id_str.parse::<u32>().ok()?;
190 Some((id, scoped_key))
191}
192
193pub fn has_base_scoped_options(options: &HashMap<String, String>) -> bool {
195 options
196 .keys()
197 .any(|key| parse_base_scoped_key(key).is_some())
198}
199
200pub fn resolve_base_scoped_options(
207 options: &HashMap<String, String>,
208 base_id: Option<u32>,
209) -> HashMap<String, String> {
210 let mut resolved = HashMap::with_capacity(options.len());
211 let mut overrides = Vec::new();
212 for (key, value) in options {
213 match parse_base_scoped_key(key) {
214 Some((id, scoped_key)) => {
215 if Some(id) == base_id {
216 overrides.push((scoped_key.to_string(), value.clone()));
217 }
218 }
219 None => {
220 resolved.insert(key.clone(), value.clone());
221 }
222 }
223 }
224 resolved.extend(overrides);
225 resolved
226}
227
228#[derive(Debug)]
236pub struct BaseScopedStorageOptionsProvider {
237 inner: Arc<StorageOptionsAccessor>,
238 base_id: Option<u32>,
239}
240
241impl BaseScopedStorageOptionsProvider {
242 pub fn new(inner: Arc<StorageOptionsAccessor>, base_id: Option<u32>) -> Self {
243 Self { inner, base_id }
244 }
245}
246
247#[async_trait]
248impl StorageOptionsProvider for BaseScopedStorageOptionsProvider {
249 async fn fetch_storage_options(&self) -> Result<Option<HashMap<String, String>>> {
250 let options = self.inner.get_storage_options().await?;
251 Ok(Some(resolve_base_scoped_options(&options.0, self.base_id)))
252 }
253
254 async fn force_fetch_storage_options(&self) -> Result<Option<HashMap<String, String>>> {
255 let options = self.inner.refresh_storage_options().await?;
256 Ok(Some(resolve_base_scoped_options(&options.0, self.base_id)))
257 }
258
259 fn provider_id(&self) -> String {
260 match self.base_id {
261 Some(id) => format!("base-scoped[base_id={}]({})", id, self.inner.accessor_id()),
262 None => format!("base-scoped[default]({})", self.inner.accessor_id()),
263 }
264 }
265}
266
267pub struct StorageOptionsAccessor {
285 initial_options: Option<HashMap<String, String>>,
287
288 provider: Option<Arc<dyn StorageOptionsProvider>>,
290
291 cache: Arc<RwLock<Option<CachedStorageOptions>>>,
293
294 refresh_offset: Duration,
296
297 scope_resolved: bool,
301}
302
303impl fmt::Debug for StorageOptionsAccessor {
304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305 f.debug_struct("StorageOptionsAccessor")
306 .field("has_initial_options", &self.initial_options.is_some())
307 .field("has_provider", &self.provider.is_some())
308 .field("refresh_offset", &self.refresh_offset)
309 .finish()
310 }
311}
312
313#[derive(Debug, Clone)]
314struct CachedStorageOptions {
315 options: HashMap<String, String>,
316 expires_at_millis: Option<u64>,
317}
318
319impl StorageOptionsAccessor {
320 fn extract_refresh_offset(options: &HashMap<String, String>) -> Duration {
322 options
323 .get(REFRESH_OFFSET_MILLIS_KEY)
324 .and_then(|s| s.parse::<u64>().ok())
325 .map(Duration::from_millis)
326 .unwrap_or(Duration::from_millis(DEFAULT_REFRESH_OFFSET_MILLIS))
327 }
328
329 fn effective_expires_at_millis(options: &HashMap<String, String>) -> Option<u64> {
337 options
338 .iter()
339 .filter(|(key, _)| {
340 key.as_str() == EXPIRES_AT_MILLIS_KEY
341 || matches!(
342 parse_base_scoped_key(key),
343 Some((_, scoped_key)) if scoped_key == EXPIRES_AT_MILLIS_KEY
344 )
345 })
346 .filter_map(|(_, value)| value.parse::<u64>().ok())
347 .min()
348 }
349
350 pub fn with_static_options(options: HashMap<String, String>) -> Self {
355 let expires_at_millis = Self::effective_expires_at_millis(&options);
356 let refresh_offset = Self::extract_refresh_offset(&options);
357
358 Self {
359 initial_options: Some(options.clone()),
360 provider: None,
361 cache: Arc::new(RwLock::new(Some(CachedStorageOptions {
362 options,
363 expires_at_millis,
364 }))),
365 refresh_offset,
366 scope_resolved: false,
367 }
368 }
369
370 pub fn with_provider(provider: Arc<dyn StorageOptionsProvider>) -> Self {
379 Self {
380 initial_options: None,
381 provider: Some(provider),
382 cache: Arc::new(RwLock::new(None)),
383 refresh_offset: Duration::from_millis(DEFAULT_REFRESH_OFFSET_MILLIS),
384 scope_resolved: false,
385 }
386 }
387
388 pub fn with_initial_and_provider(
398 initial_options: HashMap<String, String>,
399 provider: Arc<dyn StorageOptionsProvider>,
400 ) -> Self {
401 let expires_at_millis = Self::effective_expires_at_millis(&initial_options);
402 let refresh_offset = Self::extract_refresh_offset(&initial_options);
403
404 Self {
405 initial_options: Some(initial_options.clone()),
406 provider: Some(provider),
407 cache: Arc::new(RwLock::new(Some(CachedStorageOptions {
408 options: initial_options,
409 expires_at_millis,
410 }))),
411 refresh_offset,
412 scope_resolved: false,
413 }
414 }
415
416 pub async fn get_storage_options(&self) -> Result<super::StorageOptions> {
428 loop {
429 match self.do_get_storage_options().await? {
430 Some(options) => return Ok(options),
431 None => {
432 tokio::time::sleep(Duration::from_millis(10)).await;
434 continue;
435 }
436 }
437 }
438 }
439
440 pub(crate) async fn refresh_storage_options(&self) -> Result<super::StorageOptions> {
447 let Some(provider) = &self.provider else {
448 return self.get_storage_options().await;
449 };
450
451 log::debug!(
452 "Refreshing storage options from provider: {}",
453 provider.provider_id()
454 );
455
456 let storage_options_map = provider.force_fetch_storage_options().await.map_err(|e| {
457 Error::io_source(Box::new(std::io::Error::other(format!(
458 "Failed to fetch storage options: {}",
459 e
460 ))))
461 })?;
462
463 let Some(options) = storage_options_map else {
464 if let Some(initial) = &self.initial_options {
465 return Ok(super::StorageOptions(initial.clone()));
466 }
467 log::debug!(
468 "Provider {} returned no storage options, using default credentials",
469 provider.provider_id()
470 );
471 return Ok(super::StorageOptions(HashMap::new()));
472 };
473
474 let expires_at_millis = Self::effective_expires_at_millis(&options);
475
476 let mut cache = self.cache.write().await;
477 *cache = Some(CachedStorageOptions {
478 options: options.clone(),
479 expires_at_millis,
480 });
481
482 Ok(super::StorageOptions(options))
483 }
484
485 async fn do_get_storage_options(&self) -> Result<Option<super::StorageOptions>> {
486 {
488 let cached = self.cache.read().await;
489 if !self.needs_refresh(&cached)
490 && let Some(cached_opts) = &*cached
491 {
492 return Ok(Some(super::StorageOptions(cached_opts.options.clone())));
493 }
494 }
495
496 let Some(provider) = &self.provider else {
498 return if let Some(initial) = &self.initial_options {
499 Ok(Some(super::StorageOptions(initial.clone())))
500 } else {
501 Ok(Some(super::StorageOptions(HashMap::new())))
503 };
504 };
505
506 let Ok(mut cache) = self.cache.try_write() else {
508 return Ok(None);
509 };
510
511 if !self.needs_refresh(&cache)
514 && let Some(cached_opts) = &*cache
515 {
516 return Ok(Some(super::StorageOptions(cached_opts.options.clone())));
517 }
518 log::debug!(
519 "Refreshing storage options from provider: {}",
520 provider.provider_id()
521 );
522
523 let storage_options_map = provider.fetch_storage_options().await.map_err(|e| {
524 Error::io_source(Box::new(std::io::Error::other(format!(
525 "Failed to fetch storage options: {}",
526 e
527 ))))
528 })?;
529
530 let Some(options) = storage_options_map else {
531 if let Some(initial) = &self.initial_options {
533 return Ok(Some(super::StorageOptions(initial.clone())));
534 }
535 log::debug!(
539 "Provider {} returned no storage options, using default credentials",
540 provider.provider_id()
541 );
542 return Ok(Some(super::StorageOptions(HashMap::new())));
543 };
544
545 let expires_at_millis = Self::effective_expires_at_millis(&options);
546
547 if let Some(expires_at) = expires_at_millis {
548 let now_ms = SystemTime::now()
549 .duration_since(UNIX_EPOCH)
550 .unwrap_or(Duration::from_secs(0))
551 .as_millis() as u64;
552 let expires_in_secs = (expires_at.saturating_sub(now_ms)) / 1000;
553 log::debug!(
554 "Successfully refreshed storage options from provider: {}, options expire in {} seconds",
555 provider.provider_id(),
556 expires_in_secs
557 );
558 } else {
559 log::debug!(
560 "Successfully refreshed storage options from provider: {} (no expiration)",
561 provider.provider_id()
562 );
563 }
564
565 *cache = Some(CachedStorageOptions {
566 options: options.clone(),
567 expires_at_millis,
568 });
569
570 Ok(Some(super::StorageOptions(options)))
571 }
572
573 fn needs_refresh(&self, cached: &Option<CachedStorageOptions>) -> bool {
574 match cached {
575 None => true,
576 Some(cached_opts) => {
577 if let Some(expires_at_millis) = cached_opts.expires_at_millis {
578 let now_ms = SystemTime::now()
579 .duration_since(UNIX_EPOCH)
580 .unwrap_or(Duration::from_secs(0))
581 .as_millis() as u64;
582
583 let refresh_offset_millis = self.refresh_offset.as_millis() as u64;
585 now_ms + refresh_offset_millis >= expires_at_millis
586 } else {
587 false
589 }
590 }
591 }
592 }
593
594 pub fn initial_storage_options(&self) -> Option<&HashMap<String, String>> {
599 self.initial_options.as_ref()
600 }
601
602 pub fn accessor_id(&self) -> String {
607 if let Some(provider) = &self.provider {
608 provider.provider_id()
609 } else if let Some(initial) = &self.initial_options {
610 use std::collections::hash_map::DefaultHasher;
612 use std::hash::{Hash, Hasher};
613
614 let mut hasher = DefaultHasher::new();
615 let mut keys: Vec<_> = initial.keys().collect();
616 keys.sort();
617 for key in keys {
618 key.hash(&mut hasher);
619 initial.get(key).hash(&mut hasher);
620 }
621 format!("static_options_{:x}", hasher.finish())
622 } else {
623 "empty_accessor".to_string()
624 }
625 }
626
627 pub fn scoped_to_base(self: &Arc<Self>, base_id: Option<u32>) -> Arc<Self> {
643 if self.scope_resolved {
644 return self.clone();
645 }
646 if self.has_provider() {
647 let provider = Arc::new(BaseScopedStorageOptionsProvider::new(self.clone(), base_id));
648 let mut scoped = match self.initial_storage_options() {
649 Some(initial) => Self::with_initial_and_provider(
650 resolve_base_scoped_options(initial, base_id),
651 provider,
652 ),
653 None => Self::with_provider(provider),
654 };
655 scoped.scope_resolved = true;
656 Arc::new(scoped)
657 } else {
658 match self.initial_storage_options() {
659 Some(initial) if has_base_scoped_options(initial) => {
660 let mut scoped =
661 Self::with_static_options(resolve_base_scoped_options(initial, base_id));
662 scoped.scope_resolved = true;
663 Arc::new(scoped)
664 }
665 _ => self.clone(),
667 }
668 }
669 }
670
671 pub fn has_provider(&self) -> bool {
673 self.provider.is_some()
674 }
675
676 pub fn refresh_offset(&self) -> Duration {
678 self.refresh_offset
679 }
680
681 pub fn provider(&self) -> Option<&Arc<dyn StorageOptionsProvider>> {
683 self.provider.as_ref()
684 }
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690 use mock_instant::thread_local::MockClock;
691
692 #[derive(Debug)]
693 struct MockStorageOptionsProvider {
694 call_count: Arc<RwLock<usize>>,
695 expires_in_millis: Option<u64>,
696 }
697
698 impl MockStorageOptionsProvider {
699 fn new(expires_in_millis: Option<u64>) -> Self {
700 Self {
701 call_count: Arc::new(RwLock::new(0)),
702 expires_in_millis,
703 }
704 }
705
706 async fn get_call_count(&self) -> usize {
707 *self.call_count.read().await
708 }
709 }
710
711 #[async_trait]
712 impl StorageOptionsProvider for MockStorageOptionsProvider {
713 async fn fetch_storage_options(&self) -> Result<Option<HashMap<String, String>>> {
714 let count = {
715 let mut c = self.call_count.write().await;
716 *c += 1;
717 *c
718 };
719
720 let mut options = HashMap::from([
721 ("aws_access_key_id".to_string(), format!("AKID_{}", count)),
722 (
723 "aws_secret_access_key".to_string(),
724 format!("SECRET_{}", count),
725 ),
726 ("aws_session_token".to_string(), format!("TOKEN_{}", count)),
727 ]);
728
729 if let Some(expires_in) = self.expires_in_millis {
730 let now_ms = SystemTime::now()
731 .duration_since(UNIX_EPOCH)
732 .unwrap()
733 .as_millis() as u64;
734 let expires_at = now_ms + expires_in;
735 options.insert(EXPIRES_AT_MILLIS_KEY.to_string(), expires_at.to_string());
736 }
737
738 Ok(Some(options))
739 }
740
741 fn provider_id(&self) -> String {
742 let ptr = Arc::as_ptr(&self.call_count) as usize;
743 format!("MockStorageOptionsProvider {{ id: {} }}", ptr)
744 }
745 }
746
747 #[tokio::test]
748 async fn test_static_options_only() {
749 let options = HashMap::from([
750 ("key1".to_string(), "value1".to_string()),
751 ("key2".to_string(), "value2".to_string()),
752 ]);
753 let accessor = StorageOptionsAccessor::with_static_options(options.clone());
754
755 let result = accessor.get_storage_options().await.unwrap();
756 assert_eq!(result.0, options);
757 assert!(!accessor.has_provider());
758 assert_eq!(accessor.initial_storage_options(), Some(&options));
759 }
760
761 #[tokio::test]
762 async fn test_provider_only() {
763 MockClock::set_system_time(Duration::from_secs(100_000));
764
765 let mock_provider = Arc::new(MockStorageOptionsProvider::new(Some(600_000)));
766 let accessor = StorageOptionsAccessor::with_provider(mock_provider.clone());
767
768 let result = accessor.get_storage_options().await.unwrap();
769 assert!(result.0.contains_key("aws_access_key_id"));
770 assert_eq!(result.0.get("aws_access_key_id").unwrap(), "AKID_1");
771 assert!(accessor.has_provider());
772 assert_eq!(accessor.initial_storage_options(), None);
773 assert_eq!(mock_provider.get_call_count().await, 1);
774 }
775
776 #[tokio::test]
777 async fn test_initial_and_provider_uses_initial_first() {
778 MockClock::set_system_time(Duration::from_secs(100_000));
779
780 let now_ms = MockClock::system_time().as_millis() as u64;
781 let expires_at = now_ms + 600_000; let initial = HashMap::from([
784 ("aws_access_key_id".to_string(), "INITIAL_KEY".to_string()),
785 (
786 "aws_secret_access_key".to_string(),
787 "INITIAL_SECRET".to_string(),
788 ),
789 (EXPIRES_AT_MILLIS_KEY.to_string(), expires_at.to_string()),
790 ]);
791 let mock_provider = Arc::new(MockStorageOptionsProvider::new(Some(600_000)));
792
793 let accessor = StorageOptionsAccessor::with_initial_and_provider(
794 initial.clone(),
795 mock_provider.clone(),
796 );
797
798 let result = accessor.get_storage_options().await.unwrap();
800 assert_eq!(result.0.get("aws_access_key_id").unwrap(), "INITIAL_KEY");
801 assert_eq!(mock_provider.get_call_count().await, 0); }
803
804 #[tokio::test]
805 async fn test_caching_and_refresh() {
806 MockClock::set_system_time(Duration::from_secs(100_000));
807
808 let mock_provider = Arc::new(MockStorageOptionsProvider::new(Some(600_000))); let now_ms = MockClock::system_time().as_millis() as u64;
811 let expires_at = now_ms + 600_000; let initial = HashMap::from([
813 (EXPIRES_AT_MILLIS_KEY.to_string(), expires_at.to_string()),
814 (REFRESH_OFFSET_MILLIS_KEY.to_string(), "300000".to_string()), ]);
816 let accessor =
817 StorageOptionsAccessor::with_initial_and_provider(initial, mock_provider.clone());
818
819 let result = accessor.get_storage_options().await.unwrap();
821 assert!(result.0.contains_key(EXPIRES_AT_MILLIS_KEY));
822 assert_eq!(mock_provider.get_call_count().await, 0);
823
824 MockClock::set_system_time(Duration::from_secs(100_000 + 360));
826 let result = accessor.get_storage_options().await.unwrap();
827 assert_eq!(result.0.get("aws_access_key_id").unwrap(), "AKID_1");
828 assert_eq!(mock_provider.get_call_count().await, 1);
829 }
830
831 #[tokio::test]
832 async fn test_expired_initial_triggers_refresh() {
833 MockClock::set_system_time(Duration::from_secs(100_000));
834
835 let now_ms = MockClock::system_time().as_millis() as u64;
836 let expired_time = now_ms - 1_000; let initial = HashMap::from([
839 ("aws_access_key_id".to_string(), "EXPIRED_KEY".to_string()),
840 (EXPIRES_AT_MILLIS_KEY.to_string(), expired_time.to_string()),
841 ]);
842 let mock_provider = Arc::new(MockStorageOptionsProvider::new(Some(600_000)));
843
844 let accessor =
845 StorageOptionsAccessor::with_initial_and_provider(initial, mock_provider.clone());
846
847 let result = accessor.get_storage_options().await.unwrap();
849 assert_eq!(result.0.get("aws_access_key_id").unwrap(), "AKID_1");
850 assert_eq!(mock_provider.get_call_count().await, 1);
851 }
852
853 #[tokio::test]
854 async fn test_accessor_id_with_provider() {
855 let mock_provider = Arc::new(MockStorageOptionsProvider::new(None));
856 let accessor = StorageOptionsAccessor::with_provider(mock_provider);
857
858 let id = accessor.accessor_id();
859 assert!(id.starts_with("MockStorageOptionsProvider"));
860 }
861
862 #[tokio::test]
863 async fn test_accessor_id_static() {
864 let options = HashMap::from([("key".to_string(), "value".to_string())]);
865 let accessor = StorageOptionsAccessor::with_static_options(options);
866
867 let id = accessor.accessor_id();
868 assert!(id.starts_with("static_options_"));
869 }
870
871 #[tokio::test]
872 async fn test_concurrent_access() {
873 let mock_provider = Arc::new(MockStorageOptionsProvider::new(Some(9999999999999)));
875
876 let accessor = Arc::new(StorageOptionsAccessor::with_provider(mock_provider.clone()));
877
878 let mut handles = vec![];
880 for i in 0..10 {
881 let acc = accessor.clone();
882 let handle = tokio::spawn(async move {
883 let result = acc.get_storage_options().await.unwrap();
884 assert_eq!(result.0.get("aws_access_key_id").unwrap(), "AKID_1");
885 i
886 });
887 handles.push(handle);
888 }
889
890 let results: Vec<_> = futures::future::join_all(handles)
892 .await
893 .into_iter()
894 .map(|r| r.unwrap())
895 .collect();
896
897 assert_eq!(results.len(), 10);
899
900 let call_count = mock_provider.get_call_count().await;
902 assert_eq!(
903 call_count, 1,
904 "Provider should be called exactly once despite concurrent access"
905 );
906 }
907
908 #[tokio::test]
909 async fn test_no_expiration_never_refreshes() {
910 MockClock::set_system_time(Duration::from_secs(100_000));
911
912 let mock_provider = Arc::new(MockStorageOptionsProvider::new(None)); let accessor = StorageOptionsAccessor::with_provider(mock_provider.clone());
914
915 accessor.get_storage_options().await.unwrap();
917 assert_eq!(mock_provider.get_call_count().await, 1);
918
919 MockClock::set_system_time(Duration::from_secs(200_000));
921
922 accessor.get_storage_options().await.unwrap();
924 assert_eq!(mock_provider.get_call_count().await, 1);
925 }
926
927 #[test]
928 fn test_parse_base_scoped_key() {
929 assert_eq!(
930 parse_base_scoped_key("base_1.account_key"),
931 Some((1, "account_key"))
932 );
933 assert_eq!(
934 parse_base_scoped_key("base_12.headers.x-ms-version"),
935 Some((12, "headers.x-ms-version"))
936 );
937 assert_eq!(parse_base_scoped_key("base_0.region"), Some((0, "region")));
938
939 assert_eq!(parse_base_scoped_key("account_key"), None);
941 assert_eq!(parse_base_scoped_key("base_url"), None);
942 assert_eq!(parse_base_scoped_key("base_hot.account_key"), None);
943 assert_eq!(parse_base_scoped_key("base_1x.account_key"), None);
944 assert_eq!(parse_base_scoped_key("base_+1.account_key"), None);
945 assert_eq!(parse_base_scoped_key("base_.account_key"), None);
946 assert_eq!(parse_base_scoped_key("base_1."), None);
947 assert_eq!(parse_base_scoped_key("base_1"), None);
948 assert_eq!(parse_base_scoped_key("base_4294967296.key"), None);
950 }
951
952 #[test]
953 fn test_resolve_base_scoped_options() {
954 let options = HashMap::from([
955 ("region".to_string(), "us-east-1".to_string()),
956 ("account_key".to_string(), "shared-key".to_string()),
957 ("base_1.account_key".to_string(), "base1-key".to_string()),
958 ("base_2.account_key".to_string(), "base2-key".to_string()),
959 ("base_2.endpoint".to_string(), "http://b2".to_string()),
960 ]);
961 assert!(has_base_scoped_options(&options));
962
963 let base1 = resolve_base_scoped_options(&options, Some(1));
964 assert_eq!(
965 base1,
966 HashMap::from([
967 ("region".to_string(), "us-east-1".to_string()),
968 ("account_key".to_string(), "base1-key".to_string()),
969 ])
970 );
971
972 let base2 = resolve_base_scoped_options(&options, Some(2));
973 assert_eq!(
974 base2,
975 HashMap::from([
976 ("region".to_string(), "us-east-1".to_string()),
977 ("account_key".to_string(), "base2-key".to_string()),
978 ("endpoint".to_string(), "http://b2".to_string()),
979 ])
980 );
981
982 let base3 = resolve_base_scoped_options(&options, Some(3));
984 assert_eq!(
985 base3,
986 HashMap::from([
987 ("region".to_string(), "us-east-1".to_string()),
988 ("account_key".to_string(), "shared-key".to_string()),
989 ])
990 );
991
992 let default = resolve_base_scoped_options(&options, None);
994 assert_eq!(default, base3);
995
996 assert!(!has_base_scoped_options(&HashMap::from([(
997 "account_key".to_string(),
998 "shared-key".to_string()
999 )])));
1000 }
1001
1002 #[tokio::test]
1003 async fn test_scoped_to_base_identity_and_idempotency() {
1004 let accessor = Arc::new(StorageOptionsAccessor::with_static_options(HashMap::from(
1006 [("account_key".to_string(), "shared-key".to_string())],
1007 )));
1008 assert!(Arc::ptr_eq(&accessor.scoped_to_base(Some(1)), &accessor));
1009 assert!(Arc::ptr_eq(&accessor.scoped_to_base(None), &accessor));
1010
1011 let scoped = Arc::new(StorageOptionsAccessor::with_static_options(HashMap::from(
1014 [
1015 ("account_key".to_string(), "shared-key".to_string()),
1016 ("base_1.account_key".to_string(), "base1-key".to_string()),
1017 ],
1018 )))
1019 .scoped_to_base(Some(1));
1020 assert!(Arc::ptr_eq(&scoped.scoped_to_base(None), &scoped));
1021
1022 let provider_scoped = Arc::new(StorageOptionsAccessor::with_provider(Arc::new(
1023 MockStorageOptionsProvider::new(None),
1024 )))
1025 .scoped_to_base(Some(1));
1026 assert!(Arc::ptr_eq(
1027 &provider_scoped.scoped_to_base(None),
1028 &provider_scoped
1029 ));
1030 }
1031
1032 #[tokio::test]
1033 async fn test_scoped_to_base_provider_only_resolves_vended_options() {
1034 MockClock::set_system_time(Duration::from_secs(100_000));
1035
1036 let provider = Arc::new(MockBaseScopedVendingProvider {
1038 call_count: Arc::new(RwLock::new(0)),
1039 expires_in_millis: 600_000,
1040 });
1041 let parent = Arc::new(StorageOptionsAccessor::with_provider(provider.clone()));
1042
1043 let base1 = parent.scoped_to_base(Some(1));
1044 assert!(!Arc::ptr_eq(&base1, &parent));
1045 let result = base1.get_storage_options().await.unwrap();
1046 assert_eq!(result.0.get("account_key").unwrap(), "BASE1_1");
1047 assert!(!result.0.contains_key("base_1.account_key"));
1048
1049 let default = parent.scoped_to_base(None);
1050 let result = default.get_storage_options().await.unwrap();
1051 assert_eq!(result.0.get("account_key").unwrap(), "SHARED_1");
1052 assert!(!result.0.contains_key("base_1.account_key"));
1053
1054 assert_eq!(*provider.call_count.read().await, 1);
1056 }
1057
1058 #[tokio::test]
1059 async fn test_scoped_earlier_base_expiry_refreshes_parent() {
1060 MockClock::set_system_time(Duration::from_secs(100_000));
1061 let now_ms = MockClock::system_time().as_millis() as u64;
1062
1063 let provider = Arc::new(MockBaseScopedVendingProvider {
1068 call_count: Arc::new(RwLock::new(0)),
1069 expires_in_millis: 600_000,
1070 });
1071 let initial = HashMap::from([
1072 ("account_key".to_string(), "SHARED_0".to_string()),
1073 ("base_1.account_key".to_string(), "BASE1_0".to_string()),
1074 (
1075 EXPIRES_AT_MILLIS_KEY.to_string(),
1076 (now_ms + 600_000).to_string(),
1077 ),
1078 (
1079 format!("base_1.{}", EXPIRES_AT_MILLIS_KEY),
1080 (now_ms + 120_000).to_string(),
1081 ),
1082 ]);
1083 let parent = Arc::new(StorageOptionsAccessor::with_initial_and_provider(
1084 initial,
1085 provider.clone(),
1086 ));
1087
1088 let base1 = parent.scoped_to_base(Some(1));
1089 let result = base1.get_storage_options().await.unwrap();
1090 assert_eq!(result.0.get("account_key").unwrap(), "BASE1_0");
1091 assert_eq!(*provider.call_count.read().await, 0);
1092
1093 MockClock::set_system_time(Duration::from_secs(100_000 + 121));
1097 let result = base1.get_storage_options().await.unwrap();
1098 assert_eq!(result.0.get("account_key").unwrap(), "BASE1_1");
1099 assert_eq!(*provider.call_count.read().await, 1);
1100 }
1101
1102 #[tokio::test]
1103 async fn test_scoped_to_base_static() {
1104 let accessor = Arc::new(StorageOptionsAccessor::with_static_options(HashMap::from(
1105 [
1106 ("account_key".to_string(), "shared-key".to_string()),
1107 ("base_1.account_key".to_string(), "base1-key".to_string()),
1108 ],
1109 )));
1110
1111 let base1 = accessor.scoped_to_base(Some(1));
1112 let result = base1.get_storage_options().await.unwrap();
1113 assert_eq!(
1114 result.0,
1115 HashMap::from([("account_key".to_string(), "base1-key".to_string())])
1116 );
1117 assert!(!base1.has_provider());
1118
1119 let default = accessor.scoped_to_base(None);
1120 let result = default.get_storage_options().await.unwrap();
1121 assert_eq!(
1122 result.0,
1123 HashMap::from([("account_key".to_string(), "shared-key".to_string())])
1124 );
1125
1126 assert_eq!(
1128 accessor.scoped_to_base(Some(1)).accessor_id(),
1129 base1.accessor_id()
1130 );
1131 assert_ne!(base1.accessor_id(), default.accessor_id());
1132 assert_ne!(base1.accessor_id(), accessor.accessor_id());
1133 }
1134
1135 #[derive(Debug)]
1136 struct MockBaseScopedVendingProvider {
1137 call_count: Arc<RwLock<usize>>,
1138 expires_in_millis: u64,
1139 }
1140
1141 #[async_trait]
1142 impl StorageOptionsProvider for MockBaseScopedVendingProvider {
1143 async fn fetch_storage_options(&self) -> Result<Option<HashMap<String, String>>> {
1144 let count = {
1145 let mut c = self.call_count.write().await;
1146 *c += 1;
1147 *c
1148 };
1149 let now_ms = SystemTime::now()
1150 .duration_since(UNIX_EPOCH)
1151 .unwrap()
1152 .as_millis() as u64;
1153 Ok(Some(HashMap::from([
1154 ("account_key".to_string(), format!("SHARED_{}", count)),
1155 ("base_1.account_key".to_string(), format!("BASE1_{}", count)),
1156 (
1157 EXPIRES_AT_MILLIS_KEY.to_string(),
1158 (now_ms + self.expires_in_millis).to_string(),
1159 ),
1160 ])))
1161 }
1162
1163 fn provider_id(&self) -> String {
1164 "MockBaseScopedVendingProvider".to_string()
1165 }
1166 }
1167
1168 #[tokio::test]
1169 async fn test_scoped_to_base_refreshes_through_parent() {
1170 MockClock::set_system_time(Duration::from_secs(100_000));
1171 let now_ms = MockClock::system_time().as_millis() as u64;
1172
1173 let provider = Arc::new(MockBaseScopedVendingProvider {
1174 call_count: Arc::new(RwLock::new(0)),
1175 expires_in_millis: 600_000,
1176 });
1177 let initial = HashMap::from([
1178 ("account_key".to_string(), "SHARED_0".to_string()),
1179 ("base_1.account_key".to_string(), "BASE1_0".to_string()),
1180 (
1181 EXPIRES_AT_MILLIS_KEY.to_string(),
1182 (now_ms + 600_000).to_string(),
1183 ),
1184 ]);
1185 let parent = Arc::new(StorageOptionsAccessor::with_initial_and_provider(
1186 initial,
1187 provider.clone(),
1188 ));
1189
1190 let base1 = parent.scoped_to_base(Some(1));
1191 let default = parent.scoped_to_base(None);
1192 assert!(base1.has_provider());
1193
1194 let result = base1.get_storage_options().await.unwrap();
1196 assert_eq!(result.0.get("account_key").unwrap(), "BASE1_0");
1197 assert!(!result.0.contains_key("base_1.account_key"));
1198 let result = default.get_storage_options().await.unwrap();
1199 assert_eq!(result.0.get("account_key").unwrap(), "SHARED_0");
1200 assert_eq!(*provider.call_count.read().await, 0);
1201
1202 MockClock::set_system_time(Duration::from_secs(100_000 + 601));
1205 let result = base1.get_storage_options().await.unwrap();
1206 assert_eq!(result.0.get("account_key").unwrap(), "BASE1_1");
1207 assert_eq!(*provider.call_count.read().await, 1);
1208
1209 let result = default.get_storage_options().await.unwrap();
1211 assert_eq!(result.0.get("account_key").unwrap(), "SHARED_1");
1212 assert_eq!(*provider.call_count.read().await, 1);
1213 }
1214
1215 #[tokio::test]
1216 async fn test_scoped_forced_refresh_reaches_origin_provider() {
1217 MockClock::set_system_time(Duration::from_secs(100_000));
1218 let now_ms = MockClock::system_time().as_millis() as u64;
1219
1220 let provider = Arc::new(MockBaseScopedVendingProvider {
1221 call_count: Arc::new(RwLock::new(0)),
1222 expires_in_millis: 600_000,
1223 });
1224 let initial = HashMap::from([
1225 ("account_key".to_string(), "SHARED_0".to_string()),
1226 ("base_1.account_key".to_string(), "BASE1_0".to_string()),
1227 (
1228 EXPIRES_AT_MILLIS_KEY.to_string(),
1229 (now_ms + 600_000).to_string(),
1230 ),
1231 ]);
1232 let parent = Arc::new(StorageOptionsAccessor::with_initial_and_provider(
1233 initial,
1234 provider.clone(),
1235 ));
1236 let base1 = parent.scoped_to_base(Some(1));
1237
1238 let result = base1.refresh_storage_options().await.unwrap();
1241 assert_eq!(result.0.get("account_key").unwrap(), "BASE1_1");
1242 assert_eq!(*provider.call_count.read().await, 1);
1243 }
1244}