use std::collections::VecDeque;
use async_trait::async_trait;
use super::{PageResult, PipelineContext, PipelineNode, PipelineNodeState, RangedToken};
use crate::models::ChangeFeedStartFrom;
const MAX_SPLIT_RETRIES: usize = 10;
pub(crate) struct UnorderedMerge {
children: VecDeque<Box<dyn PipelineNode>>,
cursor: usize,
start_marker: Option<ChangeFeedStartFrom>,
}
impl UnorderedMerge {
pub(crate) fn new(children: Vec<Box<dyn PipelineNode>>) -> Self {
Self {
children: children.into(),
cursor: 0,
start_marker: None,
}
}
pub(crate) fn with_start_marker(mut self, start_marker: Option<ChangeFeedStartFrom>) -> Self {
self.start_marker = start_marker;
self
}
}
#[async_trait]
impl PipelineNode for UnorderedMerge {
async fn next_page(
&mut self,
context: &mut PipelineContext<'_>,
) -> crate::error::Result<PageResult> {
if self.children.is_empty() {
return Ok(PageResult::Drained);
}
let mut split_retries = 0;
let children_count = self.children.len();
let mut attempts = 0;
loop {
if self.children.is_empty() {
return Ok(PageResult::Drained);
}
let idx = self.cursor % self.children.len();
let child = &mut self.children[idx];
match child.next_page(context).await? {
PageResult::Page {
response,
is_terminal: _,
} => {
self.cursor = (idx + 1) % self.children.len();
return Ok(PageResult::Page {
response,
is_terminal: false,
});
}
PageResult::Drained => {
self.children.remove(idx);
if self.children.is_empty() {
return Ok(PageResult::Drained);
}
self.cursor %= self.children.len();
attempts += 1;
if attempts >= children_count {
return Ok(PageResult::Drained);
}
}
PageResult::SplitRequired { replacement_nodes } => {
split_retries += 1;
if split_retries > MAX_SPLIT_RETRIES {
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::CLIENT_SPLIT_RETRIES_EXHAUSTED)
.with_message(format!(
"exceeded maximum split retries ({MAX_SPLIT_RETRIES}) \
in UnorderedMerge"
))
.build());
}
self.children.remove(idx);
for (i, node) in replacement_nodes.into_iter().enumerate() {
self.children.insert(idx + i, node);
}
}
}
}
}
#[cfg(test)]
fn into_children(self) -> Vec<Box<dyn PipelineNode>> {
self.children.into_iter().collect()
}
fn snapshot_state(&self) -> crate::error::Result<PipelineNodeState> {
if self.children.is_empty() {
return Ok(PipelineNodeState::Drained);
}
let mut active_tokens: Vec<RangedToken> = Vec::new();
for (idx, child) in self.children.iter().enumerate() {
let Some(range) = child.feed_range() else {
return Err(crate::error::CosmosError::builder()
.with_status(
crate::error::CosmosStatus::CLIENT_CONTINUATION_TOKEN_UNEXPECTED_NESTED_SHAPE,
)
.with_message(format!(
"UnorderedMerge child {idx} of {total} has no feed_range; \
cannot snapshot continuation state safely",
total = self.children.len(),
))
.build());
};
let child_state = child.snapshot_state()?;
match child_state.into_child_contribution("UnorderedMerge", idx, self.children.len())? {
super::snapshot::ChildSnapshotContribution::Drained => {
}
super::snapshot::ChildSnapshotContribution::Pending {
server_continuation,
} => {
if let Some(token) = server_continuation {
active_tokens.push(RangedToken {
min_epk: range.min_inclusive().to_hex(),
max_epk: range.max_exclusive().to_hex(),
server_continuation: token,
});
}
}
}
}
Ok(PipelineNodeState::UnorderedMerge {
active_tokens,
start_from: self.start_marker.clone(),
})
}
fn topology_can_change(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::super::mocks::*;
use super::*;
#[tokio::test]
async fn polls_children_round_robin() {
let child_a = MockLeaf::with_pages(vec![
Ok(PageResult::Page {
response: response(b"a1"),
is_terminal: false,
}),
Ok(PageResult::Page {
response: response(b"a2"),
is_terminal: false,
}),
]);
let child_b = MockLeaf::with_pages(vec![Ok(PageResult::Page {
response: response(b"b1"),
is_terminal: false,
})]);
let mut merge = UnorderedMerge::new(vec![Box::new(child_a), Box::new(child_b)]);
let mut executor = NoopRequestExecutor;
let mut topology = NoopTopologyProvider;
let mut ctx = PipelineContext::new(&mut executor, Some(&mut topology));
let r = merge.next_page(&mut ctx).await.unwrap();
assert!(matches!(r, PageResult::Page { .. }));
if let PageResult::Page { response, .. } = r {
assert_eq!(response.body_bytes(), b"a1");
}
let r = merge.next_page(&mut ctx).await.unwrap();
if let PageResult::Page { response, .. } = r {
assert_eq!(response.body_bytes(), b"b1");
}
let r = merge.next_page(&mut ctx).await.unwrap();
if let PageResult::Page { response, .. } = r {
assert_eq!(response.body_bytes(), b"a2");
}
}
#[tokio::test]
async fn terminal_pages_do_not_evict_children() {
let child = MockLeaf::with_pages(vec![
Ok(PageResult::Page {
response: response(b"page1"),
is_terminal: true, }),
Ok(PageResult::Page {
response: response(b"page2"),
is_terminal: false,
}),
]);
let mut merge = UnorderedMerge::new(vec![Box::new(child)]);
let mut executor = NoopRequestExecutor;
let mut topology = NoopTopologyProvider;
let mut ctx = PipelineContext::new(&mut executor, Some(&mut topology));
let r = merge.next_page(&mut ctx).await.unwrap();
assert!(matches!(
r,
PageResult::Page {
is_terminal: false,
..
}
));
let r = merge.next_page(&mut ctx).await.unwrap();
if let PageResult::Page { response, .. } = r {
assert_eq!(response.body_bytes(), b"page2");
}
}
#[tokio::test]
async fn handles_split_required() {
let split_child = MockLeaf::with_pages(vec![Ok(PageResult::SplitRequired {
replacement_nodes: vec![
Box::new(MockLeaf::with_pages(vec![Ok(PageResult::Page {
response: response(b"split-a"),
is_terminal: false,
})])),
Box::new(MockLeaf::with_pages(vec![Ok(PageResult::Page {
response: response(b"split-b"),
is_terminal: false,
})])),
],
})]);
let mut merge = UnorderedMerge::new(vec![Box::new(split_child)]);
let mut executor = NoopRequestExecutor;
let mut topology = NoopTopologyProvider;
let mut ctx = PipelineContext::new(&mut executor, Some(&mut topology));
let r = merge.next_page(&mut ctx).await.unwrap();
if let PageResult::Page { response, .. } = r {
assert_eq!(response.body_bytes(), b"split-a");
}
let r = merge.next_page(&mut ctx).await.unwrap();
if let PageResult::Page { response, .. } = r {
assert_eq!(response.body_bytes(), b"split-b");
}
}
#[tokio::test]
async fn never_signals_terminal_to_parent() {
let child = MockLeaf::with_pages(vec![Ok(PageResult::Page {
response: response(b"data"),
is_terminal: true,
})]);
let mut merge = UnorderedMerge::new(vec![Box::new(child)]);
let mut executor = NoopRequestExecutor;
let mut topology = NoopTopologyProvider;
let mut ctx = PipelineContext::new(&mut executor, Some(&mut topology));
let r = merge.next_page(&mut ctx).await.unwrap();
match r {
PageResult::Page { is_terminal, .. } => {
assert!(!is_terminal, "UnorderedMerge must never signal terminal");
}
other => panic!("expected Page, got {other:?}"),
}
}
}