use std::collections::HashMap;
use futures::future::join_all;
use crate::bridge::envelope::PhysicalPlan;
use crate::control::gateway::dispatcher::{
DispatchRouteParams, default_deadline_ms, dispatch_route,
};
use crate::control::gateway::router::resolve_decision;
use crate::control::gateway::version_set::GatewayVersionSet;
use crate::control::gateway::{RouteDecision, TaskRoute};
use crate::control::state::SharedState;
use crate::engine::graph::edge_store::Direction;
use crate::engine::graph::traversal_options::GraphTraversalOptions;
use crate::types::{DatabaseId, TenantId, TraceId, VShardId};
use nodedb_physical::physical_plan::GraphOp;
pub(super) type NeighborTriple = (String, String, String);
pub(super) struct HopOutput {
pub local_triples: Vec<NeighborTriple>,
pub merged_destinations: Vec<String>,
}
pub(super) struct NeighborHopParams<'a> {
pub frontier: &'a [String],
pub edge_label: Option<&'a str>,
pub direction: Direction,
pub options: &'a GraphTraversalOptions,
pub discovered_so_far: usize,
}
pub(super) async fn execute_neighbor_hop(
shared: &SharedState,
tenant_id: TenantId,
database_id: DatabaseId,
params: NeighborHopParams<'_>,
) -> crate::Result<HopOutput> {
let NeighborHopParams {
frontier,
edge_label,
direction,
options,
discovered_so_far,
} = params;
let remaining_budget = options
.max_visited
.saturating_sub(discovered_so_far)
.min(u32::MAX as usize) as u32;
if shared.cluster_routing.is_none() {
let triples = expand_local(
shared,
tenant_id,
database_id,
frontier,
edge_label,
direction,
remaining_budget,
)
.await?;
let merged = dedup_destinations(&triples);
return Ok(HopOutput {
local_triples: triples,
merged_destinations: merged,
});
}
let (local_nodes, remote_by_owner) = partition_frontier_by_owner(shared, frontier)?;
let mut all_triples: Vec<NeighborTriple> = if local_nodes.is_empty() {
Vec::new()
} else {
expand_local(
shared,
tenant_id,
database_id,
&local_nodes,
edge_label,
direction,
remaining_budget,
)
.await?
};
if !remote_by_owner.is_empty() {
let remote_triples = expand_remote(
shared,
tenant_id,
database_id,
remote_by_owner,
edge_label,
direction,
remaining_budget,
)
.await?;
all_triples.extend(remote_triples);
}
let merged = dedup_destinations(&all_triples);
Ok(HopOutput {
local_triples: all_triples,
merged_destinations: merged,
})
}
struct RemoteOwnerBatch {
node_id: u64,
vshard_id: u64,
node_ids: Vec<String>,
}
fn partition_frontier_by_owner(
shared: &SharedState,
frontier: &[String],
) -> crate::Result<(Vec<String>, Vec<RemoteOwnerBatch>)> {
let routing_guard = shared
.cluster_routing
.as_ref()
.map(|rw| rw.read().unwrap_or_else(|p| p.into_inner()));
let raft_snapshot: Vec<nodedb_cluster::GroupStatus> =
shared.raft_status_fn.get().map(|f| f()).unwrap_or_default();
let live_leader = move |group_id: u64| -> u64 {
raft_snapshot
.iter()
.find(|gs| gs.group_id == group_id)
.map(|gs| gs.leader_id)
.unwrap_or(0)
};
let live_lookup: Option<&dyn Fn(u64) -> u64> = if shared.raft_status_fn.get().is_some() {
Some(&live_leader)
} else {
None
};
let mut local: Vec<String> = Vec::new();
let mut remote: HashMap<u32, RemoteOwnerBatch> = HashMap::new();
for node in frontier {
let vshard_id = VShardId::from_key(node.as_bytes()).as_u32();
let decision = resolve_decision(
vshard_id,
shared.node_id,
routing_guard.as_deref(),
live_lookup,
);
match decision {
RouteDecision::Local => local.push(node.clone()),
RouteDecision::Remote {
node_id,
vshard_id: vs,
} => {
remote
.entry(vshard_id)
.or_insert_with(|| RemoteOwnerBatch {
node_id,
vshard_id: vs,
node_ids: Vec::new(),
})
.node_ids
.push(node.clone());
}
RouteDecision::LeaderUnknown { vshard_id: vs } => {
return Err(crate::Error::NotLeader {
vshard_id: VShardId::new((vs % VShardId::COUNT as u64) as u32),
leader_node: 0,
leader_addr: String::new(),
});
}
RouteDecision::Broadcast { .. } => {
return Err(crate::Error::Internal {
detail: "graph hop: resolve_decision returned Broadcast for a single vShard"
.into(),
});
}
}
}
Ok((local, remote.into_values().collect()))
}
async fn expand_local(
shared: &SharedState,
tenant_id: TenantId,
database_id: DatabaseId,
node_ids: &[String],
edge_label: Option<&str>,
direction: Direction,
max_results: u32,
) -> crate::Result<Vec<NeighborTriple>> {
let plan = PhysicalPlan::Graph(GraphOp::NeighborsMulti {
node_ids: node_ids.to_vec(),
edge_label: edge_label.map(str::to_string),
direction,
max_results,
rls_filters: Vec::new(),
});
let resp = crate::control::server::broadcast::broadcast_to_all_cores(
shared,
tenant_id,
database_id,
plan,
TraceId::ZERO,
)
.await?;
Ok(decode_neighbor_triples(&resp.payload))
}
async fn expand_remote(
shared: &SharedState,
tenant_id: TenantId,
database_id: DatabaseId,
owners: Vec<RemoteOwnerBatch>,
edge_label: Option<&str>,
direction: Direction,
max_results: u32,
) -> crate::Result<Vec<NeighborTriple>> {
let shared_arc = super::cluster_resolve::gateway_shared(shared)?;
let deadline_ms = default_deadline_ms(&shared_arc);
let version_set = GatewayVersionSet::from_pairs(Vec::new());
let edge_label_owned = edge_label.map(str::to_string);
let dispatches = owners.into_iter().map(|owner| {
let RemoteOwnerBatch {
node_id,
vshard_id,
node_ids,
} = owner;
let plan = PhysicalPlan::Graph(GraphOp::NeighborsMulti {
node_ids,
edge_label: edge_label_owned.clone(),
direction,
max_results,
rls_filters: Vec::new(),
});
let route = TaskRoute {
plan,
decision: RouteDecision::Remote { node_id, vshard_id },
vshard_id: (vshard_id % VShardId::COUNT as u64) as u32,
};
let version_set = version_set.clone();
let shared_arc = shared_arc.clone();
Box::pin(async move {
dispatch_route(DispatchRouteParams {
route,
shared: &shared_arc,
tenant_id,
database_id,
trace_id: TraceId::ZERO,
deadline_ms,
version_set: &version_set,
txn_id: None,
})
.await
})
});
let results = join_all(dispatches).await;
let mut triples: Vec<NeighborTriple> = Vec::new();
for result in results {
let payloads = result?.payloads;
for payload in payloads {
triples.extend(decode_neighbor_triples_bytes(&payload));
}
}
Ok(triples)
}
fn dedup_destinations(triples: &[NeighborTriple]) -> Vec<String> {
let mut seen: std::collections::HashSet<&String> = std::collections::HashSet::new();
let mut out = Vec::new();
for (_, _, dst) in triples {
if seen.insert(dst) {
out.push(dst.clone());
}
}
out
}
fn decode_neighbor_triples(payload: &crate::bridge::envelope::Payload) -> Vec<NeighborTriple> {
decode_neighbor_triples_bytes(payload)
}
fn decode_neighbor_triples_bytes(payload: &[u8]) -> Vec<NeighborTriple> {
if payload.is_empty() {
return Vec::new();
}
let json_text = crate::data::executor::response_codec::decode_payload_to_json(payload);
decode_neighbor_triples_json(&json_text)
}
fn decode_neighbor_triples_json(json_text: &str) -> Vec<NeighborTriple> {
let arr = match sonic_rs::from_str::<Vec<serde_json::Value>>(json_text) {
Ok(arr) => arr,
Err(_) => return Vec::new(),
};
let mut out = Vec::with_capacity(arr.len());
for item in arr {
let src = item.get("src").and_then(|v| v.as_str());
let node = item.get("node").and_then(|v| v.as_str());
let (src, node) = match (src, node) {
(Some(s), Some(n)) if !s.is_empty() && !n.is_empty() => (s, n),
_ => continue,
};
let label = item.get("label").and_then(|v| v.as_str()).unwrap_or("");
out.push((src.to_string(), label.to_string(), node.to_string()));
}
out
}