use crate::bridge::envelope::{Response, Status};
use crate::control::server::payload_merge::merge_msgpack_arrays;
use crate::types::Lsn;
pub struct RowBatch {
pub payload: Vec<u8>,
pub watermark_lsn: Lsn,
pub read_version_lsn: Lsn,
}
pub type ResultStream =
std::pin::Pin<Box<dyn futures::Stream<Item = crate::Result<RowBatch>> + Send>>;
pub(crate) fn stream_response_channel(
mut rx: tokio::sync::mpsc::Receiver<Response>,
max_result_bytes: usize,
tolerate_not_found: bool,
) -> ResultStream {
Box::pin(async_stream::try_stream! {
let mut total: usize = 0;
while let Some(resp) = rx.recv().await {
if resp.status == Status::Error {
if tolerate_not_found
&& matches!(
resp.error_code.as_deref(),
Some(crate::bridge::envelope::ErrorCode::NotFound)
)
{
return;
}
let detail = match resp.error_code {
Some(ref ec) => format!("data plane error: {ec:?}"),
None => "unknown data plane error".to_string(),
};
Err(crate::Error::Dispatch { detail })?;
return;
}
total = total.saturating_add(resp.payload.len());
if total > max_result_bytes {
Err(crate::Error::ExecutionLimitExceeded {
detail: format!(
"query result exceeded max_query_result_bytes \
({total} > {max_result_bytes} bytes)"
),
})?;
return;
}
let is_terminal = !resp.partial;
yield RowBatch {
payload: resp.payload.to_vec(),
watermark_lsn: resp.watermark_lsn,
read_version_lsn: resp.read_version_lsn,
};
if is_terminal {
return;
}
}
})
}
pub(crate) async fn materialize(mut stream: ResultStream) -> crate::Result<(Vec<u8>, Lsn)> {
use futures::StreamExt;
let mut payloads: Vec<Vec<u8>> = Vec::new();
let mut max_lsn = Lsn::ZERO;
while let Some(batch) = stream.next().await {
let batch = batch?;
if batch.watermark_lsn > max_lsn {
max_lsn = batch.watermark_lsn;
}
payloads.push(batch.payload);
}
Ok((merge_msgpack_arrays(&payloads), max_lsn))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bridge::envelope::{ErrorCode, Payload};
use crate::control::server::payload_merge::{encode_msgpack_array, extract_msgpack_elements};
use crate::types::RequestId;
use tokio::sync::mpsc;
fn array_payload(n: usize) -> Vec<u8> {
let rows: Vec<Vec<u8>> = (0..n).map(|i| vec![(i % 128) as u8]).collect();
encode_msgpack_array(&rows)
}
fn partial(n: usize) -> Response {
Response {
request_id: RequestId::new(1),
status: Status::Partial,
attempt: 1,
partial: true,
payload: Payload::from_vec(array_payload(n)),
watermark_lsn: Lsn::ZERO,
error_code: None,
read_set_valid: None,
read_version_lsn: crate::types::Lsn::ZERO,
write_set: Vec::new(),
}
}
fn final_frame(n: usize) -> Response {
Response {
request_id: RequestId::new(1),
status: Status::Ok,
attempt: 1,
partial: false,
payload: Payload::from_vec(array_payload(n)),
watermark_lsn: Lsn::ZERO,
error_code: None,
read_set_valid: None,
read_version_lsn: crate::types::Lsn::ZERO,
write_set: Vec::new(),
}
}
fn raw_partial(bytes: usize) -> Response {
Response {
request_id: RequestId::new(1),
status: Status::Partial,
attempt: 1,
partial: true,
payload: Payload::from_vec(vec![0u8; bytes]),
watermark_lsn: Lsn::ZERO,
error_code: None,
read_set_valid: None,
read_version_lsn: crate::types::Lsn::ZERO,
write_set: Vec::new(),
}
}
fn error_frame(code: ErrorCode) -> Response {
Response {
request_id: RequestId::new(1),
status: Status::Error,
attempt: 1,
partial: false,
payload: Payload::empty(),
watermark_lsn: Lsn::ZERO,
error_code: Some(Box::new(code)),
read_set_valid: None,
read_version_lsn: crate::types::Lsn::ZERO,
write_set: Vec::new(),
}
}
#[tokio::test]
async fn materialize_merges_all_batches() {
let (tx, rx) = mpsc::channel(8);
tx.send(partial(1000)).await.unwrap();
tx.send(partial(1000)).await.unwrap();
tx.send(final_frame(500)).await.unwrap();
drop(tx);
let stream = stream_response_channel(rx, 1 << 20, false);
let (merged, _lsn) = materialize(stream).await.unwrap();
assert_eq!(
extract_msgpack_elements(&merged).len(),
2500,
"three array batches must materialize into one array of all rows"
);
}
#[tokio::test]
async fn over_budget_errors() {
let (tx, rx) = mpsc::channel(8);
tx.send(raw_partial(600)).await.unwrap();
tx.send(raw_partial(600)).await.unwrap();
drop(tx);
let stream = stream_response_channel(rx, 1000, false);
let err = materialize(stream).await.unwrap_err();
assert!(matches!(err, crate::Error::ExecutionLimitExceeded { .. }));
}
#[tokio::test]
async fn terminal_error_frame_errors() {
let (tx, rx) = mpsc::channel(8);
tx.send(partial(10)).await.unwrap();
tx.send(error_frame(ErrorCode::ResourcesExhausted))
.await
.unwrap();
drop(tx);
let stream = stream_response_channel(rx, 1 << 20, false);
let err = materialize(stream).await.unwrap_err();
assert!(matches!(err, crate::Error::Dispatch { .. }));
}
#[tokio::test]
async fn not_found_tolerated_ends_cleanly() {
let (tx, rx) = mpsc::channel(8);
tx.send(error_frame(ErrorCode::NotFound)).await.unwrap();
drop(tx);
let stream = stream_response_channel(rx, 1 << 20, true);
let (merged, _lsn) = materialize(stream).await.unwrap();
assert_eq!(
extract_msgpack_elements(&merged).len(),
0,
"tolerated NotFound yields an empty result, not an error"
);
}
}