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