Skip to content

Commit 1999560

Browse files
authored
cosmos: Add RoutingStrategy::PreferredRegions (#4485)
This adds the `PreferredRegions` routing strategy, which just accepts a flat list of regions and prioritizes read/write regions using that list. This is the same logic used by other SDKs when "Preferred Regions" is set.
1 parent 0837166 commit 1999560

5 files changed

Lines changed: 95 additions & 33 deletions

File tree

sdk/cosmos/azure_data_cosmos/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features Added
66

7+
- Added `RegionStrategy::PreferredRegions` to allow specifying a fixed region preference order for failover, hedging, and retry. ([#4485](https://github.com/Azure/azure-sdk-for-rust/pull/4485))
78
- Standardized every client-method options type with a public `operation: OperationOptions` field and `with_operation_options(OperationOptions) -> Self` setter, so any per-request `OperationOptions` setting can be configured via any options type. The following options types previously had no way to attach `OperationOptions` and now do: `ReadContainerOptions`, `ReadDatabaseOptions`, `ReplaceContainerOptions`, `CreateContainerOptions`, `CreateDatabaseOptions`, `DeleteContainerOptions`, `DeleteDatabaseOptions`, `QueryContainersOptions`, `QueryDatabasesOptions`, `ThroughputOptions`, `ReadFeedRangesOptions`. For `CreateContainerOptions` / `CreateDatabaseOptions` / `ReplaceContainerOptions`, the SDK still forces `content_response_on_write = Enabled` on the resolved options because control-plane mutations require the response body. `ReadFeedRangesOptions::operation` is currently inert (the underlying routing-map cache does not go through the operation pipeline) but is added for shape consistency with the other options types.
89
- Added `new()` constructors and `with_x` consuming setters to multi-required-field model types so callers can build them declaratively without struct-literal syntax (which is now blocked by `#[non_exhaustive]`): `VectorEmbedding::new(path, data_type, dimensions, distance_function)` + `with_path` / `with_data_type` / `with_dimensions` / `with_distance_function`; `ConflictResolutionPolicy::new(mode)` + `with_resolution_path` / `with_resolution_procedure`; `SpatialIndex::new(path)` + `with_type` (singular pusher onto `types`); `CompositeIndexProperty::new(path, order)` + `with_path` / `with_order`; `VectorIndex::new(path, index_type)` + `with_path` / `with_index_type`. These types do **not** implement `Default` — their constructors require values that have no meaningful default.
910
- Derived `Default` on `VectorEmbeddingPolicy`, `UniqueKeyPolicy`, `UniqueKey`, `PropertyPath`, and `CompositeIndex`, and added singular `with_x` pushers / setters: `VectorEmbeddingPolicy::with_embedding`, `UniqueKeyPolicy::with_unique_key`, `UniqueKey::with_path`, `PropertyPath::with_path`, and `CompositeIndex::with_property`. This matches the existing `IndexingPolicy::with_included_path` style and lets callers build these policies declaratively without constructing intermediate `Vec`s.

sdk/cosmos/azure_data_cosmos/src/clients/cosmos_client.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ impl CosmosClient {
8787
pub fn builder() -> CosmosClientBuilder {
8888
CosmosClientBuilder::new()
8989
}
90+
9091
/// Gets a [`DatabaseClient`] that can be used to access the database with the specified ID.
9192
///
9293
/// # Arguments

sdk/cosmos/azure_data_cosmos/src/clients/cosmos_client_builder.rs

Lines changed: 83 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -277,17 +277,10 @@ impl CosmosClientBuilder {
277277
///
278278
/// Returns an error if the client cannot be constructed.
279279
pub async fn build(
280-
mut self,
280+
self,
281281
account: AccountReference,
282282
routing_strategy: RoutingStrategy,
283283
) -> azure_core::Result<CosmosClient> {
284-
// Apply the region selection strategy to internal options.
285-
match routing_strategy {
286-
RoutingStrategy::ProximityTo(region) => {
287-
self.options.application_region = Some(region);
288-
}
289-
}
290-
291284
let (account_endpoint, credential) = account.into_parts();
292285
let endpoint = account_endpoint.into_url();
293286

@@ -302,20 +295,6 @@ impl CosmosClientBuilder {
302295
std::sync::Arc<azure_data_cosmos_driver::fault_injection::FaultInjectionRule>,
303296
> = self.fault_injection_rules;
304297

305-
let preferred_regions = if let Some(ref region) = self.options.application_region {
306-
crate::region_proximity::generate_preferred_region_list(region)
307-
.map(|s| s.to_vec())
308-
.unwrap_or_else(|| {
309-
tracing::warn!(
310-
region = %region,
311-
"unrecognized application region; falling back to account-defined region order"
312-
);
313-
Vec::new()
314-
})
315-
} else {
316-
Vec::new()
317-
};
318-
319298
// Preserve the SDK's historical default: per-partition circuit breaker
320299
// (PPCB) is enabled unless the user explicitly opts out via
321300
// `AZURE_COSMOS_PER_PARTITION_CIRCUIT_BREAKER_ENABLED=false`. The
@@ -394,10 +373,7 @@ impl CosmosClientBuilder {
394373
})?;
395374
}
396375
let driver_runtime = driver_runtime_builder.build().await?;
397-
let driver_options =
398-
azure_data_cosmos_driver::options::DriverOptions::builder(driver_account)
399-
.with_preferred_regions(preferred_regions)
400-
.build();
376+
let driver_options = build_driver_options(driver_account, routing_strategy);
401377
let driver = driver_runtime
402378
.get_or_create_driver(driver_options.account().clone(), Some(driver_options))
403379
.await?;
@@ -408,6 +384,38 @@ impl CosmosClientBuilder {
408384
}
409385
}
410386

387+
/// Builds [`DriverOptions`](azure_data_cosmos_driver::options::DriverOptions) for the given
388+
/// account and routing strategy.
389+
///
390+
/// The routing strategy is converted to an ordered preferred-regions list:
391+
///
392+
/// - [`RoutingStrategy::ProximityTo`] expands to a proximity-sorted list of all
393+
/// Azure regions, with the specified region first. An unrecognized region logs
394+
/// a warning and falls back to an empty list, which causes the driver to use
395+
/// the account's own region order.
396+
/// - [`RoutingStrategy::PreferredRegions`] passes the caller's list through unchanged.
397+
fn build_driver_options(
398+
account: azure_data_cosmos_driver::models::AccountReference,
399+
strategy: RoutingStrategy,
400+
) -> azure_data_cosmos_driver::options::DriverOptions {
401+
let preferred_regions = match strategy {
402+
RoutingStrategy::ProximityTo(region) =>
403+
crate::region_proximity::generate_preferred_region_list(&region)
404+
.map(|s| s.to_vec())
405+
.unwrap_or_else(|| {
406+
tracing::warn!(
407+
region = %region,
408+
"unrecognized application region; falling back to account-defined region order"
409+
);
410+
Vec::new()
411+
}),
412+
RoutingStrategy::PreferredRegions(regions) => regions,
413+
};
414+
azure_data_cosmos_driver::options::DriverOptions::builder(account)
415+
.with_preferred_regions(preferred_regions)
416+
.build()
417+
}
418+
411419
/// Builds a driver [`AccountReference`](azure_data_cosmos_driver::models::AccountReference)
412420
/// from the SDK's credential and endpoint.
413421
fn build_driver_account(
@@ -427,15 +435,10 @@ fn build_driver_account(
427435
base.with_backup_endpoints(backup_endpoints)
428436
}
429437

430-
// Unit tests for routing-strategy behavior were removed because
431-
// CosmosClient::builder().build() now eagerly creates a CosmosDriver,
432-
// which requires a real endpoint. Re-add once fault injection is linked
433-
// from the SDK to the driver.
434-
435438
#[cfg(test)]
436439
mod tests {
437440
use super::*;
438-
use crate::UserAgentSuffix;
441+
use crate::{Region, UserAgentSuffix};
439442

440443
/// Reproduces the bug where `CosmosClientBuilder::with_user_agent_suffix`
441444
/// did not forward the suffix to the driver runtime, causing the
@@ -550,4 +553,52 @@ mod tests {
550553
"explicit env-var opt-out must propagate to the driver runtime"
551554
);
552555
}
556+
557+
fn test_account() -> azure_data_cosmos_driver::models::AccountReference {
558+
azure_data_cosmos_driver::models::AccountReference::with_master_key(
559+
"https://test.documents.azure.com/".parse().unwrap(),
560+
"dGVzdA==",
561+
)
562+
}
563+
564+
/// `ProximityTo` a known region produces a non-empty preferred_regions list
565+
/// with the source region first.
566+
#[test]
567+
fn proximity_to_known_region_starts_with_source() {
568+
let opts = build_driver_options(
569+
test_account(),
570+
RoutingStrategy::ProximityTo(Region::EAST_US),
571+
);
572+
let regions = opts.preferred_regions();
573+
assert!(
574+
!regions.is_empty(),
575+
"should produce a non-empty list for a known region"
576+
);
577+
assert_eq!(regions[0], Region::EAST_US, "source region should be first");
578+
}
579+
580+
/// `ProximityTo` an unrecognized region falls back to an empty preferred_regions
581+
/// list so the driver uses the account's own region order.
582+
#[test]
583+
fn proximity_to_unknown_region_returns_empty_list() {
584+
let opts = build_driver_options(
585+
test_account(),
586+
RoutingStrategy::ProximityTo(Region::from("not-a-real-region")),
587+
);
588+
assert!(
589+
opts.preferred_regions().is_empty(),
590+
"unrecognized region should yield an empty list"
591+
);
592+
}
593+
594+
/// `PreferredRegions` passes the caller's list through to the driver options unchanged.
595+
#[test]
596+
fn preferred_regions_passes_through_unchanged() {
597+
let input = vec![Region::WEST_US, Region::EAST_US, Region::WEST_EUROPE];
598+
let opts = build_driver_options(
599+
test_account(),
600+
RoutingStrategy::PreferredRegions(input.clone()),
601+
);
602+
assert_eq!(opts.preferred_regions(), input.as_slice());
603+
}
553604
}

sdk/cosmos/azure_data_cosmos/src/options/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ pub struct CosmosClientOptions {
3030
/// unless overridden by per-request options.
3131
pub(crate) operation: OperationOptions,
3232
pub(crate) user_agent_suffix: Option<UserAgentSuffix>,
33-
pub(crate) application_region: Option<Region>,
3433
}
3534

3635
impl CosmosClientOptions {

sdk/cosmos/azure_data_cosmos/src/routing_strategy.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,14 @@ pub enum RoutingStrategy {
3333
/// Specifying an unknown region name results in undefined region
3434
/// selection behavior that may change in future versions of the SDK.
3535
ProximityTo(Region),
36+
37+
/// Select target regions using a fixed preference order.
38+
///
39+
/// This list **does not** restrict which regions the SDK may select for routing,
40+
/// only the order in which it considers them. After failover exhausts the list of preferred regions,
41+
/// the SDK will arbitrarily select from other available regions.
42+
///
43+
/// If you need to prohibit the SDK from selecting certain regions,
44+
/// use [`OperationOptions::excluded_regions`](crate::options::OperationOptions::excluded_regions).
45+
PreferredRegions(Vec<Region>),
3646
}

0 commit comments

Comments
 (0)