use nodedb_cluster::routing::{RoutingTable, vshard_for_collection};
use nodedb_types::PartitionStrategy;
use nodedb_types::id::{DatabaseId, VShardId};
use nodedb_physical::physical_plan::PhysicalPlan;
use crate::Result;
use super::key_extractor::KeyExtractor;
use super::route::{RouteDecision, TaskRoute};
use super::version_set::touched_collections;
pub fn route_plan(
plan: PhysicalPlan,
local_node_id: u64,
routing: Option<&RoutingTable>,
database_id: DatabaseId,
strategy_fn: impl Fn(&str) -> PartitionStrategy,
extractor: &dyn KeyExtractor,
) -> Result<Vec<TaskRoute>> {
{
use nodedb_physical::physical_plan::MetaOp;
if matches!(
&plan,
PhysicalPlan::Meta(MetaOp::ResolveTxn { .. } | MetaOp::TransactionBatch { .. })
) {
return Err(crate::Error::Internal {
detail: "commit meta-op cannot be routed by the gateway; \
dispatch it with the task's explicit vshard_id"
.to_owned(),
});
}
}
let Some(routing) = routing else {
let vshard_id = primary_vshard(&plan, database_id);
return Ok(vec![TaskRoute {
plan,
decision: RouteDecision::Local,
vshard_id,
}]);
};
use nodedb_physical::physical_plan::{
ExchangeMode, ExchangeOp, QueryOp, plan_contains_cluster_partitioned_leaf,
};
match plan {
PhysicalPlan::Query(QueryOp::Exchange(ExchangeOp {
child,
mode: ExchangeMode::Gather { .. },
})) => {
if plan_contains_cluster_partitioned_leaf(&child) {
Ok(route_broadcast(*child, local_node_id, routing))
} else {
Ok(route_single_collection(
*child,
local_node_id,
routing,
database_id,
&strategy_fn,
extractor,
)?)
}
}
other => Ok(route_single_collection(
other,
local_node_id,
routing,
database_id,
&strategy_fn,
extractor,
)?),
}
}
fn route_single_collection(
plan: PhysicalPlan,
local_node_id: u64,
routing: &RoutingTable,
database_id: DatabaseId,
strategy_fn: &impl Fn(&str) -> PartitionStrategy,
extractor: &dyn KeyExtractor,
) -> Result<Vec<TaskRoute>> {
let primary_name: Option<String> = touched_collections(&plan).into_iter().next();
let strategy = primary_name.as_deref().map(strategy_fn).unwrap_or_default();
match strategy {
PartitionStrategy::CollectionHomed => {
let vshard_id = primary_name
.as_deref()
.map(|name| vshard_for_collection(database_id, name))
.unwrap_or(0);
let decision = resolve_decision(vshard_id, local_node_id, Some(routing), None);
Ok(vec![TaskRoute {
plan,
decision,
vshard_id,
}])
}
PartitionStrategy::KeyPartitioned { key: key_spec } => {
let raw_keys = extractor.extract_keys(&plan, &key_spec)?;
let mut seen = std::collections::HashSet::new();
let mut routes = Vec::new();
for raw_key in raw_keys {
let vshard_id = VShardId::from_key(&raw_key).as_u32();
if seen.insert(vshard_id) {
let decision = resolve_decision(vshard_id, local_node_id, Some(routing), None);
routes.push(TaskRoute {
plan: plan.clone(),
decision,
vshard_id,
});
}
}
Ok(routes)
}
}
}
pub fn resolve_decision(
vshard_id: u32,
local_node_id: u64,
routing: Option<&RoutingTable>,
live_leader_for_group: Option<&dyn Fn(u64) -> u64>,
) -> RouteDecision {
let Some(routing) = routing else {
return RouteDecision::Local;
};
let unknown = RouteDecision::LeaderUnknown {
vshard_id: vshard_id as u64,
};
if let Some(live) = live_leader_for_group
&& let Ok(group_id) = routing.group_for_vshard(vshard_id)
{
let live_leader = live(group_id);
if live_leader == local_node_id {
return RouteDecision::Local;
}
if live_leader != 0 {
return RouteDecision::Remote {
node_id: live_leader,
vshard_id: vshard_id as u64,
};
}
}
match routing.leader_for_vshard(vshard_id) {
Ok(0) => unknown,
Ok(leader) if leader == local_node_id => RouteDecision::Local,
Ok(leader) => RouteDecision::Remote {
node_id: leader,
vshard_id: vshard_id as u64,
},
Err(_) => unknown,
}
}
fn route_broadcast(
plan: PhysicalPlan,
local_node_id: u64,
routing: &RoutingTable,
) -> Vec<TaskRoute> {
use nodedb_cluster::routing::VSHARD_COUNT;
let mut routes = Vec::with_capacity(VSHARD_COUNT as usize);
for vshard_id in 0u32..VSHARD_COUNT {
let decision = resolve_decision(vshard_id, local_node_id, Some(routing), None);
routes.push(TaskRoute {
plan: plan.clone(),
decision,
vshard_id,
});
}
routes
}
fn primary_vshard(plan: &PhysicalPlan, database_id: DatabaseId) -> u32 {
touched_collections(plan)
.into_iter()
.next()
.map(|name| vshard_for_collection(database_id, &name))
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use nodedb_physical::physical_plan::{DocumentOp, KvOp, PhysicalPlan};
fn single_node_table() -> RoutingTable {
RoutingTable::uniform(1, &[1], 1)
}
fn two_node_table() -> RoutingTable {
RoutingTable::uniform(2, &[1, 2], 1)
}
#[test]
fn single_node_routes_locally() {
let table = single_node_table();
let plan = PhysicalPlan::Kv(KvOp::Get {
collection: "users".into(),
key: vec![],
rls_filters: vec![],
surrogate_ceiling: None,
});
let routes = route_plan(
plan,
1,
Some(&table),
DatabaseId::DEFAULT,
|_| PartitionStrategy::CollectionHomed,
&crate::control::gateway::UnwiredKeyExtractor,
)
.expect("route");
assert_eq!(routes.len(), 1);
assert_eq!(routes[0].decision, RouteDecision::Local);
}
#[test]
fn no_routing_table_routes_locally() {
let plan = PhysicalPlan::Kv(KvOp::Put {
collection: "x".into(),
key: vec![],
value: vec![],
ttl_ms: 0,
surrogate: nodedb_types::Surrogate::ZERO,
});
let routes = route_plan(
plan,
99,
None,
DatabaseId::DEFAULT,
|_| PartitionStrategy::CollectionHomed,
&crate::control::gateway::UnwiredKeyExtractor,
)
.expect("route");
assert_eq!(routes.len(), 1);
assert_eq!(routes[0].decision, RouteDecision::Local);
}
#[test]
fn remote_route_when_different_leader() {
let mut table = two_node_table();
let group = table.group_for_vshard(0).unwrap();
table.set_leader(group, 2);
let collection = find_collection_for_vshard(0);
let plan = PhysicalPlan::Kv(KvOp::Get {
collection,
key: vec![],
rls_filters: vec![],
surrogate_ceiling: None,
});
let routes = route_plan(
plan,
1,
Some(&table),
DatabaseId::DEFAULT,
|_| PartitionStrategy::CollectionHomed,
&crate::control::gateway::UnwiredKeyExtractor,
)
.expect("route");
assert_eq!(routes.len(), 1);
match &routes[0].decision {
RouteDecision::Remote { node_id, .. } => assert_eq!(*node_id, 2),
other => panic!("expected Remote, got {other:?}"),
}
}
#[test]
fn single_homed_gather_routes_to_one_vshard() {
let table = two_node_table();
let scan = PhysicalPlan::Document(DocumentOp::Scan {
collection: "events".into(),
limit: 100,
offset: 0,
sort_keys: vec![],
filters: vec![],
distinct: false,
projection: vec![],
computed_columns: vec![],
window_functions: vec![],
system_time: nodedb_types::SystemTimeScope::Current,
valid_at_ms: None,
prefilter: None,
});
let plan = PhysicalPlan::Query(nodedb_physical::physical_plan::QueryOp::Exchange(
nodedb_physical::physical_plan::ExchangeOp {
child: Box::new(scan),
mode: nodedb_physical::physical_plan::ExchangeMode::Gather {
as_aggregate: false,
},
},
));
let routes = route_plan(
plan,
1,
Some(&table),
DatabaseId::DEFAULT,
|_| PartitionStrategy::CollectionHomed,
&crate::control::gateway::UnwiredKeyExtractor,
)
.expect("route");
assert_eq!(
routes.len(),
1,
"single-homed Exchange{{Gather}} must route to one vShard, not broadcast"
);
assert!(
matches!(
routes[0].plan,
PhysicalPlan::Document(DocumentOp::Scan { .. })
),
"route must carry the unwrapped scan child, got {:?}",
routes[0].plan
);
assert_eq!(
routes[0].vshard_id,
vshard_for_collection(DatabaseId::DEFAULT, "events")
);
}
fn find_collection_for_vshard(target: u32) -> String {
for i in 0u64.. {
let name = format!("col_{i}");
if vshard_for_collection(DatabaseId::DEFAULT, &name) == target {
return name;
}
}
unreachable!()
}
#[test]
fn commit_meta_ops_are_rejected() {
use nodedb_physical::physical_plan::MetaOp;
for plan in [
PhysicalPlan::Meta(MetaOp::TransactionBatch {
plans: vec![],
txn_id: None,
}),
PhysicalPlan::Meta(MetaOp::ResolveTxn {
txn_id: nodedb_types::id::TxnId::new(7),
plans: vec![],
}),
] {
for table in [None, Some(single_node_table())] {
let result = route_plan(
plan.clone(),
1,
table.as_ref(),
DatabaseId::DEFAULT,
|_| PartitionStrategy::CollectionHomed,
&crate::control::gateway::UnwiredKeyExtractor,
);
assert!(
result.is_err(),
"commit meta-op must not be routable via the gateway: {plan:?}"
);
}
}
}
}