Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0efa7b8
Restrict cross-partition query fan-out to feed-range scope
simorenoh Jul 7, 2026
ba795b8
Add PR link to changelog entry
simorenoh Jul 7, 2026
6c81640
Make feed-range fan-out tests observe the clip
simorenoh Jul 7, 2026
ad9e832
Mirror feed-range fan-out fix in driver CHANGELOG
simorenoh Jul 7, 2026
96390fd
Pin disjoint-target error behavior with a test
simorenoh Jul 8, 2026
b97b0cf
Merge branch 'main' into simorenoh/cosmos-feedrange-query-investigation
simorenoh Jul 8, 2026
19bb6d4
Add e2e regression test for feed-range-scoped queries
simorenoh Jul 8, 2026
1d74c9d
Add feed-range test words to Cosmos cspell ignore list
simorenoh Jul 8, 2026
9f3e986
Merge branch 'main' into simorenoh/cosmos-feedrange-query-investigation
simorenoh Jul 8, 2026
6a29dde
Merge remote-tracking branch 'origin/main' into simorenoh/cosmos-feed…
simorenoh Jul 9, 2026
b817f11
Reframe #4705 changelog entries as regression tests
simorenoh Jul 9, 2026
5c9b577
Attribute feed-range bugfix changelog to #4319
simorenoh Jul 9, 2026
9d90f1a
Merge remote-tracking branch 'origin/main' into simorenoh/cosmos-feed…
simorenoh Jul 9, 2026
0d29d2e
Assert per-range results in feed-range e2e test
simorenoh Jul 9, 2026
0d3c8dc
Merge branch 'main' into simorenoh/cosmos-feedrange-query-investigation
simorenoh Jul 13, 2026
194b406
Merge remote-tracking branch 'origin/main' into simorenoh/cosmos-feed…
simorenoh Jul 13, 2026
140feda
Assert full per-range grouping in feed-range e2e test
simorenoh Jul 13, 2026
ab07e94
Hardcode per-range grouping in feed-range e2e test
simorenoh Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sdk/cosmos/.cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
"mysproc",
"myudf",
"napi",
"neighbouring",
"newtypes",
"nocoll",
"normalises",
Expand Down Expand Up @@ -289,6 +290,7 @@
"uksouth",
"ukwest",
"unavail",
"unclipped",
"uncontended",
"undrained",
"unfaulted",
Expand Down
1 change: 1 addition & 0 deletions sdk/cosmos/azure_data_cosmos/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

### Bugs Fixed

- Fixed cross-partition queries scoped to an explicit feed range (`ContainerClient::query_items` with `FeedScope::range`) fanning out to partitions outside the requested range. The query planner now intersects the query plan's ranges with the requested feed-range target, so only the partitions overlapping the requested range are queried. This prevents returning documents outside the requested feed range and the extra scans / RU usage that occurred when enumerating `read_feed_ranges()` and querying each range. ([#4705](https://github.com/Azure/azure-sdk-for-rust/pull/4705))
Comment thread
FabianMeiswinkel marked this conversation as resolved.
Outdated
- Fixed the `AZURE_COSMOS_PPCB_*` environment variables (including `AZURE_COSMOS_PPCB_ENABLED` and the `AZURE_COSMOS_PPCB_ENABLED_OVERRIDE` kill switch) being ignored when a `CosmosClient` was built without calling `CosmosClientBuilder::with_partition_failover_options`. The per-partition circuit breaker (PPCB) stayed enabled even with `AZURE_COSMOS_PPCB_ENABLED=false`. The client's driver now resolves these options from the environment when they are not supplied explicitly. ([#4655](https://github.com/Azure/azure-sdk-for-rust/pull/4655))

### Other Changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ use std::error::Error;

use azure_data_cosmos::feed::ContinuationToken;
use azure_data_cosmos::{
clients::DatabaseClient,
clients::{ContainerClient, DatabaseClient},
feed::FeedScope,
models::CosmosStatus,
models::{CosmosStatus, ThroughputProperties},
options::{MaxItemCountHint, QueryOptions},
Query,
};
Expand Down Expand Up @@ -119,6 +119,22 @@ struct ItemProjection {
merge_order: usize,
}

/// Drains `select value c.id from c` for the given scope and returns the ids.
async fn collect_ids_for_scope(
container_client: &ContainerClient,
scope: FeedScope,
) -> Result<Vec<String>, Box<dyn Error>> {
let mut pages = container_client
.query_items::<String>("select value c.id from c", scope, None)
.await?
.into_pages();
let mut ids = Vec::new();
while let Some(page) = pages.next().await {
ids.extend(page?.into_items());
}
Ok(ids)
}

#[tokio::test]
#[cfg_attr(
not(any(test_category = "emulator", test_category = "emulator_vnext")),
Expand Down Expand Up @@ -480,6 +496,73 @@ pub async fn cross_partition_query_pagination() -> Result<(), Box<dyn Error>> {
.await
}

#[tokio::test]
#[cfg_attr(
not(any(test_category = "emulator", test_category = "emulator_vnext")),
ignore = "requires test_category 'emulator' or 'emulator_vnext'"
)]
#[cfg_attr(
test_category = "emulator_vnext",
ignore = "skipped on vnext emulator: behavioral divergence"
)]
pub async fn feed_range_scoped_query_honors_range() -> Result<(), Box<dyn Error>> {
TestClient::run_with_unique_db(
async |_, db_client| {
// 10 logical partitions × 2 items. Provision 11000 RU/s so the
// service creates at least 2 physical partitions — a scoped query
// then has a neighbouring range it must NOT touch.
let items = test_data::generate_mock_items(10, 2);
let throughput = ThroughputProperties::manual(11000);
let container_client =
test_data::create_container_with_items(db_client, items.clone(), Some(throughput))
.await?;

let ranges = container_client.read_feed_ranges(None).await?;
assert!(
ranges.len() >= 2,
"expected at least 2 physical partitions with 11000 RU/s, got {}",
ranges.len()
);

// Baseline: the full container returns every seeded item.
let mut all_ids =
collect_ids_for_scope(&container_client, FeedScope::full_container()).await?;
all_ids.sort();
assert_eq!(
all_ids.len(),
items.len(),
"full-container query should return every seeded item"
);

// Enumerate each feed range and query it in isolation — exactly the
// customer scenario (`read_feed_ranges()` then query each range).
// Before the fan-out clip fix, a `FeedScope::range` query was planned
// as a full cross-partition fan-out, so every range returned the
// ENTIRE container and the union below contained each id
// `ranges.len()` times. With the fix each range is clipped to its
// own slice, so every id appears exactly once across all ranges.
let mut union_ids = Vec::new();
for range in &ranges {
let ids = collect_ids_for_scope(&container_client, FeedScope::range(range.clone()))
.await?;
union_ids.extend(ids);
}
union_ids.sort();

assert_eq!(
union_ids, all_ids,
"each feed range must return only its own items: the union across \
ranges should equal the full set exactly once, with no over-scan \
or duplication"
);

Ok(())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer this test actually capture each range's results as a separate list and assert those (i.e. a Vec<Vec<String>> and then seen an assert_eq!(vec![vec!["id1", "id2", ...], vec!["idX", "idY", ...]], all_range_ids). If we returned 0 results from one range and all the results in a second range, that would be incorrect but would pass this test.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call - updated in 0d29d2e. The test now captures each range's result set into a Vec<Vec<String>> and asserts (1) every range is non-empty, (2) the ranges are pairwise disjoint (no duplicated ids), and (3) flattened they cover the full container exactly once. The empty-range-plus-full-range case you flagged now fails on the non-empty check.

},
Some(TestOptions::for_emulator()),
)
.await
}

#[tokio::test]
#[cfg_attr(
not(test_category = "emulator"),
Expand Down
1 change: 1 addition & 0 deletions sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

### Bugs Fixed

- Fixed cross-partition query fan-out ignoring an operation's explicit feed-range target. When a query was scoped to an explicit EPK range (`CosmosOperation::query_items` with a `FeedRange` target, surfaced as `FeedScope::range`), the query planner built request leaves from the query plan's ranges — which are derived from the query text alone — without intersecting them against the requested target. Both fan-out paths (`plan_fresh` and the resume path) now clip each query-plan range to the operation's target, so only partitions overlapping the requested range are queried. Absent, full, and logical-partition/prefix targets are unaffected. ([#4705](https://github.com/Azure/azure-sdk-for-rust/pull/4705))
- Fixed session-token parsing rejecting the `-1` region-progress sentinel that multi-region accounts emit for regions with no local progress. Region LSNs in the version vector are now parsed as signed values, with `-1` (and any negative value) modeled as "no progress" instead of failing the whole token as malformed. This applies to all Session-consistency traffic (not just distributed transactions) and prevents otherwise-valid multi-region session tokens from being discarded. ([#4702](https://github.com/Azure/azure-sdk-for-rust/pull/4702))
- Fixed `AZURE_COSMOS_PPCB_*` environment variables (including the `AZURE_COSMOS_PPCB_ENABLED` master switch and the `AZURE_COSMOS_PPCB_ENABLED_OVERRIDE` kill switch) being silently ignored when a caller built `DriverOptions` without calling `DriverOptionsBuilder::with_partition_failover_options`. The environment is read only by `PartitionFailoverOptionsBuilder::build`, but the omitted-options path used a bare `PartitionFailoverOptions::default()` (which hard-codes PPCB enabled and reads no environment), so PPCB stayed on even with `AZURE_COSMOS_PPCB_ENABLED=false`. `DriverOptionsBuilder::build` now resolves the partition-failover options from the environment when the caller does not supply them (falling back to defaults, fail-soft, if an environment value is out of bounds). An explicitly supplied `PartitionFailoverOptions` continues to take precedence. ([#4655](https://github.com/Azure/azure-sdk-for-rust/pull/4655))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,45 @@ impl TopologyProvider for MockTopologyProvider {
}
}

/// A range-aware topology provider backed by a fixed physical partition layout.
///
/// Unlike [`MockTopologyProvider`] (which ignores the requested range and
/// replays a queue), this resolves *against* the requested range:
/// `resolve_ranges(range)` returns exactly the configured physical partitions
/// whose EPK span overlaps `range`, mirroring real routing. This lets fan-out
/// tests observe when the planner resolves partitions outside the requested
/// scope — a plain replay mock cannot, because it returns the same partitions
/// regardless of the range it is asked about.
pub(crate) struct PhysicalTopologyProvider {
partitions: Vec<ResolvedRange>,
}

impl PhysicalTopologyProvider {
pub fn new(partitions: Vec<ResolvedRange>) -> Self {
Self { partitions }
}
}

impl TopologyProvider for PhysicalTopologyProvider {
fn resolve_ranges<'a>(
&'a mut self,
range: &'a FeedRange,
_refresh: PartitionRoutingRefresh,
) -> BoxFuture<'a, crate::error::Result<Vec<ResolvedRange>>> {
let resolved: Vec<ResolvedRange> = self
.partitions
.iter()
.filter(|p| {
// Half-open overlap: [a, b) intersects [c, d) iff a < d && c < b.
p.range.min_inclusive() < range.max_exclusive()
&& range.min_inclusive() < p.range.max_exclusive()
})
.cloned()
.collect();
Box::pin(async move { Ok(resolved) })
}
}

// ── Test helpers ────────────────────────────────────────────────────────────

/// Extracts the `CosmosResponse` from a `PageResult::Page`, panicking otherwise.
Expand Down
Loading
Loading