use crate::{
diagnostics::{
DiagnosticsContextBuilder, ExecutionContext, PipelineType, RequestSentStatus,
TransportHttpVersion, TransportSecurity,
},
driver::{
cache::{PartitionKeyRangeCache, PkRangeFetchResult},
dataflow::{
planner, query_plan::QueryPlan, CachedTopologyProvider, OperationPlan,
PartitionRoutingRefresh, PipelineContext, PipelineNodeState, RequestExecutor,
RequestTarget, TopologyProvider,
},
pipeline::operation_pipeline::OperationOverrides,
routing::{
partition_endpoint_state::PartitionFailoverConfig,
partition_key_range_id::PartitionKeyRangeId, session_manager::SessionManager,
CosmosEndpoint, LocationStateStore,
},
transport::{is_emulator_host, uses_dataplane_pipeline},
},
models::{
effective_partition_key::EffectivePartitionKey, AccountEndpoint, AccountReference,
ContainerProperties, ContainerReference, ContinuationToken, CosmosOperation,
DatabaseReference, PartitionKey, ResolvedToken, ResourceType,
},
options::{
ConnectionPoolOptions, DriverOptions, OperationOptions, OperationOptionsView,
ThroughputControlGroupSnapshot,
},
ActivityId, CosmosResponse,
};
use arc_swap::ArcSwap;
use futures::future::BoxFuture;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use url::Url;
use super::{
cache::{parse_pk_ranges_response, AccountRegion},
transport::{
cosmos_headers, cosmos_transport_client::HttpRequest, request_signing,
AuthorizationContext, CosmosTransport,
},
CosmosDriverRuntime,
};
struct DriverRequestExecutor<'a> {
driver: &'a CosmosDriver,
options: &'a OperationOptions,
}
fn request_target_overrides(
target: RequestTarget,
continuation: Option<String>,
) -> OperationOverrides {
match target {
RequestTarget::LogicalPartitionKey(pk) => OperationOverrides {
partition_key: Some(pk),
continuation,
..Default::default()
},
RequestTarget::EffectivePartitionKeyRange {
partition_key_range_id,
range,
..
} => OperationOverrides {
partition_key_range_id: Some(partition_key_range_id),
feed_range: range,
continuation,
..Default::default()
},
RequestTarget::NonPartitioned => OperationOverrides {
continuation,
..Default::default()
},
}
}
impl RequestExecutor for DriverRequestExecutor<'_> {
fn execute_request<'a>(
&'a mut self,
operation: &'a CosmosOperation,
target: RequestTarget,
_partition_routing_refresh: PartitionRoutingRefresh,
continuation: Option<String>,
) -> BoxFuture<'a, crate::error::Result<CosmosResponse>> {
let driver = self.driver;
let overrides = request_target_overrides(target, continuation);
Box::pin(async move {
driver
.execute_operation_direct(operation, overrides, self.options)
.await
})
}
}
#[non_exhaustive]
#[derive(Debug)]
pub struct CosmosDriver {
runtime: Arc<CosmosDriverRuntime>,
options: DriverOptions,
transport: Arc<ArcSwap<CosmosTransport>>,
location_state_store: Arc<LocationStateStore>,
pk_range_cache: PartitionKeyRangeCache,
session_manager: SessionManager,
initialized: AtomicBool,
}
impl CosmosDriver {
#[cfg(feature = "reqwest")]
fn has_explicit_http2_incompatibility(error: &crate::error::CosmosError) -> bool {
if error.status().sub_status()
== Some(crate::models::SubStatusCode::TRANSPORT_HTTP2_INCOMPATIBLE)
{
return true;
}
let mut source = std::error::Error::source(error);
while let Some(cause) = source {
if let Some(h2_error) = cause.downcast_ref::<h2::Error>() {
return matches!(
h2_error.reason(),
Some(
h2::Reason::HTTP_1_1_REQUIRED
| h2::Reason::PROTOCOL_ERROR
| h2::Reason::FRAME_SIZE_ERROR
)
);
}
source = cause.source();
}
false
}
#[cfg(not(feature = "reqwest"))]
fn has_explicit_http2_incompatibility(_error: &crate::error::CosmosError) -> bool {
false
}
fn should_downgrade_http2(
current_version: TransportHttpVersion,
error: &crate::error::CosmosError,
http2_allowed: bool,
) -> bool {
http2_allowed
&& matches!(current_version, TransportHttpVersion::Http2)
&& Self::has_explicit_http2_incompatibility(error)
}
fn alternate_http_version(current_version: TransportHttpVersion) -> TransportHttpVersion {
match current_version {
TransportHttpVersion::Http2 => TransportHttpVersion::Http11,
TransportHttpVersion::Http11 => TransportHttpVersion::Http2,
}
}
fn build_metadata_transport_for_version(
connection_pool: &ConnectionPoolOptions,
http_client_factory: Arc<dyn super::transport::http_client_factory::HttpClientFactory>,
version: TransportHttpVersion,
endpoint: &AccountEndpoint,
) -> crate::error::Result<(
CosmosTransport,
super::transport::adaptive_transport::AdaptiveTransport,
)> {
let transport =
CosmosTransport::with_factory(connection_pool.clone(), http_client_factory, version)?;
let metadata_transport = transport.get_metadata_transport(endpoint)?;
Ok((transport, metadata_transport))
}
async fn fetch_account_properties_with_version(
runtime: &CosmosDriverRuntime,
account: &AccountReference,
version: TransportHttpVersion,
) -> crate::error::Result<(super::cache::AccountProperties, CosmosTransport)> {
let endpoint = AccountEndpoint::from(account);
let (transport, metadata_transport) = Self::build_metadata_transport_for_version(
runtime.connection_pool(),
Arc::clone(runtime.http_client_factory()),
version,
&endpoint,
)?;
let user_agent = Self::user_agent_header(runtime);
let props = Self::fetch_account_properties_with_transport(
runtime,
&metadata_transport,
account,
None,
&user_agent,
)
.await?;
Ok((props, transport))
}
async fn fetch_account_properties_with_runtime(
runtime: &CosmosDriverRuntime,
account: &AccountReference,
) -> crate::error::Result<super::cache::AccountProperties> {
let endpoint = AccountEndpoint::from(account);
let transport = runtime.bootstrap_transport();
let metadata_transport = transport.get_metadata_transport(&endpoint)?;
let user_agent =
azure_core::http::headers::HeaderValue::from(runtime.user_agent().as_str().to_owned());
Self::fetch_account_properties_with_transport(
runtime,
&metadata_transport,
account,
None,
&user_agent,
)
.await
}
async fn fetch_initial_account_properties(
runtime: &CosmosDriverRuntime,
account: &AccountReference,
) -> crate::error::Result<(TransportHttpVersion, super::cache::AccountProperties)> {
match Self::fetch_initial_account_properties_for_endpoint(runtime, account).await {
Ok(result) => Ok(result),
Err(primary_error) if !account.backup_endpoints().is_empty() => {
tracing::warn!(
endpoint = %AccountEndpoint::from(account),
error = %primary_error,
"primary endpoint probe failed; trying backup endpoints"
);
for backup_url in account.backup_endpoints() {
let backup_account = Self::with_endpoint(account, backup_url.clone());
match Self::fetch_initial_account_properties_for_endpoint(
runtime,
&backup_account,
)
.await
{
Ok(result) => {
return Ok(result);
}
Err(e) => {
tracing::warn!(
backup_endpoint = %backup_url,
error = %e,
"backup endpoint probe failed; trying next"
);
}
}
}
tracing::error!(
endpoint = %AccountEndpoint::from(account),
backup_count = account.backup_endpoints().len(),
"all endpoints exhausted during HTTP version probe"
);
Err(primary_error)
}
Err(error) => Err(error),
}
}
async fn fetch_initial_account_properties_for_endpoint(
runtime: &CosmosDriverRuntime,
account: &AccountReference,
) -> crate::error::Result<(TransportHttpVersion, super::cache::AccountProperties)> {
if !runtime.connection_pool().is_http2_allowed() {
let (props, _) = Self::fetch_account_properties_with_version(
runtime,
account,
TransportHttpVersion::Http11,
)
.await?;
return Ok((TransportHttpVersion::Http11, props));
}
match Self::fetch_account_properties_with_runtime(runtime, account).await {
Ok(props) => {
tracing::trace!(
endpoint = %AccountEndpoint::from(account),
"HTTP/2 probe succeeded; using HTTP/2 transport"
);
Ok((TransportHttpVersion::Http2, props))
}
Err(error)
if Self::should_downgrade_http2(
TransportHttpVersion::Http2,
&error,
runtime.connection_pool().is_http2_allowed(),
) =>
{
tracing::warn!(
endpoint = %AccountEndpoint::from(account),
error = %error,
"HTTP/2 probe failed with protocol incompatibility; falling back to HTTP/1.1"
);
let (props, _) = Self::fetch_account_properties_with_version(
runtime,
account,
TransportHttpVersion::Http11,
)
.await?;
Ok((TransportHttpVersion::Http11, props))
}
Err(error) => Err(error),
}
}
fn with_endpoint(account: &AccountReference, endpoint: Url) -> AccountReference {
AccountReference::builder(endpoint)
.auth(account.auth().clone())
.build()
.expect("auth is always present when cloned from existing AccountReference")
}
fn new_diagnostics_envelope(
runtime: &CosmosDriverRuntime,
activity_id: crate::models::ActivityId,
endpoint: &AccountEndpoint,
) -> (DiagnosticsContextBuilder, TransportSecurity) {
let mut diagnostics = DiagnosticsContextBuilder::new(
activity_id,
Arc::new(crate::options::DiagnosticsOptions::default()),
);
diagnostics.set_cpu_monitor(runtime.cpu_monitor().clone());
diagnostics.set_machine_id(Arc::clone(runtime.machine_id()));
#[cfg(feature = "fault_injection")]
if runtime.fault_injection_enabled() {
diagnostics.set_fault_injection_enabled(true);
}
let transport_security =
if bool::from(runtime.connection_pool().emulator_server_cert_validation())
&& is_emulator_host(endpoint)
{
TransportSecurity::EmulatorWithInsecureCertificates
} else {
TransportSecurity::Secure
};
(diagnostics, transport_security)
}
async fn fetch_account_properties_with_transport(
runtime: &CosmosDriverRuntime,
transport: &super::transport::adaptive_transport::AdaptiveTransport,
account: &AccountReference,
region: Option<&crate::options::Region>,
user_agent: &azure_core::http::headers::HeaderValue,
) -> crate::error::Result<super::cache::AccountProperties> {
let endpoint = AccountEndpoint::from(account);
let endpoint_url = endpoint.join_path("/");
let cosmos_endpoint = match region {
Some(region) => CosmosEndpoint::regional(region.clone(), endpoint_url.clone()),
None => CosmosEndpoint::global(endpoint_url.clone()),
};
let (mut diagnostics, transport_security) = Self::new_diagnostics_envelope(
runtime,
crate::models::ActivityId::new_uuid(),
&endpoint,
);
let request_handle = diagnostics.start_request(
ExecutionContext::Initial,
PipelineType::Metadata,
transport_security,
transport.diagnostics_kind(),
transport.diagnostics_http_version(),
&cosmos_endpoint,
);
let mut request = HttpRequest {
url: endpoint_url,
method: azure_core::http::Method::Get,
headers: azure_core::http::headers::Headers::new(),
body: None,
timeout: None,
#[cfg(feature = "fault_injection")]
evaluation_collector: None,
};
cosmos_headers::apply_cosmos_headers(&mut request, user_agent);
#[cfg(feature = "fault_injection")]
cosmos_headers::apply_fault_injection_operation_tag(
&mut request.headers,
crate::fault_injection::FaultOperationType::MetadataReadDatabaseAccount,
);
if let Err(err) = request_signing::sign_request(
&mut request,
account.auth(),
&AuthorizationContext::new(
azure_core::http::Method::Get,
ResourceType::DatabaseAccount,
"",
),
)
.await
{
let sign_status = err.status();
diagnostics.fail_transport_request(
request_handle,
err.to_string(),
RequestSentStatus::NotSent,
sign_status,
);
diagnostics.set_operation_status(sign_status.status_code(), sign_status.sub_status());
return Err(crate::error::CosmosErrorBuilder::from_error(err)
.with_context(format!("AccountProperties sign_request for {endpoint}"))
.with_diagnostics(Arc::new(diagnostics.complete()))
.build());
}
let response = match transport.send(&request).await {
Ok(r) => r,
Err(e) => {
let send_status = e.error.status();
diagnostics.fail_transport_request(
request_handle,
e.error.to_string(),
e.request_sent,
send_status,
);
diagnostics
.set_operation_status(send_status.status_code(), send_status.sub_status());
return Err(crate::error::CosmosErrorBuilder::from_error(e.error)
.with_context(format!("AccountProperties fetch from {endpoint}"))
.with_diagnostics(Arc::new(diagnostics.complete()))
.build());
}
};
let cosmos_headers = crate::models::CosmosResponseHeaders::from_headers(&response.headers);
let status_code = azure_core::http::StatusCode::from(response.status);
let sub_status = cosmos_headers.substatus;
let cosmos_status = crate::error::CosmosStatus::from_parts(status_code, sub_status);
diagnostics.record_response(request_handle, status_code, &cosmos_headers);
if !status_code.is_success() {
diagnostics.set_operation_status(status_code, sub_status);
let diagnostics_arc = Arc::new(diagnostics.complete());
return Err(crate::error::CosmosError::builder()
.with_status(cosmos_status)
.with_response_parts(crate::models::CosmosResponsePayload::new(
response.body,
cosmos_headers,
))
.with_diagnostics(diagnostics_arc)
.with_message(format!(
"AccountProperties fetch from {endpoint} returned HTTP {status_code}"
))
.build());
}
let props = match Self::parse_account_properties_payload(&response.body) {
Ok(props) => props,
Err(err) => {
let parse_status = err.status();
diagnostics
.set_operation_status(parse_status.status_code(), parse_status.sub_status());
let diagnostics_arc = Arc::new(diagnostics.complete());
return Err(crate::error::CosmosErrorBuilder::from_error(err)
.with_response_parts(crate::models::CosmosResponsePayload::new(
crate::models::ResponseBody::NoPayload,
cosmos_headers,
))
.with_diagnostics(diagnostics_arc)
.with_context(format!("AccountProperties payload from {endpoint}"))
.build());
}
};
tracing::info!(
endpoint = %endpoint,
write_region = ?props.write_region(),
"AccountProperties retrieved successfully"
);
Ok(props)
}
fn parse_account_properties_payload(
payload: &[u8],
) -> crate::error::Result<super::cache::AccountProperties> {
serde_json::from_slice(payload).map_err(|e| {
crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID)
.with_message("failed to parse AccountProperties")
.with_source(e)
.build()
})
}
fn user_agent_header(runtime: &CosmosDriverRuntime) -> azure_core::http::headers::HeaderValue {
azure_core::http::headers::HeaderValue::from(runtime.user_agent().as_str().to_owned())
}
fn endpoint_for_write_region(
account: &AccountReference,
write_region: Option<&AccountRegion>,
) -> AccountEndpoint {
if let Some(region) = write_region {
return region.database_account_endpoint.clone();
}
AccountEndpoint::from(account)
}
async fn fetch_account_properties(
&self,
account: &AccountReference,
) -> crate::error::Result<super::cache::AccountProperties> {
Self::refresh_account_properties(&self.runtime, account, &self.transport, None).await
}
async fn refresh_account_properties(
runtime: &CosmosDriverRuntime,
account: &AccountReference,
transport_holder: &Arc<ArcSwap<CosmosTransport>>,
previous_props: Option<Arc<super::cache::AccountProperties>>,
) -> crate::error::Result<super::cache::AccountProperties> {
let current_transport = transport_holder.load_full();
let current_version = current_transport.negotiated_version();
let endpoint = AccountEndpoint::from(account);
let metadata_transport = current_transport.get_metadata_transport(&endpoint)?;
let user_agent = Self::user_agent_header(runtime);
match Self::fetch_account_properties_with_transport(
runtime,
&metadata_transport,
account,
None,
&user_agent,
)
.await
{
Ok(props) => {
Self::maybe_restore_http2_after_refresh(
runtime,
account,
transport_holder,
current_version,
&endpoint,
)
.await;
Ok(props)
}
Err(error) => {
match Self::handle_refresh_failure(
runtime,
account,
transport_holder,
current_version,
&endpoint,
error,
)
.await
{
Ok(props) => Ok(props),
Err(primary_error) => {
Self::refresh_via_regional_endpoints(
runtime,
account,
transport_holder,
&endpoint,
primary_error,
previous_props,
)
.await
}
}
}
}
}
async fn refresh_via_regional_endpoints(
runtime: &CosmosDriverRuntime,
account: &AccountReference,
transport_holder: &Arc<ArcSwap<CosmosTransport>>,
primary_endpoint: &AccountEndpoint,
primary_error: crate::error::CosmosError,
previous_props: Option<Arc<super::cache::AccountProperties>>,
) -> crate::error::Result<super::cache::AccountProperties> {
let Some(cached_props) = previous_props else {
return Err(primary_error);
};
let regional_endpoints: Vec<(crate::options::Region, Url)> = cached_props
.readable_locations
.iter()
.filter_map(|loc| {
let url = loc.database_account_endpoint.url().clone();
let ep = AccountEndpoint::from(url.clone());
if ep == *primary_endpoint {
None
} else {
Some((loc.name.clone(), url))
}
})
.collect();
if regional_endpoints.is_empty() {
return Err(primary_error);
}
tracing::warn!(
endpoint = %primary_endpoint,
error = %primary_error,
"primary endpoint refresh failed; trying regional endpoints"
);
for (region, regional_url) in ®ional_endpoints {
let regional_account = Self::with_endpoint(account, regional_url.clone());
let regional_ep = AccountEndpoint::from(®ional_account);
let current_transport = transport_holder.load_full();
let Ok(regional_transport) = current_transport.get_metadata_transport(®ional_ep)
else {
continue;
};
let user_agent = Self::user_agent_header(runtime);
match Self::fetch_account_properties_with_transport(
runtime,
®ional_transport,
®ional_account,
Some(region),
&user_agent,
)
.await
{
Ok(props) => {
return Ok(props);
}
Err(e) => {
tracing::warn!(
regional_endpoint = %regional_url,
error = %e,
"regional endpoint refresh failed; trying next"
);
}
}
}
tracing::error!(
endpoint = %primary_endpoint,
regional_count = regional_endpoints.len(),
"all endpoints exhausted during account properties refresh"
);
Err(primary_error)
}
async fn maybe_restore_http2_after_refresh(
runtime: &CosmosDriverRuntime,
account: &AccountReference,
transport_holder: &Arc<ArcSwap<CosmosTransport>>,
current_version: TransportHttpVersion,
endpoint: &AccountEndpoint,
) {
if !matches!(current_version, TransportHttpVersion::Http11)
|| !runtime.connection_pool().is_http2_allowed()
{
return;
}
match Self::fetch_account_properties_with_runtime(runtime, account).await {
Ok(_) => match CosmosTransport::with_factory(
runtime.connection_pool().clone(),
Arc::clone(runtime.http_client_factory()),
TransportHttpVersion::Http2,
) {
Ok(transport) => {
transport_holder.store(Arc::new(transport));
tracing::info!(
endpoint = %endpoint,
"Metadata refresh restored HTTP/2 transport after successful probe"
);
}
Err(error) => {
tracing::warn!(
endpoint = %endpoint,
%error,
"HTTP/2 probe succeeded after metadata refresh, but recreating the HTTP/2 transport failed"
);
}
},
Err(error) => {
tracing::debug!(
endpoint = %endpoint,
%error,
"Metadata refresh succeeded over HTTP/1.1; HTTP/2 reprobe failed, keeping HTTP/1.1 transport"
);
}
}
}
async fn handle_refresh_failure(
runtime: &CosmosDriverRuntime,
account: &AccountReference,
transport_holder: &Arc<ArcSwap<CosmosTransport>>,
current_version: TransportHttpVersion,
endpoint: &AccountEndpoint,
error: crate::error::CosmosError,
) -> crate::error::Result<super::cache::AccountProperties> {
if Self::should_downgrade_http2(
current_version,
&error,
runtime.connection_pool().is_http2_allowed(),
) {
let fallback_version = Self::alternate_http_version(current_version);
tracing::warn!(
endpoint = %endpoint,
current = ?current_version,
fallback = ?fallback_version,
error = %error,
"Metadata refresh failed with protocol incompatibility; falling back to alternate HTTP version"
);
let (props, fallback_transport) =
Self::fetch_account_properties_with_version(runtime, account, fallback_version)
.await?;
transport_holder.store(Arc::new(fallback_transport));
return Ok(props);
}
Err(error)
}
async fn fetch_container_by_name(
&self,
db_name: &str,
container_name: &str,
) -> crate::error::Result<ContainerReference> {
let db_ref = DatabaseReference::from_name(self.account().clone(), db_name.to_owned());
let options = OperationOptions::default();
let container_result = self
.execute_singleton_operation(
CosmosOperation::read_container_by_name(db_ref, container_name.to_owned()),
options,
)
.await?;
let container_headers = container_result.headers().clone();
let container_diagnostics = container_result.diagnostics();
let container_props: ContainerProperties =
container_result.into_body().into_single().map_err(|e| {
crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID)
.with_message("failed to deserialize container response")
.with_response_parts(crate::models::CosmosResponsePayload::new(
crate::models::ResponseBody::NoPayload,
container_headers.clone(),
))
.with_diagnostics(container_diagnostics.clone())
.with_source(e)
.build()
})?;
let container_rid = container_props
.system_properties
.rid
.clone()
.ok_or_else(|| {
crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID)
.with_message("container response missing _rid")
.with_response_parts(crate::models::CosmosResponsePayload::new(
crate::models::ResponseBody::NoPayload,
container_headers.clone(),
))
.with_diagnostics(container_diagnostics.clone())
.with_source(std::io::Error::other("missing _rid"))
.build()
})?;
let db_rid = crate::models::resource_id::ResourceId::new(container_rid.clone())
.database_rid()
.ok_or_else(|| {
crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID)
.with_message(format!(
"failed to extract database RID from container RID '{container_rid}'"
))
.with_response_parts(crate::models::CosmosResponsePayload::new(
crate::models::ResponseBody::NoPayload,
container_headers,
))
.with_diagnostics(container_diagnostics)
.with_source(std::io::Error::other("invalid container _rid"))
.build()
})?;
Ok(ContainerReference::new(
self.account().clone(),
db_name.to_owned(),
db_rid.as_str().to_owned(),
container_props.id.clone().into_owned(),
container_rid,
&container_props,
))
}
pub(crate) fn new(runtime: Arc<CosmosDriverRuntime>, options: DriverOptions) -> Self {
let account = options.account().clone();
let account_endpoint = AccountEndpoint::from(&account);
let default_endpoint = CosmosEndpoint::global(account.endpoint().clone());
let transport: Arc<ArcSwap<CosmosTransport>> =
Arc::new(ArcSwap::from(Arc::clone(runtime.bootstrap_transport())));
let runtime_for_callback = Arc::clone(&runtime);
let account_for_callback = account.clone();
let transport_for_callback = Arc::clone(&transport);
let refresh_callback = Arc::new(
move |previous_props: Option<Arc<super::cache::AccountProperties>>| {
let runtime = Arc::clone(&runtime_for_callback);
let account = account_for_callback.clone();
let transport_holder = Arc::clone(&transport_for_callback);
let fut: BoxFuture<'static, crate::error::Result<super::cache::AccountProperties>> =
Box::pin(async move {
CosmosDriver::refresh_account_properties(
&runtime,
&account,
&transport_holder,
previous_props,
)
.await
});
fut
},
);
let endpoint_unavailability_ttl = options
.operation_options()
.endpoint_unavailability_ttl
.or(runtime.operation_options().endpoint_unavailability_ttl)
.unwrap_or_else(|| {
std::env::var("AZURE_COSMOS_ENDPOINT_UNAVAILABLE_TTL_MS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.map(Duration::from_millis)
.unwrap_or(Duration::from_secs(60))
});
let init_view = OperationOptionsView::new(
Some(Arc::clone(runtime.env_operation_options())),
Some(runtime.operation_options()),
Some(options.operation_options().clone()),
None,
);
let partition_failover_config = PartitionFailoverConfig::from_options(&init_view);
let location_state_store = Arc::new(LocationStateStore::new(
runtime.account_metadata_cache().clone(),
account_endpoint,
default_endpoint,
refresh_callback,
runtime.connection_pool().is_gateway20_allowed(),
endpoint_unavailability_ttl,
partition_failover_config,
options.preferred_regions().to_vec(),
));
#[cfg(feature = "tokio")]
location_state_store.start_failback_loop();
#[cfg(feature = "tokio")]
location_state_store.start_account_refresh_loop();
Self {
runtime,
options,
transport,
location_state_store,
pk_range_cache: PartitionKeyRangeCache::new(),
session_manager: SessionManager::new(),
initialized: AtomicBool::new(false),
}
}
pub fn account(&self) -> &AccountReference {
self.options.account()
}
pub fn runtime(&self) -> &CosmosDriverRuntime {
&self.runtime
}
pub fn options(&self) -> &DriverOptions {
&self.options
}
fn transport(&self) -> Arc<CosmosTransport> {
self.transport.load_full()
}
pub async fn initialize(&self) -> crate::error::Result<()> {
let account = self.options.account();
let account_endpoint = AccountEndpoint::from(account);
let (negotiated_version, properties) =
Self::fetch_initial_account_properties(&self.runtime, account).await?;
tracing::info!(
endpoint = %account_endpoint,
version = ?negotiated_version,
"HTTP version negotiated for account"
);
self.runtime
.account_metadata_cache()
.get_or_fetch(account_endpoint, || async { Ok(properties) })
.await?;
let new_transport = Arc::new(CosmosTransport::with_factory(
self.runtime.connection_pool().clone(),
Arc::clone(self.runtime.http_client_factory()),
negotiated_version,
)?);
self.transport.store(new_transport);
self.initialized.store(true, Ordering::Release);
Ok(())
}
pub async fn prime_container(
&self,
db_name: &str,
container_name: &str,
) -> crate::error::Result<()> {
self.resolve_container_by_name(db_name, container_name)
.await?;
Ok(())
}
pub fn operation_options_view<'a>(
&self,
operation_options: &'a OperationOptions,
) -> OperationOptionsView<'a> {
OperationOptionsView::new(
Some(Arc::clone(self.runtime.env_operation_options())),
Some(self.runtime.operation_options()),
Some(self.options.operation_options().clone()),
Some(operation_options),
)
}
pub(crate) fn effective_throughput_control_group(
&self,
effective_options: &OperationOptionsView<'_>,
container: &ContainerReference,
) -> crate::error::Result<Option<ThroughputControlGroupSnapshot>> {
if let Some(name) = effective_options.throughput_control_group() {
let group = self
.runtime
.get_throughput_control_group(container, name)
.ok_or_else(|| {
crate::error::CosmosError::builder().with_status(crate::error::CosmosStatus::CLIENT_THROUGHPUT_CONTROL_GROUP_NOT_REGISTERED)
.with_message(format!(
"throughput control group '{}' not found in registry for container '{}'",
name,
container.name()
))
.build()
})?;
return Ok(Some(ThroughputControlGroupSnapshot::from(group.as_ref())));
}
Ok(self
.runtime
.get_default_throughput_control_group(container)
.map(|group| ThroughputControlGroupSnapshot::from(group.as_ref())))
}
async fn fetch_pk_ranges_from_service(
&self,
container: ContainerReference,
continuation: Option<String>,
) -> Option<PkRangeFetchResult> {
let mut operation = CosmosOperation::read_all_partition_key_ranges(container.clone());
if let Some(token) = continuation.as_deref() {
operation = operation
.with_precondition(crate::models::Precondition::if_none_match(token.to_owned()));
}
let mut request_headers = operation.request_headers().clone();
request_headers.incremental_feed = true;
request_headers.max_item_count = Some(crate::models::MaxItemCountHint::ServerDecides);
operation = operation.with_request_headers(request_headers);
let options = OperationOptions::default();
match self
.execute_operation_direct(&operation, OperationOverrides::default(), &options)
.await
{
Ok(response) => {
let etag = response.headers().etag.as_ref().map(|e| e.to_string());
if response.status().status_code() == azure_core::http::StatusCode::NotModified {
return Some(PkRangeFetchResult {
ranges: vec![],
continuation,
not_modified: true,
});
}
let body_bytes = match response.into_body().single() {
Ok(b) => b,
Err(_) => {
tracing::error!(
container = %container.name(),
"Partition key ranges response was a feed body, expected single payload"
);
return None;
}
};
match parse_pk_ranges_response(&body_bytes) {
Some(ranges) => Some(PkRangeFetchResult {
ranges,
continuation: etag,
not_modified: false,
}),
None => {
tracing::error!(
container = %container.name(),
"Failed to parse partition key ranges response body"
);
None
}
}
}
Err(e) => {
let http_status = if e.is_from_wire() {
Some(e.status().status_code())
} else {
None
};
if let Some(status) = http_status {
if matches!(
status,
azure_core::http::StatusCode::Unauthorized
| azure_core::http::StatusCode::Forbidden
| azure_core::http::StatusCode::NotFound
) {
tracing::error!(
container = %container.name(),
status = %status,
error = %e,
"Permanent error fetching partition key ranges — check account credentials and container existence"
);
return None;
}
}
tracing::warn!(
container = %container.name(),
error = %e,
"Transient error fetching partition key ranges from service after exhausting pipeline cross-region retries"
);
None
}
}
}
async fn pre_resolve_partition_key_range_id(
&self,
operation: &CosmosOperation,
) -> Option<PartitionKeyRangeId> {
if !operation
.resource_type()
.is_partitioned(operation.operation_type())
{
return None;
}
let snapshot = self.location_state_store.snapshot();
let partition_state = snapshot.partitions.as_ref();
if !partition_state.per_partition_automatic_failover_enabled
&& !partition_state.per_partition_circuit_breaker_enabled
{
return None;
}
let container = operation.container()?;
let Some(partition_key) = operation.target().and_then(|t| t.partition_key()) else {
return None;
};
self.pk_range_cache
.resolve_partition_key_range_id(container, partition_key, false, |c, cont| {
Box::pin(self.fetch_pk_ranges_from_service(c, cont))
})
.await
.map(PartitionKeyRangeId::from)
}
pub async fn execute_operation(
&self,
operation: CosmosOperation,
options: OperationOptions,
) -> crate::error::Result<Option<crate::models::CosmosResponse>> {
if operation.operation_type() == crate::models::OperationType::Patch {
let max_attempts = operation.patch_max_attempts();
return Box::pin(async {
let result = crate::driver::pipeline::patch_handler::execute(
self,
operation,
options,
max_attempts,
)
.await?;
Ok(Some(result))
})
.await;
}
Box::pin(async {
let container = operation.container().cloned();
let mut plan = self.plan_operation(operation, &options, None).await?;
self.execute_plan(&mut plan, container, options).await
})
.await
}
pub async fn execute_singleton_operation(
&self,
operation: CosmosOperation,
options: OperationOptions,
) -> crate::error::Result<crate::models::CosmosResponse> {
debug_assert!(
!operation.operation_type().is_feed(),
"execute_singleton_operation should only be used for operations that return a single result, but '{} {}' is a feed operation",
operation.operation_type(),
operation.resource_type()
);
match self.execute_operation(operation, options).await {
Ok(Some(r)) => Ok(r),
Ok(None) => {
if cfg!(debug_assertions) {
panic!("singleton operation returned an empty page")
}
Err(crate::error::CosmosError::builder()
.with_status(
crate::error::CosmosStatus::CLIENT_SINGLETON_OPERATION_RETURNED_EMPTY_PAGE,
)
.with_message("internal error: singleton operation returned an empty page")
.build())
}
Err(e) => Err(e),
}
}
pub async fn execute_plan(
&self,
plan: &mut OperationPlan,
container: Option<ContainerReference>,
options: OperationOptions,
) -> crate::error::Result<Option<crate::models::CosmosResponse>> {
if !self.initialized.load(Ordering::Acquire) {
let endpoint = AccountEndpoint::from(self.options.account());
return Err(crate::error::CosmosError::builder().with_status(crate::error::CosmosStatus::CLIENT_DRIVER_NOT_INITIALIZED)
.with_message(format!(
"CosmosDriver for {endpoint} has not been initialized; call initialize() or \
use CosmosDriverRuntime::get_or_create_driver() which initializes automatically"
))
.build());
}
tracing::debug!("plan execution started");
let mut executor = DriverRequestExecutor {
driver: self,
options: &options,
};
let mut topology = container.map(|c| {
CachedTopologyProvider::new(&self.pk_range_cache, c, |container, continuation| {
self.fetch_pk_ranges_from_service(container, continuation)
})
});
let mut context = PipelineContext::new(
&mut executor,
topology.as_mut().map(|t| t as &mut dyn TopologyProvider),
);
plan.pipeline.next_page(&mut context).await
}
async fn execute_operation_direct(
&self,
operation: &CosmosOperation,
overrides: OperationOverrides,
options: &OperationOptions,
) -> crate::error::Result<CosmosResponse> {
tracing::debug!(
operation_type = ?operation.operation_type(),
resource_type = ?operation.resource_type(),
resource_reference = ?operation.resource_reference(),
overrides = ?overrides,
body_length = operation.body().map(|b| b.len()),
"executing operation");
let effective_options = self.operation_options_view(options);
let effective_control_group = match operation.container() {
Some(container) => {
self.effective_throughput_control_group(&effective_options, container)?
}
None => None,
};
let activity_id = ActivityId::new_uuid();
let account = operation.resource_reference().account();
let auth = account.auth();
let account_endpoint = AccountEndpoint::from(account);
let account_properties = self
.runtime
.account_metadata_cache()
.get_or_fetch(account_endpoint, || self.fetch_account_properties(account))
.await?;
self.location_state_store.sync_account_properties(
Arc::clone(&account_properties),
self.location_state_store.default_endpoint(),
);
let write_region = account_properties.write_account_region();
let endpoint = Self::endpoint_for_write_region(account, write_region);
let pre_resolved_pk_range_id = self.pre_resolve_partition_key_range_id(operation).await;
let transport = self.transport();
let operation_type = operation.operation_type();
let resource_type = operation.resource_type();
let is_dataplane = uses_dataplane_pipeline(resource_type, operation_type);
let (diagnostics_builder, transport_security) =
Self::new_diagnostics_envelope(&self.runtime, activity_id.clone(), &endpoint);
let pipeline_type = if is_dataplane {
PipelineType::DataPlane
} else {
PipelineType::Metadata
};
let user_agent = azure_core::http::headers::HeaderValue::from(
self.runtime.user_agent().as_str().to_owned(),
);
super::pipeline::operation_pipeline::execute_operation_pipeline(
operation,
overrides,
&effective_options,
options.custom_headers.as_ref(),
self.location_state_store.as_ref(),
&transport,
&endpoint,
auth,
&user_agent,
&activity_id,
pipeline_type,
transport_security,
diagnostics_builder,
&self.session_manager,
account_properties
.user_consistency_policy
.default_consistency_level,
effective_control_group.as_ref(),
pre_resolved_pk_range_id,
)
.await
}
pub async fn resolve_container(
&self,
db_name: &str,
container_name: &str,
) -> crate::error::Result<ContainerReference> {
self.resolve_container_by_name(db_name, container_name)
.await
}
pub async fn resolve_container_by_name(
&self,
db_name: &str,
container_name: &str,
) -> crate::error::Result<ContainerReference> {
let endpoint = self.account().endpoint().as_str().to_owned();
let db_name_owned = db_name.to_owned();
let container_name_owned = container_name.to_owned();
let resolved = self
.runtime
.container_cache()
.get_or_fetch_by_name(&endpoint, db_name, container_name, || async move {
self.fetch_container_by_name(&db_name_owned, &container_name_owned)
.await
.map_err(|err| {
crate::error::CosmosErrorBuilder::from_error(err)
.with_context(format!(
"resolve container by name (db='{db_name_owned}', container='{container_name_owned}')"
))
.build()
})
})
.await?;
Ok(resolved.as_ref().clone())
}
pub async fn plan_operation(
&self,
operation: CosmosOperation,
options: &OperationOptions,
continuation: Option<&ContinuationToken>,
) -> crate::error::Result<OperationPlan> {
if !self.initialized.load(Ordering::Acquire) {
let endpoint = AccountEndpoint::from(self.options.account());
return Err(crate::error::CosmosError::builder().with_status(crate::error::CosmosStatus::CLIENT_DRIVER_NOT_INITIALIZED)
.with_message(format!(
"CosmosDriver for {endpoint} has not been initialized; call initialize() or \
use CosmosDriverRuntime::get_or_create_driver() which initializes automatically"
))
.build());
}
tracing::debug!(operation_type = ?operation.operation_type(), resource_type = ?operation.resource_type(), resource_reference = ?operation.resource_reference(), "planning operation");
let operation = Arc::new(operation);
let resume_state = match continuation {
None => None,
Some(token) => {
match token.resolve()? {
ResolvedToken::ClientV1(state) => {
state.is_valid_for_operation(&operation)?;
Some(state.into_root_node_state())
}
ResolvedToken::ServerOpaque(server_token) => {
if !operation.is_trivial() {
return Err(crate::error::CosmosError::builder().with_status(crate::error::CosmosStatus::CLIENT_OPAQUE_TOKEN_INVALID_FOR_CROSS_PARTITION_QUERY)
.with_message(
"an opaque server continuation token cannot be used to resume a \
cross-partition query; use the SDK-issued continuation token from \
QueryPageIterator::to_continuation_token()",
)
.build());
}
Some(PipelineNodeState::Request {
server_continuation: Some(server_token),
})
}
}
}
};
if operation.is_trivial() {
let pipeline = planner::build_trivial_pipeline(operation.clone(), resume_state)?;
return Ok(OperationPlan::new(pipeline, operation));
}
let container = operation.container().ok_or_else(|| {
crate::error::CosmosError::builder()
.with_status(
crate::error::CosmosStatus::CLIENT_CROSS_PARTITION_QUERY_REQUIRES_CONTAINER_REF,
)
.with_message("cross-partition query requires a container reference")
.build()
})?;
let query_plan_operation = CosmosOperation::query_plan(container.clone(), "".into())
.with_body(operation.body().unwrap_or_default().to_vec());
let response = self
.execute_operation_direct(
&query_plan_operation,
OperationOverrides::default(),
options,
)
.await?;
let query_plan_body = match response.body() {
crate::models::ResponseBody::Bytes(b) => b.clone(),
_ => {
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID)
.with_message("query plan response did not contain a body")
.with_source(std::io::Error::other("missing body"))
.build());
}
};
let query_plan: QueryPlan = serde_json::from_slice(&query_plan_body).map_err(|e| {
crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID)
.with_message("failed to parse query plan response")
.with_source(e)
.build()
})?;
let container_ref = container.clone();
let mut topology = CachedTopologyProvider::new(
&self.pk_range_cache,
container_ref,
|container, continuation| self.fetch_pk_ranges_from_service(container, continuation),
);
let pipeline =
planner::build_sequential_drain(&query_plan, &mut topology, &operation, resume_state)
.await?;
Ok(OperationPlan::new(pipeline, operation))
}
pub async fn resolve_all_partition_key_ranges(
&self,
container: &ContainerReference,
force_refresh: bool,
) -> Option<Vec<crate::models::partition_key_range::PartitionKeyRange>> {
let routing_map = self
.pk_range_cache
.try_lookup(container, force_refresh, |c, cont| {
Box::pin(self.fetch_pk_ranges_from_service(c, cont))
})
.await?;
let ranges = routing_map.ranges();
if ranges.is_empty() {
return None;
}
Some(ranges.to_vec())
}
pub async fn resolve_partition_key_ranges_for_key(
&self,
container: &ContainerReference,
partition_key: &PartitionKey,
force_refresh: bool,
) -> Option<Vec<crate::models::partition_key_range::PartitionKeyRange>> {
if partition_key.is_empty() {
return None;
}
let pk_def = container.partition_key_definition();
let epk_range = match EffectivePartitionKey::compute_range(partition_key.values(), pk_def) {
Ok(range) => range,
Err(e) => {
tracing::warn!("EPK computation failed for partition key: {e}");
return None;
}
};
if epk_range.start == epk_range.end {
let routing_map = self
.pk_range_cache
.try_lookup(container, force_refresh, |c, cont| {
Box::pin(self.fetch_pk_ranges_from_service(c, cont))
})
.await?;
if routing_map.ranges().is_empty() {
return None;
}
Some(
routing_map
.get_range_by_effective_partition_key(&epk_range.start)
.cloned()
.map_or_else(Vec::new, |r| vec![r]),
)
} else {
self.pk_range_cache
.resolve_overlapping_ranges(
container,
&epk_range.start..&epk_range.end,
force_refresh,
|c, cont| Box::pin(self.fetch_pk_ranges_from_service(c, cont)),
)
.await
}
}
}
#[cfg(test)]
mod tests {
use std::collections::VecDeque;
use std::sync::Mutex;
use async_trait::async_trait;
use azure_core::http::headers::Headers;
use url::Url;
use crate::{
driver::CosmosDriverRuntimeBuilder,
models::AccountReference,
options::{
ContentResponseOnWrite, CorrelationId, OperationOptionsBuilder, UserAgentSuffix,
WorkloadId,
},
};
use super::*;
use crate::driver::cache::AccountProperties as CachedAccountProperties;
use crate::options::Region;
use crate::{
driver::transport::{
cosmos_transport_client::{HttpRequest, HttpResponse, TransportClient, TransportError},
http_client_factory::{HttpClientConfig, HttpClientFactory, HttpVersionPolicy},
},
options::ConnectionPoolOptions,
};
const ACCOUNT_PROPERTIES_PAYLOAD: &str = r#"{
"_self": "",
"id": "test",
"_rid": "test.documents.azure.com",
"media": "//media/",
"addresses": "//addresses/",
"_dbs": "//dbs/",
"writableLocations": [
{ "name": "West US 2", "databaseAccountEndpoint": "https://test-westus2.documents.azure.com:443/" }
],
"readableLocations": [
{ "name": "West US 2", "databaseAccountEndpoint": "https://test-westus2.documents.azure.com:443/" }
],
"enableMultipleWriteLocations": false,
"userReplicationPolicy": { "minReplicaSetSize": 3, "maxReplicasetSize": 4 },
"userConsistencyPolicy": { "defaultConsistencyLevel": "Session" },
"systemReplicationPolicy": { "minReplicaSetSize": 3, "maxReplicasetSize": 4 },
"readPolicy": { "primaryReadCoefficient": 1, "secondaryReadCoefficient": 1 },
"queryEngineConfiguration": "{}"
}"#;
fn signed_test_account(url: &str) -> AccountReference {
AccountReference::with_master_key(Url::parse(url).unwrap(), "dGVzdA==")
}
#[derive(Clone, Debug)]
enum ResponsePlan {
Success,
Http2Incompatible,
ConnectionError,
ServiceUnavailable503,
}
#[derive(Debug)]
struct ScriptedClient {
plan: ResponsePlan,
}
#[async_trait]
impl TransportClient for ScriptedClient {
async fn send(&self, _request: &HttpRequest) -> Result<HttpResponse, TransportError> {
match self.plan {
ResponsePlan::Success => Ok(HttpResponse {
status: 200,
headers: Headers::new(),
body: ACCOUNT_PROPERTIES_PAYLOAD.as_bytes().to_vec(),
}),
ResponsePlan::Http2Incompatible => Err(TransportError::new(
crate::error::CosmosError::builder()
.with_status(crate::models::CosmosStatus::TRANSPORT_HTTP2_INCOMPATIBLE)
.with_message("http2 not supported")
.with_source(h2::Error::from(h2::Reason::HTTP_1_1_REQUIRED))
.build(),
crate::diagnostics::RequestSentStatus::NotSent,
)),
ResponsePlan::ConnectionError => Err(TransportError::new(
crate::error::CosmosError::builder()
.with_status(crate::models::CosmosStatus::TRANSPORT_CONNECTION_FAILED)
.with_message("simulated connection refused")
.build(),
crate::diagnostics::RequestSentStatus::NotSent,
)),
ResponsePlan::ServiceUnavailable503 => Ok(HttpResponse {
status: 503,
headers: Headers::new(),
body: br#"{"code":"ServiceUnavailable","message":"pgcosmos extension is still starting; retry request shortly"}"#.to_vec(),
}),
}
}
}
#[derive(Debug)]
struct ScriptedFactory {
configs: Mutex<Vec<HttpClientConfig>>,
plans: Mutex<VecDeque<ResponsePlan>>,
}
impl ScriptedFactory {
fn new(plans: impl IntoIterator<Item = ResponsePlan>) -> Self {
Self {
configs: Mutex::new(Vec::new()),
plans: Mutex::new(plans.into_iter().collect()),
}
}
fn configs(&self) -> Vec<HttpClientConfig> {
self.configs.lock().expect("config lock poisoned").clone()
}
}
impl HttpClientFactory for ScriptedFactory {
fn build(
&self,
_connection_pool: &ConnectionPoolOptions,
config: HttpClientConfig,
) -> crate::error::Result<Arc<dyn TransportClient>> {
self.configs
.lock()
.expect("config lock poisoned")
.push(config);
let plan = self
.plans
.lock()
.expect("plan lock poisoned")
.pop_front()
.unwrap_or(ResponsePlan::Success);
Ok(Arc::new(ScriptedClient { plan }))
}
}
fn test_account() -> AccountReference {
AccountReference::with_master_key(
Url::parse("https://test.documents.azure.com:443/").unwrap(),
"test-key",
)
}
#[tokio::test]
async fn default_operation_options() {
let runtime = CosmosDriverRuntimeBuilder::new().build().await.unwrap();
assert!(runtime
.operation_options()
.throughput_control_group
.is_none());
assert!(runtime
.operation_options()
.max_failover_retry_count
.is_none());
assert!(runtime
.user_agent()
.as_str()
.starts_with("azsdk-rust-cosmos-driver/"));
assert!(runtime.user_agent().suffix().is_none());
assert!(runtime.workload_id().is_none());
assert!(runtime.correlation_id().is_none());
assert!(runtime.user_agent_suffix().is_none());
}
#[tokio::test]
async fn builder_sets_operation_options() {
let opts = OperationOptionsBuilder::new()
.with_max_failover_retry_count(7)
.build();
let runtime = CosmosDriverRuntimeBuilder::new()
.with_operation_options(opts)
.build()
.await
.unwrap();
assert_eq!(
runtime.operation_options().max_failover_retry_count,
Some(7)
);
}
#[tokio::test]
async fn builder_sets_identity_fields() {
let runtime = CosmosDriverRuntimeBuilder::new()
.with_workload_id(WorkloadId::new(25))
.with_correlation_id(CorrelationId::new("aks-prod-eastus"))
.with_user_agent_suffix(UserAgentSuffix::new("myapp-westus2"))
.build()
.await
.unwrap();
assert!(runtime.user_agent().as_str().contains("myapp-westus2"));
assert_eq!(runtime.user_agent().suffix(), Some("myapp-westus2"));
assert_eq!(runtime.workload_id().unwrap().value(), 25);
assert_eq!(
runtime.correlation_id().unwrap().as_str(),
"aks-prod-eastus"
);
assert_eq!(
runtime.user_agent_suffix().unwrap().as_str(),
"myapp-westus2"
);
}
#[tokio::test]
async fn user_agent_computed_from_suffix() {
let runtime = CosmosDriverRuntimeBuilder::new()
.with_user_agent_suffix(UserAgentSuffix::new("my-suffix"))
.build()
.await
.unwrap();
assert!(runtime
.user_agent()
.as_str()
.starts_with("azsdk-rust-cosmos-driver/"));
assert!(runtime.user_agent().as_str().contains("my-suffix"));
assert_eq!(runtime.user_agent().suffix(), Some("my-suffix"));
}
#[tokio::test]
async fn user_agent_computed_from_workload_id() {
let runtime = CosmosDriverRuntimeBuilder::new()
.with_workload_id(WorkloadId::new(42))
.build()
.await
.unwrap();
assert!(runtime
.user_agent()
.as_str()
.starts_with("azsdk-rust-cosmos-driver/"));
assert!(runtime.user_agent().as_str().contains("w42"));
}
#[tokio::test]
async fn user_agent_computed_from_correlation_id() {
let runtime = CosmosDriverRuntimeBuilder::new()
.with_correlation_id(CorrelationId::new("my-correlation"))
.build()
.await
.unwrap();
assert!(runtime
.user_agent()
.as_str()
.starts_with("azsdk-rust-cosmos-driver/"));
assert!(runtime.user_agent().as_str().contains("my-correlation"));
}
#[tokio::test]
async fn user_agent_suffix_takes_priority_over_workload_id() {
let runtime = CosmosDriverRuntimeBuilder::new()
.with_user_agent_suffix(UserAgentSuffix::new("suffix"))
.with_workload_id(WorkloadId::new(25))
.with_correlation_id(CorrelationId::new("correlation"))
.build()
.await
.unwrap();
assert!(runtime.user_agent().as_str().contains("suffix"));
assert!(!runtime.user_agent().as_str().contains("w25"));
assert!(!runtime.user_agent().as_str().contains("correlation"));
}
#[tokio::test]
async fn workload_id_takes_priority_over_correlation_id() {
let runtime = CosmosDriverRuntimeBuilder::new()
.with_workload_id(WorkloadId::new(25))
.with_correlation_id(CorrelationId::new("correlation"))
.build()
.await
.unwrap();
assert!(runtime.user_agent().as_str().contains("w25"));
assert!(!runtime.user_agent().as_str().contains("correlation"));
}
#[tokio::test]
async fn effective_correlation_prefers_correlation_id() {
let runtime = CosmosDriverRuntimeBuilder::new()
.with_correlation_id(CorrelationId::new("correlation"))
.with_user_agent_suffix(UserAgentSuffix::new("suffix"))
.build()
.await
.unwrap();
assert_eq!(runtime.effective_correlation(), Some("correlation"));
}
#[tokio::test]
async fn effective_correlation_falls_back_to_suffix() {
let runtime = CosmosDriverRuntimeBuilder::new()
.with_user_agent_suffix(UserAgentSuffix::new("suffix"))
.build()
.await
.unwrap();
assert_eq!(runtime.effective_correlation(), Some("suffix"));
}
#[tokio::test]
async fn effective_correlation_none_when_both_unset() {
let runtime = CosmosDriverRuntimeBuilder::new().build().await.unwrap();
assert!(runtime.effective_correlation().is_none());
}
#[tokio::test]
async fn runtime_modification() {
let runtime = CosmosDriverRuntimeBuilder::new().build().await.unwrap();
assert!(runtime
.operation_options()
.max_failover_retry_count
.is_none());
let new_opts = OperationOptionsBuilder::new()
.with_max_failover_retry_count(5)
.build();
runtime.set_operation_options(new_opts);
assert_eq!(
runtime.operation_options().max_failover_retry_count,
Some(5)
);
}
#[tokio::test]
async fn effective_options_merge_priority() {
let cosmos_runtime = CosmosDriverRuntimeBuilder::new().build().await.unwrap();
let driver_options = DriverOptions::builder(test_account()).build();
let driver = CosmosDriver::new(cosmos_runtime, driver_options);
let op_options = OperationOptionsBuilder::new()
.with_content_response_on_write(ContentResponseOnWrite::Disabled)
.build();
let view = driver.operation_options_view(&op_options);
assert_eq!(
view.content_response_on_write(),
Some(&ContentResponseOnWrite::Disabled)
);
let op_options = OperationOptionsBuilder::new()
.with_content_response_on_write(ContentResponseOnWrite::Enabled)
.build();
let view = driver.operation_options_view(&op_options);
assert_eq!(
view.content_response_on_write(),
Some(&ContentResponseOnWrite::Enabled)
);
}
#[tokio::test]
async fn effective_options_falls_back_to_runtime() {
let cosmos_runtime = CosmosDriverRuntimeBuilder::new().build().await.unwrap();
let driver_options = DriverOptions::builder(test_account()).build();
let driver = CosmosDriver::new(cosmos_runtime, driver_options);
let op_options = OperationOptionsBuilder::new()
.with_content_response_on_write(ContentResponseOnWrite::Enabled)
.build();
let view = driver.operation_options_view(&op_options);
assert_eq!(
view.content_response_on_write(),
Some(&ContentResponseOnWrite::Enabled)
);
let op_options = OperationOptions::default();
let view = driver.operation_options_view(&op_options);
assert!(view.content_response_on_write().is_none());
}
#[test]
fn endpoint_for_write_region_uses_service_uri() {
let account = AccountReference::with_master_key(
Url::parse("https://myaccount.documents.azure.com:443/").unwrap(),
"test-key",
);
let region = AccountRegion {
name: Region::new("West US"),
database_account_endpoint: AccountEndpoint::try_from(
"https://myaccount-westus.documents.azure.com:443/",
)
.unwrap(),
};
let endpoint = CosmosDriver::endpoint_for_write_region(&account, Some(®ion));
assert_eq!(
endpoint.url().host_str(),
Some("myaccount-westus.documents.azure.com")
);
assert_eq!(endpoint.url().port_or_known_default(), Some(443));
}
#[test]
fn endpoint_for_write_region_falls_back_when_none() {
let account = AccountReference::with_master_key(
Url::parse("https://myaccount.documents.azure.com:443/").unwrap(),
"test-key",
);
let endpoint = CosmosDriver::endpoint_for_write_region(&account, None);
assert_eq!(endpoint.url().as_str(), account.endpoint().as_str());
}
#[test]
fn parse_account_properties_uses_first_writable_and_readable_regions() {
let payload = br#"{
"_self": "",
"id": "test",
"_rid": "test.documents.azure.com",
"media": "//media/",
"addresses": "//addresses/",
"_dbs": "//dbs/",
"writableLocations": [
{ "name": "West US 2", "databaseAccountEndpoint": "https://test-westus2.documents.azure.com:443/" },
{ "name": "East US", "databaseAccountEndpoint": "https://test-eastus.documents.azure.com:443/" }
],
"readableLocations": [
{ "name": "West US 2", "databaseAccountEndpoint": "https://test-westus2.documents.azure.com:443/" },
{ "name": " East US ", "databaseAccountEndpoint": "https://test-eastus.documents.azure.com:443/" }
],
"enableMultipleWriteLocations": false,
"userReplicationPolicy": { "minReplicaSetSize": 3, "maxReplicasetSize": 4 },
"userConsistencyPolicy": { "defaultConsistencyLevel": "Session" },
"systemReplicationPolicy": { "minReplicaSetSize": 3, "maxReplicasetSize": 4 },
"readPolicy": { "primaryReadCoefficient": 1, "secondaryReadCoefficient": 1 },
"queryEngineConfiguration": "{}"
}"#;
let properties = CosmosDriver::parse_account_properties_payload(payload).unwrap();
assert_eq!(properties.write_region().unwrap().as_str(), "westus2");
assert_eq!(properties.readable_regions().len(), 2);
assert_eq!(properties.readable_regions()[0].as_str(), "westus2");
assert_eq!(properties.readable_regions()[1].as_str(), "eastus");
}
#[test]
fn parse_account_properties_returns_none_when_locations_missing() {
let payload = br#"{
"_self": "",
"id": "test",
"_rid": "test.documents.azure.com",
"media": "//media/",
"addresses": "//addresses/",
"_dbs": "//dbs/",
"writableLocations": [],
"readableLocations": [],
"enableMultipleWriteLocations": false,
"userReplicationPolicy": { "minReplicaSetSize": 0, "maxReplicasetSize": 0 },
"userConsistencyPolicy": { "defaultConsistencyLevel": "Session" },
"systemReplicationPolicy": { "minReplicaSetSize": 0, "maxReplicasetSize": 0 },
"readPolicy": { "primaryReadCoefficient": 0, "secondaryReadCoefficient": 0 },
"queryEngineConfiguration": "{}"
}"#;
let properties = CosmosDriver::parse_account_properties_payload(payload).unwrap();
assert!(properties.write_region().is_none());
assert!(properties.readable_regions().is_empty());
}
#[test]
#[cfg(feature = "reqwest")]
fn http2_reason_http11_required_triggers_http11_downgrade() {
let error = crate::error::CosmosError::builder()
.with_status(crate::models::CosmosStatus::TRANSPORT_HTTP2_INCOMPATIBLE)
.with_message("http2 not supported")
.with_source(h2::Error::from(h2::Reason::HTTP_1_1_REQUIRED))
.build();
assert!(CosmosDriver::should_downgrade_http2(
TransportHttpVersion::Http2,
&error,
true,
));
}
#[test]
fn connection_error_without_http2_signal_does_not_trigger_downgrade() {
let error = crate::error::CosmosError::builder()
.with_status(crate::models::CosmosStatus::TRANSPORT_CONNECTION_FAILED)
.with_message("connect failed")
.build();
assert!(!CosmosDriver::should_downgrade_http2(
TransportHttpVersion::Http2,
&error,
true,
));
}
#[test]
fn io_error_without_http2_signal_does_not_trigger_downgrade() {
let error = crate::error::CosmosError::builder()
.with_status(crate::models::CosmosStatus::TRANSPORT_IO_FAILED)
.with_message("socket reset")
.build();
assert!(!CosmosDriver::should_downgrade_http2(
TransportHttpVersion::Http2,
&error,
true,
));
}
#[test]
fn http11_errors_do_not_trigger_probe_back_to_http2() {
let error = crate::error::CosmosError::builder()
.with_status(crate::models::CosmosStatus::TRANSPORT_CONNECTION_FAILED)
.with_message("connect failed")
.build();
assert!(!CosmosDriver::should_downgrade_http2(
TransportHttpVersion::Http11,
&error,
true,
));
}
#[test]
fn downgrade_requires_http2_to_be_enabled() {
let error = crate::error::CosmosError::builder()
.with_status(crate::models::CosmosStatus::TRANSPORT_CONNECTION_FAILED)
.with_message("connect failed")
.build();
assert!(!CosmosDriver::should_downgrade_http2(
TransportHttpVersion::Http2,
&error,
false,
));
}
#[test]
fn alternate_http_version_switches_between_http11_and_http2() {
assert_eq!(
CosmosDriver::alternate_http_version(TransportHttpVersion::Http11),
TransportHttpVersion::Http2
);
assert_eq!(
CosmosDriver::alternate_http_version(TransportHttpVersion::Http2),
TransportHttpVersion::Http11
);
}
#[test]
fn build_metadata_transport_for_version_uses_emulator_transport_selection() {
let connection_pool = ConnectionPoolOptions::builder()
.with_emulator_server_cert_validation(
crate::options::EmulatorServerCertValidation::DangerousDisabled,
)
.build()
.unwrap();
let factory = Arc::new(ScriptedFactory::new([
ResponsePlan::Success,
ResponsePlan::Success,
]));
let endpoint = AccountEndpoint::try_from("https://localhost:8081/").unwrap();
let _ = CosmosDriver::build_metadata_transport_for_version(
&connection_pool,
factory.clone(),
TransportHttpVersion::Http11,
&endpoint,
)
.unwrap();
assert!(factory.configs().iter().any(|config| {
matches!(config.version_policy, HttpVersionPolicy::Http11Only)
&& config.allow_invalid_cert
}));
}
#[tokio::test]
async fn fetch_initial_account_properties_falls_back_to_http11_for_emulator_accounts() {
let factory = Arc::new(ScriptedFactory::new([
ResponsePlan::Success, ResponsePlan::Success, ResponsePlan::Http2Incompatible, ResponsePlan::Success, ResponsePlan::Success, ResponsePlan::Success, ]));
let runtime = CosmosDriverRuntimeBuilder::new()
.with_connection_pool(
ConnectionPoolOptions::builder()
.with_emulator_server_cert_validation(
crate::options::EmulatorServerCertValidation::DangerousDisabled,
)
.build()
.unwrap(),
)
.with_http_client_factory(factory.clone())
.build()
.await
.unwrap();
let account = signed_test_account("https://localhost:8081/");
let (version, properties) =
CosmosDriver::fetch_initial_account_properties(&runtime, &account)
.await
.unwrap();
assert_eq!(version, TransportHttpVersion::Http11);
assert_eq!(properties.write_region().unwrap().as_str(), "westus2");
assert!(factory.configs().iter().any(|config| {
matches!(config.version_policy, HttpVersionPolicy::Http11Only)
&& config.allow_invalid_cert
}));
}
#[tokio::test]
async fn refresh_account_properties_restores_http2_after_http11_success() {
let factory = Arc::new(ScriptedFactory::new([ResponsePlan::Success]));
let runtime = CosmosDriverRuntimeBuilder::new()
.with_http_client_factory(factory)
.build()
.await
.unwrap();
let account = signed_test_account("https://test.documents.azure.com:443/");
let current_transport = Arc::new(
CosmosTransport::with_factory(
runtime.connection_pool().clone(),
Arc::clone(runtime.http_client_factory()),
TransportHttpVersion::Http11,
)
.unwrap(),
);
let transport_holder = Arc::new(ArcSwap::from(current_transport));
let properties =
CosmosDriver::refresh_account_properties(&runtime, &account, &transport_holder, None)
.await
.unwrap();
assert_eq!(properties.write_region().unwrap().as_str(), "westus2");
assert_eq!(
transport_holder.load().negotiated_version(),
TransportHttpVersion::Http2
);
}
#[tokio::test]
async fn refresh_account_properties_keeps_http11_when_http2_reprobe_fails() {
let factory = Arc::new(ScriptedFactory::new([
ResponsePlan::Http2Incompatible,
ResponsePlan::Success,
ResponsePlan::Success,
ResponsePlan::Success,
]));
let runtime = CosmosDriverRuntimeBuilder::new()
.with_http_client_factory(factory)
.build()
.await
.unwrap();
let account = signed_test_account("https://test.documents.azure.com:443/");
let current_transport = Arc::new(
CosmosTransport::with_factory(
runtime.connection_pool().clone(),
Arc::clone(runtime.http_client_factory()),
TransportHttpVersion::Http11,
)
.unwrap(),
);
let transport_holder = Arc::new(ArcSwap::from(current_transport));
let properties =
CosmosDriver::refresh_account_properties(&runtime, &account, &transport_holder, None)
.await
.unwrap();
assert_eq!(properties.write_region().unwrap().as_str(), "westus2");
assert_eq!(
transport_holder.load().negotiated_version(),
TransportHttpVersion::Http11
);
}
#[tokio::test]
async fn refresh_account_properties_downgrades_to_http11_after_http2_incompatibility() {
let factory = Arc::new(ScriptedFactory::new([
ResponsePlan::Success,
ResponsePlan::Success,
ResponsePlan::Http2Incompatible,
ResponsePlan::Success,
ResponsePlan::Success,
]));
let runtime = CosmosDriverRuntimeBuilder::new()
.with_http_client_factory(factory)
.build()
.await
.unwrap();
let account = signed_test_account("https://test.documents.azure.com:443/");
let current_transport = Arc::new(
CosmosTransport::with_factory(
runtime.connection_pool().clone(),
Arc::clone(runtime.http_client_factory()),
TransportHttpVersion::Http2,
)
.unwrap(),
);
let transport_holder = Arc::new(ArcSwap::from(current_transport));
let properties =
CosmosDriver::refresh_account_properties(&runtime, &account, &transport_holder, None)
.await
.unwrap();
assert_eq!(properties.write_region().unwrap().as_str(), "westus2");
assert_eq!(
transport_holder.load().negotiated_version(),
TransportHttpVersion::Http11
);
}
#[allow(dead_code, unreachable_code, unused_variables)]
fn _assert_functions_are_send() {
fn assert_send<T: Send>(_: T) {}
let driver: &CosmosDriver = todo!();
assert_send(driver.execute_operation(todo!(), todo!()));
assert_send(driver.execute_singleton_operation(todo!(), todo!()));
assert_send(driver.execute_plan(todo!(), todo!(), todo!()));
assert_send(driver.plan_operation(todo!(), todo!(), todo!()));
}
const MULTI_REGION_ACCOUNT_PROPERTIES: &str = r#"{
"_self": "",
"id": "test",
"_rid": "test.documents.azure.com",
"media": "//media/",
"addresses": "//addresses/",
"_dbs": "//dbs/",
"writableLocations": [
{ "name": "West US 2", "databaseAccountEndpoint": "https://test-westus2.documents.azure.com:443/" }
],
"readableLocations": [
{ "name": "West US 2", "databaseAccountEndpoint": "https://test-westus2.documents.azure.com:443/" },
{ "name": "East US", "databaseAccountEndpoint": "https://test-eastus.documents.azure.com:443/" }
],
"enableMultipleWriteLocations": false,
"userReplicationPolicy": { "minReplicaSetSize": 3, "maxReplicasetSize": 4 },
"userConsistencyPolicy": { "defaultConsistencyLevel": "Session" },
"systemReplicationPolicy": { "minReplicaSetSize": 3, "maxReplicasetSize": 4 },
"readPolicy": { "primaryReadCoefficient": 1, "secondaryReadCoefficient": 1 },
"queryEngineConfiguration": "{}"
}"#;
fn multi_region_previous_props() -> Arc<CachedAccountProperties> {
Arc::new(serde_json::from_str(MULTI_REGION_ACCOUNT_PROPERTIES).unwrap())
}
#[test]
fn effective_partition_key_range_override_sets_feed_range() {
let range = crate::models::FeedRange::new(
EffectivePartitionKey::from("10"),
EffectivePartitionKey::from("20"),
)
.unwrap();
let overrides = request_target_overrides(
RequestTarget::effective_partition_key_range(
range.clone(),
"merged".to_string(),
crate::models::FeedRange::new(
EffectivePartitionKey::from("00"),
EffectivePartitionKey::from("40"),
)
.unwrap(),
),
Some("ct".to_string()),
);
assert_eq!(overrides.partition_key_range_id.as_deref(), Some("merged"));
assert_eq!(overrides.continuation.as_deref(), Some("ct"));
assert_eq!(overrides.feed_range, Some(range));
}
#[test]
fn effective_partition_key_range_override_omits_exact_feed_range() {
let range = crate::models::FeedRange::new(
EffectivePartitionKey::from("10"),
EffectivePartitionKey::from("20"),
)
.unwrap();
let overrides = request_target_overrides(
RequestTarget::effective_partition_key_range(
range.clone(),
"pkrange".to_string(),
range,
),
None,
);
assert_eq!(overrides.partition_key_range_id.as_deref(), Some("pkrange"));
assert_eq!(overrides.feed_range, None);
}
#[tokio::test]
async fn refresh_falls_back_to_regional_endpoints_when_primary_fails() {
let factory = Arc::new(ScriptedFactory::new([
ResponsePlan::ConnectionError, ResponsePlan::ConnectionError, ResponsePlan::Success, ]));
let runtime = CosmosDriverRuntimeBuilder::new()
.with_http_client_factory(factory)
.build()
.await
.unwrap();
let account = signed_test_account("https://test.documents.azure.com:443/");
let current_transport = Arc::new(
CosmosTransport::with_factory(
runtime.connection_pool().clone(),
Arc::clone(runtime.http_client_factory()),
TransportHttpVersion::Http2,
)
.unwrap(),
);
let transport_holder = Arc::new(ArcSwap::from(current_transport));
let result = CosmosDriver::refresh_account_properties(
&runtime,
&account,
&transport_holder,
Some(multi_region_previous_props()),
)
.await;
assert!(
result.is_ok(),
"should succeed via regional fallback: {:?}",
result.err()
);
}
#[tokio::test]
async fn refresh_returns_primary_error_when_all_endpoints_fail() {
let factory = Arc::new(ScriptedFactory::new(std::iter::repeat_n(
ResponsePlan::ConnectionError,
20,
)));
let runtime = CosmosDriverRuntimeBuilder::new()
.with_http_client_factory(factory)
.build()
.await
.unwrap();
let account = signed_test_account("https://test.documents.azure.com:443/");
let current_transport = Arc::new(
CosmosTransport::with_factory(
runtime.connection_pool().clone(),
Arc::clone(runtime.http_client_factory()),
TransportHttpVersion::Http2,
)
.unwrap(),
);
let transport_holder = Arc::new(ArcSwap::from(current_transport));
let result = CosmosDriver::refresh_account_properties(
&runtime,
&account,
&transport_holder,
Some(multi_region_previous_props()),
)
.await;
assert!(result.is_err(), "should fail when all endpoints exhausted");
}
#[tokio::test]
async fn refresh_skips_regional_fallback_without_previous_props() {
let factory = Arc::new(ScriptedFactory::new(std::iter::repeat_n(
ResponsePlan::ConnectionError,
20,
)));
let runtime = CosmosDriverRuntimeBuilder::new()
.with_http_client_factory(factory)
.build()
.await
.unwrap();
let account = signed_test_account("https://test.documents.azure.com:443/");
let current_transport = Arc::new(
CosmosTransport::with_factory(
runtime.connection_pool().clone(),
Arc::clone(runtime.http_client_factory()),
TransportHttpVersion::Http2,
)
.unwrap(),
);
let transport_holder = Arc::new(ArcSwap::from(current_transport));
let result =
CosmosDriver::refresh_account_properties(&runtime, &account, &transport_holder, None)
.await;
assert!(result.is_err(), "should fail without previous props");
}
#[tokio::test]
async fn fetch_account_properties_surfaces_5xx_body_as_status_error() {
let client: Arc<dyn TransportClient> = Arc::new(ScriptedClient {
plan: ResponsePlan::ServiceUnavailable503,
});
let transport =
crate::driver::transport::adaptive_transport::AdaptiveTransport::Gateway(client);
let runtime = CosmosDriverRuntimeBuilder::new().build().await.unwrap();
let account = signed_test_account("https://test.documents.azure.com:443/");
let user_agent = azure_core::http::headers::HeaderValue::from("cosmos-driver-test/0.0.0");
let err = CosmosDriver::fetch_account_properties_with_transport(
&runtime,
&transport,
&account,
None,
&user_agent,
)
.await
.expect_err(
"503 ServiceUnavailable response with a non-empty JSON envelope must surface as an error",
);
let status = err.status();
let rendered = format!("{err:?}");
assert_ne!(
status,
crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID,
"5xx body must NOT be reported as a deserialization failure; \
expected an upstream-status error (e.g. 503 ServiceUnavailable). \
Got status={status:?} err={rendered}"
);
assert!(
!rendered.contains("missing field `_self`"),
"the user-visible error must not leak the internal \
`missing field \\`_self\\`` serde detail. Got: {rendered}"
);
assert_eq!(
u16::from(status.status_code()),
503,
"the surfaced error should reflect the upstream HTTP 503 status. \
Got status={status:?} err={rendered}"
);
assert_eq!(
status.sub_status(),
None,
"no x-ms-substatus header should remain None, not Some(0). Got: {status:?}"
);
let diag = err.diagnostics().expect(
"Wire-attached diagnostics must be present once the metadata fetch is enveloped",
);
assert_eq!(
diag.requests().len(),
1,
"single bootstrap request must produce exactly one request record. Got: {diag:?}"
);
let req = &diag.requests()[0];
assert_eq!(
u16::from(req.status().status_code()),
503,
"request diagnostics must echo the upstream HTTP 503. Got: {req:?}"
);
assert!(
req.endpoint().contains("test.documents.azure.com"),
"request diagnostics must record the regional endpoint contacted. Got: {req:?}"
);
assert!(
err.response().is_some(),
"with_response_parts + with_diagnostics must promote the error to Wire, exposing response(). Got: {err:?}"
);
}
#[derive(Debug)]
struct RawResponseClient {
status: u16,
body: Vec<u8>,
}
#[async_trait]
impl TransportClient for RawResponseClient {
async fn send(&self, _request: &HttpRequest) -> Result<HttpResponse, TransportError> {
Ok(HttpResponse {
status: self.status,
headers: Headers::new(),
body: self.body.clone(),
})
}
}
async fn drive_fetch_with(
status: u16,
body: Vec<u8>,
) -> std::result::Result<crate::driver::cache::AccountProperties, crate::error::CosmosError>
{
let client: Arc<dyn TransportClient> = Arc::new(RawResponseClient { status, body });
let transport =
crate::driver::transport::adaptive_transport::AdaptiveTransport::Gateway(client);
let runtime = CosmosDriverRuntimeBuilder::new().build().await.unwrap();
let account = signed_test_account("https://test.documents.azure.com:443/");
let user_agent = azure_core::http::headers::HeaderValue::from("cosmos-driver-test/0.0.0");
CosmosDriver::fetch_account_properties_with_transport(
&runtime,
&transport,
&account,
None,
&user_agent,
)
.await
}
#[tokio::test]
async fn fetch_account_properties_surfaces_aad_401_envelope() {
let body =
br#"{"code":"Unauthorized","message":"The input authorization token can't serve the request."}"#
.to_vec();
let err = drive_fetch_with(401, body)
.await
.expect_err("401 must surface as an error");
let status = err.status();
let rendered = format!("{err:?}");
assert_ne!(
status,
crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID,
"401 AAD envelope must not be relabeled as a serde failure. Got: {rendered}"
);
assert_eq!(
u16::from(status.status_code()),
401,
"expected the upstream HTTP 401 to be preserved. Got status={status:?} err={rendered}"
);
assert!(
!rendered.contains("missing field `_self`"),
"must not leak the internal serde `missing field _self` detail. Got: {rendered}"
);
}
#[tokio::test]
async fn fetch_account_properties_surfaces_plain_text_non_2xx_body() {
let err = drive_fetch_with(502, b"Bad Gateway - injected upstream proxy fault".to_vec())
.await
.expect_err("502 must surface as an error");
let status = err.status();
let rendered = format!("{err:?}");
assert_ne!(
status,
crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID,
"plain-text non-2xx body must not be relabeled as a serde failure. Got: {rendered}"
);
assert_eq!(
u16::from(status.status_code()),
502,
"expected upstream HTTP 502 to be preserved. Got status={status:?} err={rendered}"
);
let payload = err
.wire_payload()
.expect("non-2xx must attach the upstream wire payload for correlation");
let body_text = match payload.body() {
crate::models::ResponseBody::Bytes(b) => std::str::from_utf8(b).unwrap_or_default(),
_ => "",
};
assert!(
body_text.contains("Bad Gateway"),
"wire_payload() must preserve the upstream body verbatim. Got: {body_text}"
);
}
#[tokio::test]
async fn fetch_account_properties_surfaces_empty_non_2xx_body() {
let err = drive_fetch_with(503, Vec::new())
.await
.expect_err("503 with empty body must still surface as an error");
let status = err.status();
let rendered = format!("{err:?}");
assert_ne!(
status,
crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID,
"empty non-2xx body must not be relabeled as a serde failure. Got: {rendered}"
);
assert_eq!(
u16::from(status.status_code()),
503,
"expected upstream HTTP 503 to be preserved. Got status={status:?} err={rendered}"
);
}
#[tokio::test]
async fn fetch_account_properties_preserves_large_non_2xx_body_via_wire_payload() {
let mut body = vec![b'A'; 600];
body.extend_from_slice(b"TAIL_SENTINEL");
let err = drive_fetch_with(500, body.clone())
.await
.expect_err("500 must surface as an error");
let rendered = format!("{err}");
assert!(
!rendered.contains("…[truncated]"),
"error message must no longer embed a body excerpt or truncation marker. Got: {rendered}"
);
let payload = err
.wire_payload()
.expect("non-2xx must attach the wire payload");
let body_bytes: &[u8] = match payload.body() {
crate::models::ResponseBody::Bytes(b) => b.as_ref(),
_ => &[],
};
assert_eq!(
body_bytes.len(),
body.len(),
"wire_payload() must preserve the full upstream body verbatim"
);
assert!(
body_bytes.ends_with(b"TAIL_SENTINEL"),
"wire_payload() must not truncate the tail of the body"
);
assert_eq!(
u16::from(err.status().status_code()),
500,
"upstream HTTP 500 must still be preserved alongside the body"
);
}
#[tokio::test]
async fn fetch_account_properties_handles_non_ascii_body_without_panicking() {
let mut body = vec![b'A'; 511];
body.extend_from_slice("é".as_bytes());
body.extend_from_slice(b"tail");
let err = drive_fetch_with(500, body.clone())
.await
.expect_err("500 must surface as an error");
let payload = err
.wire_payload()
.expect("non-2xx must attach the wire payload");
let body_bytes: &[u8] = match payload.body() {
crate::models::ResponseBody::Bytes(b) => b.as_ref(),
_ => &[],
};
assert_eq!(
body_bytes,
body.as_slice(),
"multi-byte codepoints must round-trip through wire_payload() unchanged"
);
}
#[tokio::test]
async fn fetch_account_properties_2xx_invalid_body_still_reports_serialization_error() {
let err = drive_fetch_with(200, br#"{"unexpected":"shape"}"#.to_vec())
.await
.expect_err("2xx with non-AccountProperties body must still error");
assert_eq!(
err.status(),
crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID,
"2xx parse failures must continue to be classified as \
SERIALIZATION_RESPONSE_BODY_INVALID (the status-gating fix only \
changes the non-2xx branch). Got: {err:?}"
);
assert!(
err.wire_payload().is_some(),
"parse-failure branch must still attach CosmosResponseHeaders / payload. Got: {err:?}"
);
assert!(
err.diagnostics().is_some(),
"parse-failure branch must also carry diagnostics now that the bootstrap fetch is enveloped. Got: {err:?}"
);
let diagnostics = err.diagnostics().expect("diagnostics attached above");
assert_eq!(
diagnostics.status(),
Some(&crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID),
"operation_status must reflect the synthetic serialization status, not the wire 200. \
Otherwise diagnostics consumers see an HTTP 200 alongside a parse error. Got: {:?}",
diagnostics.status()
);
}
#[tokio::test]
async fn fetch_account_properties_surfaces_3xx_as_non_success_with_wire_payload() {
let body = br#"<html><body>Moved</body></html>"#.to_vec();
let err = drive_fetch_with(307, body.clone())
.await
.expect_err("3xx must surface as an error — the bootstrap fetch must not parse a redirect body as AccountProperties");
assert_ne!(
err.status(),
crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID,
"3xx must NOT be reclassified as a deserialization failure (the parse must be skipped). Got: {err:?}"
);
assert_eq!(
u16::from(err.status().status_code()),
307,
"the surfaced error must reflect the upstream redirect status. Got: {err:?}"
);
let payload = err
.wire_payload()
.expect("3xx must attach the wire payload so callers can inspect the redirect body");
match payload.body() {
crate::models::ResponseBody::Bytes(b) => {
assert_eq!(
b.as_ref(),
body.as_slice(),
"redirect body must round-trip through wire_payload() unchanged"
);
}
other => panic!("expected Bytes payload, got: {other:?}"),
}
assert!(
err.diagnostics().is_some(),
"3xx must also carry diagnostics, matching every other status-error path. Got: {err:?}"
);
}
#[tokio::test]
async fn fetch_account_properties_transport_error_produces_diagnostics() {
#[derive(Debug)]
struct FailingTransportClient;
#[async_trait]
impl TransportClient for FailingTransportClient {
async fn send(&self, _request: &HttpRequest) -> Result<HttpResponse, TransportError> {
let err = crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::TRANSPORT_CONNECTION_FAILED)
.with_message("connection refused")
.build();
Err(TransportError::new(
err,
crate::diagnostics::RequestSentStatus::Sent,
))
}
}
let client: Arc<dyn TransportClient> = Arc::new(FailingTransportClient);
let transport =
crate::driver::transport::adaptive_transport::AdaptiveTransport::Gateway(client);
let runtime = CosmosDriverRuntimeBuilder::new().build().await.unwrap();
let account = signed_test_account("https://test.documents.azure.com:443/");
let user_agent = azure_core::http::headers::HeaderValue::from("cosmos-driver-test/0.0.0");
let err = CosmosDriver::fetch_account_properties_with_transport(
&runtime,
&transport,
&account,
None,
&user_agent,
)
.await
.expect_err("transport-layer failure must surface as an error");
let diag = err.diagnostics().expect(
"transport-error path must attach diagnostics so off-pipeline failures stay debuggable",
);
assert_eq!(
diag.requests().len(),
1,
"single bootstrap request must produce exactly one request record. Got: {diag:?}"
);
let req = &diag.requests()[0];
assert!(
req.endpoint().contains("test.documents.azure.com"),
"request diagnostics must record the endpoint contacted. Got: {req:?}"
);
assert_eq!(
req.request_sent(),
crate::diagnostics::RequestSentStatus::Sent,
"transport.send returned an error after invocation; the request reached the wire side. Got: {req:?}"
);
}
#[tokio::test]
async fn fetch_account_properties_sign_failure_produces_diagnostics_not_sent() {
use azure_core::credentials::{AccessToken, TokenCredential, TokenRequestOptions};
#[derive(Debug)]
struct BrokenCredential;
#[async_trait]
impl TokenCredential for BrokenCredential {
async fn get_token(
&self,
_scopes: &[&str],
_options: Option<TokenRequestOptions<'_>>,
) -> azure_core::Result<AccessToken> {
Err(azure_core::Error::with_message(
azure_core::error::ErrorKind::Credential,
"broken credential",
))
}
}
let client: Arc<dyn TransportClient> = Arc::new(ScriptedClient {
plan: ResponsePlan::Success,
});
let transport =
crate::driver::transport::adaptive_transport::AdaptiveTransport::Gateway(client);
let runtime = CosmosDriverRuntimeBuilder::new().build().await.unwrap();
let account = AccountReference::with_credential(
Url::parse("https://test.documents.azure.com:443/").unwrap(),
Arc::new(BrokenCredential),
);
let user_agent = azure_core::http::headers::HeaderValue::from("cosmos-driver-test/0.0.0");
let err = CosmosDriver::fetch_account_properties_with_transport(
&runtime,
&transport,
&account,
None,
&user_agent,
)
.await
.expect_err("sign_request failure must surface as an error");
let diag = err.diagnostics().expect(
"sign-failure path must attach diagnostics so credential/IMDS failures stay debuggable",
);
assert_eq!(
diag.requests().len(),
1,
"single bootstrap request entry must exist even when sign fails. Got: {diag:?}"
);
let req = &diag.requests()[0];
assert_eq!(
req.request_sent(),
crate::diagnostics::RequestSentStatus::NotSent,
"sign_request runs before transport.send; the request must be recorded as NotSent. Got: {req:?}"
);
assert!(
req.endpoint().contains("test.documents.azure.com"),
"request diagnostics must record the endpoint that would have been contacted. Got: {req:?}"
);
}
#[tokio::test]
async fn bootstrap_transport_follows_3xx_redirects_against_real_server() {
use std::sync::atomic::{AtomicU32, Ordering as AtomicOrdering};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let port = addr.port();
let request_count = Arc::new(AtomicU32::new(0));
let counter = Arc::clone(&request_count);
let server = tokio::spawn(async move {
for _ in 0..2 {
let Ok((mut socket, _peer)) = listener.accept().await else {
return;
};
let mut buf = [0u8; 8192];
let mut read = 0;
loop {
let n = match socket.read(&mut buf[read..]).await {
Ok(0) | Err(_) => break,
Ok(n) => n,
};
read += n;
if buf[..read].windows(4).any(|w| w == b"\r\n\r\n") {
break;
}
if read == buf.len() {
break;
}
}
let request = std::str::from_utf8(&buf[..read]).unwrap_or("");
let request_line = request.lines().next().unwrap_or("");
let n = counter.fetch_add(1, AtomicOrdering::SeqCst);
let response = if n == 0 {
assert!(
request_line.starts_with("GET / "),
"first request must hit the root path; got: {request_line:?}"
);
format!(
"HTTP/1.1 307 Temporary Redirect\r\n\
Location: http://127.0.0.1:{port}/follow\r\n\
Content-Length: 5\r\n\
Connection: close\r\n\
\r\n\
MOVED"
)
} else {
assert!(
request_line.starts_with("GET /follow "),
"redirected request must hit /follow; got: {request_line:?}"
);
format!(
"HTTP/1.1 200 OK\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
Connection: close\r\n\
\r\n\
{}",
ACCOUNT_PROPERTIES_PAYLOAD.len(),
ACCOUNT_PROPERTIES_PAYLOAD,
)
};
let _ = socket.write_all(response.as_bytes()).await;
let _ = socket.shutdown().await;
}
});
let pool = ConnectionPoolOptions::default();
let config = HttpClientConfig {
version_policy: HttpVersionPolicy::Http11Only,
request_timeout: std::time::Duration::from_secs(5),
allow_invalid_cert: false,
http2_keep_alive_while_idle: false,
};
let transport_client =
crate::driver::transport::http_client_factory::DefaultHttpClientFactory::new()
.build(&pool, config)
.expect("DefaultHttpClientFactory must build a real reqwest-backed transport");
let transport = crate::driver::transport::adaptive_transport::AdaptiveTransport::Gateway(
transport_client,
);
let runtime = CosmosDriverRuntimeBuilder::new().build().await.unwrap();
let account = signed_test_account(&format!("http://127.0.0.1:{port}/"));
let user_agent = azure_core::http::headers::HeaderValue::from("cosmos-driver-test/0.0.0");
let result = CosmosDriver::fetch_account_properties_with_transport(
&runtime,
&transport,
&account,
None,
&user_agent,
)
.await;
let _ = server.await;
let final_count = request_count.load(AtomicOrdering::SeqCst);
let props = result.unwrap_or_else(|err| panic!(
"bootstrap fetch must succeed against a redirecting proxy that returns 307 -> 200 JSON; \
this proves the reqwest transport follows redirects. saw {final_count} request(s). err: {err:?}"
));
assert_eq!(
props.id, "test",
"fetched AccountProperties must come from the /follow hop, proving the transport followed the 307"
);
assert_eq!(
final_count, 2,
"transport must have made exactly 2 wire requests (initial + one redirect follow). Got: {final_count}"
);
}
}