use std::time::Duration;
use futures::future::join_all;
use crate::bridge::envelope::{Payload, PhysicalPlan, Response, Status};
use crate::control::server::exchange::gather::eager_dispatch_to_all_cores;
use crate::control::server::payload_merge::{encode_msgpack_array, extract_msgpack_elements};
use crate::data::executor::handlers::graph_match::{
MATCH_ENVELOPE_FRONTIER_KEY, MATCH_ENVELOPE_RESUME_KEY, MATCH_ENVELOPE_ROWS_KEY,
};
use crate::engine::graph::pattern::executor::{UnresolvedExpansion, VarLenResume};
use crate::types::{DatabaseId, TenantId, TraceId, TxnId};
use nodedb_query::msgpack_scan::reader::{map_header, read_str_advance, skip_value};
pub struct MatchBroadcastOutcome {
pub rows_payload: Payload,
pub frontier: Vec<UnresolvedExpansion>,
pub resume: Vec<VarLenResume>,
pub partial: bool,
}
fn map_value_raw<'a>(payload: &'a [u8], key: &str) -> Option<&'a [u8]> {
let (count, mut pos) = map_header(payload, 0)?;
for _ in 0..count {
let k = read_str_advance(payload, &mut pos)?;
let val_start = pos;
let val_end = skip_value(payload, pos)?;
if k == key {
return Some(&payload[val_start..val_end]);
}
pos = val_end;
}
None
}
pub(crate) struct DecodedMatchEnvelope {
pub(crate) row_elements: Vec<Vec<u8>>,
pub(crate) frontier: Vec<UnresolvedExpansion>,
pub(crate) resume: Vec<VarLenResume>,
}
fn decode_match_envelope(payload: &[u8]) -> crate::Result<DecodedMatchEnvelope> {
let rows_bytes =
map_value_raw(payload, MATCH_ENVELOPE_ROWS_KEY).ok_or_else(|| crate::Error::Codec {
detail: "match envelope: missing or malformed 'rows' field".into(),
})?;
let frontier_bytes =
map_value_raw(payload, MATCH_ENVELOPE_FRONTIER_KEY).ok_or_else(|| crate::Error::Codec {
detail: "match envelope: missing or malformed 'frontier' field".into(),
})?;
let row_elements = extract_msgpack_elements(rows_bytes);
let frontier: Vec<UnresolvedExpansion> =
zerompk::from_msgpack(frontier_bytes).map_err(|e| crate::Error::Codec {
detail: format!("match envelope: invalid frontier: {e}"),
})?;
let resume: Vec<VarLenResume> = match map_value_raw(payload, MATCH_ENVELOPE_RESUME_KEY) {
Some(resume_bytes) => {
zerompk::from_msgpack(resume_bytes).map_err(|e| crate::Error::Codec {
detail: format!("match envelope: invalid resume: {e}"),
})?
}
None => Vec::new(),
};
Ok(DecodedMatchEnvelope {
row_elements,
frontier,
resume,
})
}
pub struct UnwrappedMatchEnvelope {
pub(crate) rows_payload: Payload,
pub(crate) frontier: Vec<UnresolvedExpansion>,
pub(crate) resume: Vec<VarLenResume>,
}
pub fn unwrap_match_envelope(payload: &Payload) -> crate::Result<UnwrappedMatchEnvelope> {
if payload.is_empty() {
return Ok(UnwrappedMatchEnvelope {
rows_payload: payload.clone(),
frontier: Vec::new(),
resume: Vec::new(),
});
}
let decoded = decode_match_envelope(payload.as_ref())?;
let merged_rows = encode_msgpack_array(&decoded.row_elements);
Ok(UnwrappedMatchEnvelope {
rows_payload: Payload::from_vec(merged_rows),
frontier: decoded.frontier,
resume: decoded.resume,
})
}
pub async fn broadcast_match_to_all_cores(
state: &crate::control::state::SharedState,
tenant_id: TenantId,
database_id: DatabaseId,
plan: PhysicalPlan,
trace_id: TraceId,
txn_id: Option<TxnId>,
) -> crate::Result<MatchBroadcastOutcome> {
crate::control::server::broadcast::broadcast_call_count_increment();
let deadline_secs = state.tuning.network.default_deadline_secs;
let receivers =
eager_dispatch_to_all_cores(state, tenant_id, database_id, trace_id, txn_id, |_| {
plan.clone()
})?;
let deadline = Duration::from_secs(deadline_secs);
let max_result_bytes = state.tuning.network.max_query_result_bytes as usize;
let response_futures = receivers.into_iter().map(|(core_id, mut rx)| async move {
match tokio::time::timeout(
deadline,
crate::control::server::dispatch_utils::collect_bounded_response(
&mut rx,
max_result_bytes,
),
)
.await
.map_err(|_| crate::Error::Dispatch {
detail: format!("match gather timeout on core {core_id}"),
})? {
Ok(resp) => Ok(resp),
Err(crate::control::server::dispatch_utils::DispatchCollectError::OverBudget {
bytes,
}) => Err(crate::Error::ExecutionLimitExceeded {
detail: format!(
"match gather on core {core_id} exceeded max_query_result_bytes \
({bytes} > {max_result_bytes} bytes)"
),
}),
Err(crate::control::server::dispatch_utils::DispatchCollectError::ChannelClosed) => {
Err(crate::Error::Dispatch {
detail: format!("match gather channel closed on core {core_id}"),
})
}
}
});
let results: Vec<crate::Result<Response>> = join_all(response_futures).await;
let mut all_row_elements: Vec<Vec<u8>> = Vec::new();
let mut frontier: Vec<UnresolvedExpansion> = Vec::new();
let mut resume: Vec<VarLenResume> = Vec::new();
let mut partial = false;
let mut had_error = false;
let mut error_msg = String::new();
for result in results {
let resp = match result {
Ok(r) => r,
Err(e) => {
had_error = true;
error_msg = e.to_string();
continue;
}
};
if resp.status == Status::Error {
if let Some(ec) = resp.error_code.as_deref() {
match ec {
crate::bridge::envelope::ErrorCode::NotFound => continue,
_ => {
had_error = true;
error_msg = format!("{ec:?}");
}
}
}
continue;
}
if resp.partial {
partial = true;
}
if resp.payload.is_empty() {
continue;
}
let mut decoded = decode_match_envelope(resp.payload.as_ref())?;
all_row_elements.append(&mut decoded.row_elements);
frontier.append(&mut decoded.frontier);
resume.append(&mut decoded.resume);
}
if had_error && all_row_elements.is_empty() {
return Err(crate::Error::Dispatch { detail: error_msg });
}
let merged_rows = encode_msgpack_array(&all_row_elements);
Ok(MatchBroadcastOutcome {
rows_payload: Payload::from_vec(merged_rows),
frontier,
resume,
partial,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::executor::handlers::graph_match::{
encode_match_envelope, encode_match_envelope_raw,
};
use crate::engine::graph::pattern::executor::{BindingRow, UnresolvedExpansion, VarLenResume};
use nodedb_query::msgpack_scan::writer::{write_kv_raw, write_map_header};
fn row(pairs: &[(&str, &str)]) -> BindingRow {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
fn resume_cursor() -> VarLenResume {
VarLenResume {
triple_idx: 2,
source_row: row(&[("a", "alice"), ("b", "bob")]),
frontier: vec![
("bob".into(), "alice->bob".into()),
("carol".into(), "alice->carol".into()),
],
depth: 3,
}
}
#[test]
fn envelope_round_trips_rows_and_frontier() {
let rows = vec![row(&[("a", "alice"), ("b", "bob")]), row(&[("a", "carol")])];
let frontier = vec![UnresolvedExpansion {
binding_var: "b".into(),
node_name: "bob".into(),
triple_idx: 1,
partial_row: row(&[("a", "alice"), ("b", "bob")]),
}];
let payload = encode_match_envelope(&rows, &frontier, &[]).unwrap();
let decoded = decode_match_envelope(&payload).unwrap();
assert!(decoded.resume.is_empty());
assert_eq!(decoded.row_elements.len(), 2);
let merged = encode_msgpack_array(&decoded.row_elements);
let envelope_rows = map_value_raw(&payload, MATCH_ENVELOPE_ROWS_KEY).unwrap();
assert_eq!(
merged, envelope_rows,
"merged rows must equal the envelope's bare rows array byte-for-byte"
);
let decoded_json = nodedb_types::json_from_msgpack(&merged).unwrap();
let arr = decoded_json.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["a"], "alice");
assert_eq!(arr[0]["b"], "bob");
assert_eq!(arr[1]["a"], "carol");
assert_eq!(decoded.frontier.len(), 1);
assert_eq!(decoded.frontier[0].node_name, "bob");
assert_eq!(decoded.frontier[0].binding_var, "b");
assert_eq!(decoded.frontier[0].triple_idx, 1);
assert_eq!(
decoded.frontier[0].partial_row.get("a").map(String::as_str),
Some("alice")
);
}
#[test]
fn envelope_round_trips_empty() {
let payload = encode_match_envelope(&[], &[], &[]).unwrap();
let decoded = decode_match_envelope(&payload).unwrap();
assert!(decoded.row_elements.is_empty());
assert!(decoded.frontier.is_empty());
assert!(decoded.resume.is_empty());
let merged = encode_msgpack_array(&decoded.row_elements);
let expected = crate::engine::graph::pattern::executor::rows_to_msgpack(&[]).unwrap();
assert_eq!(merged, expected);
}
#[test]
fn envelope_round_trips_resume_cursor() {
let rows = vec![row(&[("a", "alice"), ("b", "bob")])];
let cursor = resume_cursor();
let payload = encode_match_envelope(&rows, &[], std::slice::from_ref(&cursor)).unwrap();
let decoded = decode_match_envelope(&payload).unwrap();
assert_eq!(decoded.resume.len(), 1);
assert_eq!(decoded.resume[0], cursor);
let rows_array = crate::engine::graph::pattern::executor::rows_to_msgpack(&rows).unwrap();
let two = vec![cursor.clone(), resume_cursor()];
let raw = encode_match_envelope_raw(&rows_array, &[], &two).unwrap();
let decoded_raw = decode_match_envelope(&raw).unwrap();
assert_eq!(decoded_raw.resume.len(), 2);
assert_eq!(decoded_raw.resume[0], cursor);
assert_eq!(decoded_raw.resume[0].frontier, cursor.frontier);
assert_eq!(decoded_raw.resume[0].depth, cursor.depth);
assert_eq!(decoded_raw.resume[0].triple_idx, cursor.triple_idx);
assert_eq!(decoded_raw.resume[0].source_row, cursor.source_row);
}
#[test]
fn legacy_two_key_envelope_decodes_resume_as_empty() {
let rows_array =
crate::engine::graph::pattern::executor::rows_to_msgpack(&[row(&[("a", "x")])])
.unwrap();
let frontier_bytes = zerompk::to_msgpack_vec(&Vec::<UnresolvedExpansion>::new()).unwrap();
let mut buf = Vec::new();
write_map_header(&mut buf, 2);
write_kv_raw(&mut buf, MATCH_ENVELOPE_ROWS_KEY, &rows_array);
write_kv_raw(&mut buf, MATCH_ENVELOPE_FRONTIER_KEY, &frontier_bytes);
let decoded = decode_match_envelope(&buf).unwrap();
assert_eq!(decoded.row_elements.len(), 1);
assert!(decoded.frontier.is_empty());
assert!(decoded.resume.is_empty());
}
#[test]
fn malformed_envelope_is_typed_error() {
let bogus = crate::engine::graph::pattern::executor::rows_to_msgpack(&[row(&[("a", "x")])])
.unwrap();
let err = decode_match_envelope(&bogus);
assert!(
err.is_err(),
"bare array must not decode as an envelope map"
);
}
}