use azure_core::http::ClientOptions;
use std::{
sync::{
atomic::{AtomicUsize, Ordering},
Arc, RwLock,
},
time::Duration,
};
use crate::{
diagnostics::ProxyConfiguration,
models::{normalize_wrapping_sdk_identifier, UserAgent},
options::{
parse_duration_millis_from_env, ConnectionPoolOptions, CorrelationId, DriverOptions,
OperationOptions, UserAgentSuffix, WorkloadId,
},
system::{CpuMemoryMonitor, VmMetadataService},
};
use super::cache::{AccountMetadataCache, ContainerCache};
use super::{
transport::{
http_client_factory::{DefaultHttpClientFactory, HttpClientFactory},
CosmosTransport,
},
CosmosDriver,
};
#[non_exhaustive]
#[derive(Debug)]
pub struct CosmosDriverRuntime {
id: usize,
client_options: ClientOptions,
connection_pool: ConnectionPoolOptions,
bootstrap_transport: Arc<CosmosTransport>,
http_client_factory: Arc<dyn HttpClientFactory>,
env_operation_options: Arc<OperationOptions>,
env_override_operation_options: Arc<OperationOptions>,
operation_options: RwLock<Arc<OperationOptions>>,
user_agent: Arc<UserAgent>,
workload_id: Option<WorkloadId>,
correlation_id: Option<CorrelationId>,
user_agent_suffix: Option<UserAgentSuffix>,
wrapping_sdk_identifier: Option<String>,
container_cache: ContainerCache,
account_metadata_cache: Arc<AccountMetadataCache>,
cpu_monitor: CpuMemoryMonitor,
machine_id: Arc<String>,
proxy_configuration: ProxyConfiguration,
}
impl CosmosDriverRuntime {
pub fn builder() -> CosmosDriverRuntimeBuilder {
CosmosDriverRuntimeBuilder::new()
}
#[expect(dead_code, reason = "will be used when tracing spans are re-added")]
pub(crate) fn id(&self) -> usize {
self.id
}
pub fn client_options(&self) -> &ClientOptions {
&self.client_options
}
pub fn connection_pool(&self) -> &ConnectionPoolOptions {
&self.connection_pool
}
pub(crate) fn bootstrap_transport(&self) -> &Arc<CosmosTransport> {
&self.bootstrap_transport
}
pub(crate) fn http_client_factory(&self) -> &Arc<dyn HttpClientFactory> {
&self.http_client_factory
}
pub(crate) fn container_cache(&self) -> &ContainerCache {
&self.container_cache
}
pub(crate) fn account_metadata_cache(&self) -> &Arc<AccountMetadataCache> {
&self.account_metadata_cache
}
pub(crate) fn cpu_monitor(&self) -> &CpuMemoryMonitor {
&self.cpu_monitor
}
pub(crate) fn machine_id(&self) -> &Arc<String> {
&self.machine_id
}
pub fn proxy_configuration(&self) -> &ProxyConfiguration {
&self.proxy_configuration
}
pub fn env_operation_options(&self) -> &Arc<OperationOptions> {
&self.env_operation_options
}
pub fn env_override_operation_options(&self) -> &Arc<OperationOptions> {
&self.env_override_operation_options
}
pub fn default_operation_options(&self) -> Arc<OperationOptions> {
self.operation_options
.read()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
pub fn set_default_operation_options(&self, options: OperationOptions) {
*self
.operation_options
.write()
.unwrap_or_else(|e| e.into_inner()) = Arc::new(options);
}
pub fn user_agent(&self) -> &Arc<UserAgent> {
&self.user_agent
}
pub fn workload_id(&self) -> Option<WorkloadId> {
self.workload_id
}
pub fn correlation_id(&self) -> Option<&CorrelationId> {
self.correlation_id.as_ref()
}
pub fn user_agent_suffix(&self) -> Option<&UserAgentSuffix> {
self.user_agent_suffix.as_ref()
}
pub fn wrapping_sdk_identifier(&self) -> Option<&str> {
self.wrapping_sdk_identifier.as_deref()
}
pub fn effective_correlation(&self) -> Option<&str> {
self.correlation_id
.as_ref()
.map(|c| c.as_str())
.or_else(|| self.user_agent_suffix.as_ref().map(|s| s.as_str()))
}
pub async fn create_driver(
self: &Arc<Self>,
driver_options: DriverOptions,
) -> crate::error::Result<Arc<CosmosDriver>> {
tracing::trace!("creating new driver");
let driver = Arc::new(CosmosDriver::new(Arc::clone(self), driver_options)?);
driver.initialize().await?;
Ok(driver)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default)]
pub struct CosmosDriverRuntimeBuilder {
client_options: Option<ClientOptions>,
connection_pool: Option<ConnectionPoolOptions>,
operation_options: Option<OperationOptions>,
workload_id: Option<WorkloadId>,
correlation_id: Option<CorrelationId>,
user_agent_suffix: Option<UserAgentSuffix>,
wrapping_sdk_identifier: Option<String>,
cpu_refresh_interval: Option<Duration>,
#[cfg(any(
test,
feature = "__internal_in_memory_emulator",
feature = "__internal_mocking"
))]
http_client_factory: Option<Arc<dyn HttpClientFactory>>,
}
impl CosmosDriverRuntimeBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_client_options(mut self, options: ClientOptions) -> Self {
self.client_options = Some(options);
self
}
pub fn with_connection_pool(mut self, options: ConnectionPoolOptions) -> Self {
self.connection_pool = Some(options);
self
}
pub fn with_default_operation_options(mut self, options: OperationOptions) -> Self {
self.operation_options = Some(options);
self
}
pub fn with_workload_id(mut self, workload_id: WorkloadId) -> Self {
self.workload_id = Some(workload_id);
self
}
pub fn with_correlation_id(mut self, correlation_id: CorrelationId) -> Self {
self.correlation_id = Some(correlation_id);
self
}
pub fn with_user_agent_suffix(mut self, suffix: UserAgentSuffix) -> Self {
self.user_agent_suffix = Some(suffix);
self
}
pub fn with_wrapping_sdk_identifier(mut self, identifier: impl Into<String>) -> Self {
let raw = identifier.into();
self.wrapping_sdk_identifier = normalize_wrapping_sdk_identifier(&raw);
self
}
pub fn with_cpu_refresh_interval(mut self, interval: Duration) -> Self {
self.cpu_refresh_interval = Some(interval);
self
}
#[cfg(any(test, feature = "__internal_in_memory_emulator"))]
pub(crate) fn with_http_client_factory(mut self, factory: Arc<dyn HttpClientFactory>) -> Self {
self.http_client_factory = Some(factory);
self
}
#[cfg(feature = "__internal_mocking")]
pub fn with_mock_http_client_factory(mut self, factory: Arc<dyn HttpClientFactory>) -> Self {
self.http_client_factory = Some(factory);
self
}
pub async fn build(self) -> crate::error::Result<Arc<CosmosDriverRuntime>> {
let wrapping = self.wrapping_sdk_identifier.as_deref();
let user_agent = Arc::new(if let Some(ref suffix) = self.user_agent_suffix {
UserAgent::from_suffix(wrapping, suffix)
} else if let Some(workload_id) = self.workload_id {
UserAgent::from_workload_id(wrapping, workload_id)
} else if let Some(ref correlation_id) = self.correlation_id {
UserAgent::from_correlation_id(wrapping, correlation_id)
} else {
UserAgent::from_wrapping_sdk_identifier(wrapping)
});
let connection_pool = self.connection_pool.unwrap_or_default();
let proxy_configuration = ProxyConfiguration::from_env(connection_pool.proxy_allowed());
let http_client_factory: Arc<dyn HttpClientFactory> = {
#[cfg(any(
test,
feature = "__internal_in_memory_emulator",
feature = "__internal_mocking"
))]
{
self.http_client_factory
.unwrap_or_else(|| Arc::new(DefaultHttpClientFactory::new()))
}
#[cfg(not(any(
test,
feature = "__internal_in_memory_emulator",
feature = "__internal_mocking"
)))]
{
Arc::new(DefaultHttpClientFactory::new())
}
};
let bootstrap_version = if connection_pool.is_http2_allowed() {
crate::diagnostics::TransportHttpVersion::Http2
} else {
crate::diagnostics::TransportHttpVersion::Http11
};
let bootstrap_transport = Arc::new(CosmosTransport::bootstrap_metadata_only(
connection_pool.clone(),
http_client_factory.clone(),
bootstrap_version,
)?);
let refresh_interval = parse_duration_millis_from_env(
self.cpu_refresh_interval,
"AZURE_COSMOS_CPU_REFRESH_INTERVAL_MS",
5_000,
1_000,
60_000,
)?;
let cpu_monitor = CpuMemoryMonitor::get_or_init(refresh_interval);
let vm_metadata = VmMetadataService::get_or_init().await;
Ok(Arc::new(CosmosDriverRuntime {
id: NEXT_RUNTIME_ID.fetch_add(1, Ordering::Relaxed),
client_options: self.client_options.unwrap_or_default(),
connection_pool,
bootstrap_transport,
http_client_factory,
env_operation_options: Arc::new(OperationOptions {
throttling_retry_options: Some(crate::options::ThrottlingRetryOptions::from_env()),
..OperationOptions::from_env()
}),
env_override_operation_options: Arc::new(OperationOptions::from_env_override()),
operation_options: RwLock::new(Arc::new(self.operation_options.unwrap_or_default())),
user_agent,
workload_id: self.workload_id,
correlation_id: self.correlation_id,
user_agent_suffix: self.user_agent_suffix,
wrapping_sdk_identifier: self.wrapping_sdk_identifier,
container_cache: ContainerCache::new(),
account_metadata_cache: Arc::new(AccountMetadataCache::new()),
cpu_monitor,
machine_id: Arc::new(vm_metadata.machine_id().to_owned()),
proxy_configuration,
}))
}
}
static NEXT_RUNTIME_ID: AtomicUsize = AtomicUsize::new(0);
#[cfg(test)]
mod tests {
use super::*;
use crate::models::AccountReference;
use url::Url;
#[tokio::test]
async fn create_driver_propagates_initialization_failures() {
let runtime = CosmosDriverRuntimeBuilder::new().build().await.unwrap();
let account = AccountReference::with_master_key(
Url::parse("https://test.documents.azure.com:443/").unwrap(),
"***not-base64***",
);
let error = runtime
.create_driver(DriverOptions::builder(account.clone()).build())
.await
.expect_err("invalid signing key should fail initialization");
assert!(!error.to_string().is_empty());
let second_error = runtime
.create_driver(DriverOptions::builder(account).build())
.await
.expect_err("subsequent attempts must also surface the failure");
assert!(!second_error.to_string().is_empty());
}
#[tokio::test]
async fn user_agent_suffix_appears_in_request_headers() {
use crate::driver::transport::{
cosmos_headers::apply_cosmos_headers, cosmos_transport_client::HttpRequest,
};
use azure_core::http::headers::{HeaderValue, Headers, USER_AGENT};
use azure_core::http::Method;
let suffix = UserAgentSuffix::try_new("test-app").expect("valid suffix");
let runtime = CosmosDriverRuntimeBuilder::new()
.with_user_agent_suffix(suffix)
.build()
.await
.unwrap();
let user_agent_hv = HeaderValue::from(runtime.user_agent().as_str().to_owned());
let mut request = HttpRequest {
url: Url::parse("https://test.documents.azure.com/").unwrap(),
method: Method::Get,
headers: Headers::new(),
body: None,
timeout: None,
#[cfg(feature = "fault_injection")]
evaluation_collector: None,
};
apply_cosmos_headers(&mut request, &user_agent_hv);
let header_value = request
.headers
.get_optional_str(&USER_AGENT)
.expect("User-Agent header should be set by apply_cosmos_headers");
assert!(
header_value.contains("test-app"),
"User-Agent header '{header_value}' should contain the suffix 'test-app'"
);
}
#[tokio::test]
async fn wrapping_sdk_identifier_is_normalized_at_set_time() {
for raw in ["", " ", "\t\n"] {
let runtime = CosmosDriverRuntimeBuilder::new()
.with_wrapping_sdk_identifier(raw)
.build()
.await
.unwrap();
assert!(
runtime.wrapping_sdk_identifier().is_none(),
"expected wrapping identifier {raw:?} to normalize to None",
);
assert!(
runtime
.user_agent()
.as_str()
.starts_with("azsdk-rust-cosmos-driver/"),
"User-Agent should not start with empty wrapping prefix: {}",
runtime.user_agent().as_str(),
);
}
let runtime = CosmosDriverRuntimeBuilder::new()
.with_wrapping_sdk_identifier(" azsdk-rust-café/1.0 ")
.build()
.await
.unwrap();
let identifier = runtime
.wrapping_sdk_identifier()
.expect("wrapping identifier should be set");
assert_eq!(identifier, "azsdk-rust-caf_/1.0");
assert!(
runtime
.user_agent()
.as_str()
.starts_with("azsdk-rust-caf_/1.0 azsdk-rust-cosmos-driver/"),
"unexpected User-Agent: {}",
runtime.user_agent().as_str(),
);
}
}