use std::sync::Arc;
use super::super::{
mocks::{MockRequestExecutor, MockTopologyProvider},
planner::build_sequential_drain,
query_plan::{QueryPlan, QueryRange},
Pipeline, PipelineContext, PipelineNodeState, RangedToken, ResolvedRange,
};
use crate::{
diagnostics::DiagnosticsContextBuilder,
error::Result,
models::{
effective_partition_key::EffectivePartitionKey, AccountReference, ActivityId,
ContainerProperties, ContainerReference, ContinuationToken, CosmosOperation,
CosmosResponse, CosmosResponseHeaders, CosmosStatus, FeedRange, ResolvedToken,
SystemProperties,
},
options::DiagnosticsOptions,
};
fn test_account() -> AccountReference {
AccountReference::with_master_key(
url::Url::parse("https://test.documents.azure.com:443/").unwrap(),
"dGVzdA==",
)
}
fn test_container_props() -> ContainerProperties {
use std::borrow::Cow;
ContainerProperties {
id: Cow::Owned("coll".into()),
partition_key: serde_json::from_str(r#"{"paths":["/pk"]}"#).unwrap(),
system_properties: SystemProperties::default(),
}
}
fn test_container() -> ContainerReference {
ContainerReference::new(
test_account(),
"db",
"db_rid",
"coll",
"coll_rid",
&test_container_props(),
)
}
fn cross_partition_query_operation() -> Arc<CosmosOperation> {
Arc::new(
CosmosOperation::query_items(test_container(), Some(FeedRange::full()))
.with_body(br#"{"query":"SELECT * FROM c"}"#.to_vec()),
)
}
fn full_range_plan() -> QueryPlan {
QueryPlan {
partitioned_query_execution_info_version: 1,
query_info: None,
query_ranges: vec![QueryRange {
min: String::new(),
max: "FF".to_string(),
is_min_inclusive: true,
is_max_inclusive: false,
}],
hybrid_search_query_info: None,
}
}
fn resolved(min: &str, max: &str, pk_range_id: &str) -> ResolvedRange {
ResolvedRange {
partition_key_range_id: pk_range_id.to_string(),
range: FeedRange::new(
EffectivePartitionKey::from(min),
EffectivePartitionKey::from(max),
)
.unwrap(),
}
}
fn page_response(body: &[u8], continuation: Option<&str>) -> CosmosResponse {
let mut diagnostics = DiagnosticsContextBuilder::new(
ActivityId::new_uuid(),
Arc::new(DiagnosticsOptions::default()),
);
diagnostics.set_operation_status(azure_core::http::StatusCode::Ok, None);
let mut headers = CosmosResponseHeaders::new();
headers.continuation = continuation.map(str::to_owned);
CosmosResponse::new(
body.to_vec(),
headers,
CosmosStatus::new(azure_core::http::StatusCode::Ok),
Arc::new(diagnostics.complete()),
)
}
async fn drain_all(pipeline: &mut Pipeline, executor: &mut MockRequestExecutor) -> Vec<Vec<u8>> {
let mut pages = Vec::new();
let mut topology = super::super::mocks::NoopTopologyProvider;
loop {
let mut context = PipelineContext::new(executor, Some(&mut topology));
match pipeline.next_page(&mut context).await.unwrap() {
Some(response) => pages.push(response.body_bytes().to_vec()),
None => break,
}
}
pages
}
async fn drain_pages(
pipeline: &mut Pipeline,
executor: &mut MockRequestExecutor,
n: usize,
) -> Vec<Vec<u8>> {
let mut pages = Vec::with_capacity(n);
let mut topology = super::super::mocks::NoopTopologyProvider;
for _ in 0..n {
let mut context = PipelineContext::new(executor, Some(&mut topology));
let response = pipeline
.next_page(&mut context)
.await
.unwrap()
.expect("expected page, not drained");
pages.push(response.body_bytes().to_vec());
}
pages
}
fn round_trip_state(state: PipelineNodeState, op: &CosmosOperation) -> PipelineNodeState {
let token = ContinuationToken::encode_v1(op, &state).expect("encode succeeds");
let resolved = token.resolve().expect("decode succeeds");
match resolved {
ResolvedToken::ClientV1(token_state) => {
token_state
.is_valid_for_operation(op)
.expect("operation compatible");
token_state.into_root_node_state()
}
ResolvedToken::ServerOpaque(_) => panic!("expected ClientV1 token"),
}
}
#[tokio::test]
async fn single_partition_resume_roundtrips_cleanly() {
let op = cross_partition_query_operation();
let plan = full_range_plan();
let mut topology1 = MockTopologyProvider::new(vec![Ok(vec![resolved("", "FF", "pk-0")])]);
let mut executor1 = MockRequestExecutor::new(vec![Ok(page_response(b"page-1", Some("ct-1")))]);
let mut pipeline1 = build_sequential_drain(&plan, &mut topology1, &op, None)
.await
.unwrap();
let pages1 = drain_pages(&mut pipeline1, &mut executor1, 1).await;
assert_eq!(pages1, vec![b"page-1".to_vec()]);
assert_eq!(executor1.continuation_calls, vec![None]);
let state = pipeline1.snapshot_state().unwrap();
drop(pipeline1);
let resumed_state = round_trip_state(state, &op);
let mut topology2 = MockTopologyProvider::new(vec![Ok(vec![resolved("", "FF", "pk-0")])]);
let mut executor2 = MockRequestExecutor::new(vec![Ok(page_response(b"page-2", None))]);
let mut pipeline2 = build_sequential_drain(&plan, &mut topology2, &op, Some(resumed_state))
.await
.unwrap();
let pages2 = drain_all(&mut pipeline2, &mut executor2).await;
assert_eq!(pages2, vec![b"page-2".to_vec()]);
assert_eq!(
executor2.continuation_calls,
vec![Some("ct-1".to_owned())],
"page 2 must be requested with the continuation page 1 returned",
);
}
#[tokio::test]
async fn resume_after_split_forwards_continuation_to_every_surviving_leaf() {
let op = cross_partition_query_operation();
let plan = full_range_plan();
let mut topology1 = MockTopologyProvider::new(vec![Ok(vec![resolved("", "FF", "pk-0")])]);
let mut executor1 = MockRequestExecutor::new(vec![Ok(page_response(
b"page-1-presplit",
Some("ct-pre-split"),
))]);
let mut pipeline1 = build_sequential_drain(&plan, &mut topology1, &op, None)
.await
.unwrap();
let pages1 = drain_pages(&mut pipeline1, &mut executor1, 1).await;
let state = pipeline1.snapshot_state().unwrap();
drop(pipeline1);
let resumed_state = round_trip_state(state, &op);
let mut topology2 = MockTopologyProvider::new(vec![Ok(vec![
resolved("", "80", "pk-left"),
resolved("80", "FF", "pk-right"),
])]);
let mut executor2 = MockRequestExecutor::new(vec![
Ok(page_response(b"page-left", None)),
Ok(page_response(b"page-right", None)),
]);
let mut pipeline2 = build_sequential_drain(&plan, &mut topology2, &op, Some(resumed_state))
.await
.unwrap();
let pages2 = drain_all(&mut pipeline2, &mut executor2).await;
assert_eq!(pages1, vec![b"page-1-presplit".to_vec()]);
assert_eq!(pages2, vec![b"page-left".to_vec(), b"page-right".to_vec()]);
assert_eq!(
executor2.continuation_calls,
vec![
Some("ct-pre-split".to_owned()),
Some("ct-pre-split".to_owned()),
],
"both post-split leaves must resume with the saved continuation",
);
}
#[tokio::test]
async fn resume_mid_fanout_preserves_every_sibling_state() {
let op = cross_partition_query_operation();
let plan = full_range_plan();
let mut topology1 = MockTopologyProvider::new(vec![Ok(vec![
resolved("", "80", "pk-left"),
resolved("80", "FF", "pk-right"),
])]);
let mut executor1 =
MockRequestExecutor::new(vec![Ok(page_response(b"left-page-1", Some("ct-left")))]);
let mut pipeline1 = build_sequential_drain(&plan, &mut topology1, &op, None)
.await
.unwrap();
let pages1 = drain_pages(&mut pipeline1, &mut executor1, 1).await;
assert_eq!(pages1, vec![b"left-page-1".to_vec()]);
assert_eq!(executor1.continuation_calls, vec![None]);
let state = pipeline1.snapshot_state().unwrap();
drop(pipeline1);
match &state {
PipelineNodeState::SequentialDrain {
left_most_undrained_epk,
active_tokens,
} => {
assert_eq!(left_most_undrained_epk, "");
assert_eq!(
active_tokens.len(),
1,
"snapshot must record exactly one active token (left sibling); got {active_tokens:?}",
);
assert_eq!(active_tokens[0].min_epk, "");
assert_eq!(active_tokens[0].max_epk, "80");
assert_eq!(active_tokens[0].server_continuation, "ct-left");
}
other => panic!("expected SequentialDrain snapshot, got {other:?}"),
}
let resumed_state = round_trip_state(state, &op);
let mut topology2 = MockTopologyProvider::new(vec![Ok(vec![
resolved("", "80", "pk-left"),
resolved("80", "FF", "pk-right"),
])]);
let mut executor2 = MockRequestExecutor::new(vec![
Ok(page_response(b"left-page-2", None)),
Ok(page_response(b"right-page-1", None)),
]);
let mut pipeline2 = build_sequential_drain(&plan, &mut topology2, &op, Some(resumed_state))
.await
.unwrap();
let pages2 = drain_all(&mut pipeline2, &mut executor2).await;
assert_eq!(
pages2,
vec![b"left-page-2".to_vec(), b"right-page-1".to_vec()],
"resume must drain the rest of left (using ct-left) THEN the untouched right sibling",
);
assert_eq!(
executor2.continuation_calls,
vec![Some("ct-left".to_owned()), None],
"left resumes with ct-left; right resumes fresh (its un-started pre-split state)",
);
let mut all_pages: Vec<Vec<u8>> = pages1.into_iter().chain(pages2.into_iter()).collect();
all_pages.sort();
let mut expected: Vec<Vec<u8>> = vec![
b"left-page-1".to_vec(),
b"left-page-2".to_vec(),
b"right-page-1".to_vec(),
];
expected.sort();
assert_eq!(all_pages, expected);
}
#[tokio::test]
async fn resume_mid_fanout_then_split_preserves_state_and_fans_out_continuation() {
let op = cross_partition_query_operation();
let plan = full_range_plan();
let mut topology1 = MockTopologyProvider::new(vec![Ok(vec![
resolved("", "80", "pk-left"),
resolved("80", "FF", "pk-right"),
])]);
let mut executor1 =
MockRequestExecutor::new(vec![Ok(page_response(b"left-page-1", Some("ct-left")))]);
let mut pipeline1 = build_sequential_drain(&plan, &mut topology1, &op, None)
.await
.unwrap();
let pages1 = drain_pages(&mut pipeline1, &mut executor1, 1).await;
let state = pipeline1.snapshot_state().unwrap();
drop(pipeline1);
let resumed_state = round_trip_state(state, &op);
let mut topology2 = MockTopologyProvider::new(vec![Ok(vec![
resolved("", "40", "pk-left-l"),
resolved("40", "80", "pk-left-r"),
resolved("80", "FF", "pk-right"),
])]);
let mut executor2 = MockRequestExecutor::new(vec![
Ok(page_response(b"left-l-page-1", None)),
Ok(page_response(b"left-r-page-1", None)),
Ok(page_response(b"right-page-1", None)),
]);
let mut pipeline2 = build_sequential_drain(&plan, &mut topology2, &op, Some(resumed_state))
.await
.unwrap();
let pages2 = drain_all(&mut pipeline2, &mut executor2).await;
assert_eq!(
pages2,
vec![
b"left-l-page-1".to_vec(),
b"left-r-page-1".to_vec(),
b"right-page-1".to_vec(),
],
);
assert_eq!(
executor2.continuation_calls,
vec![Some("ct-left".to_owned()), Some("ct-left".to_owned()), None,],
);
let mut all_pages: Vec<Vec<u8>> = pages1.into_iter().chain(pages2.into_iter()).collect();
all_pages.sort();
let mut expected: Vec<Vec<u8>> = vec![
b"left-page-1".to_vec(),
b"left-l-page-1".to_vec(),
b"left-r-page-1".to_vec(),
b"right-page-1".to_vec(),
];
expected.sort();
assert_eq!(all_pages, expected);
}
#[tokio::test]
async fn resume_does_not_requery_already_drained_sibling_scope() {
let op = cross_partition_query_operation();
let plan = full_range_plan();
let saved_state = PipelineNodeState::SequentialDrain {
left_most_undrained_epk: "80".to_string(),
active_tokens: vec![RangedToken {
min_epk: "80".to_string(),
max_epk: "FF".to_string(),
server_continuation: "ct-right".to_owned(),
}],
};
let resumed_state = round_trip_state(saved_state, &op);
let mut topology = MockTopologyProvider::new(vec![Ok(vec![
resolved("", "40", "pk-left-l"),
resolved("40", "80", "pk-left-r"),
resolved("80", "FF", "pk-right"),
])]);
let mut executor = MockRequestExecutor::new(vec![Ok(page_response(b"right-page-1", None))]);
let mut pipeline = build_sequential_drain(&plan, &mut topology, &op, Some(resumed_state))
.await
.unwrap();
let pages = drain_all(&mut pipeline, &mut executor).await;
assert_eq!(pages, vec![b"right-page-1".to_vec()]);
assert_eq!(
executor.continuation_calls,
vec![Some("ct-right".to_owned())],
"drained left scope must not be re-queried; only the right sibling executes",
);
}
#[tokio::test]
async fn resume_fails_loudly_when_saved_range_cannot_be_covered() {
let op = cross_partition_query_operation();
let plan = full_range_plan();
let saved_state = PipelineNodeState::SequentialDrain {
left_most_undrained_epk: "55".to_string(),
active_tokens: vec![RangedToken {
min_epk: "55".to_string(),
max_epk: "AA".to_string(),
server_continuation: "ct-orphan".to_owned(),
}],
};
let resumed_state = round_trip_state(saved_state, &op);
let mut topology = MockTopologyProvider::new(vec![Ok(vec![
resolved("", "70", "pk-a"),
resolved("70", "80", "pk-b"),
])]);
let err: Result<Pipeline> =
build_sequential_drain(&plan, &mut topology, &op, Some(resumed_state)).await;
let err = err.expect_err("expected unhonored-saved-range error");
let rendered = err.to_string();
assert!(
rendered.contains("saved") || rendered.contains("unhonored") || rendered.contains("cover"),
"error message should describe the unhonored-saved-range failure; got: {rendered}"
);
}
#[tokio::test]
async fn three_session_loop_propagates_presplit_token_through_two_snapshots() {
let op = cross_partition_query_operation();
let plan = full_range_plan();
let mut topology1 = MockTopologyProvider::new(vec![Ok(vec![resolved("", "FF", "pk-pre")])]);
let mut executor1 =
MockRequestExecutor::new(vec![Ok(page_response(b"page-1-pre", Some("T1")))]);
let mut pipeline1 = build_sequential_drain(&plan, &mut topology1, &op, None)
.await
.unwrap();
let pages_s1 = drain_pages(&mut pipeline1, &mut executor1, 1).await;
assert_eq!(pages_s1, vec![b"page-1-pre".to_vec()]);
assert_eq!(executor1.continuation_calls, vec![None]);
let state_s1 = pipeline1.snapshot_state().unwrap();
drop(pipeline1);
match &state_s1 {
PipelineNodeState::SequentialDrain {
left_most_undrained_epk,
active_tokens,
} => {
assert_eq!(left_most_undrained_epk, "");
assert_eq!(active_tokens.len(), 1);
assert_eq!(active_tokens[0].min_epk, "");
assert_eq!(active_tokens[0].max_epk, "FF");
assert_eq!(active_tokens[0].server_continuation, "T1");
}
other => panic!("expected SequentialDrain snapshot at session 1, got {other:?}"),
}
let resumed_s2 = round_trip_state(state_s1, &op);
let mut topology2 = MockTopologyProvider::new(vec![Ok(vec![
resolved("", "80", "pk-left"),
resolved("80", "FF", "pk-right"),
])]);
let mut executor2 = MockRequestExecutor::new(vec![Ok(page_response(
b"page-1-postsplit-left",
Some("T2_a"),
))]);
let mut pipeline2 = build_sequential_drain(&plan, &mut topology2, &op, Some(resumed_s2))
.await
.unwrap();
let pages_s2 = drain_pages(&mut pipeline2, &mut executor2, 1).await;
assert_eq!(pages_s2, vec![b"page-1-postsplit-left".to_vec()]);
assert_eq!(
executor2.continuation_calls,
vec![Some("T1".to_owned())],
"session 2's first executor call must be the front child carrying the pre-split token",
);
let state_s2 = pipeline2.snapshot_state().unwrap();
drop(pipeline2);
match &state_s2 {
PipelineNodeState::SequentialDrain {
left_most_undrained_epk,
active_tokens,
} => {
assert_eq!(left_most_undrained_epk, "");
assert_eq!(
active_tokens.len(),
2,
"session 2 snapshot must preserve both post-split children, not just the front; got {active_tokens:?}",
);
assert_eq!(active_tokens[0].min_epk, "");
assert_eq!(active_tokens[0].max_epk, "80");
assert_eq!(active_tokens[0].server_continuation, "T2_a");
assert_eq!(active_tokens[1].min_epk, "80");
assert_eq!(active_tokens[1].max_epk, "FF");
assert_eq!(
active_tokens[1].server_continuation, "T1",
"back child must still owe pre-split T1, not None or T2_a",
);
}
other => panic!("expected SequentialDrain snapshot at session 2, got {other:?}"),
}
let resumed_s3 = round_trip_state(state_s2, &op);
let mut topology3 = MockTopologyProvider::new(vec![Ok(vec![
resolved("", "80", "pk-left"),
resolved("80", "FF", "pk-right"),
])]);
let mut executor3 = MockRequestExecutor::new(vec![
Ok(page_response(b"page-2-postsplit-left", None)),
Ok(page_response(b"page-1-postsplit-right", None)),
]);
let mut pipeline3 = build_sequential_drain(&plan, &mut topology3, &op, Some(resumed_s3))
.await
.unwrap();
let pages_s3 = drain_all(&mut pipeline3, &mut executor3).await;
assert_eq!(
pages_s3,
vec![
b"page-2-postsplit-left".to_vec(),
b"page-1-postsplit-right".to_vec(),
],
);
assert_eq!(
executor3.continuation_calls,
vec![Some("T2_a".to_owned()), Some("T1".to_owned())],
"session 3 must drain front from T2_a then visit back with the preserved pre-split T1",
);
let mut all_pages: Vec<Vec<u8>> = pages_s1
.into_iter()
.chain(pages_s2.into_iter())
.chain(pages_s3.into_iter())
.collect();
all_pages.sort();
let mut expected: Vec<Vec<u8>> = vec![
b"page-1-pre".to_vec(),
b"page-1-postsplit-left".to_vec(),
b"page-2-postsplit-left".to_vec(),
b"page-1-postsplit-right".to_vec(),
];
expected.sort();
assert_eq!(all_pages, expected);
}
#[tokio::test]
async fn cascading_split_propagates_back_sibling_token_to_every_grand_child() {
let op = cross_partition_query_operation();
let plan = full_range_plan();
let mut topology1 = MockTopologyProvider::new(vec![Ok(vec![resolved("", "FF", "pk-pre")])]);
let mut executor1 =
MockRequestExecutor::new(vec![Ok(page_response(b"page-1-pre", Some("T1")))]);
let mut pipeline1 = build_sequential_drain(&plan, &mut topology1, &op, None)
.await
.unwrap();
let pages_s1 = drain_pages(&mut pipeline1, &mut executor1, 1).await;
assert_eq!(pages_s1, vec![b"page-1-pre".to_vec()]);
let state_s1 = pipeline1.snapshot_state().unwrap();
drop(pipeline1);
let resumed_s2 = round_trip_state(state_s1, &op);
let mut topology2 = MockTopologyProvider::new(vec![Ok(vec![
resolved("", "80", "pk-left"),
resolved("80", "FF", "pk-right"),
])]);
let mut executor2 =
MockRequestExecutor::new(vec![Ok(page_response(b"page-1-postsplit-left", None))]);
let mut pipeline2 = build_sequential_drain(&plan, &mut topology2, &op, Some(resumed_s2))
.await
.unwrap();
let pages_s2 = drain_pages(&mut pipeline2, &mut executor2, 1).await;
assert_eq!(pages_s2, vec![b"page-1-postsplit-left".to_vec()]);
let state_s2 = pipeline2.snapshot_state().unwrap();
drop(pipeline2);
match &state_s2 {
PipelineNodeState::SequentialDrain {
left_most_undrained_epk,
active_tokens,
} => {
assert_eq!(left_most_undrained_epk, "80");
assert_eq!(active_tokens.len(), 1);
assert_eq!(active_tokens[0].min_epk, "80");
assert_eq!(active_tokens[0].max_epk, "FF");
assert_eq!(active_tokens[0].server_continuation, "T1");
}
other => panic!("expected SequentialDrain snapshot at session 2, got {other:?}"),
}
let resumed_s3 = round_trip_state(state_s2, &op);
let mut topology3 = MockTopologyProvider::new(vec![Ok(vec![
resolved("80", "C0", "pk-back-left"),
resolved("C0", "FF", "pk-back-right"),
])]);
let mut executor3 = MockRequestExecutor::new(vec![
Ok(page_response(b"page-1-back-left", None)),
Ok(page_response(b"page-1-back-right", None)),
]);
let mut pipeline3 = build_sequential_drain(&plan, &mut topology3, &op, Some(resumed_s3))
.await
.unwrap();
let pages_s3 = drain_all(&mut pipeline3, &mut executor3).await;
assert_eq!(
pages_s3,
vec![b"page-1-back-left".to_vec(), b"page-1-back-right".to_vec()],
);
assert_eq!(
executor3.continuation_calls,
vec![Some("T1".to_owned()), Some("T1".to_owned())],
"both back-range grand-children must receive the preserved pre-split T1",
);
let mut all_pages: Vec<Vec<u8>> = pages_s1
.into_iter()
.chain(pages_s2.into_iter())
.chain(pages_s3.into_iter())
.collect();
all_pages.sort();
let mut expected: Vec<Vec<u8>> = vec![
b"page-1-pre".to_vec(),
b"page-1-postsplit-left".to_vec(),
b"page-1-back-left".to_vec(),
b"page-1-back-right".to_vec(),
];
expected.sort();
assert_eq!(all_pages, expected);
}