1use std::sync::atomic::{AtomicU64, Ordering};
46use std::sync::Arc;
47
48use tokio::sync::mpsc;
49
50use crate::cluster::pool::ServerPool;
51use crate::cluster::snitch::{rack_distance, RackDistance};
52use crate::cluster::vnode;
53use crate::conf::HashType as ConfHashType;
54use crate::hashkit::{self, HashType};
55use crate::io::mbuf::MbufPool;
56use crate::msg::{ConsistencyLevel, Msg, MsgRouting, MsgType};
57use crate::net::dispatcher::{DispatchOutcome, Dispatcher, OutboundEnvelope, ServerSink};
58use crate::net::server::OutboundRequest;
59
60#[must_use]
74pub fn distribution_shadow_disagreement_total() -> u64 {
75 SHADOW_DISAGREEMENTS.load(Ordering::Relaxed)
76}
77
78pub fn reset_distribution_shadow_disagreement_total() {
89 SHADOW_DISAGREEMENTS.store(0, Ordering::Relaxed);
90}
91
92static SHADOW_DISAGREEMENTS: AtomicU64 = AtomicU64::new(0);
93
94fn bump_shadow_disagreement() {
95 SHADOW_DISAGREEMENTS.fetch_add(1, Ordering::Relaxed);
96}
97
98fn enter_plan_span(
104 req_id: u64,
105 plan: &DispatchPlan,
106) -> (tracing::Span, tracing::span::EnteredSpan) {
107 let req_span = tracing::Span::current();
108 let kind: &'static str = match plan {
109 DispatchPlan::Drop => "drop",
110 DispatchPlan::NoTargets => "no_targets",
111 DispatchPlan::LocalDatastore => "local_datastore",
112 DispatchPlan::Replicas { .. } => "replicas",
113 };
114 let targets = match plan {
115 DispatchPlan::Replicas { targets, .. } => targets.len(),
116 _ => 0,
117 };
118 let span = tracing::info_span!("dispatch.plan", req_id, plan = kind, targets,).entered();
119 (req_span, span)
120}
121
122fn map_hash(h: ConfHashType) -> HashType {
123 match h {
124 ConfHashType::OneAtATime => HashType::OneAtATime,
125 ConfHashType::Md5 => HashType::Md5,
126 ConfHashType::Crc16 => HashType::Crc16,
127 ConfHashType::Crc32 => HashType::Crc32,
128 ConfHashType::Crc32a => HashType::Crc32a,
129 ConfHashType::Fnv1_64 => HashType::Fnv1_64,
130 ConfHashType::Fnv1a64 => HashType::Fnv1a_64,
131 ConfHashType::Fnv1_32 => HashType::Fnv1_32,
132 ConfHashType::Fnv1a32 => HashType::Fnv1a_32,
133 ConfHashType::Hsieh => HashType::Hsieh,
134 ConfHashType::Murmur => HashType::Murmur,
135 ConfHashType::Jenkins => HashType::Jenkins,
136 ConfHashType::Murmur3 => HashType::Murmur3,
137 ConfHashType::Murmur3X64_64 => HashType::Murmur3X64_64,
138 }
139}
140
141#[derive(Clone, Debug, Eq, PartialEq)]
143pub struct ReplicaTarget {
144 pub peer_idx: u32,
146 pub dc: String,
148 pub rack: String,
150 pub is_local: bool,
152}
153
154#[derive(Clone, Debug, Eq, PartialEq)]
162pub enum DispatchPlan {
163 LocalDatastore,
165 Replicas {
171 targets: Vec<ReplicaTarget>,
173 consistency: ConsistencyLevel,
175 },
176 NoTargets,
179 Drop,
181}
182
183#[derive(Clone)]
185pub struct ClusterDispatcher {
186 pool: Arc<ServerPool>,
187 backend: Option<mpsc::Sender<OutboundRequest>>,
194 peer_backends: std::collections::HashMap<u32, mpsc::Sender<OutboundRequest>>,
202 mbuf_pool: MbufPool,
207 hint_store: Option<Arc<crate::cluster::hints::HintStore>>,
214 failure_metrics: Option<Arc<crate::stats::FailureMetrics>>,
220 command_extension: Option<Arc<dyn crate::embed::CommandExtension>>,
231 local_datastore: Option<Arc<dyn crate::embed::hooks::Datastore>>,
241}
242
243impl std::fmt::Debug for ClusterDispatcher {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 f.debug_struct("ClusterDispatcher")
250 .field("backend", &self.backend.is_some())
251 .field("peer_backends", &self.peer_backends.len())
252 .field("hint_store", &self.hint_store.is_some())
253 .field("failure_metrics", &self.failure_metrics.is_some())
254 .field("command_extension", &self.command_extension)
255 .field("local_datastore", &self.local_datastore.is_some())
256 .finish_non_exhaustive()
257 }
258}
259
260impl ClusterDispatcher {
261 #[must_use]
281 pub fn new(pool: Arc<ServerPool>) -> Self {
282 Self {
283 pool,
284 backend: None,
285 peer_backends: std::collections::HashMap::new(),
286 mbuf_pool: MbufPool::default(),
287 hint_store: None,
288 failure_metrics: None,
289 command_extension: None,
290 local_datastore: None,
291 }
292 }
293
294 #[must_use]
316 pub fn with_mbuf_pool(mut self, pool: MbufPool) -> Self {
317 self.mbuf_pool = pool;
318 self
319 }
320
321 #[must_use]
325 pub fn mbuf_pool(&self) -> &MbufPool {
326 &self.mbuf_pool
327 }
328
329 #[must_use]
338 pub fn with_backend(mut self, backend: mpsc::Sender<OutboundRequest>) -> Self {
339 self.backend = Some(backend);
340 self
341 }
342
343 #[must_use]
354 pub fn with_local_datastore(
355 mut self,
356 datastore: Arc<dyn crate::embed::hooks::Datastore>,
357 ) -> Self {
358 self.local_datastore = Some(datastore);
359 self
360 }
361
362 #[must_use]
374 pub fn with_peer_backend(
375 mut self,
376 peer_idx: u32,
377 sender: mpsc::Sender<OutboundRequest>,
378 ) -> Self {
379 self.peer_backends.insert(peer_idx, sender);
380 self
381 }
382
383 #[must_use]
385 pub fn has_backend(&self) -> bool {
386 self.backend.is_some()
387 }
388
389 #[must_use]
391 pub fn peer_backend_count(&self) -> usize {
392 self.peer_backends.len()
393 }
394
395 #[must_use]
397 pub fn pool(&self) -> &Arc<ServerPool> {
398 &self.pool
399 }
400
401 #[must_use]
434 pub fn with_hint_store(mut self, store: Arc<crate::cluster::hints::HintStore>) -> Self {
435 self.hint_store = Some(store);
436 self
437 }
438
439 #[must_use]
441 pub fn hint_store(&self) -> Option<&Arc<crate::cluster::hints::HintStore>> {
442 self.hint_store.as_ref()
443 }
444
445 #[must_use]
475 pub fn with_failure_metrics(mut self, metrics: Arc<crate::stats::FailureMetrics>) -> Self {
476 self.failure_metrics = Some(metrics);
477 self
478 }
479
480 #[must_use]
482 pub fn failure_metrics(&self) -> Option<&Arc<crate::stats::FailureMetrics>> {
483 self.failure_metrics.as_ref()
484 }
485
486 #[must_use]
529 pub fn with_command_extension(mut self, ext: Arc<dyn crate::embed::CommandExtension>) -> Self {
530 self.command_extension = Some(ext);
531 self
532 }
533
534 #[must_use]
536 pub fn command_extension(&self) -> Option<&Arc<dyn crate::embed::CommandExtension>> {
537 self.command_extension.as_ref()
538 }
539
540 #[must_use]
543 pub fn hinted_handoff_active(&self) -> bool {
544 self.hint_store.is_some() && self.pool.config().enable_hinted_handoff
545 }
546
547 #[must_use]
562 pub fn plan(&self, req: &Msg, key: &[u8]) -> DispatchPlan {
563 let cfg = self.pool.config();
564 let peers = self.pool.peers().read();
565 if peers.is_empty() {
566 self.record_no_targets_metric(cfg, ConsistencyLevel::default());
567 return DispatchPlan::NoTargets;
568 }
569 if matches!(req.routing(), MsgRouting::LocalNodeOnly) {
570 return DispatchPlan::LocalDatastore;
571 }
572 if key.is_empty() {
573 return DispatchPlan::LocalDatastore;
574 }
575 let token = hashkit::hash(map_hash(cfg.hash), key);
576 let key_hash64 = hashkit::hash64(map_hash(cfg.hash), key);
577 let bucket = crate::proto::redis::bucket_name(key);
578 let bucket_type = cfg.resolve_bucket_type(bucket);
579 let is_read = matches!(req.ty(), MsgType::Unknown) || req.flags().is_read;
580 let consistency = match (bucket_type, is_read) {
581 (Some(bt), true) => bt.read_consistency,
582 (Some(bt), false) => bt.write_consistency,
583 (None, true) => cfg.read_consistency,
584 (None, false) => cfg.write_consistency,
585 };
586 let n_val_cap = bucket_type.map_or(0, |bt| bt.n_val);
587 let dcs = self.pool.datacenters().read();
588 let include_down = self.hinted_handoff_active() && !is_read;
597 let routable = collect_routable(
598 &dcs,
599 &peers,
600 &token,
601 key_hash64,
602 cfg.distribution,
603 include_down,
604 );
605 if let Some(shadow) = cfg.distribution_shadow {
606 if shadow != cfg.distribution {
607 let shadow_routable =
608 collect_routable(&dcs, &peers, &token, key_hash64, shadow, include_down);
609 if !plans_agree(&routable, &shadow_routable) {
610 bump_shadow_disagreement();
611 tracing::debug!(
612 target: "dynomite::dispatch::shadow",
613 live = cfg.distribution.as_str(),
614 shadow = shadow.as_str(),
615 "shadow distribution disagreed on key route"
616 );
617 }
618 }
619 }
620 if routable.is_empty() {
621 self.record_no_targets_metric(cfg, consistency);
622 return DispatchPlan::NoTargets;
623 }
624 let (local, remote): (Vec<_>, Vec<_>) = routable
625 .into_iter()
626 .partition(|(dc_idx, _, _)| dcs[*dc_idx].name() == cfg.dc);
627 let plan =
628 plan_with_consistency(cfg, &dcs, &peers, consistency, req.routing(), local, remote);
629 let plan = cap_replicas(plan, n_val_cap);
630 if matches!(plan, DispatchPlan::NoTargets) {
631 self.record_no_targets_metric(cfg, consistency);
632 }
633 plan
634 }
635
636 fn record_no_targets_metric(
639 &self,
640 cfg: &crate::cluster::pool::PoolConfig,
641 consistency: ConsistencyLevel,
642 ) {
643 if let Some(m) = self.failure_metrics.as_ref() {
644 m.record_no_targets(&cfg.dc, &cfg.rack, consistency);
645 }
646 }
647
648 fn peer_dc_label(&self, peer_idx: u32) -> String {
652 let peers = self.pool.peers().read();
653 peers
654 .get(peer_idx as usize)
655 .map_or_else(|| self.pool.config().dc.clone(), |p| p.dc().to_string())
656 }
657}
658
659fn cap_replicas(plan: DispatchPlan, cap: u8) -> DispatchPlan {
665 if cap == 0 {
666 return plan;
667 }
668 let cap = cap as usize;
669 match plan {
670 DispatchPlan::Replicas {
671 mut targets,
672 consistency,
673 } if targets.len() > cap => {
674 targets.truncate(cap);
675 DispatchPlan::Replicas {
676 targets,
677 consistency,
678 }
679 }
680 other => other,
681 }
682}
683
684fn plans_agree(a: &[(usize, usize, u32)], b: &[(usize, usize, u32)]) -> bool {
685 if a.len() != b.len() {
686 return false;
687 }
688 let mut a_idx: Vec<u32> = a.iter().map(|t| t.2).collect();
689 let mut b_idx: Vec<u32> = b.iter().map(|t| t.2).collect();
690 a_idx.sort_unstable();
691 b_idx.sort_unstable();
692 a_idx == b_idx
693}
694
695fn collect_routable(
696 dcs: &[crate::cluster::Datacenter],
697 peers: &[crate::cluster::peer::Peer],
698 token: &crate::hashkit::DynToken,
699 hash64: u64,
700 distribution: crate::conf::Distribution,
701 include_down: bool,
702) -> Vec<(usize, usize, u32)> {
703 let mut routable: Vec<(usize, usize, u32)> = Vec::new();
704 for (dc_idx, dc) in dcs.iter().enumerate() {
705 for (rack_idx, rack) in dc.racks().iter().enumerate() {
706 let candidate = match (distribution, rack.random_slices()) {
707 (crate::conf::Distribution::RandomSlicing, Some(slices)) => {
708 slices.claimant_for(hash64).and_then(|name| {
715 peers.iter().find_map(|p| {
716 if p.dc() == dc.name()
717 && p.rack() == rack.name()
718 && p.endpoint().pname() == name
719 {
720 Some(p.idx())
721 } else {
722 None
723 }
724 })
725 })
726 }
727 _ => vnode::dispatch(rack.continuums(), token),
728 };
729 if let Some(peer_idx) = candidate {
730 if let Some(peer) = peers.get(peer_idx as usize) {
731 let state = peer.state();
732 let accept = state.is_routable()
733 || (include_down && matches!(state, crate::cluster::peer::PeerState::Down));
734 if accept {
735 routable.push((dc_idx, rack_idx, peer_idx));
736 }
737 }
738 }
739 }
740 }
741 routable
742}
743
744fn build_target(
745 dcs: &[crate::cluster::Datacenter],
746 peers: &[crate::cluster::peer::Peer],
747 dc_idx: usize,
748 rack_idx: usize,
749 peer_idx: u32,
750) -> ReplicaTarget {
751 let dc_name = dcs[dc_idx].name().to_string();
752 let rack_name = dcs[dc_idx].racks()[rack_idx].name().to_string();
753 let is_local = peers
754 .get(peer_idx as usize)
755 .is_some_and(crate::cluster::peer::Peer::is_local);
756 ReplicaTarget {
757 peer_idx,
758 dc: dc_name,
759 rack: rack_name,
760 is_local,
761 }
762}
763
764fn plan_with_consistency(
765 cfg: &crate::cluster::pool::PoolConfig,
766 dcs: &[crate::cluster::Datacenter],
767 peers: &[crate::cluster::peer::Peer],
768 consistency: ConsistencyLevel,
769 routing: MsgRouting,
770 local: Vec<(usize, usize, u32)>,
771 remote: Vec<(usize, usize, u32)>,
772) -> DispatchPlan {
773 let want_per_dc_fanout = matches!(consistency, ConsistencyLevel::DcEachSafeQuorum)
774 || matches!(routing, MsgRouting::AllNodesAllRacksAllDcs);
775 let mut targets: Vec<ReplicaTarget> = Vec::new();
776 match consistency {
777 ConsistencyLevel::DcOne => {
778 if local.is_empty() {
779 return DispatchPlan::NoTargets;
780 }
781 let mut best: Option<(RackDistance, (usize, usize, u32))> = None;
782 for (dc_idx, rack_idx, peer_idx) in local {
783 let rack_name = dcs[dc_idx].racks()[rack_idx].name();
784 let d = rack_distance(&cfg.dc, &cfg.rack, &cfg.dc, rack_name);
785 let take = match best {
786 None => true,
787 Some((bd, _)) => d.cost() < bd.cost(),
788 };
789 if take {
790 best = Some((d, (dc_idx, rack_idx, peer_idx)));
791 }
792 }
793 if let Some((_, (dc_idx, rack_idx, peer_idx))) = best {
794 let is_local_node = peers
795 .get(peer_idx as usize)
796 .is_some_and(crate::cluster::peer::Peer::is_local);
797 if is_local_node {
798 return DispatchPlan::LocalDatastore;
799 }
800 targets.push(build_target(dcs, peers, dc_idx, rack_idx, peer_idx));
801 }
802 }
803 ConsistencyLevel::DcQuorum | ConsistencyLevel::DcSafeQuorum => {
804 if local.is_empty() {
805 return DispatchPlan::NoTargets;
806 }
807 for (dc_idx, rack_idx, peer_idx) in local {
808 targets.push(build_target(dcs, peers, dc_idx, rack_idx, peer_idx));
809 }
810 }
811 ConsistencyLevel::DcEachSafeQuorum => {
812 if local.is_empty() && remote.is_empty() {
813 return DispatchPlan::NoTargets;
814 }
815 for (dc_idx, rack_idx, peer_idx) in local.iter().chain(remote.iter()) {
816 targets.push(build_target(dcs, peers, *dc_idx, *rack_idx, *peer_idx));
817 }
818 }
819 }
820 if want_per_dc_fanout && !remote.is_empty() {
821 for (dc_idx, rack_idx, peer_idx) in remote {
822 if !targets.iter().any(|t| t.peer_idx == peer_idx) {
823 targets.push(build_target(dcs, peers, dc_idx, rack_idx, peer_idx));
824 }
825 }
826 }
827 if targets.is_empty() {
828 return DispatchPlan::LocalDatastore;
829 }
830 DispatchPlan::Replicas {
831 targets,
832 consistency,
833 }
834}
835
836impl Dispatcher for ClusterDispatcher {
837 #[allow(
838 clippy::too_many_lines,
839 reason = "single dispatch fn must enumerate every plan; splitting hides the planner-to-effect mapping"
840 )]
841 fn dispatch(&self, req: Msg, responder: ServerSink) -> DispatchOutcome {
842 if req.flags().quit {
843 return DispatchOutcome::Drop;
844 }
845 if let Some(ext) = self.command_extension.as_ref() {
851 if let Some(outcome) = self.intercept_command(ext.as_ref(), &req) {
852 return outcome;
853 }
854 }
855 let key: Vec<u8> = req
863 .keys()
864 .first()
865 .map(|kp| kp.tag_bytes().to_vec())
866 .unwrap_or_default();
867 let plan = self.plan(&req, &key);
868 let (req_span, _plan_span) = enter_plan_span(req.id(), &plan);
869 match plan {
870 DispatchPlan::Drop => DispatchOutcome::Drop,
871 DispatchPlan::NoTargets => {
872 let err_type = if matches!(req.ty(), MsgType::ReqRedisGet | MsgType::ReqRedisSet) {
873 MsgType::RspRedisError
874 } else {
875 MsgType::RspMcServerError
876 };
877 let rsp = crate::msg::response::make_error(
878 &req,
879 err_type,
880 0,
881 crate::msg::DynErrorCode::DynomiteNoQuorumAchieved,
882 &self.mbuf_pool,
883 );
884 DispatchOutcome::Error(rsp)
885 }
886 DispatchPlan::LocalDatastore => {
887 if let Some(ds) = self.local_datastore.as_ref() {
895 let ds = Arc::clone(ds);
896 let req_id = req.id();
897 let span = req_span.clone();
898 tokio::spawn(async move {
899 if let Ok(rsp) = ds.dispatch(req).await {
900 let _ = responder
901 .send(OutboundEnvelope {
902 req_id,
903 rsp,
904 span,
905 source_peer_idx: None,
906 })
907 .await;
908 }
909 });
910 return DispatchOutcome::Pending;
911 }
912 if let Some(tx) = self.backend.as_ref() {
913 let bytes: Vec<u8> = req
918 .mbufs()
919 .iter()
920 .flat_map(|b| b.readable().to_vec())
921 .collect();
922 if bytes.is_empty() {
923 return DispatchOutcome::Drop;
927 }
928 let env = OutboundRequest {
929 bytes,
930 req_id: req.id(),
931 responder,
932 span: req_span.clone(),
933 ty: crate::proto::dnode::DmsgType::Req,
934 target_peer_idx: None,
935 };
936 if let Err(err) = tx.try_send(env) {
937 if let Some(m) = self.failure_metrics.as_ref() {
940 match err {
941 tokio::sync::mpsc::error::TrySendError::Full(_) => {
942 m.record_backend_send_full();
943 }
944 tokio::sync::mpsc::error::TrySendError::Closed(_) => {
945 m.record_backend_send_closed();
946 }
947 }
948 }
949 let err_type =
950 if matches!(req.ty(), MsgType::ReqRedisGet | MsgType::ReqRedisSet) {
951 MsgType::RspRedisError
952 } else {
953 MsgType::RspMcServerError
954 };
955 let rsp = crate::msg::response::make_error(
956 &req,
957 err_type,
958 0,
959 crate::msg::DynErrorCode::DynomiteNoQuorumAchieved,
960 &self.mbuf_pool,
961 );
962 return DispatchOutcome::Error(rsp);
963 }
964 }
965 DispatchOutcome::Pending
966 }
967 DispatchPlan::Replicas {
968 targets,
969 consistency,
970 } => self.dispatch_replicas(&req, &req_span, &targets, consistency, responder),
971 }
972 }
973}
974
975impl ClusterDispatcher {
976 fn dispatch_replicas(
999 &self,
1000 req: &Msg,
1001 req_span: &tracing::Span,
1002 targets: &[ReplicaTarget],
1003 consistency: ConsistencyLevel,
1004 responder: ServerSink,
1005 ) -> DispatchOutcome {
1006 if targets.is_empty() {
1007 return DispatchOutcome::Drop;
1008 }
1009 let bytes: Vec<u8> = req
1013 .mbufs()
1014 .iter()
1015 .flat_map(|b| b.readable().to_vec())
1016 .collect();
1017 if bytes.is_empty() {
1018 return DispatchOutcome::Drop;
1019 }
1020 let peer_states = self.snapshot_peer_states(targets);
1024 let is_read = matches!(req.ty(), MsgType::Unknown) || req.flags().is_read;
1025 let is_write = !is_read;
1026 let handoff_active = self.hinted_handoff_active() && is_write;
1027 if targets.len() == 1 {
1030 return self.dispatch_replicas_direct(
1031 req,
1032 req_span,
1033 targets,
1034 &bytes,
1035 &responder,
1036 &HandoffCtx {
1037 handoff_active,
1038 peer_states: &peer_states,
1039 },
1040 );
1041 }
1042 let cfg = self.pool.config();
1044 let local_dc = cfg.dc.clone();
1045 let (intermediate_tx, intermediate_rx) =
1050 mpsc::channel::<OutboundEnvelope>(targets.len() + 1);
1051 let target_pairs: Vec<(u32, String)> =
1054 targets.iter().map(|t| (t.peer_idx, t.dc.clone())).collect();
1055 let repair_key: Option<Vec<u8>> = req
1058 .keys()
1059 .first()
1060 .map(|kp| kp.tag_bytes().to_vec())
1061 .filter(|k| !k.is_empty());
1062 let repair_ctx = repair_key.map(|key| ReadRepairContext {
1063 req_id: req.id(),
1064 req_ty: req.ty(),
1065 key,
1066 mbuf_pool: self.mbuf_pool.clone(),
1067 peer_backends: self.peer_backends.clone(),
1068 local_backend: self.backend.clone(),
1069 target_is_local: targets.iter().map(|t| (t.peer_idx, t.is_local)).collect(),
1070 });
1071 let mut sent = 0usize;
1074 let mut hinted = 0usize;
1075 for target in targets {
1076 let action = Self::choose_target_action(target, handoff_active, &peer_states);
1077 match action {
1078 TargetAction::Send => {
1079 if self.fanout_send(target, req, req_span, &bytes, &intermediate_tx) {
1080 sent += 1;
1081 } else if handoff_active
1082 && self.hint_target(target, &bytes, req, req_span, &intermediate_tx)
1083 {
1084 hinted += 1;
1085 }
1086 }
1087 TargetAction::Hint => {
1088 if self.hint_target(target, &bytes, req, req_span, &intermediate_tx) {
1089 hinted += 1;
1090 }
1091 }
1092 }
1093 }
1094 drop(intermediate_tx);
1099 if sent + hinted == 0 {
1100 return DispatchOutcome::Error(self.no_quorum_error(req));
1101 }
1102 let req_id = req.id();
1103 let req_ty = req.ty();
1104 let mbuf_pool = self.mbuf_pool.clone();
1105 let failure_metrics = self.failure_metrics.clone();
1106 tokio::spawn(coalesce_actor(
1107 req_id,
1108 req_ty,
1109 consistency,
1110 target_pairs,
1111 local_dc,
1112 intermediate_rx,
1113 responder,
1114 mbuf_pool,
1115 repair_ctx,
1116 failure_metrics,
1117 ));
1118 DispatchOutcome::Pending
1119 }
1120
1121 fn snapshot_peer_states(
1126 &self,
1127 targets: &[ReplicaTarget],
1128 ) -> std::collections::HashMap<u32, crate::cluster::peer::PeerState> {
1129 use crate::cluster::peer::PeerState;
1130 let peers = self.pool.peers().read();
1131 let mut out = std::collections::HashMap::with_capacity(targets.len());
1132 for t in targets {
1133 let state = if t.is_local {
1134 PeerState::Normal
1135 } else {
1136 peers
1137 .get(t.peer_idx as usize)
1138 .map_or(PeerState::Unknown, crate::cluster::peer::Peer::state)
1139 };
1140 out.insert(t.peer_idx, state);
1141 }
1142 out
1143 }
1144
1145 fn choose_target_action(
1146 target: &ReplicaTarget,
1147 handoff_active: bool,
1148 peer_states: &std::collections::HashMap<u32, crate::cluster::peer::PeerState>,
1149 ) -> TargetAction {
1150 use crate::cluster::peer::PeerState;
1151 if !handoff_active {
1152 return TargetAction::Send;
1153 }
1154 let state = peer_states
1155 .get(&target.peer_idx)
1156 .copied()
1157 .unwrap_or(PeerState::Unknown);
1158 match state {
1159 PeerState::Down => TargetAction::Hint,
1160 _ => TargetAction::Send,
1161 }
1162 }
1163
1164 fn fanout_send(
1167 &self,
1168 target: &ReplicaTarget,
1169 req: &Msg,
1170 req_span: &tracing::Span,
1171 bytes: &[u8],
1172 intermediate_tx: &mpsc::Sender<OutboundEnvelope>,
1173 ) -> bool {
1174 let env = OutboundRequest {
1175 bytes: bytes.to_vec(),
1176 req_id: req.id(),
1177 responder: intermediate_tx.clone(),
1178 span: req_span.clone(),
1179 ty: crate::proto::dnode::DmsgType::Req,
1180 target_peer_idx: Some(target.peer_idx),
1181 };
1182 let send_result = if target.is_local {
1183 self.backend.as_ref().map(|tx| tx.try_send(env))
1184 } else {
1185 self.peer_backends
1186 .get(&target.peer_idx)
1187 .map(|tx| tx.try_send(env))
1188 };
1189 match send_result {
1190 Some(Ok(())) => true,
1191 Some(Err(err)) => {
1192 self.observe_send_error(target, &err);
1193 false
1194 }
1195 None => false,
1196 }
1197 }
1198
1199 fn observe_send_error(
1204 &self,
1205 target: &ReplicaTarget,
1206 err: &tokio::sync::mpsc::error::TrySendError<OutboundRequest>,
1207 ) {
1208 let Some(m) = self.failure_metrics.as_ref() else {
1209 return;
1210 };
1211 if target.is_local {
1212 match err {
1213 tokio::sync::mpsc::error::TrySendError::Full(_) => m.record_backend_send_full(),
1214 tokio::sync::mpsc::error::TrySendError::Closed(_) => {
1215 m.record_backend_send_closed();
1216 }
1217 }
1218 } else {
1219 let peer_dc = self.peer_dc_label(target.peer_idx);
1220 match err {
1221 tokio::sync::mpsc::error::TrySendError::Full(_) => {
1222 m.record_peer_send_full(target.peer_idx, &peer_dc);
1223 }
1224 tokio::sync::mpsc::error::TrySendError::Closed(_) => {
1225 m.record_peer_send_closed(target.peer_idx, &peer_dc);
1226 }
1227 }
1228 }
1229 }
1230
1231 fn hint_target(
1235 &self,
1236 target: &ReplicaTarget,
1237 bytes: &[u8],
1238 req: &Msg,
1239 req_span: &tracing::Span,
1240 intermediate_tx: &mpsc::Sender<OutboundEnvelope>,
1241 ) -> bool {
1242 let Some(store) = self.hint_store.as_ref() else {
1243 return false;
1244 };
1245 let cfg = self.pool.config();
1246 let ttl = std::time::Duration::from_secs(cfg.hint_ttl_seconds.max(1));
1247 match store.enqueue(target.peer_idx, bytes.to_vec(), ttl) {
1248 Ok(()) => {}
1249 Err(e) => {
1250 tracing::debug!(
1251 target: "dynomite::hints",
1252 peer_idx = target.peer_idx,
1253 error = %e,
1254 "hint enqueue failed"
1255 );
1256 return false;
1257 }
1258 }
1259 let synth = synth_hint_reply(req, &self.mbuf_pool);
1260 let env = OutboundEnvelope {
1261 req_id: req.id(),
1262 rsp: synth,
1263 span: req_span.clone(),
1264 source_peer_idx: Some(target.peer_idx),
1265 };
1266 if intermediate_tx.try_send(env).is_err() {
1267 tracing::debug!(
1272 target: "dynomite::hints",
1273 peer_idx = target.peer_idx,
1274 "hint synth-reply could not be queued; coalescer absent"
1275 );
1276 }
1277 tracing::debug!(
1278 target: "dynomite::hints",
1279 peer_idx = target.peer_idx,
1280 bytes = bytes.len(),
1281 "stored hint for down peer"
1282 );
1283 true
1284 }
1285
1286 fn dispatch_replicas_direct(
1287 &self,
1288 req: &Msg,
1289 req_span: &tracing::Span,
1290 targets: &[ReplicaTarget],
1291 bytes: &[u8],
1292 responder: &ServerSink,
1293 ctx: &HandoffCtx<'_>,
1294 ) -> DispatchOutcome {
1295 debug_assert_eq!(targets.len(), 1);
1296 let target = &targets[0];
1297 if let TargetAction::Hint =
1301 Self::choose_target_action(target, ctx.handoff_active, ctx.peer_states)
1302 {
1303 if self.hint_single_target_direct(target, bytes, req, req_span, responder) {
1304 return DispatchOutcome::Pending;
1305 }
1306 return DispatchOutcome::Error(self.no_quorum_error(req));
1307 }
1308 let env = OutboundRequest {
1309 bytes: bytes.to_vec(),
1310 req_id: req.id(),
1311 responder: responder.clone(),
1312 span: req_span.clone(),
1313 ty: crate::proto::dnode::DmsgType::Req,
1314 target_peer_idx: Some(target.peer_idx),
1315 };
1316 let send_result = if target.is_local {
1317 self.backend.as_ref().map(|tx| tx.try_send(env))
1318 } else {
1319 self.peer_backends
1320 .get(&target.peer_idx)
1321 .map(|tx| tx.try_send(env))
1322 };
1323 let sent = match send_result {
1324 Some(Ok(())) => true,
1325 Some(Err(ref err)) => {
1326 self.observe_send_error(target, err);
1327 false
1328 }
1329 None => false,
1330 };
1331 if sent {
1332 return DispatchOutcome::Pending;
1333 }
1334 if ctx.handoff_active
1335 && self.hint_single_target_direct(target, bytes, req, req_span, responder)
1336 {
1337 return DispatchOutcome::Pending;
1338 }
1339 DispatchOutcome::Error(self.no_quorum_error(req))
1340 }
1341
1342 fn hint_single_target_direct(
1347 &self,
1348 target: &ReplicaTarget,
1349 bytes: &[u8],
1350 req: &Msg,
1351 req_span: &tracing::Span,
1352 responder: &ServerSink,
1353 ) -> bool {
1354 let Some(store) = self.hint_store.as_ref() else {
1355 return false;
1356 };
1357 let cfg = self.pool.config();
1358 let ttl = std::time::Duration::from_secs(cfg.hint_ttl_seconds.max(1));
1359 if let Err(e) = store.enqueue(target.peer_idx, bytes.to_vec(), ttl) {
1360 tracing::debug!(
1361 target: "dynomite::hints",
1362 peer_idx = target.peer_idx,
1363 error = %e,
1364 "hint enqueue failed (single-target)"
1365 );
1366 return false;
1367 }
1368 let synth = synth_hint_reply(req, &self.mbuf_pool);
1369 let env = OutboundEnvelope {
1370 req_id: req.id(),
1371 rsp: synth,
1372 span: req_span.clone(),
1373 source_peer_idx: Some(target.peer_idx),
1374 };
1375 let _ = responder.try_send(env);
1376 true
1377 }
1378
1379 fn no_quorum_error(&self, req: &Msg) -> Msg {
1380 let err_type = if matches!(req.ty(), MsgType::ReqRedisGet | MsgType::ReqRedisSet) {
1381 MsgType::RspRedisError
1382 } else {
1383 MsgType::RspMcServerError
1384 };
1385 crate::msg::response::make_error(
1386 req,
1387 err_type,
1388 0,
1389 crate::msg::DynErrorCode::DynomiteNoQuorumAchieved,
1390 &self.mbuf_pool,
1391 )
1392 }
1393
1394 fn intercept_command(
1402 &self,
1403 ext: &dyn crate::embed::CommandExtension,
1404 req: &Msg,
1405 ) -> Option<DispatchOutcome> {
1406 if ext.handles_msg_type(req.ty()) {
1407 return Some(self.run_extension_command(ext, req));
1408 }
1409 if matches!(req.ty(), MsgType::ReqRedisHset) {
1410 return self.intercept_extension_hset(ext, req);
1411 }
1412 None
1413 }
1414
1415 fn run_extension_command(
1422 &self,
1423 ext: &dyn crate::embed::CommandExtension,
1424 req: &Msg,
1425 ) -> DispatchOutcome {
1426 let recovered_kw: Vec<u8>;
1434 let keyword: &[u8] = match req.ty() {
1435 MsgType::ReqRedisFtCreate => b"FT.CREATE",
1436 MsgType::ReqRedisFtSearch => b"FT.SEARCH",
1437 MsgType::ReqRedisFtInfo => b"FT.INFO",
1438 MsgType::ReqRedisFtList => b"FT.LIST",
1439 MsgType::ReqRedisFtDropindex => b"FT.DROPINDEX",
1440 MsgType::ReqRedisFtRegex => b"FT.REGEX",
1441 MsgType::ReqRedisFtSugadd => b"FT.SUGADD",
1442 MsgType::ReqRedisFtSugget => b"FT.SUGGET",
1443 MsgType::ReqRedisFtSugdel => b"FT.SUGDEL",
1444 MsgType::ReqRedisFtSuglen => b"FT.SUGLEN",
1445 MsgType::ReqRedisFtUnknown => {
1446 recovered_kw = first_bulk_token(req).unwrap_or_else(|| b"FT.UNKNOWN".to_vec());
1447 recovered_kw.as_slice()
1448 }
1449 _ => return DispatchOutcome::Drop,
1454 };
1455 let mut args: Vec<&[u8]> = Vec::with_capacity(1 + req.keys().len() + req.args().len());
1456 args.push(keyword);
1457 for k in req.keys() {
1458 args.push(k.key());
1459 }
1460 for a in req.args() {
1461 args.push(a.bytes());
1462 }
1463 let bytes = ext.try_dispatch(&args).unwrap_or_else(|| {
1464 let kw = String::from_utf8_lossy(keyword);
1465 format!("-ERR not supported in this build: {kw}\r\n").into_bytes()
1466 });
1467 DispatchOutcome::Inline(synthetic_redis_reply(req, &self.mbuf_pool, &bytes))
1468 }
1469
1470 fn intercept_extension_hset(
1476 &self,
1477 ext: &dyn crate::embed::CommandExtension,
1478 req: &Msg,
1479 ) -> Option<DispatchOutcome> {
1480 let mut args: Vec<&[u8]> = Vec::with_capacity(req.keys().len() + req.args().len());
1481 for k in req.keys() {
1482 args.push(k.key());
1483 }
1484 for a in req.args() {
1485 args.push(a.bytes());
1486 }
1487 match ext.try_intercept_hset(&args) {
1488 crate::embed::HsetOutcome::Absorbed | crate::embed::HsetOutcome::NotIndexed => None,
1489 crate::embed::HsetOutcome::Error(message) => {
1490 let payload = format!("-ERR {message}\r\n");
1491 Some(DispatchOutcome::Error(synthetic_redis_reply(
1492 req,
1493 &self.mbuf_pool,
1494 payload.as_bytes(),
1495 )))
1496 }
1497 }
1498 }
1499}
1500
1501fn synthetic_redis_reply(req: &Msg, pool: &MbufPool, payload: &[u8]) -> Msg {
1507 let mut rsp = Msg::new(req.id(), MsgType::RspRedisStatus, false);
1508 rsp.set_parent_id(req.id());
1509 let mut written = 0usize;
1510 while written < payload.len() {
1511 let mut buf = pool.get();
1512 let n = buf.recv(&payload[written..]);
1513 debug_assert!(
1514 n > 0,
1515 "MbufPool returned a buffer with zero writable capacity"
1516 );
1517 rsp.mbufs_mut().push_back(buf);
1518 written += n;
1519 }
1520 rsp.recompute_mlen();
1521 rsp
1522}
1523
1524fn first_bulk_token(req: &Msg) -> Option<Vec<u8>> {
1534 let mut wire: Vec<u8> = Vec::new();
1535 for buf in req.mbufs() {
1536 wire.extend_from_slice(buf.readable());
1537 if wire.len() > 256 {
1538 break;
1539 }
1540 }
1541 let mut p = 0usize;
1542 if wire.first() == Some(&b'*') {
1543 let cr = wire.iter().position(|&b| b == b'\r')?;
1544 if wire.get(cr + 1) != Some(&b'\n') {
1545 return None;
1546 }
1547 p = cr + 2;
1548 }
1549 if wire.get(p) != Some(&b'$') {
1550 return None;
1551 }
1552 let header_start = p + 1;
1553 let header_cr = wire[header_start..]
1554 .iter()
1555 .position(|&b| b == b'\r')
1556 .map(|i| header_start + i)?;
1557 if wire.get(header_cr + 1) != Some(&b'\n') {
1558 return None;
1559 }
1560 let len_str = std::str::from_utf8(&wire[header_start..header_cr]).ok()?;
1561 let len: usize = len_str.parse().ok()?;
1562 let body_start = header_cr + 2;
1563 let body_end = body_start.checked_add(len)?;
1564 if wire.len() < body_end + 2 {
1565 return None;
1566 }
1567 Some(wire[body_start..body_end].to_vec())
1568}
1569
1570#[derive(Clone)]
1573struct ReadRepairContext {
1574 req_id: crate::core::types::MsgId,
1575 req_ty: MsgType,
1576 key: Vec<u8>,
1580 mbuf_pool: MbufPool,
1581 peer_backends: std::collections::HashMap<u32, mpsc::Sender<OutboundRequest>>,
1582 local_backend: Option<mpsc::Sender<OutboundRequest>>,
1583 target_is_local: std::collections::HashMap<u32, bool>,
1584}
1585
1586#[allow(
1588 clippy::too_many_arguments,
1589 reason = "actor task captures the entire dispatch context; bundling into a struct adds churn for no callsite gain"
1590)]
1591async fn coalesce_actor(
1592 req_id: crate::core::types::MsgId,
1593 req_ty: MsgType,
1594 consistency: ConsistencyLevel,
1595 targets: Vec<(u32, String)>,
1596 local_dc: String,
1597 mut intermediate_rx: mpsc::Receiver<OutboundEnvelope>,
1598 client_tx: ServerSink,
1599 mbuf_pool: MbufPool,
1600 repair_ctx: Option<ReadRepairContext>,
1601 failure_metrics: Option<Arc<crate::stats::FailureMetrics>>,
1602) {
1603 use crate::proto::redis::{CoalesceOutcome, CoalesceTracker};
1604 let mut tracker = CoalesceTracker::new(req_id, consistency, targets, &local_dc);
1605 let mut emitted = false;
1606 while let Some(env) = intermediate_rx.recv().await {
1607 let source = env.source_peer_idx.unwrap_or(u32::MAX);
1608 let span = env.span.clone();
1609 let outcome = tracker.record_reply(source, env.rsp);
1610 match outcome {
1611 CoalesceOutcome::Pending => {}
1612 CoalesceOutcome::Ready {
1613 winner,
1614 divergent_targets,
1615 } => {
1616 if !emitted {
1617 let winner_bytes: Vec<u8> = winner
1618 .mbufs()
1619 .iter()
1620 .flat_map(|b| b.readable().to_vec())
1621 .collect();
1622 let out_env = OutboundEnvelope {
1623 req_id,
1624 rsp: *winner,
1625 span: span.clone(),
1626 source_peer_idx: None,
1627 };
1628 let _ = client_tx.send(out_env).await;
1629 emitted = true;
1630 if !divergent_targets.is_empty() {
1631 if let Some(ctx) = repair_ctx.as_ref() {
1632 schedule_read_repair(ctx, &divergent_targets, &winner_bytes, &span);
1633 }
1634 }
1635 }
1636 }
1637 CoalesceOutcome::Error(reason) => {
1638 if !emitted {
1639 let err_type = if matches!(req_ty, MsgType::ReqRedisGet | MsgType::ReqRedisSet)
1640 {
1641 MsgType::RspRedisError
1642 } else {
1643 MsgType::RspMcServerError
1644 };
1645 let anchor = Msg::new(req_id, req_ty, true);
1646 let rsp = crate::msg::response::make_error(
1647 &anchor,
1648 err_type,
1649 0,
1650 crate::msg::DynErrorCode::DynomiteNoQuorumAchieved,
1651 &mbuf_pool,
1652 );
1653 let _ = client_tx
1654 .send(OutboundEnvelope {
1655 req_id,
1656 rsp,
1657 span: span.clone(),
1658 source_peer_idx: None,
1659 })
1660 .await;
1661 emitted = true;
1662 }
1663 tracing::debug!(target: "dynomite::coalesce", req_id, reason = %reason, "coalesce error");
1664 }
1665 }
1666 }
1667 if !emitted {
1668 if let Some(m) = failure_metrics.as_ref() {
1675 m.record_response_timeout(consistency);
1676 }
1677 let err_type = if matches!(req_ty, MsgType::ReqRedisGet | MsgType::ReqRedisSet) {
1678 MsgType::RspRedisError
1679 } else {
1680 MsgType::RspMcServerError
1681 };
1682 let anchor = Msg::new(req_id, req_ty, true);
1683 let rsp = crate::msg::response::make_error(
1684 &anchor,
1685 err_type,
1686 0,
1687 crate::msg::DynErrorCode::DynomiteNoQuorumAchieved,
1688 &mbuf_pool,
1689 );
1690 let _ = client_tx
1691 .send(OutboundEnvelope {
1692 req_id,
1693 rsp,
1694 span: tracing::Span::none(),
1695 source_peer_idx: None,
1696 })
1697 .await;
1698 }
1699}
1700
1701fn repair_sink() -> ServerSink {
1704 let (tx, mut rx) = mpsc::channel::<OutboundEnvelope>(8);
1705 tokio::spawn(async move {
1706 while rx.recv().await.is_some() {
1707 }
1710 });
1711 tx
1712}
1713
1714fn decode_winner_for_repair(payload: &[u8]) -> Option<RepairAction> {
1723 if payload == b"$-1\r\n" {
1724 return Some(RepairAction::Delete);
1725 }
1726 if !payload.starts_with(b"$") {
1727 return None;
1728 }
1729 let crlf = payload.iter().position(|&b| b == b'\r')?;
1731 if payload.get(crlf + 1).copied() != Some(b'\n') {
1732 return None;
1733 }
1734 let len_str = std::str::from_utf8(&payload[1..crlf]).ok()?;
1735 let len: usize = len_str.parse().ok()?;
1736 let body_start = crlf + 2;
1737 let body_end = body_start.checked_add(len)?;
1738 if payload.len() < body_end + 2 {
1739 return None;
1740 }
1741 if &payload[body_end..body_end + 2] != b"\r\n" {
1742 return None;
1743 }
1744 Some(RepairAction::Write(payload[body_start..body_end].to_vec()))
1745}
1746
1747struct HandoffCtx<'a> {
1753 handoff_active: bool,
1754 peer_states: &'a std::collections::HashMap<u32, crate::cluster::peer::PeerState>,
1755}
1756
1757#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1760enum TargetAction {
1761 Send,
1763 Hint,
1766}
1767
1768fn synth_hint_reply(req: &Msg, pool: &MbufPool) -> Msg {
1779 crate::msg::response::make_simple_redis(req, pool, b"+OK\r\n")
1780}
1781
1782enum RepairAction {
1785 Write(Vec<u8>),
1787 Delete,
1790}
1791
1792fn build_repair_bytes(action: &RepairAction, key: &[u8]) -> Vec<u8> {
1794 match action {
1795 RepairAction::Write(value) => {
1796 let mut out = Vec::with_capacity(key.len() + value.len() + 32);
1797 out.extend_from_slice(b"*3\r\n$3\r\nSET\r\n$");
1798 out.extend_from_slice(key.len().to_string().as_bytes());
1799 out.extend_from_slice(b"\r\n");
1800 out.extend_from_slice(key);
1801 out.extend_from_slice(b"\r\n$");
1802 out.extend_from_slice(value.len().to_string().as_bytes());
1803 out.extend_from_slice(b"\r\n");
1804 out.extend_from_slice(value);
1805 out.extend_from_slice(b"\r\n");
1806 out
1807 }
1808 RepairAction::Delete => {
1809 let mut out = Vec::with_capacity(key.len() + 24);
1810 out.extend_from_slice(b"*2\r\n$3\r\nDEL\r\n$");
1811 out.extend_from_slice(key.len().to_string().as_bytes());
1812 out.extend_from_slice(b"\r\n");
1813 out.extend_from_slice(key);
1814 out.extend_from_slice(b"\r\n");
1815 out
1816 }
1817 }
1818}
1819
1820fn schedule_read_repair(
1838 ctx: &ReadRepairContext,
1839 divergent: &[u32],
1840 winner_bytes: &[u8],
1841 span: &tracing::Span,
1842) {
1843 if !matches!(ctx.req_ty, MsgType::ReqRedisGet) {
1844 return;
1845 }
1846 let Some(action) = decode_winner_for_repair(winner_bytes) else {
1847 return;
1848 };
1849 let bytes = build_repair_bytes(&action, &ctx.key);
1850 let sink = repair_sink();
1851 for &peer_idx in divergent {
1852 let is_local = ctx.target_is_local.get(&peer_idx).copied().unwrap_or(false);
1853 let env = OutboundRequest {
1854 bytes: bytes.clone(),
1855 req_id: ctx.req_id,
1856 responder: sink.clone(),
1857 span: span.clone(),
1858 ty: crate::proto::dnode::DmsgType::ReqForward,
1859 target_peer_idx: Some(peer_idx),
1860 };
1861 let sent = if is_local {
1862 ctx.local_backend
1863 .as_ref()
1864 .is_some_and(|tx| tx.try_send(env).is_ok())
1865 } else {
1866 ctx.peer_backends
1867 .get(&peer_idx)
1868 .is_some_and(|tx| tx.try_send(env).is_ok())
1869 };
1870 if sent {
1871 let _ = &ctx.mbuf_pool;
1872 tracing::debug!(
1873 target: "dynomite::read_repair",
1874 req_id = ctx.req_id,
1875 peer_idx,
1876 bytes = bytes.len(),
1877 "scheduled read-repair write",
1878 );
1879 } else {
1880 tracing::debug!(
1881 target: "dynomite::read_repair",
1882 req_id = ctx.req_id,
1883 peer_idx,
1884 "read-repair drop: backend channel unavailable or full",
1885 );
1886 }
1887 }
1888}
1889
1890#[cfg(test)]
1891mod tests {
1892 use super::*;
1893 use crate::cluster::peer::{Peer, PeerEndpoint, PeerState};
1894 use crate::conf::DataStore;
1895 use crate::hashkit::DynToken;
1896
1897 fn cfg(read: ConsistencyLevel, write: ConsistencyLevel) -> crate::cluster::PoolConfig {
1898 crate::cluster::PoolConfig {
1899 read_consistency: read,
1900 write_consistency: write,
1901 dc: "dc1".into(),
1902 rack: "rA".into(),
1903 ..crate::cluster::PoolConfig::default()
1904 }
1905 }
1906
1907 fn peer(idx: u32, dc: &str, rack: &str, tok: u32, is_local: bool, is_same: bool) -> Peer {
1908 let mut p = Peer::new(
1909 idx,
1910 PeerEndpoint::tcp("h".into(), 8101 + u16::try_from(idx).unwrap_or(0)),
1911 rack.into(),
1912 dc.into(),
1913 vec![DynToken::from_u32(tok)],
1914 is_local,
1915 is_same,
1916 false,
1917 );
1918 p.set_state(PeerState::Normal, 0);
1919 p
1920 }
1921
1922 fn pool(read: ConsistencyLevel, write: ConsistencyLevel, peers: Vec<Peer>) -> Arc<ServerPool> {
1923 let pool = ServerPool::new(cfg(read, write), peers);
1924 pool.preselect_remote_racks();
1925 Arc::new(pool)
1926 }
1927
1928 #[test]
1929 fn local_node_only_short_circuits() {
1930 let p = pool(
1931 ConsistencyLevel::DcOne,
1932 ConsistencyLevel::DcOne,
1933 vec![peer(0, "dc1", "rA", 10, true, true)],
1934 );
1935 let mut req = Msg::new(1, MsgType::ReqRedisGet, true);
1936 req.set_routing(MsgRouting::LocalNodeOnly);
1937 assert_eq!(
1938 ClusterDispatcher::new(p).plan(&req, b"k"),
1939 DispatchPlan::LocalDatastore,
1940 );
1941 }
1942
1943 #[test]
1944 fn dc_one_read_targets_local_rack_when_present() {
1945 let p = pool(
1946 ConsistencyLevel::DcOne,
1947 ConsistencyLevel::DcOne,
1948 vec![
1949 peer(0, "dc1", "rA", 10, true, true),
1950 peer(1, "dc1", "rB", 20, false, true),
1951 peer(2, "dc2", "rA", 30, false, false),
1952 ],
1953 );
1954 let req = Msg::new(1, MsgType::ReqRedisGet, true);
1955 let plan = ClusterDispatcher::new(p).plan(&req, b"hello");
1957 assert!(matches!(plan, DispatchPlan::LocalDatastore));
1958 }
1959
1960 #[test]
1961 fn dc_quorum_fans_out_local_dc() {
1962 let p = pool(
1963 ConsistencyLevel::DcQuorum,
1964 ConsistencyLevel::DcQuorum,
1965 vec![
1966 peer(0, "dc1", "rA", 10, true, true),
1967 peer(1, "dc1", "rB", 20, false, true),
1968 peer(2, "dc2", "rA", 30, false, false),
1969 ],
1970 );
1971 let req = Msg::new(1, MsgType::ReqRedisGet, true);
1972 let plan = ClusterDispatcher::new(p).plan(&req, b"k");
1973 match plan {
1974 DispatchPlan::Replicas { targets: rs, .. } => {
1975 assert_eq!(rs.len(), 2);
1976 for r in rs {
1977 assert_eq!(r.dc, "dc1");
1978 }
1979 }
1980 _ => panic!("expected replicas"),
1981 }
1982 }
1983
1984 #[test]
1985 fn dc_each_safe_quorum_fans_out_per_dc() {
1986 let p = pool(
1987 ConsistencyLevel::DcEachSafeQuorum,
1988 ConsistencyLevel::DcEachSafeQuorum,
1989 vec![
1990 peer(0, "dc1", "rA", 10, true, true),
1991 peer(1, "dc2", "rA", 20, false, false),
1992 ],
1993 );
1994 let req = Msg::new(1, MsgType::ReqRedisGet, true);
1995 let plan = ClusterDispatcher::new(p).plan(&req, b"k");
1996 match plan {
1997 DispatchPlan::Replicas { targets: rs, .. } => {
1998 assert_eq!(rs.len(), 2);
1999 let dcs: Vec<&str> = rs.iter().map(|r| r.dc.as_str()).collect();
2000 assert!(dcs.contains(&"dc1"));
2001 assert!(dcs.contains(&"dc2"));
2002 }
2003 _ => panic!("expected replicas"),
2004 }
2005 }
2006
2007 #[test]
2008 fn no_routable_peers_returns_no_targets() {
2009 let mut p0 = peer(0, "dc1", "rA", 10, true, true);
2010 p0.set_state(PeerState::Down, 0);
2011 let p = pool(
2012 ConsistencyLevel::DcQuorum,
2013 ConsistencyLevel::DcQuorum,
2014 vec![p0],
2015 );
2016 let req = Msg::new(1, MsgType::ReqRedisGet, true);
2017 let plan = ClusterDispatcher::new(p).plan(&req, b"k");
2018 assert_eq!(plan, DispatchPlan::NoTargets);
2019 }
2020
2021 #[test]
2029 fn no_targets_error_response_carries_dynomite_wire_bytes() {
2030 let mut p0 = peer(0, "dc1", "rA", 10, true, true);
2031 p0.set_state(PeerState::Down, 0);
2032 let p = pool(
2033 ConsistencyLevel::DcQuorum,
2034 ConsistencyLevel::DcQuorum,
2035 vec![p0],
2036 );
2037 let disp = ClusterDispatcher::new(p);
2038 let mut req = Msg::new(1, MsgType::ReqRedisGet, true);
2039 req.push_key(crate::msg::keypos::KeyPos::without_tag(b"k".to_vec()));
2040 let (tx, _rx) = mpsc::channel(1);
2041 let outcome = disp.dispatch(req, tx);
2042 match outcome {
2043 DispatchOutcome::Error(rsp) => {
2044 assert_eq!(rsp.ty(), MsgType::RspRedisError);
2045 assert!(rsp.flags().is_error);
2046 let bytes: Vec<u8> = rsp
2047 .mbufs()
2048 .iter()
2049 .flat_map(|b| b.readable().to_vec())
2050 .collect();
2051 assert!(
2052 !bytes.is_empty(),
2053 "NoTargets must produce on-wire bytes, not a 0-byte hang"
2054 );
2055 assert!(bytes.starts_with(b"-Dynomite: "));
2056 assert!(bytes.ends_with(b"\r\n"));
2057 assert_eq!(rsp.mlen() as usize, bytes.len());
2058 }
2059 other => panic!("expected DispatchOutcome::Error, got {other:?}"),
2060 }
2061 }
2062
2063 #[test]
2067 fn no_targets_error_response_memcache_wire_bytes() {
2068 let mut cfg = cfg(ConsistencyLevel::DcQuorum, ConsistencyLevel::DcQuorum);
2074 cfg.data_store = DataStore::Memcache;
2075 let mut p0 = peer(0, "dc1", "rA", 10, true, true);
2076 p0.set_state(PeerState::Down, 0);
2077 let pool_arc = ServerPool::new(cfg, vec![p0]);
2078 pool_arc.preselect_remote_racks();
2079 let disp = ClusterDispatcher::new(Arc::new(pool_arc));
2080 let mut req = Msg::new(1, MsgType::ReqMcGet, true);
2081 req.push_key(crate::msg::keypos::KeyPos::without_tag(b"k".to_vec()));
2082 let (tx, _rx) = mpsc::channel(1);
2083 let outcome = disp.dispatch(req, tx);
2084 match outcome {
2085 DispatchOutcome::Error(rsp) => {
2086 assert_eq!(rsp.ty(), MsgType::RspMcServerError);
2087 let bytes: Vec<u8> = rsp
2088 .mbufs()
2089 .iter()
2090 .flat_map(|b| b.readable().to_vec())
2091 .collect();
2092 assert!(
2093 !bytes.is_empty(),
2094 "NoTargets must produce on-wire bytes, not a 0-byte hang"
2095 );
2096 assert!(bytes.starts_with(b"SERVER_ERROR "));
2097 assert!(bytes.ends_with(b"\r\n"));
2098 }
2099 other => panic!("expected DispatchOutcome::Error, got {other:?}"),
2100 }
2101 }
2102
2103 use crate::cluster::pool::{BucketType, PoolConfig};
2104
2105 fn pool_with_bucket_types(
2106 pool_read: ConsistencyLevel,
2107 pool_write: ConsistencyLevel,
2108 bucket_types: Vec<BucketType>,
2109 default_bucket_type: Option<&str>,
2110 peers: Vec<Peer>,
2111 ) -> Arc<ServerPool> {
2112 let cfg = PoolConfig {
2113 read_consistency: pool_read,
2114 write_consistency: pool_write,
2115 dc: "dc1".into(),
2116 rack: "rA".into(),
2117 bucket_types,
2118 default_bucket_type: default_bucket_type.map(str::to_string),
2119 ..PoolConfig::default()
2120 };
2121 let pool = ServerPool::new(cfg, peers);
2122 pool.preselect_remote_racks();
2123 Arc::new(pool)
2124 }
2125
2126 fn three_local_peers() -> Vec<Peer> {
2127 vec![
2128 peer(0, "dc1", "rA", 10, true, true),
2129 peer(1, "dc1", "rB", 20, false, true),
2130 peer(2, "dc1", "rC", 30, false, true),
2131 ]
2132 }
2133
2134 #[test]
2135 fn bucket_type_overrides_pool_consistency() {
2136 let bts = vec![BucketType {
2138 name: "hot".into(),
2139 read_consistency: ConsistencyLevel::DcQuorum,
2140 write_consistency: ConsistencyLevel::DcQuorum,
2141 n_val: 0,
2142 }];
2143 let p = pool_with_bucket_types(
2144 ConsistencyLevel::DcOne,
2145 ConsistencyLevel::DcOne,
2146 bts,
2147 None,
2148 three_local_peers(),
2149 );
2150 let req = Msg::new(1, MsgType::ReqRedisGet, true);
2151 let plan = ClusterDispatcher::new(p).plan(&req, b"hot/key1");
2152 match plan {
2153 DispatchPlan::Replicas { targets: rs, .. } => assert_eq!(rs.len(), 3),
2154 other => panic!("expected DC_QUORUM fan-out, got {other:?}"),
2155 }
2156 }
2157
2158 #[test]
2159 fn slashless_key_falls_back_to_pool_default() {
2160 let bts = vec![BucketType {
2161 name: "hot".into(),
2162 read_consistency: ConsistencyLevel::DcQuorum,
2163 write_consistency: ConsistencyLevel::DcQuorum,
2164 n_val: 0,
2165 }];
2166 let p = pool_with_bucket_types(
2167 ConsistencyLevel::DcOne,
2168 ConsistencyLevel::DcOne,
2169 bts,
2170 None,
2171 three_local_peers(),
2172 );
2173 let req = Msg::new(1, MsgType::ReqRedisGet, true);
2174 let plan = ClusterDispatcher::new(p).plan(&req, b"plain-key");
2175 assert!(matches!(plan, DispatchPlan::LocalDatastore));
2178 }
2179
2180 #[test]
2181 fn unknown_bucket_uses_default_bucket_type_when_set() {
2182 let bts = vec![BucketType {
2183 name: "safe".into(),
2184 read_consistency: ConsistencyLevel::DcQuorum,
2185 write_consistency: ConsistencyLevel::DcQuorum,
2186 n_val: 0,
2187 }];
2188 let p = pool_with_bucket_types(
2189 ConsistencyLevel::DcOne,
2190 ConsistencyLevel::DcOne,
2191 bts,
2192 Some("safe"),
2193 three_local_peers(),
2194 );
2195 let req = Msg::new(1, MsgType::ReqRedisGet, true);
2196 let plan = ClusterDispatcher::new(p.clone()).plan(&req, b"plain-key");
2199 match plan {
2200 DispatchPlan::Replicas { targets: rs, .. } => assert_eq!(rs.len(), 3),
2201 other => panic!("expected DC_QUORUM via default bucket, got {other:?}"),
2202 }
2203 let plan = ClusterDispatcher::new(p).plan(&req, b"unknown-bucket/key");
2206 match plan {
2207 DispatchPlan::Replicas { targets: rs, .. } => assert_eq!(rs.len(), 3),
2208 other => panic!("expected DC_QUORUM via default bucket, got {other:?}"),
2209 }
2210 }
2211
2212 #[test]
2213 fn unknown_bucket_with_no_default_uses_pool_default() {
2214 let bts = vec![BucketType {
2215 name: "safe".into(),
2216 read_consistency: ConsistencyLevel::DcQuorum,
2217 write_consistency: ConsistencyLevel::DcQuorum,
2218 n_val: 0,
2219 }];
2220 let p = pool_with_bucket_types(
2221 ConsistencyLevel::DcOne,
2222 ConsistencyLevel::DcOne,
2223 bts,
2224 None,
2225 three_local_peers(),
2226 );
2227 let req = Msg::new(1, MsgType::ReqRedisGet, true);
2228 let plan = ClusterDispatcher::new(p).plan(&req, b"unknown-bucket/key");
2229 assert!(matches!(plan, DispatchPlan::LocalDatastore));
2230 }
2231
2232 #[test]
2233 fn n_val_one_caps_replicas_to_first_target() {
2234 let bts = vec![BucketType {
2235 name: "thin".into(),
2236 read_consistency: ConsistencyLevel::DcQuorum,
2237 write_consistency: ConsistencyLevel::DcQuorum,
2238 n_val: 1,
2239 }];
2240 let p = pool_with_bucket_types(
2241 ConsistencyLevel::DcOne,
2242 ConsistencyLevel::DcOne,
2243 bts,
2244 None,
2245 three_local_peers(),
2246 );
2247 let req = Msg::new(1, MsgType::ReqRedisGet, true);
2248 let plan = ClusterDispatcher::new(p).plan(&req, b"thin/key");
2249 match plan {
2250 DispatchPlan::Replicas { targets: rs, .. } => assert_eq!(rs.len(), 1),
2251 other => panic!("expected single-target plan, got {other:?}"),
2252 }
2253 }
2254
2255 #[test]
2256 fn n_val_two_caps_replicas_to_first_two_targets() {
2257 let bts = vec![BucketType {
2258 name: "medium".into(),
2259 read_consistency: ConsistencyLevel::DcQuorum,
2260 write_consistency: ConsistencyLevel::DcQuorum,
2261 n_val: 2,
2262 }];
2263 let p = pool_with_bucket_types(
2264 ConsistencyLevel::DcOne,
2265 ConsistencyLevel::DcOne,
2266 bts,
2267 None,
2268 three_local_peers(),
2269 );
2270 let req = Msg::new(1, MsgType::ReqRedisGet, true);
2271 let plan = ClusterDispatcher::new(p).plan(&req, b"medium/key");
2272 match plan {
2273 DispatchPlan::Replicas { targets: rs, .. } => assert_eq!(rs.len(), 2),
2274 other => panic!("expected two-target plan, got {other:?}"),
2275 }
2276 }
2277
2278 #[test]
2279 fn n_val_zero_does_not_cap() {
2280 let bts = vec![BucketType {
2281 name: "any".into(),
2282 read_consistency: ConsistencyLevel::DcQuorum,
2283 write_consistency: ConsistencyLevel::DcQuorum,
2284 n_val: 0,
2285 }];
2286 let p = pool_with_bucket_types(
2287 ConsistencyLevel::DcOne,
2288 ConsistencyLevel::DcOne,
2289 bts,
2290 None,
2291 three_local_peers(),
2292 );
2293 let req = Msg::new(1, MsgType::ReqRedisGet, true);
2294 let plan = ClusterDispatcher::new(p).plan(&req, b"any/key");
2295 match plan {
2296 DispatchPlan::Replicas { targets: rs, .. } => assert_eq!(rs.len(), 3),
2297 other => panic!("expected uncapped plan, got {other:?}"),
2298 }
2299 }
2300
2301 #[test]
2302 fn n_val_larger_than_replicas_is_a_no_op() {
2303 let bts = vec![BucketType {
2304 name: "big".into(),
2305 read_consistency: ConsistencyLevel::DcQuorum,
2306 write_consistency: ConsistencyLevel::DcQuorum,
2307 n_val: 7,
2308 }];
2309 let p = pool_with_bucket_types(
2310 ConsistencyLevel::DcOne,
2311 ConsistencyLevel::DcOne,
2312 bts,
2313 None,
2314 three_local_peers(),
2315 );
2316 let req = Msg::new(1, MsgType::ReqRedisGet, true);
2317 let plan = ClusterDispatcher::new(p).plan(&req, b"big/key");
2318 match plan {
2319 DispatchPlan::Replicas { targets: rs, .. } => assert_eq!(rs.len(), 3),
2320 other => panic!("expected uncapped plan, got {other:?}"),
2321 }
2322 }
2323
2324 #[test]
2328 fn no_targets_records_failure_metric() {
2329 let mut p0 = peer(0, "dc1", "rA", 10, true, true);
2330 p0.set_state(PeerState::Down, 0);
2331 let p = pool(
2332 ConsistencyLevel::DcQuorum,
2333 ConsistencyLevel::DcQuorum,
2334 vec![p0],
2335 );
2336 let metrics = Arc::new(crate::stats::FailureMetrics::new());
2337 let disp = ClusterDispatcher::new(p).with_failure_metrics(metrics.clone());
2338 let req = Msg::new(1, MsgType::ReqRedisGet, true);
2339 assert_eq!(disp.plan(&req, b"k"), DispatchPlan::NoTargets);
2340 let snap = metrics.snapshot();
2341 assert_eq!(snap.no_targets.len(), 1);
2342 let entry = &snap.no_targets[0];
2343 assert_eq!(entry.dc, "dc1");
2344 assert_eq!(entry.rack, "rA");
2345 assert_eq!(entry.consistency, ConsistencyLevel::DcQuorum);
2346 assert_eq!(entry.count, 1);
2347 }
2348
2349 #[tokio::test]
2355 async fn closed_backend_channel_records_closed_metric() {
2356 let p = pool(
2357 ConsistencyLevel::DcOne,
2358 ConsistencyLevel::DcOne,
2359 vec![peer(0, "dc1", "rA", 10, true, true)],
2360 );
2361 let (tx, rx) = mpsc::channel::<crate::net::server::OutboundRequest>(4);
2362 drop(rx);
2363 let metrics = Arc::new(crate::stats::FailureMetrics::new());
2364 let disp = ClusterDispatcher::new(p)
2365 .with_backend(tx)
2366 .with_failure_metrics(metrics.clone());
2367 let mut req = Msg::new(1, MsgType::ReqRedisGet, true);
2368 let pool_buf = crate::io::mbuf::MbufPool::default();
2372 let mut buf = pool_buf.get();
2373 buf.copy_from_slice(b"PING\r\n");
2374 req.mbufs_mut().push_back(buf);
2375 let (resp_tx, _resp_rx) = mpsc::channel(1);
2376 let outcome = disp.dispatch(req, resp_tx);
2377 assert!(matches!(outcome, DispatchOutcome::Error(_)));
2378 let snap = metrics.snapshot();
2379 assert_eq!(snap.backend_send_closed, 1);
2380 assert_eq!(snap.backend_send_full, 0);
2381 }
2382
2383 #[test]
2388 fn two_peer_pool_with_one_down_records_per_key_no_targets() {
2389 let cfg = crate::cluster::PoolConfig {
2390 dc: "dc1".into(),
2391 rack: "rA".into(),
2392 read_consistency: ConsistencyLevel::DcQuorum,
2393 write_consistency: ConsistencyLevel::DcQuorum,
2394 ..crate::cluster::PoolConfig::default()
2395 };
2396 let p0 = peer(0, "dc1", "rA", 2_147_483_648, true, true);
2404 let mut p1 = peer(1, "dc1", "rA", 0, false, true);
2405 p1.set_state(PeerState::Down, 0);
2406 let pool_arc = ServerPool::new(cfg, vec![p0, p1]);
2407 pool_arc.preselect_remote_racks();
2408 let metrics = Arc::new(crate::stats::FailureMetrics::new());
2409 let disp = ClusterDispatcher::new(Arc::new(pool_arc)).with_failure_metrics(metrics.clone());
2410 let mut planned_no_targets = 0u64;
2411 let mut planned_routable = 0u64;
2412 for i in 0..100u32 {
2413 let key = format!("k{i:03}");
2414 let req = Msg::new(u64::from(i), MsgType::ReqRedisGet, true);
2415 match disp.plan(&req, key.as_bytes()) {
2416 DispatchPlan::NoTargets => planned_no_targets += 1,
2417 DispatchPlan::Replicas { .. } | DispatchPlan::LocalDatastore => {
2418 planned_routable += 1;
2419 }
2420 DispatchPlan::Drop => panic!("unexpected Drop in plan"),
2421 }
2422 }
2423 assert!(planned_no_targets > 0, "expected some NoTargets dispatches");
2424 assert!(planned_routable > 0, "expected some routable dispatches");
2425 let snap = metrics.snapshot();
2426 let counter_total: u64 = snap.no_targets.iter().map(|e| e.count).sum();
2427 assert_eq!(
2428 counter_total, planned_no_targets,
2429 "dispatch_no_targets_total must match observed NoTargets count",
2430 );
2431 assert_eq!(snap.backend_send_full, 0);
2434 assert_eq!(snap.backend_send_closed, 0);
2435 assert!(snap.peer_send_full.is_empty());
2436 assert!(snap.peer_send_closed.is_empty());
2437 }
2438}