use std::sync::Arc;
use std::time::Duration;
use nodedb_cluster::rpc_codec::TypedClusterError;
use crate::Error;
use crate::control::server::dispatch_utils::dispatch_to_data_plane_with_txn;
use crate::control::server::result_stream::ResultStream;
use crate::control::state::SharedState;
use crate::types::{DatabaseId, Lsn, TenantId, TraceId, TxnId, VShardId};
use super::dispatch_remote::{RemoteDispatchArgs, dispatch_remote, dispatch_remote_stream};
use super::route::{RouteDecision, TaskRoute};
use super::version_set::GatewayVersionSet;
pub struct DispatchOutcome {
pub payloads: Vec<Vec<u8>>,
pub shard_watermarks: Vec<(VShardId, Lsn)>,
pub read_version_lsn: Lsn,
}
pub struct DispatchRouteParams<'a> {
pub route: TaskRoute,
pub shared: &'a Arc<SharedState>,
pub tenant_id: TenantId,
pub database_id: DatabaseId,
pub trace_id: TraceId,
pub deadline_ms: u64,
pub version_set: &'a GatewayVersionSet,
pub txn_id: Option<TxnId>,
}
pub async fn dispatch_route(params: DispatchRouteParams<'_>) -> Result<DispatchOutcome, Error> {
let DispatchRouteParams {
route,
shared,
tenant_id,
database_id,
trace_id,
deadline_ms,
version_set,
txn_id,
} = params;
match route.decision {
RouteDecision::Local => {
dispatch_local(route, shared, tenant_id, database_id, trace_id, txn_id).await
}
RouteDecision::Remote { node_id, vshard_id } => {
dispatch_remote(RemoteDispatchArgs {
plan: route.plan,
shared,
node_id,
vshard_id,
tenant_id,
database_id,
trace_id,
deadline_ms,
version_set,
txn_id,
})
.await
}
RouteDecision::Broadcast { .. } => {
Err(Error::Internal {
detail: "dispatcher: Broadcast route reached dispatch — should have been split"
.into(),
})
}
RouteDecision::LeaderUnknown { vshard_id } => {
Err(Error::NotLeader {
vshard_id: VShardId::new(vshard_id as u32),
leader_node: 0,
leader_addr: String::new(),
})
}
}
}
pub struct DispatchRouteStreamParams<'a> {
pub route: TaskRoute,
pub shared: &'a Arc<SharedState>,
pub tenant_id: TenantId,
pub database_id: DatabaseId,
pub trace_id: TraceId,
pub deadline_ms: u64,
pub version_set: &'a GatewayVersionSet,
}
pub async fn dispatch_route_stream(
args: DispatchRouteStreamParams<'_>,
) -> Result<ResultStream, Error> {
let DispatchRouteStreamParams {
route,
shared,
tenant_id,
database_id,
trace_id,
deadline_ms,
version_set,
} = args;
match route.decision {
RouteDecision::Local => crate::control::server::exchange::gather::gather_all_cores_stream(
shared,
tenant_id,
database_id,
route.plan,
trace_id,
None,
),
RouteDecision::Remote { node_id, vshard_id } => {
dispatch_remote_stream(RemoteDispatchArgs {
plan: route.plan,
shared,
node_id,
vshard_id,
tenant_id,
database_id,
trace_id,
deadline_ms,
version_set,
txn_id: None,
})
.await
}
RouteDecision::Broadcast { .. } => Err(Error::Internal {
detail: "dispatcher: Broadcast route reached stream dispatch — should have been split"
.into(),
}),
RouteDecision::LeaderUnknown { vshard_id } => Err(Error::NotLeader {
vshard_id: VShardId::new(vshard_id as u32),
leader_node: 0,
leader_addr: String::new(),
}),
}
}
async fn dispatch_local(
route: TaskRoute,
shared: &Arc<SharedState>,
tenant_id: TenantId,
database_id: DatabaseId,
trace_id: TraceId,
txn_id: Option<TxnId>,
) -> Result<DispatchOutcome, Error> {
let vshard_id = VShardId::new(route.vshard_id);
if txn_id.is_none()
&& let Some(proposer) = shared.async_raft_proposer.get()
&& let Some(entry) = crate::control::wal_replication::to_replicated_entry(
tenant_id,
database_id,
vshard_id,
&route.plan,
)
{
let (payload, write_version) =
crate::control::wal_replication::propose_replicated_entry(shared, proposer, entry)
.await?;
return Ok(DispatchOutcome {
payloads: vec![payload],
shard_watermarks: vec![(vshard_id, Lsn::ZERO)],
read_version_lsn: write_version,
});
}
let resp = dispatch_to_data_plane_with_txn(
shared,
tenant_id,
database_id,
vshard_id,
route.plan,
trace_id,
txn_id,
)
.await?;
Ok(DispatchOutcome {
payloads: vec![resp.payload.to_vec()],
shard_watermarks: vec![(vshard_id, resp.watermark_lsn)],
read_version_lsn: resp.read_version_lsn,
})
}
pub(super) fn map_typed_cluster_error(err: TypedClusterError, vshard_id: u64) -> Error {
match err {
TypedClusterError::NotLeader {
leader_node_id,
leader_addr,
..
} => Error::NotLeader {
vshard_id: VShardId::new((vshard_id % VShardId::COUNT as u64) as u32),
leader_node: leader_node_id.unwrap_or(0),
leader_addr: leader_addr.unwrap_or_default(),
},
TypedClusterError::DescriptorMismatch { collection, .. } => Error::RetryableSchemaChanged {
descriptor: collection,
},
TypedClusterError::DeadlineExceeded { .. } => Error::DeadlineExceeded {
request_id: crate::types::RequestId::new(0),
},
TypedClusterError::Internal { message, .. } => Error::Internal { detail: message },
}
}
pub fn default_deadline_ms(shared: &SharedState) -> u64 {
Duration::from_secs(shared.tuning.network.default_deadline_secs).as_millis() as u64
}
#[cfg(test)]
mod tests {
use super::*;
use nodedb_cluster::rpc_codec::TypedClusterError;
#[test]
fn map_not_leader() {
let err = TypedClusterError::NotLeader {
group_id: 0,
leader_node_id: Some(5),
leader_addr: Some("10.0.0.5:9400".into()),
term: 3,
};
match map_typed_cluster_error(err, 7) {
Error::NotLeader { leader_node, .. } => assert_eq!(leader_node, 5),
other => panic!("expected NotLeader, got {other:?}"),
}
}
#[test]
fn map_descriptor_mismatch() {
let err = TypedClusterError::DescriptorMismatch {
collection: "orders".into(),
expected_version: 1,
actual_version: 2,
};
match map_typed_cluster_error(err, 0) {
Error::RetryableSchemaChanged { descriptor } => assert_eq!(descriptor, "orders"),
other => panic!("expected RetryableSchemaChanged, got {other:?}"),
}
}
#[test]
fn map_deadline_exceeded() {
let err = TypedClusterError::DeadlineExceeded { elapsed_ms: 100 };
assert!(matches!(
map_typed_cluster_error(err, 0),
Error::DeadlineExceeded { .. }
));
}
}