1use std::collections::HashSet;
16
17use api::v1::meta::heartbeat_request::NodeWorkloads;
18use common_meta::cluster::NodeInfo;
19use common_meta::peer::Peer;
20use snafu::{ResultExt, ensure};
21
22use crate::error;
23use crate::error::Result;
24use crate::metasrv::{SelectTarget, SelectorContext};
25use crate::selector::SelectorOptions;
26use crate::selector::weighted_choose::{WeightedChoose, WeightedItem};
27
28pub(crate) async fn active_schedulable_datanodes(
30 ctx: &SelectorContext,
31 filter: Option<for<'a> fn(&'a NodeWorkloads) -> bool>,
32) -> Result<Vec<NodeInfo>> {
33 let datanodes = ctx
34 .peer_discovery
35 .active_datanodes(filter)
36 .await
37 .context(error::ListActiveDatanodesSnafu)?;
38 Ok(datanodes
39 .into_iter()
40 .filter(NodeInfo::is_schedulable)
41 .collect())
42}
43
44pub fn filter_out_excluded_peers(
46 weight_array: &mut Vec<WeightedItem<Peer>>,
47 exclude_peer_ids: &HashSet<u64>,
48) {
49 weight_array.retain(|peer| !exclude_peer_ids.contains(&peer.item.id));
50}
51
52pub fn choose_items<W>(opts: &SelectorOptions<'_>, weighted_choose: &mut W) -> Result<Vec<Peer>>
54where
55 W: WeightedChoose<Peer>,
56{
57 let min_required_items = opts.min_required_items;
58 ensure!(
59 !weighted_choose.is_empty(),
60 error::NoEnoughAvailableNodeSnafu {
61 required: min_required_items,
62 available: 0_usize,
63 select_target: SelectTarget::Datanode
64 }
65 );
66
67 if min_required_items == 1 {
68 return Ok(vec![weighted_choose.choose_one()?]);
70 }
71
72 let available_count = weighted_choose.len();
73
74 if opts.allow_duplication {
75 let complete_batches = min_required_items / available_count;
78 let leftover_items = min_required_items % available_count;
79 if complete_batches == 0 {
80 return weighted_choose.choose_multiple(leftover_items);
81 }
82
83 let mut result = Vec::with_capacity(min_required_items);
84 for _ in 0..complete_batches {
85 result.extend(weighted_choose.choose_multiple(available_count)?);
86 }
87 result.extend(weighted_choose.choose_multiple(leftover_items)?);
88
89 Ok(result)
90 } else {
91 ensure!(
93 available_count >= min_required_items,
94 error::NoEnoughAvailableNodeSnafu {
95 required: min_required_items,
96 available: available_count,
97 select_target: SelectTarget::Datanode
98 }
99 );
100
101 weighted_choose.choose_multiple(min_required_items)
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use std::any::Any;
108 use std::collections::{HashMap, HashSet};
109 use std::sync::Arc;
110 use std::sync::atomic::{AtomicUsize, Ordering};
111
112 use api::v1::meta::DatanodeWorkloads;
113 use api::v1::meta::heartbeat_request::NodeWorkloads;
114 use common_meta::affinity::{Affinity, AffinityFallback, AffinityRequirements, NoopAffinity};
115 use common_meta::cluster::{DatanodeStatus, NodeInfo, NodeStatus};
116 use common_meta::datanode::{DatanodeStatKey, DatanodeStatValue, Stat};
117 use common_meta::error::{Error as MetaError, Result as MetaResult};
118 use common_meta::kv_backend::memory::MemoryKvBackend;
119 use common_meta::kv_backend::{KvBackend, KvBackendRef, ResettableKvBackend, TxnService};
120 use common_meta::peer::{Peer, PeerDiscovery};
121 use common_meta::rpc::store::{
122 BatchDeleteRequest, BatchDeleteResponse, BatchGetRequest, BatchGetResponse,
123 BatchPutRequest, BatchPutResponse, DeleteRangeRequest, DeleteRangeResponse, PutRequest,
124 PutResponse, RangeRequest, RangeResponse,
125 };
126
127 use crate::cluster::MetaPeerClientBuilder;
128 use crate::discovery::utils::accept_ingest_workload;
129 use crate::metasrv::{SelectTarget, SelectorContext, SelectorRef};
130 use crate::selector::common::{choose_items, filter_out_excluded_peers};
131 use crate::selector::lease_based::LeaseBasedSelector;
132 use crate::selector::load_based::LoadBasedSelector;
133 use crate::selector::round_robin::RoundRobinSelector;
134 use crate::selector::weight_compute::RegionNumsBasedWeightCompute;
135 use crate::selector::weighted_choose::{RandomWeightedChoose, WeightedItem};
136 use crate::selector::{Selector, SelectorOptions};
137
138 #[test]
139 fn test_choose_peers() {
140 let weight_array = vec![
141 WeightedItem {
142 item: Peer {
143 id: 1,
144 addr: "127.0.0.1:3001".to_string(),
145 },
146 weight: 1.0,
147 },
148 WeightedItem {
149 item: Peer {
150 id: 2,
151 addr: "127.0.0.1:3001".to_string(),
152 },
153 weight: 1.0,
154 },
155 WeightedItem {
156 item: Peer {
157 id: 3,
158 addr: "127.0.0.1:3001".to_string(),
159 },
160 weight: 1.0,
161 },
162 WeightedItem {
163 item: Peer {
164 id: 4,
165 addr: "127.0.0.1:3001".to_string(),
166 },
167 weight: 1.0,
168 },
169 WeightedItem {
170 item: Peer {
171 id: 5,
172 addr: "127.0.0.1:3001".to_string(),
173 },
174 weight: 1.0,
175 },
176 ];
177
178 for i in 1..=5 {
179 let opts = SelectorOptions {
180 affinity: None,
181 min_required_items: i,
182 allow_duplication: false,
183 exclude_peer_ids: HashSet::new(),
184 workload_filter: None,
185 };
186
187 let selected_peers: HashSet<_> =
188 choose_items(&opts, &mut RandomWeightedChoose::new(weight_array.clone()))
189 .unwrap()
190 .into_iter()
191 .collect();
192
193 assert_eq!(i, selected_peers.len());
194 }
195
196 let opts = SelectorOptions {
197 affinity: None,
198 min_required_items: 6,
199 allow_duplication: false,
200 exclude_peer_ids: HashSet::new(),
201 workload_filter: None,
202 };
203
204 let selected_result =
205 choose_items(&opts, &mut RandomWeightedChoose::new(weight_array.clone()));
206 assert!(selected_result.is_err());
207
208 for i in 1..=50 {
209 let opts = SelectorOptions {
210 affinity: None,
211 min_required_items: i,
212 allow_duplication: true,
213 exclude_peer_ids: HashSet::new(),
214 workload_filter: None,
215 };
216
217 let selected_peers =
218 choose_items(&opts, &mut RandomWeightedChoose::new(weight_array.clone())).unwrap();
219
220 assert_eq!(i, selected_peers.len());
221 }
222 }
223
224 #[test]
225 fn test_filter_out_excluded_peers() {
226 let mut weight_array = vec![
227 WeightedItem {
228 item: Peer {
229 id: 1,
230 addr: "127.0.0.1:3001".to_string(),
231 },
232 weight: 1.0,
233 },
234 WeightedItem {
235 item: Peer {
236 id: 2,
237 addr: "127.0.0.1:3002".to_string(),
238 },
239 weight: 1.0,
240 },
241 ];
242
243 let exclude_peer_ids = HashSet::from([1]);
244 filter_out_excluded_peers(&mut weight_array, &exclude_peer_ids);
245
246 assert_eq!(weight_array.len(), 1);
247 assert_eq!(weight_array[0].item.id, 2);
248 }
249
250 struct ExactAffinity;
251
252 impl Affinity for ExactAffinity {
253 fn matches(&self, requirements: &AffinityRequirements, candidate: &NodeInfo) -> bool {
254 requirements
255 .attributes()
256 .iter()
257 .all(|(key, value)| candidate.env_vars.get(key) == Some(value))
258 }
259 }
260
261 fn requirements() -> AffinityRequirements {
262 AffinityRequirements::new(HashMap::from([("pool".into(), "a".into())])).unwrap()
263 }
264
265 fn node(id: u64, pool: Option<&str>, accepts_ingest: bool) -> NodeInfo {
266 NodeInfo {
267 peer: Peer::empty(id),
268 last_activity_ts: 0,
269 status: NodeStatus::Datanode(DatanodeStatus {
270 rcus: 0,
271 wcus: 0,
272 leader_regions: 0,
273 follower_regions: 0,
274 workloads: DatanodeWorkloads {
275 types: if accepts_ingest { vec![0] } else { vec![] },
276 },
277 }),
278 version: String::new(),
279 git_commit: String::new(),
280 start_time_ms: 0,
281 total_cpu_millicores: 0,
282 total_memory_bytes: 0,
283 cpu_usage_millicores: 0,
284 memory_usage_bytes: 0,
285 hostname: String::new(),
286 env_vars: pool
287 .map(|pool| HashMap::from([("pool".into(), pool.into())]))
288 .unwrap_or_default(),
289 schedulable: None,
290 }
291 }
292
293 struct Discovery {
294 snapshots: Vec<Vec<NodeInfo>>,
295 fail: bool,
296 reads: AtomicUsize,
297 }
298
299 #[async_trait::async_trait]
300 impl PeerDiscovery for Discovery {
301 async fn active_frontends(&self) -> MetaResult<Vec<NodeInfo>> {
302 Ok(vec![])
303 }
304
305 async fn active_datanodes(
306 &self,
307 filter: Option<for<'a> fn(&'a NodeWorkloads) -> bool>,
308 ) -> MetaResult<Vec<NodeInfo>> {
309 let index = self.reads.fetch_add(1, Ordering::Relaxed);
310 if self.fail {
311 return common_meta::error::UnexpectedSnafu {
312 err_msg: "discovery unavailable",
313 }
314 .fail();
315 }
316 let index = index.min(self.snapshots.len() - 1);
317 Ok(self.snapshots[index]
318 .iter()
319 .filter(|node| {
320 let NodeStatus::Datanode(status) = &node.status else {
321 unreachable!()
322 };
323 filter.is_none_or(|filter| {
324 filter(&NodeWorkloads::Datanode(status.workloads.clone()))
325 })
326 })
327 .cloned()
328 .collect())
329 }
330
331 async fn active_flownodes(
332 &self,
333 filter: Option<for<'a> fn(&'a NodeWorkloads) -> bool>,
334 ) -> MetaResult<Vec<NodeInfo>> {
335 self.active_datanodes(filter).await
336 }
337 }
338
339 #[tokio::test]
340 async fn test_affinity_constrains_base_selectors() {
341 let affinity_requirements = requirements();
342 for lease_based in [false, true] {
343 for (use_exact_affinity, has_requirements) in
344 [(true, false), (true, true), (false, true)]
345 {
346 let enabled = use_exact_affinity && has_requirements;
347 let selector: SelectorRef = if lease_based {
348 Arc::new(LeaseBasedSelector)
349 } else {
350 Arc::new(RoundRobinSelector::new(SelectTarget::Datanode))
351 };
352 let ctx = SelectorContext {
353 affinity_fallback: AffinityFallback::Error,
354 affinity: if use_exact_affinity {
355 Arc::new(ExactAffinity)
356 } else {
357 Arc::new(NoopAffinity)
358 },
359 peer_discovery: Arc::new(Discovery {
360 snapshots: vec![vec![
361 node(1, Some("b"), true),
362 node(2, Some("a"), true),
363 node(3, Some("a"), true),
364 node(4, None, true),
365 node(5, Some("a"), false),
366 ]],
367 reads: AtomicUsize::new(0),
368 fail: false,
369 }),
370 };
371 let peers = selector
373 .select(
374 &ctx,
375 SelectorOptions {
376 affinity: has_requirements.then_some(&affinity_requirements),
377 min_required_items: if enabled { 1 } else { 3 },
378 exclude_peer_ids: HashSet::from([3]),
379 workload_filter: Some(accept_ingest_workload),
380 ..Default::default()
381 },
382 )
383 .await
384 .unwrap();
385 let ids = peers
386 .into_iter()
387 .map(|peer| peer.id)
388 .collect::<HashSet<_>>();
389 assert_eq!(
390 ids,
391 if enabled {
392 HashSet::from([2])
393 } else {
394 HashSet::from([1, 2, 4])
395 }
396 );
397 }
398 }
399 }
400
401 #[tokio::test]
402 async fn test_round_robin_filters_each_discovery_read_for_both_targets() {
403 let affinity_requirements = requirements();
404 for target in [SelectTarget::Datanode, SelectTarget::Flownode] {
405 let discovery = Arc::new(Discovery {
406 snapshots: vec![
407 vec![node(1, Some("b"), true), node(2, Some("a"), true)],
408 vec![
409 node(1, Some("b"), true),
410 node(2, Some("a"), true),
411 node(3, None, true),
412 ],
413 ],
414 reads: AtomicUsize::new(0),
415 fail: false,
416 });
417 let ctx = SelectorContext {
418 affinity_fallback: AffinityFallback::Error,
419 peer_discovery: discovery.clone(),
420 affinity: Arc::new(ExactAffinity),
421 };
422 let selector = RoundRobinSelector::new(target);
423 let opts = SelectorOptions {
424 affinity: Some(&affinity_requirements),
425 ..Default::default()
426 };
427 for _ in 0..2 {
428 assert_eq!(
429 selector.select(&ctx, opts.clone()).await.unwrap(),
430 vec![Peer::empty(2)]
431 );
432 }
433 assert_eq!(discovery.reads.load(Ordering::Relaxed), 2);
434 }
435 }
436
437 #[tokio::test]
438 async fn test_round_robin_excludes_peers_for_both_targets() {
439 let affinity_requirements = requirements();
440 for target in [SelectTarget::Datanode, SelectTarget::Flownode] {
441 let ctx = SelectorContext {
442 affinity_fallback: AffinityFallback::Error,
443 peer_discovery: Arc::new(Discovery {
444 snapshots: vec![vec![node(1, Some("a"), true), node(2, Some("a"), true)]],
445 reads: AtomicUsize::new(0),
446 fail: false,
447 }),
448 affinity: Arc::new(ExactAffinity),
449 };
450 for affinity in [None, Some(&affinity_requirements)] {
451 for (excluded, expected) in [(vec![], vec![1, 2]), (vec![1], vec![2, 2])] {
453 let selector = RoundRobinSelector::new(target);
454 let peers = selector
455 .select(
456 &ctx,
457 SelectorOptions {
458 affinity,
459 min_required_items: 2,
460 allow_duplication: true,
461 exclude_peer_ids: excluded.into_iter().collect(),
462 ..Default::default()
463 },
464 )
465 .await
466 .unwrap();
467 assert_eq!(
468 peers.into_iter().map(|peer| peer.id).collect::<Vec<_>>(),
469 expected
470 );
471 }
472 let selector = RoundRobinSelector::new(target);
473 let error = selector
474 .select(
475 &ctx,
476 SelectorOptions {
477 affinity,
478 exclude_peer_ids: HashSet::from([1, 2]),
479 ..Default::default()
480 },
481 )
482 .await
483 .unwrap_err();
484 assert!(matches!(
485 error,
486 crate::error::Error::NoEnoughAvailableNode { available: 0, .. }
487 ));
488 }
489 }
490 }
491
492 #[tokio::test]
493 async fn test_base_selector_affinity_fallback() {
494 let affinity_requirements = requirements();
495 for lease_based in [false, true] {
496 for fallback in [AffinityFallback::Error, AffinityFallback::Unconstrained] {
497 for has_requirements in [false, true] {
498 for required in [2, 3] {
499 let discovery = Arc::new(Discovery {
500 snapshots: vec![vec![
501 node(1, Some("a"), true),
502 node(2, Some("b"), true),
503 node(3, Some("a"), true),
504 node(4, Some("a"), false),
505 ]],
506 reads: AtomicUsize::new(0),
507 fail: false,
508 });
509 let ctx = SelectorContext {
510 peer_discovery: discovery.clone(),
511 affinity: Arc::new(ExactAffinity),
512 affinity_fallback: fallback,
513 };
514 let selector: SelectorRef = if lease_based {
515 Arc::new(LeaseBasedSelector)
516 } else {
517 Arc::new(RoundRobinSelector::new(SelectTarget::Datanode))
518 };
519 let result = selector
520 .select(
521 &ctx,
522 SelectorOptions {
523 affinity: has_requirements.then_some(&affinity_requirements),
524 min_required_items: required,
525 allow_duplication: false,
526 exclude_peer_ids: HashSet::from([3]),
527 workload_filter: Some(accept_ingest_workload),
528 },
529 )
530 .await;
531 let retries =
532 has_requirements && fallback == AffinityFallback::Unconstrained;
533 assert_eq!(
534 discovery.reads.load(Ordering::Relaxed),
535 1 + usize::from(retries)
536 );
537 let constrained = has_requirements && !retries;
538 if required == 2 && !constrained {
539 assert_eq!(
540 result
541 .unwrap()
542 .into_iter()
543 .map(|peer| peer.id)
544 .collect::<HashSet<_>>(),
545 HashSet::from([1, 2])
546 );
547 } else {
548 let available = if constrained { 1 } else { 2 };
549 assert!(
550 matches!(result.unwrap_err(), crate::error::Error::NoEnoughAvailableNode {
551 required: actual_required, available: actual_available, ..
552 } if actual_required == required && actual_available == available)
553 );
554 }
555 }
556 }
557 }
558 }
559 }
560
561 #[tokio::test]
562 async fn test_base_selector_discovery_error_does_not_fallback() {
563 let affinity_requirements = requirements();
564 for lease_based in [false, true] {
565 let discovery = Arc::new(Discovery {
566 snapshots: vec![],
567 reads: AtomicUsize::new(0),
568 fail: true,
569 });
570 let ctx = SelectorContext {
571 peer_discovery: discovery.clone(),
572 affinity: Arc::new(ExactAffinity),
573 affinity_fallback: AffinityFallback::Unconstrained,
574 };
575 let selector: SelectorRef = if lease_based {
576 Arc::new(LeaseBasedSelector)
577 } else {
578 Arc::new(RoundRobinSelector::new(SelectTarget::Datanode))
579 };
580 let error = selector
581 .select(
582 &ctx,
583 SelectorOptions {
584 affinity: Some(&affinity_requirements),
585 ..Default::default()
586 },
587 )
588 .await
589 .unwrap_err();
590 assert!(matches!(
591 error,
592 crate::error::Error::ListActiveDatanodes { .. }
593 ));
594 assert_eq!(discovery.reads.load(Ordering::Relaxed), 1);
595 }
596 }
597
598 struct CountingStatBackend {
599 inner: MemoryKvBackend<MetaError>,
600 reads: AtomicUsize,
601 fail: bool,
602 }
603
604 #[async_trait::async_trait]
605 impl TxnService for CountingStatBackend {
606 type Error = MetaError;
607 }
608
609 #[async_trait::async_trait]
610 impl KvBackend for CountingStatBackend {
611 fn name(&self) -> &str {
612 "counting-stat-backend"
613 }
614
615 fn as_any(&self) -> &dyn Any {
616 self
617 }
618
619 async fn batch_get(&self, req: BatchGetRequest) -> MetaResult<BatchGetResponse> {
620 self.reads.fetch_add(1, Ordering::Relaxed);
621 if self.fail {
622 return common_meta::error::UnexpectedSnafu {
623 err_msg: "stat backend unavailable",
624 }
625 .fail();
626 }
627 self.inner.batch_get(req).await
628 }
629
630 async fn range(&self, req: RangeRequest) -> MetaResult<RangeResponse> {
631 self.inner.range(req).await
632 }
633
634 async fn put(&self, req: PutRequest) -> MetaResult<PutResponse> {
635 self.inner.put(req).await
636 }
637
638 async fn batch_put(&self, req: BatchPutRequest) -> MetaResult<BatchPutResponse> {
639 self.inner.batch_put(req).await
640 }
641
642 async fn delete_range(&self, req: DeleteRangeRequest) -> MetaResult<DeleteRangeResponse> {
643 self.inner.delete_range(req).await
644 }
645
646 async fn batch_delete(&self, req: BatchDeleteRequest) -> MetaResult<BatchDeleteResponse> {
647 self.inner.batch_delete(req).await
648 }
649 }
650
651 impl ResettableKvBackend for CountingStatBackend {
652 fn reset(&self) {
653 self.inner.reset();
654 }
655
656 fn as_kv_backend_ref(self: Arc<Self>) -> KvBackendRef {
657 self
658 }
659 }
660
661 #[tokio::test]
662 async fn test_load_based_affinity_fallback_queries() {
663 let affinity_requirements = requirements();
664 for (has_requirements, fallback, required, fail_stats, expected_reads, available) in [
665 (false, AffinityFallback::Unconstrained, 2, false, 1, 2),
666 (true, AffinityFallback::Error, 2, false, 1, 1),
667 (true, AffinityFallback::Unconstrained, 2, false, 2, 2),
668 (true, AffinityFallback::Unconstrained, 3, false, 2, 2),
669 (true, AffinityFallback::Unconstrained, 2, true, 1, 0),
670 ] {
671 let backend = Arc::new(CountingStatBackend {
672 inner: MemoryKvBackend::new(),
673 reads: AtomicUsize::new(0),
674 fail: fail_stats,
675 });
676 for id in 1..=4 {
677 let value = DatanodeStatValue {
678 stats: vec![Stat {
679 id,
680 addr: format!("node-{id}"),
681 region_num: id,
682 ..Default::default()
683 }],
684 };
685 backend
686 .put(
687 PutRequest::new()
688 .with_key(Vec::<u8>::from(DatanodeStatKey { node_id: id }))
689 .with_value(Vec::<u8>::try_from(value).unwrap()),
690 )
691 .await
692 .unwrap();
693 }
694 let client = Arc::new(
695 MetaPeerClientBuilder::default()
696 .election(None)
697 .in_memory(backend.clone())
698 .build()
699 .unwrap(),
700 );
701 let selector = LoadBasedSelector::new(RegionNumsBasedWeightCompute, client);
702 let discovery = Arc::new(Discovery {
703 snapshots: vec![vec![
704 node(1, Some("a"), true),
705 node(2, Some("b"), true),
706 node(3, Some("a"), true),
707 node(4, Some("a"), false),
708 ]],
709 reads: AtomicUsize::new(0),
710 fail: false,
711 });
712 let ctx = SelectorContext {
713 peer_discovery: discovery.clone(),
714 affinity: Arc::new(ExactAffinity),
715 affinity_fallback: fallback,
716 };
717 let result = selector
718 .select(
719 &ctx,
720 SelectorOptions {
721 affinity: has_requirements.then_some(&affinity_requirements),
722 min_required_items: required,
723 allow_duplication: false,
724 exclude_peer_ids: HashSet::from([3]),
725 workload_filter: Some(accept_ingest_workload),
726 },
727 )
728 .await;
729 assert_eq!(discovery.reads.load(Ordering::Relaxed), expected_reads);
730 assert_eq!(backend.reads.load(Ordering::Relaxed), expected_reads);
731 if fail_stats {
732 assert!(matches!(
733 result.unwrap_err(),
734 crate::error::Error::KvBackend { .. }
735 ));
736 } else if available < required {
737 assert!(
738 matches!(result.unwrap_err(), crate::error::Error::NoEnoughAvailableNode {
739 required: actual_required, available: actual_available, ..
740 } if actual_required == required && actual_available == available)
741 );
742 } else {
743 assert_eq!(
744 result
745 .unwrap()
746 .into_iter()
747 .map(|peer| peer.id)
748 .collect::<HashSet<_>>(),
749 HashSet::from([1, 2])
750 );
751 }
752 }
753 }
754}