#[cfg(feature = "fault_injection")]
use std::sync::Arc;
use crate::{
clients::ClientContext,
options::{
CosmosClientOptions, OperationOptions, PartitionFailoverOptions,
ThroughputControlGroupOptions, UserAgentSuffix,
},
AccountReference, CosmosClient, CosmosCredential, CosmosRuntime, RoutingStrategy,
};
#[derive(Default)]
pub struct CosmosClientBuilder {
options: CosmosClientOptions,
runtime: Option<CosmosRuntime>,
throughput_control_groups: Vec<ThroughputControlGroupOptions>,
#[cfg(feature = "fault_injection")]
fault_injection_rules: Vec<Arc<azure_data_cosmos_driver::fault_injection::FaultInjectionRule>>,
backup_endpoints: Vec<azure_core::http::Url>,
partition_failover_options: Option<PartitionFailoverOptions>,
}
impl CosmosClientBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_runtime(mut self, runtime: CosmosRuntime) -> Self {
self.runtime = Some(runtime);
self
}
pub fn with_default_operation_options(mut self, options: OperationOptions) -> Self {
self.options.operation = options;
self
}
pub fn with_partition_failover_options(mut self, options: PartitionFailoverOptions) -> Self {
self.partition_failover_options = Some(options);
self
}
pub fn with_user_agent_suffix(mut self, suffix: UserAgentSuffix) -> Self {
self.options.user_agent_suffix = Some(suffix);
self
}
#[cfg(feature = "fault_injection")]
pub fn with_fault_injection_rules(
mut self,
rules: Vec<Arc<azure_data_cosmos_driver::fault_injection::FaultInjectionRule>>,
) -> crate::Result<Self> {
self.fault_injection_rules = rules;
Ok(self)
}
pub fn register_throughput_control_group(
mut self,
group: ThroughputControlGroupOptions,
) -> crate::Result<Self> {
self.throughput_control_groups.push(group);
Ok(self)
}
pub fn with_backup_endpoints(mut self, endpoints: Vec<crate::AccountEndpoint>) -> Self {
self.backup_endpoints = endpoints.into_iter().map(|e| e.into_url()).collect();
self
}
pub async fn build(
self,
account: AccountReference,
routing_strategy: RoutingStrategy,
) -> crate::Result<CosmosClient> {
let (account_endpoint, credential) = account.into_parts();
let endpoint = account_endpoint.into_url();
let driver_credential = credential.clone();
let runtime = match self.runtime {
Some(rt) => rt,
None => CosmosRuntime::global().await?,
};
let driver_account =
build_driver_account(endpoint, driver_credential, self.backup_endpoints);
let driver_options = build_driver_options(
driver_account,
routing_strategy,
self.options.operation,
self.options.user_agent_suffix,
self.partition_failover_options,
#[cfg(feature = "fault_injection")]
self.fault_injection_rules,
self.throughput_control_groups,
)?;
let driver = runtime.into_inner().create_driver(driver_options).await?;
Ok(CosmosClient {
context: ClientContext { driver },
})
}
}
fn build_driver_options(
account: azure_data_cosmos_driver::models::AccountReference,
strategy: RoutingStrategy,
operation_options: OperationOptions,
user_agent_suffix: Option<UserAgentSuffix>,
partition_failover_options: Option<PartitionFailoverOptions>,
#[cfg(feature = "fault_injection")] fault_injection_rules: Vec<
Arc<azure_data_cosmos_driver::fault_injection::FaultInjectionRule>,
>,
throughput_control_groups: Vec<ThroughputControlGroupOptions>,
) -> crate::Result<azure_data_cosmos_driver::options::DriverOptions> {
let preferred_regions = match strategy {
RoutingStrategy::ProximityTo(region) =>
crate::region_proximity::generate_preferred_region_list(®ion)
.map(|s| s.to_vec())
.unwrap_or_else(|| {
tracing::warn!(
region = %region,
"unrecognized application region; falling back to account-defined region order"
);
Vec::new()
}),
RoutingStrategy::PreferredRegions(regions) => regions,
};
let mut builder = azure_data_cosmos_driver::options::DriverOptions::builder(account)
.with_preferred_regions(preferred_regions)
.with_operation_options(operation_options);
if let Some(suffix) = user_agent_suffix {
builder = builder.with_user_agent_suffix(suffix);
}
if let Some(pfo) = partition_failover_options {
builder = builder.with_partition_failover_options(pfo);
}
#[cfg(feature = "fault_injection")]
if !fault_injection_rules.is_empty() {
builder = builder
.with_fault_injection_rules(fault_injection_rules)
.map_err(crate::CosmosError::from)?;
}
for group in throughput_control_groups {
builder = builder
.register_throughput_control_group(group)
.map_err(crate::CosmosError::from)?;
}
Ok(builder.build())
}
fn build_driver_account(
endpoint: azure_core::http::Url,
credential: CosmosCredential,
backup_endpoints: Vec<azure_core::http::Url>,
) -> azure_data_cosmos_driver::models::AccountReference {
let base = match credential {
CosmosCredential::TokenCredential(tc) => {
azure_data_cosmos_driver::models::AccountReference::with_credential(endpoint, tc)
}
#[cfg(feature = "key_auth")]
CosmosCredential::MasterKey(key) => {
azure_data_cosmos_driver::models::AccountReference::with_master_key(endpoint, key)
}
};
base.with_backup_endpoints(backup_endpoints)
}
#[cfg(test)]
mod tests {
use azure_data_cosmos_driver::CosmosDriverRuntimeBuilder;
use super::*;
use crate::{
options::{PartitionFailoverOptions, Region, UserAgentSuffix},
RoutingStrategy,
};
#[tokio::test]
async fn user_agent_suffix_is_forwarded_to_driver_runtime() {
let suffix = UserAgentSuffix::new("myapp-westus2");
let options = CosmosClientOptions {
user_agent_suffix: Some(suffix.clone()),
..Default::default()
};
let mut driver_builder = CosmosDriverRuntimeBuilder::new();
if let Some(s) = options.user_agent_suffix.clone() {
driver_builder = driver_builder.with_user_agent_suffix(s);
}
let runtime = driver_builder.build().await.expect("runtime builds");
assert_eq!(
runtime.user_agent_suffix(),
Some(&suffix),
"driver runtime did not receive the user-agent suffix"
);
assert!(
runtime.user_agent().as_str().contains(suffix.as_str()),
"computed driver user-agent {:?} does not contain suffix {:?}",
runtime.user_agent().as_str(),
suffix.as_str(),
);
}
#[tokio::test]
async fn no_user_agent_suffix_yields_no_driver_suffix() {
let runtime = CosmosDriverRuntimeBuilder::new()
.build()
.await
.expect("runtime builds");
assert!(runtime.user_agent_suffix().is_none());
}
#[test]
fn user_agent_suffix_setter_records_value() {
let suffix = UserAgentSuffix::new("myapp-westus2");
let builder = CosmosClientBuilder::new().with_user_agent_suffix(suffix.clone());
assert_eq!(builder.options.user_agent_suffix.as_ref(), Some(&suffix));
}
fn test_account() -> azure_data_cosmos_driver::models::AccountReference {
azure_data_cosmos_driver::models::AccountReference::with_master_key(
"https://test.documents.azure.com/".parse().unwrap(),
"dGVzdA==",
)
}
#[test]
fn proximity_to_known_region_starts_with_source() {
let opts = build_driver_options(
test_account(),
RoutingStrategy::ProximityTo(Region::EAST_US),
OperationOptions::default(),
None,
None,
#[cfg(feature = "fault_injection")]
Vec::new(),
Vec::new(),
)
.expect("build_driver_options should succeed");
let regions = opts.preferred_regions();
assert!(
!regions.is_empty(),
"should produce a non-empty list for a known region"
);
assert_eq!(regions[0], Region::EAST_US, "source region should be first");
}
#[test]
fn proximity_to_unknown_region_returns_empty_list() {
let opts = build_driver_options(
test_account(),
RoutingStrategy::ProximityTo(Region::from("not-a-real-region")),
OperationOptions::default(),
None,
None,
#[cfg(feature = "fault_injection")]
Vec::new(),
Vec::new(),
)
.expect("build_driver_options should succeed");
assert!(
opts.preferred_regions().is_empty(),
"unrecognized region should yield an empty list"
);
}
#[test]
fn preferred_regions_passes_through_unchanged() {
let input = vec![Region::WEST_US, Region::EAST_US, Region::WEST_EUROPE];
let opts = build_driver_options(
test_account(),
RoutingStrategy::PreferredRegions(input.clone()),
OperationOptions::default(),
None,
None,
#[cfg(feature = "fault_injection")]
Vec::new(),
Vec::new(),
)
.expect("build_driver_options should succeed");
assert_eq!(opts.preferred_regions(), input.as_slice());
}
#[test]
fn user_agent_suffix_flows_to_driver_options() {
let suffix = UserAgentSuffix::new("myapp-westus2");
let opts = build_driver_options(
test_account(),
RoutingStrategy::PreferredRegions(Vec::new()),
OperationOptions::default(),
Some(suffix.clone()),
None,
#[cfg(feature = "fault_injection")]
Vec::new(),
Vec::new(),
)
.expect("build_driver_options should succeed");
assert_eq!(opts.user_agent_suffix(), Some(&suffix));
}
#[test]
fn partition_failover_options_flow_to_driver_options() {
let pfo = PartitionFailoverOptions::builder()
.with_circuit_breaker_enabled(true)
.with_read_failure_threshold(42)
.build()
.expect("valid partition failover options");
let opts = build_driver_options(
test_account(),
RoutingStrategy::PreferredRegions(Vec::new()),
OperationOptions::default(),
None,
Some(pfo),
#[cfg(feature = "fault_injection")]
Vec::new(),
Vec::new(),
)
.expect("build_driver_options should succeed");
assert!(opts.partition_failover_options().circuit_breaker_enabled());
assert_eq!(
opts.partition_failover_options().read_failure_threshold(),
42
);
}
#[test]
fn missing_partition_failover_options_uses_default() {
let opts = build_driver_options(
test_account(),
RoutingStrategy::PreferredRegions(Vec::new()),
OperationOptions::default(),
None,
None,
#[cfg(feature = "fault_injection")]
Vec::new(),
Vec::new(),
)
.expect("build_driver_options should succeed");
assert_eq!(
opts.partition_failover_options().circuit_breaker_enabled(),
PartitionFailoverOptions::default().circuit_breaker_enabled(),
);
}
}