#![cfg(feature = "fault_injection")]
use crate::framework::DriverTestClient;
use azure_data_cosmos_driver::fault_injection::{
FaultInjectionConditionBuilder, FaultInjectionErrorType, FaultInjectionResultBuilder,
FaultInjectionRuleBuilder, FaultOperationType,
};
use azure_data_cosmos_driver::options::{PartitionFailoverOptions, Region};
use std::error::Error;
use std::sync::Arc;
const HUB_REGION: Region = Region::EAST_US_2;
#[tokio::test]
#[cfg_attr(
not(test_category = "multi_write"),
ignore = "requires test_category 'multi_write'"
)]
#[ignore = "Requires the fault injection http client bug to be fixed."]
pub async fn ppcb_enabled_503_on_create_fails_over_after_threshold() -> Result<(), Box<dyn Error>> {
let condition = FaultInjectionConditionBuilder::new()
.with_operation_type(FaultOperationType::CreateItem)
.with_region(HUB_REGION)
.build();
let result = FaultInjectionResultBuilder::new()
.with_error(FaultInjectionErrorType::ServiceUnavailable)
.with_probability(1.0)
.build();
let rule = Arc::new(
FaultInjectionRuleBuilder::new("ppcb-503-create-multi-write", result)
.with_condition(condition)
.build(),
);
let rules = vec![Arc::clone(&rule)];
let partition_failover_options = PartitionFailoverOptions::builder()
.with_circuit_breaker_enabled(true)
.build()?;
DriverTestClient::run_with_unique_db_and_fault_injection_partition_failover_options(
rules,
partition_failover_options,
async |context, database| {
let container_name = context.unique_container_name();
let container = context
.create_container(&database, &container_name, "/pk")
.await?;
let mut create_success_count = 0;
let total_creates = 10;
for i in 0..total_creates {
let body = format!(
r#"{{"id": "ppcb-create-503-{i}", "pk": "pk1", "value": "test"}}"#
);
match context.create_item_with_pk(&container, "pk1", body.as_bytes()).await {
Ok(_) => create_success_count += 1,
Err(e) => {
tracing::info!("Create failed (expected during threshold ramp-up): {e}");
}
}
}
assert!(
create_success_count > 0,
"At least some creates should succeed via failover to the non-faulted write region"
);
assert!(
rule.hit_count() > 0,
"Fault injection rule should have been hit on the hub region for creates"
);
for i in 0..3 {
let body = format!(
r#"{{"id": "ppcb-create-503-post-{i}", "pk": "pk1", "value": "test"}}"#
);
let response = context
.create_item_with_pk(&container, "pk1", body.as_bytes())
.await
.expect(
"Post-threshold creates should succeed directly via the alternate write region",
);
let diagnostics = response.diagnostics();
let regions = diagnostics.regions_contacted();
assert!(
!regions.contains(&HUB_REGION),
"Create {i} after circuit breaker trip should NOT contact the hub region, \
but regions_contacted={:?}",
regions
);
}
Ok(())
},
)
.await
}
#[tokio::test]
#[cfg_attr(
not(test_category = "multi_write"),
ignore = "requires test_category 'multi_write'"
)]
#[ignore = "Requires the fault injection http client bug to be fixed."]
pub async fn ppcb_failback_to_hub_region_after_write_fault_clears() -> Result<(), Box<dyn Error>> {
use std::time::Duration;
use tokio::time::sleep;
let condition = FaultInjectionConditionBuilder::new()
.with_operation_type(FaultOperationType::CreateItem)
.with_region(HUB_REGION)
.build();
let result = FaultInjectionResultBuilder::new()
.with_error(FaultInjectionErrorType::ServiceUnavailable)
.with_probability(1.0)
.build();
let rule = Arc::new(
FaultInjectionRuleBuilder::new("ppcb-failback-503-create", result)
.with_condition(condition)
.build(),
);
let rules = vec![Arc::clone(&rule)];
const FAILBACK_SWEEP_SECS: u64 = 5;
const PARTITION_UNAVAILABILITY_SECS: u64 = 5;
let partition_failover_options = PartitionFailoverOptions::builder()
.with_circuit_breaker_enabled(true)
.with_failback_sweep_interval(std::time::Duration::from_secs(FAILBACK_SWEEP_SECS))
.with_partition_unavailability_duration(std::time::Duration::from_secs(
PARTITION_UNAVAILABILITY_SECS,
))
.build()?;
DriverTestClient::run_with_unique_db_and_fault_injection_partition_failover_options(
rules,
partition_failover_options,
async |context, database| {
let container_name = context.unique_container_name();
let container = context
.create_container(&database, &container_name, "/pk")
.await?;
let total_creates = 10;
for i in 0..total_creates {
let body = format!(
r#"{{"id": "ppcb-write-failback-trip-{i}", "pk": "pk1", "value": "test"}}"#
);
let _ = context.create_item_with_pk(&container, "pk1", body.as_bytes()).await;
}
assert!(
rule.hit_count() > 0,
"Fault injection rule should have been hit on the hub region while tripping the breaker"
);
let tripped_body =
br#"{"id": "ppcb-write-failback-tripped", "pk": "pk1", "value": "test"}"#;
let tripped_response = context
.create_item_with_pk(&container, "pk1", tripped_body)
.await
.expect("Create after breaker trip should succeed via the alternate write region");
assert!(
!tripped_response
.diagnostics()
.regions_contacted()
.contains(&HUB_REGION),
"After tripping, writes should bypass the hub region. regions_contacted={:?}",
tripped_response.diagnostics().regions_contacted()
);
rule.disable();
let wait =
Duration::from_secs((PARTITION_UNAVAILABILITY_SECS + FAILBACK_SWEEP_SECS + 5) as u64);
tracing::info!(
"Disabled fault rule; waiting {wait:?} for failback sweep to mark entry as ProbeCandidate"
);
sleep(wait).await;
let hits_before_probe = rule.hit_count();
let probe_body =
br#"{"id": "ppcb-write-failback-probe", "pk": "pk1", "value": "test"}"#;
let probe_response = context
.create_item_with_pk(&container, "pk1", probe_body)
.await
.expect("Probe write after failback sweep should succeed against the hub region");
let probe_regions = probe_response.diagnostics().regions_contacted();
assert!(
probe_regions.contains(&HUB_REGION),
"Probe write should route back to the hub region (failback). \
regions_contacted={probe_regions:?}"
);
assert_eq!(
rule.hit_count(),
hits_before_probe,
"Disabled rule must not register any additional hits during the probe"
);
for i in 0..3 {
let body = format!(
r#"{{"id": "ppcb-write-failback-steady-{i}", "pk": "pk1", "value": "test"}}"#
);
let response = context
.create_item_with_pk(&container, "pk1", body.as_bytes())
.await
.expect("Post-failback writes should succeed via normal routing");
let regions = response.diagnostics().regions_contacted();
assert!(
regions.contains(&HUB_REGION),
"Post-failback write {i} should route through the hub region. \
regions_contacted={regions:?}"
);
assert_eq!(
response.diagnostics().request_count(),
1,
"Post-failback write {i} should succeed on the first attempt (no retry needed)"
);
}
Ok(())
},
)
.await
}