Skip to main content

meta_srv/selector/
common.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
28/// Returns active datanodes that may receive new regions.
29pub(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
44/// Filter out the excluded peers from the `weight_array`.
45pub 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
52/// According to the `opts`, choose peers from the `weight_array` through `weighted_choose`.
53pub 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        // fast path
69        return Ok(vec![weighted_choose.choose_one()?]);
70    }
71
72    let available_count = weighted_choose.len();
73
74    if opts.allow_duplication {
75        // Calculate how many complete rounds of `available_count` items to select,
76        // plus any additional items needed after complete rounds.
77        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 the available items are sufficient when duplication is not allowed.
92        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                // Selecting a complete round makes the lease-based result deterministic as a set.
372                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                // Keep the topology fixed and vary only the exclusion set.
452                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}