use std::collections::HashMap;
use std::time::Duration;
use azure_core::http::headers::{HeaderName, HeaderValue};
use azure_data_cosmos_macros::CosmosOptions;
use crate::{
models::ThroughputControlGroupName,
options::{
AvailabilityStrategy, ContentResponseOnWrite, EndToEndOperationLatencyPolicy,
ExcludedRegions, PriorityLevel, ReadConsistencyStrategy,
},
};
#[derive(CosmosOptions, Clone, Debug)]
#[options(layers(runtime, account, operation))]
#[non_exhaustive]
pub struct OperationOptions {
#[option(env = "AZURE_COSMOS_READ_CONSISTENCY_STRATEGY")]
pub read_consistency_strategy: Option<ReadConsistencyStrategy>,
pub excluded_regions: Option<ExcludedRegions>,
#[option(env = "AZURE_COSMOS_CONTENT_RESPONSE_ON_WRITE")]
pub content_response_on_write: Option<ContentResponseOnWrite>,
#[option(nested)]
pub throughput_control: Option<ThroughputControlOptions>,
pub end_to_end_latency_policy: Option<EndToEndOperationLatencyPolicy>,
#[option(env = "AZURE_COSMOS_MAX_FAILOVER_RETRY_COUNT")]
pub max_failover_retry_count: Option<u32>,
pub endpoint_unavailability_ttl: Option<Duration>,
pub session_capturing_disabled: Option<bool>,
#[option(env = "AZURE_COSMOS_MAX_SESSION_RETRY_COUNT")]
pub max_session_retry_count: Option<u32>,
#[option(nested)]
pub throttling_retry_options: Option<ThrottlingRetryOptions>,
#[option(env = "AZURE_COSMOS_HEDGING_ENABLED", overridable)]
pub hedging_enabled: Option<bool>,
pub availability_strategy: Option<AvailabilityStrategy>,
pub custom_headers: Option<HashMap<HeaderName, HeaderValue>>,
}
#[derive(CosmosOptions, Clone, Debug)]
#[options(layers(runtime, account, operation))]
#[non_exhaustive]
pub struct ThrottlingRetryOptions {
#[option(env = "AZURE_COSMOS_MAX_THROTTLE_RETRY_COUNT")]
pub max_retry_count: Option<u32>,
pub max_retry_wait_time: Option<Duration>,
}
#[derive(CosmosOptions, Clone, Debug)]
#[options(layers(runtime, account, operation))]
#[non_exhaustive]
pub struct ThroughputControlOptions {
pub group_name: Option<ThroughputControlGroupName>,
pub throughput_bucket: Option<u32>,
pub priority_level: Option<PriorityLevel>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_operation_options() {
let options = OperationOptions::default();
assert!(options.read_consistency_strategy.is_none());
assert!(options.excluded_regions.is_none());
assert!(options.content_response_on_write.is_none());
assert!(options.throughput_control.is_none());
assert!(options.max_failover_retry_count.is_none());
assert!(options.max_session_retry_count.is_none());
}
#[test]
fn builder_creates_options() {
let throttling = ThrottlingRetryOptionsBuilder::new()
.with_max_retry_count(4)
.with_max_retry_wait_time(Duration::from_secs(12))
.build();
let options = OperationOptionsBuilder::new()
.with_content_response_on_write(ContentResponseOnWrite::Disabled)
.with_read_consistency_strategy(ReadConsistencyStrategy::Session)
.with_max_failover_retry_count(5)
.with_max_session_retry_count(3)
.with_throttling_retry_options(throttling)
.build();
assert_eq!(
options.content_response_on_write,
Some(ContentResponseOnWrite::Disabled)
);
assert_eq!(
options.read_consistency_strategy,
Some(ReadConsistencyStrategy::Session)
);
assert_eq!(options.max_failover_retry_count, Some(5));
assert_eq!(options.max_session_retry_count, Some(3));
let throttling = options
.throttling_retry_options
.expect("throttling group should be set");
assert_eq!(throttling.max_retry_count, Some(4));
assert_eq!(
throttling.max_retry_wait_time,
Some(Duration::from_secs(12))
);
}
#[test]
fn view_resolves_across_layers() {
use std::sync::Arc;
let env = Arc::new(OperationOptions {
read_consistency_strategy: Some(ReadConsistencyStrategy::Eventual),
max_failover_retry_count: Some(3),
..Default::default()
});
let runtime = Arc::new(OperationOptions {
content_response_on_write: Some(ContentResponseOnWrite::Enabled),
..Default::default()
});
let account = Arc::new(OperationOptions {
max_failover_retry_count: Some(5),
content_response_on_write: Some(ContentResponseOnWrite::Disabled),
..Default::default()
});
let operation = OperationOptions {
read_consistency_strategy: Some(ReadConsistencyStrategy::Session),
..Default::default()
};
let view =
OperationOptionsView::new(Some(env), Some(runtime), Some(account), Some(&operation));
assert_eq!(
view.read_consistency_strategy(),
Some(&ReadConsistencyStrategy::Session)
);
assert_eq!(
view.content_response_on_write(),
Some(&ContentResponseOnWrite::Disabled)
);
assert_eq!(view.max_failover_retry_count(), Some(&5));
assert!(view.excluded_regions().is_none());
assert!(view.max_session_retry_count().is_none());
}
#[test]
fn from_env_vars_parses_known_vars() {
let options = OperationOptions::from_env_vars(|key| match key {
"AZURE_COSMOS_READ_CONSISTENCY_STRATEGY" => Ok("Session".to_string()),
"AZURE_COSMOS_CONTENT_RESPONSE_ON_WRITE" => Ok("true".to_string()),
"AZURE_COSMOS_MAX_FAILOVER_RETRY_COUNT" => Ok("7".to_string()),
"AZURE_COSMOS_MAX_SESSION_RETRY_COUNT" => Ok("3".to_string()),
"AZURE_COSMOS_HEDGING_ENABLED" => Ok("false".to_string()),
_ => Err(std::env::VarError::NotPresent),
});
assert_eq!(
options.read_consistency_strategy,
Some(ReadConsistencyStrategy::Session)
);
assert_eq!(
options.content_response_on_write,
Some(ContentResponseOnWrite::Enabled)
);
assert_eq!(options.max_failover_retry_count, Some(7));
assert_eq!(options.max_session_retry_count, Some(3));
assert_eq!(options.hedging_enabled, Some(false));
assert!(options.excluded_regions.is_none());
assert!(options.throttling_retry_options.is_none());
}
#[test]
fn throttling_retry_options_from_env() {
let throttling = ThrottlingRetryOptions::from_env_vars(|key| match key {
"AZURE_COSMOS_MAX_THROTTLE_RETRY_COUNT" => Ok("4".to_string()),
_ => Err(std::env::VarError::NotPresent),
});
assert_eq!(throttling.max_retry_count, Some(4));
assert!(throttling.max_retry_wait_time.is_none());
}
#[test]
fn from_env_vars_returns_none_for_missing_vars() {
let options = OperationOptions::from_env_vars(|_| Err(std::env::VarError::NotPresent));
assert!(options.read_consistency_strategy.is_none());
assert!(options.content_response_on_write.is_none());
assert!(options.excluded_regions.is_none());
assert!(options.max_failover_retry_count.is_none());
assert!(options.max_session_retry_count.is_none());
assert!(options.availability_strategy.is_none());
assert!(options.hedging_enabled.is_none());
}
#[test]
fn builder_round_trips_availability_strategy() {
use crate::options::{HedgeThreshold, HedgingStrategy};
use std::time::Duration;
let strategy = AvailabilityStrategy::Hedging(HedgingStrategy::new(
HedgeThreshold::new(Duration::from_millis(500)).unwrap(),
));
let options = OperationOptionsBuilder::new()
.with_availability_strategy(strategy)
.build();
assert_eq!(options.availability_strategy, Some(strategy));
}
#[test]
fn builder_round_trips_disabled_availability_strategy() {
let options = OperationOptionsBuilder::new()
.with_availability_strategy(AvailabilityStrategy::Disabled)
.build();
assert_eq!(
options.availability_strategy,
Some(AvailabilityStrategy::Disabled)
);
}
#[test]
fn availability_strategy_resolves_via_view() {
use crate::options::{HedgeThreshold, HedgingStrategy};
use std::sync::Arc;
use std::time::Duration;
let account_strategy = AvailabilityStrategy::Hedging(HedgingStrategy::new(
HedgeThreshold::new(Duration::from_millis(800)).unwrap(),
));
let operation_strategy = AvailabilityStrategy::Disabled;
let account = Arc::new(OperationOptions {
availability_strategy: Some(account_strategy),
..Default::default()
});
let operation = OperationOptions {
availability_strategy: Some(operation_strategy),
..Default::default()
};
let view_op_overrides =
OperationOptionsView::new(None, None, Some(account.clone()), Some(&operation));
assert_eq!(
view_op_overrides.availability_strategy(),
Some(&operation_strategy)
);
let empty_operation = OperationOptions::default();
let view_account_wins =
OperationOptionsView::new(None, None, Some(account), Some(&empty_operation));
assert_eq!(
view_account_wins.availability_strategy(),
Some(&account_strategy)
);
}
#[test]
fn nested_throttling_retry_options_resolves_across_layers() {
use std::sync::Arc;
use std::time::Duration;
let runtime = Arc::new(OperationOptions {
throttling_retry_options: Some(ThrottlingRetryOptions {
max_retry_count: Some(9),
max_retry_wait_time: Some(Duration::from_secs(15)),
}),
..Default::default()
});
let operation = OperationOptions {
throttling_retry_options: Some(ThrottlingRetryOptions {
max_retry_count: Some(0),
max_retry_wait_time: None,
}),
..Default::default()
};
let view = OperationOptionsView::new(None, Some(runtime), None, Some(&operation));
let throttling = view.throttling_retry_options();
assert_eq!(
throttling.max_retry_count(),
Some(&0),
"operation-layer override must win over runtime layer for `max_retry_count`",
);
assert_eq!(
throttling.max_retry_wait_time(),
Some(&Duration::from_secs(15)),
"missing inner field on the operation layer must fall through to runtime",
);
}
#[test]
fn nested_throttling_retry_options_view_is_none_when_unset_at_every_layer() {
let op = OperationOptions::default();
let view = OperationOptionsView::new(None, None, None, Some(&op));
let throttling = view.throttling_retry_options();
assert!(throttling.max_retry_count().is_none());
assert!(throttling.max_retry_wait_time().is_none());
}
#[test]
fn env_override_layer_wins_over_operation_for_hedging_enabled() {
use std::sync::Arc;
let env_override = Arc::new(OperationOptions {
hedging_enabled: Some(false),
..Default::default()
});
let operation = OperationOptions {
hedging_enabled: Some(true),
..Default::default()
};
let view = OperationOptionsView::new_with_override(
Some(env_override),
None,
None,
None,
Some(&operation),
);
assert_eq!(
view.hedging_enabled(),
Some(&false),
"env_override must beat the operation layer for hedging_enabled",
);
}
#[test]
fn env_override_unset_falls_through_to_operation() {
let operation = OperationOptions {
hedging_enabled: Some(true),
..Default::default()
};
let env_override = std::sync::Arc::new(OperationOptions::default());
let view = OperationOptionsView::new_with_override(
Some(env_override),
None,
None,
None,
Some(&operation),
);
assert_eq!(view.hedging_enabled(), Some(&true));
}
#[test]
fn from_env_override_vars_reads_only_override_variants() {
let options = OperationOptions::from_env_override_vars(|key| match key {
"AZURE_COSMOS_HEDGING_ENABLED_OVERRIDE" => Ok("false".to_string()),
"AZURE_COSMOS_HEDGING_ENABLED" => Ok("true".to_string()),
_ => Err(std::env::VarError::NotPresent),
});
assert_eq!(options.hedging_enabled, Some(false));
assert!(options.availability_strategy.is_none());
}
#[test]
fn from_env_override_vars_returns_none_when_unset() {
let options =
OperationOptions::from_env_override_vars(|_| Err(std::env::VarError::NotPresent));
assert!(options.hedging_enabled.is_none());
assert!(options.availability_strategy.is_none());
}
#[test]
fn nested_throughput_control_resolves_across_layers() {
use std::sync::Arc;
let runtime = Arc::new(OperationOptions {
throughput_control: Some(ThroughputControlOptions {
group_name: Some(ThroughputControlGroupName::new("runtime-group")),
throughput_bucket: Some(7),
priority_level: Some(PriorityLevel::Low),
}),
..Default::default()
});
let operation = OperationOptions {
throughput_control: Some(ThroughputControlOptions {
group_name: None,
throughput_bucket: Some(99),
priority_level: None,
}),
..Default::default()
};
let view = OperationOptionsView::new(None, Some(runtime), None, Some(&operation));
let throughput = view.throughput_control();
assert_eq!(
throughput.group_name(),
Some(&ThroughputControlGroupName::new("runtime-group")),
"missing inner field on the operation layer must fall through to runtime",
);
assert_eq!(
throughput.throughput_bucket(),
Some(&99),
"operation-layer override must win over runtime for `throughput_bucket`",
);
assert_eq!(
throughput.priority_level(),
Some(&PriorityLevel::Low),
"missing inner field on the operation layer must fall through to runtime",
);
}
#[test]
fn nested_throughput_control_view_is_none_when_unset_at_every_layer() {
let op = OperationOptions::default();
let view = OperationOptionsView::new(None, None, None, Some(&op));
let throughput = view.throughput_control();
assert!(throughput.group_name().is_none());
assert!(throughput.throughput_bucket().is_none());
assert!(throughput.priority_level().is_none());
}
}