Skip to main content

picodata_driver/
topology.rs

1use std::{
2    collections::{HashMap, hash_map::Entry},
3    sync::{
4        Arc,
5        atomic::{AtomicUsize, Ordering},
6    },
7};
8
9use serde::Deserialize;
10use uuid::Uuid;
11
12use crate::instance::{Instance, State};
13
14/// Global counter for assigning unique pool slot indices to instances.
15static NEXT_IDX: AtomicUsize = AtomicUsize::new(0);
16
17/// Name of a connection type in `_pico_peer_address`, such as `pgproto` or `iproto`.
18type ConnectionType = String;
19
20/// A single bucket range entry from `_pico_bucket()` response.
21#[derive(Clone, Debug, Deserialize)]
22pub(crate) struct BucketRangeEntry {
23    pub tier_name: String,
24    pub bucket_id_start: u64,
25    pub bucket_id_end: u64,
26    pub current_replicaset_uuid: Uuid,
27}
28
29/// The cluster topology as handed out by [`crate::Client::topology`].
30pub mod view {
31    use std::{collections::HashMap, ops::RangeInclusive};
32
33    use uuid::Uuid;
34
35    use super::ConnectionType;
36    use crate::error::{Error, IncompleteBucketMap};
37
38    /// Cluster topology: tiers with their replicasets and bucket ranges.
39    #[derive(Debug, Clone, PartialEq, Eq)]
40    pub struct Topology {
41        pub(super) tiers: Vec<Tier>,
42    }
43
44    /// A tier with its replicasets.
45    #[derive(Debug, Clone, PartialEq, Eq)]
46    pub struct Tier {
47        pub(super) name: String,
48        pub(super) bucket_count: u64,
49        pub(super) replicasets: Vec<Replicaset>,
50    }
51
52    /// A replicaset with its instances and the buckets it owns.
53    #[derive(Debug, Clone, PartialEq, Eq)]
54    pub struct Replicaset {
55        pub(super) uuid: Uuid,
56        pub(super) name: String,
57        pub(super) master: Option<Instance>,
58        pub(super) replicas: Vec<Instance>,
59        pub(super) ranges: Vec<RangeInclusive<u64>>,
60    }
61
62    /// An instance of a replicaset.
63    #[derive(Debug, Clone, PartialEq, Eq)]
64    pub struct Instance {
65        pub(super) name: String,
66        /// The address of each connection type the instance accepts.
67        pub(super) addresses: HashMap<ConnectionType, String>,
68        pub(super) online: bool,
69        pub(super) is_master: bool,
70        pub(super) raft_id: u64,
71        pub(super) uuid: Uuid,
72    }
73
74    /// Bucket ranges of a tier by the UUID of the replicaset owning them.
75    pub type BucketMap<'a> = HashMap<Uuid, &'a [RangeInclusive<u64>]>;
76
77    impl Topology {
78        /// The cluster's tiers.
79        #[must_use]
80        pub fn tiers(&self) -> &[Tier] {
81            &self.tiers
82        }
83    }
84
85    impl Tier {
86        /// Tier name.
87        #[must_use]
88        pub fn name(&self) -> &str {
89            &self.name
90        }
91
92        /// The tier's replicasets.
93        #[must_use]
94        pub fn replicasets(&self) -> &[Replicaset] {
95            &self.replicasets
96        }
97
98        /// Number of buckets the tier is configured with.
99        #[must_use]
100        pub fn bucket_count(&self) -> u64 {
101            self.bucket_count
102        }
103
104        /// Bucket ranges by the UUID of the replicaset owning them.
105        ///
106        /// # Errors
107        ///
108        /// Returns [`Error::IncompleteBucketMap`] if the ranges leave some of the tier's
109        /// buckets uncovered.
110        pub fn bucket_map(&self) -> Result<BucketMap<'_>, Error> {
111            let mut ranges: Vec<_> = self.replicasets.iter().flat_map(|rs| &rs.ranges).collect();
112            ranges.sort_unstable_by_key(|r| *r.start());
113            // Buckets covered without a gap, counting from bucket 1.
114            let mut covered = 0;
115            for r in ranges {
116                if *r.start() != covered + 1 || r.is_empty() {
117                    break;
118                }
119                covered = *r.end();
120            }
121            if covered != self.bucket_count {
122                return Err(Error::IncompleteBucketMap(Box::new(IncompleteBucketMap {
123                    tier: self.name.clone(),
124                    covered,
125                    expected: self.bucket_count,
126                })));
127            }
128            Ok(self
129                .replicasets
130                .iter()
131                .map(|rs| (rs.uuid(), rs.ranges()))
132                .collect())
133        }
134    }
135
136    impl Replicaset {
137        /// UUID of the replicaset.
138        #[must_use]
139        pub fn uuid(&self) -> Uuid {
140            self.uuid
141        }
142
143        /// The current master, or `None` while the cluster has not named one.
144        #[must_use]
145        pub fn master(&self) -> Option<&Instance> {
146            self.master.as_ref()
147        }
148
149        /// The remaining instances.
150        #[must_use]
151        pub fn replicas(&self) -> &[Instance] {
152            &self.replicas
153        }
154
155        /// All the instances, the master first.
156        pub fn instances(&self) -> impl Iterator<Item = &Instance> {
157            self.master.iter().chain(&self.replicas)
158        }
159
160        /// Bucket ranges the replicaset owns, ordered by start.
161        #[must_use]
162        pub fn ranges(&self) -> &[RangeInclusive<u64>] {
163            &self.ranges
164        }
165
166        /// Name of the replicaset.
167        #[must_use]
168        pub fn name(&self) -> &str {
169            &self.name
170        }
171    }
172
173    impl Instance {
174        /// Instance name.
175        #[must_use]
176        pub fn name(&self) -> &str {
177            &self.name
178        }
179
180        /// The address the instance accepts `connection_type`, such as `iproto`, on;
181        /// `None` if it does not.
182        #[must_use]
183        pub fn address(&self, connection_type: impl AsRef<str>) -> Option<&str> {
184            self.addresses
185                .get(connection_type.as_ref())
186                .map(String::as_str)
187        }
188
189        /// Returns `true` if the instance is online.
190        #[must_use]
191        pub fn is_online(&self) -> bool {
192            self.online
193        }
194
195        /// Returns `true` if the instance is the master of its replicaset.
196        #[must_use]
197        pub fn is_master(&self) -> bool {
198            self.is_master
199        }
200
201        /// Raft id of the instance.
202        #[must_use]
203        pub fn raft_id(&self) -> u64 {
204            self.raft_id
205        }
206
207        /// UUID of the instance.
208        #[must_use]
209        pub fn uuid(&self) -> Uuid {
210            self.uuid
211        }
212    }
213}
214
215/// Cluster topology: instances, tiers, replicasets, and bucket ranges.
216#[derive(Debug, Default)]
217pub(crate) struct Topology {
218    pub(crate) instances: HashMap<String, Arc<Instance>>,
219    pgproto_tiers: Vec<Tier>,
220    /// Tiers by every other connection type, such as `iproto`; never routed to.
221    other_tiers: HashMap<ConnectionType, Vec<Tier>>,
222}
223
224/// A single tier with its replicasets and bucket-to-replicaset mapping.
225#[derive(Debug, Clone)]
226struct Tier {
227    name: String,
228    bucket_count: u64,
229    replicasets: Vec<Replicaset>,
230    bucket_map: Vec<BucketSlot>,
231}
232
233/// Maps a contiguous bucket range [start, end] to a replicaset index.
234#[derive(Debug, Clone)]
235struct BucketSlot {
236    start: u64,
237    end: u64,
238    replicaset_idx: usize,
239}
240
241/// A replicaset with its master and replica instances.
242#[derive(Debug, Clone)]
243struct Replicaset {
244    uuid: Uuid,
245    master: Arc<Instance>,
246    replicas: Vec<Arc<Instance>>,
247}
248
249impl From<(Vec<pg::Row>, Vec<pg::Row>)> for Topology {
250    fn from((topology_rows, bucket_rows): (Vec<pg::Row>, Vec<pg::Row>)) -> Self {
251        // One row per advertised address: a topology per connection type, `pgproto` is routed to.
252        let mut by_type: HashMap<ConnectionType, Vec<pg::Row>> = HashMap::new();
253        for row in topology_rows {
254            let Ok(connection_type) = row.try_get::<_, &str>("connection_type") else {
255                continue;
256            };
257            if let Some(rows) = by_type.get_mut(connection_type) {
258                rows.push(row);
259            } else {
260                by_type.insert(connection_type.to_string(), vec![row]);
261            }
262        }
263        // Parsed once: every connection type shares the same bucket ranges.
264        let mut buckets: Vec<BucketRangeEntry> = Vec::new();
265        for row in bucket_rows {
266            let entries = if let Ok(json) = row.try_get::<_, serde_json::Value>(0) {
267                serde_json::from_value::<Vec<BucketRangeEntry>>(json)
268            } else if let Ok(s) = row.try_get::<_, &str>(0) {
269                serde_json::from_str(s)
270            } else {
271                log::error!("failed to read bucket ranges column");
272                continue;
273            };
274            match entries {
275                Ok(v) => buckets.extend(v),
276                Err(e) => log::error!("failed to parse bucket ranges: {e}"),
277            }
278        }
279
280        let mut other_tiers: HashMap<ConnectionType, Vec<Tier>> = by_type
281            .into_iter()
282            .map(|(connection_type, rows)| (connection_type, build_tiers(rows, &buckets)))
283            .collect();
284        let pgproto_tiers = other_tiers.remove("pgproto").unwrap_or_default();
285        // A master placeholder that is not the master also sits in `replicas`.
286        let instances = pgproto_tiers
287            .iter()
288            .flat_map(|t| &t.replicasets)
289            .flat_map(|rs| {
290                rs.master
291                    .is_master
292                    .then_some(&rs.master)
293                    .into_iter()
294                    .chain(&rs.replicas)
295            })
296            .map(|inst| (inst.name.clone(), inst.clone()))
297            .collect();
298        Topology {
299            instances,
300            pgproto_tiers,
301            other_tiers,
302        }
303    }
304}
305
306/// Builds the tiers from the rows of one connection type.
307fn build_tiers(topology_rows: Vec<pg::Row>, buckets: &[BucketRangeEntry]) -> Vec<Tier> {
308    let mut tiers: Vec<Tier> = Vec::new();
309    for row in topology_rows {
310        let inst = match Instance::try_from(row) {
311            Ok(v) => v,
312            Err(e) => {
313                log::error!("failed to convert instance: {e}");
314                continue;
315            }
316        };
317
318        let inst = Arc::new(inst);
319
320        let tier = if let Some(t) = tiers.iter_mut().find(|t| t.name == inst.tier) {
321            t
322        } else {
323            tiers.push(Tier {
324                name: inst.tier.clone(),
325                bucket_count: inst.bucket_count,
326                replicasets: Vec::new(),
327                bucket_map: Vec::new(),
328            });
329            tiers.last_mut().expect("just pushed")
330        };
331
332        let rs = if let Some(r) = tier
333            .replicasets
334            .iter_mut()
335            .find(|r| r.uuid == inst.replicaset_uuid)
336        {
337            r
338        } else {
339            tier.replicasets.push(Replicaset {
340                uuid: inst.replicaset_uuid,
341                master: inst.clone(),
342                replicas: Vec::new(),
343            });
344            tier.replicasets.last_mut().expect("just pushed")
345        };
346
347        if inst.is_master {
348            rs.master = inst.clone();
349        } else {
350            rs.replicas.push(inst.clone());
351        }
352    }
353
354    for entry in buckets {
355        let Some(tier) = tiers.iter_mut().find(|t| t.name == entry.tier_name) else {
356            continue;
357        };
358        let Some(rs_idx) = tier
359            .replicasets
360            .iter()
361            .position(|r| r.uuid == entry.current_replicaset_uuid)
362        else {
363            continue;
364        };
365        tier.bucket_map.push(BucketSlot {
366            start: entry.bucket_id_start,
367            end: entry.bucket_id_end,
368            replicaset_idx: rs_idx,
369        });
370    }
371
372    for tier in &mut tiers {
373        tier.bucket_map.sort_unstable_by_key(|s| s.start);
374    }
375
376    tiers
377}
378
379impl Topology {
380    /// Creates an empty topology.
381    pub(crate) fn new() -> Self {
382        Self::default()
383    }
384
385    /// Merges incoming topology: updates existing instance states, adds new instances,
386    /// marks missing instances as offline, and replaces tier/bucket data.
387    pub(crate) fn merge(&mut self, incoming: Topology) {
388        for (name, existing) in &self.instances {
389            if !incoming.instances.contains_key(name) {
390                existing.set_state(State::Offline);
391            }
392        }
393
394        // The map keeps the same Arc the new tiers hold, so each instance is one object;
395        // only its pool slot index carries over from the previous refresh.
396        for (name, inc_inst) in incoming.instances {
397            match self.instances.entry(name) {
398                Entry::Occupied(mut e) => {
399                    inc_inst.set_idx(e.get().idx());
400                    e.insert(inc_inst);
401                }
402                Entry::Vacant(e) => {
403                    inc_inst.set_idx(NEXT_IDX.fetch_add(1, Ordering::Relaxed));
404                    e.insert(inc_inst);
405                }
406            }
407        }
408
409        self.pgproto_tiers = incoming.pgproto_tiers;
410        self.other_tiers = incoming.other_tiers;
411    }
412
413    /// Resolves the pool slot index for a given tier, `bucket_id`, and master preference.
414    /// Returns `None` if the bucket or replicaset cannot be found, or no instance is online.
415    pub(crate) fn resolve(&self, tier: &str, bucket_id: u64, on_master: bool) -> Option<usize> {
416        let t = self.pgproto_tiers.iter().find(|t| t.name == tier)?;
417        let rs = t.lookup(bucket_id)?;
418
419        if on_master || rs.replicas.is_empty() {
420            return if rs.master.is_online() {
421                Some(rs.master.idx())
422            } else {
423                None
424            };
425        }
426
427        let online: Vec<_> = rs.replicas.iter().filter(|r| r.is_online()).collect();
428        if online.is_empty() {
429            return if rs.master.is_online() {
430                Some(rs.master.idx())
431            } else {
432                None
433            };
434        }
435
436        let idx =
437            usize::try_from(bucket_id % online.len() as u64).expect("modulo result fits in usize");
438        Some(online[idx].idx())
439    }
440
441    /// Builds the topology handed out to the user: each instance once, with its address
442    /// of every connection type.
443    pub(crate) fn snapshot(&self) -> view::Topology {
444        let mut view_tiers = Vec::with_capacity(self.pgproto_tiers.len());
445        add_to_view(&mut view_tiers, "pgproto", &self.pgproto_tiers);
446        for (connection_type, tiers) in &self.other_tiers {
447            add_to_view(&mut view_tiers, connection_type, tiers);
448        }
449        view_tiers.sort_unstable_by(|a, b| a.name.cmp(&b.name));
450        for tier in &mut view_tiers {
451            sort(&mut tier.replicasets);
452        }
453        view::Topology { tiers: view_tiers }
454    }
455
456    /// Returns the total bucket count for a tier.
457    pub(crate) fn bucket_count(&self, tier: &str) -> Option<u64> {
458        self.pgproto_tiers
459            .iter()
460            .find(|t| t.name == tier)
461            .map(|t| t.bucket_count)
462    }
463}
464
465/// Adds the instances of `tiers` to `view_tiers` with their `connection_type` address,
466/// next to what the other connection types have added.
467fn add_to_view(view_tiers: &mut Vec<view::Tier>, connection_type: &str, tiers: &[Tier]) {
468    for tier in tiers {
469        let view_tier = if let Some(t) = view_tiers.iter_mut().find(|t| t.name == tier.name) {
470            t
471        } else {
472            view_tiers.push(view::Tier {
473                name: tier.name.clone(),
474                bucket_count: tier.bucket_count,
475                replicasets: Vec::new(),
476            });
477            view_tiers.last_mut().expect("just pushed")
478        };
479
480        for (idx, rs) in tier.replicasets.iter().enumerate() {
481            let view_rs =
482                if let Some(r) = view_tier.replicasets.iter_mut().find(|r| r.uuid == rs.uuid) {
483                    r
484                } else {
485                    view_tier.replicasets.push(view::Replicaset {
486                        uuid: rs.uuid,
487                        name: rs.master.replicaset_name.clone(),
488                        master: None,
489                        replicas: Vec::new(),
490                        ranges: tier
491                            .bucket_map
492                            .iter()
493                            .filter(|s| s.replicaset_idx == idx)
494                            .map(|s| s.start..=s.end)
495                            .collect(),
496                    });
497                    view_tier.replicasets.last_mut().expect("just pushed")
498                };
499
500            if rs.master.is_master {
501                match &mut view_rs.master {
502                    Some(m) => {
503                        m.addresses
504                            .insert(connection_type.to_string(), rs.master.addr.to_string());
505                    }
506                    None => view_rs.master = Some(instance(&rs.master, connection_type)),
507                }
508            }
509            for r in &rs.replicas {
510                if let Some(i) = view_rs.replicas.iter_mut().find(|i| i.name == r.name) {
511                    i.addresses
512                        .insert(connection_type.to_string(), r.addr.to_string());
513                } else {
514                    view_rs.replicas.push(instance(r, connection_type));
515                }
516            }
517        }
518    }
519}
520
521impl Tier {
522    /// Finds the replicaset owning `bucket_id` via binary search on the bucket map.
523    fn lookup(&self, bucket_id: u64) -> Option<&Replicaset> {
524        let i = self
525            .bucket_map
526            .partition_point(|s| s.start <= bucket_id)
527            .checked_sub(1)?;
528        let slot = &self.bucket_map[i];
529        if bucket_id <= slot.end {
530            self.replicasets.get(slot.replicaset_idx)
531        } else {
532            None
533        }
534    }
535}
536
537/// Orders replicasets by UUID and their replicas by name, so equal topologies compare equal.
538fn sort(replicasets: &mut [view::Replicaset]) {
539    replicasets.sort_unstable_by_key(|rs| rs.uuid);
540    for rs in replicasets {
541        rs.replicas.sort_unstable_by(|a, b| a.name.cmp(&b.name));
542    }
543}
544
545/// Builds the view of an instance with its `connection_type` address.
546fn instance(inst: &Instance, connection_type: &str) -> view::Instance {
547    view::Instance {
548        name: inst.name.clone(),
549        addresses: HashMap::from([(connection_type.to_string(), inst.addr.to_string())]),
550        online: inst.is_online(),
551        is_master: inst.is_master,
552        raft_id: inst.raft_id,
553        uuid: inst.uuid,
554    }
555}
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560    use crate::error::Error;
561
562    const RS1: Uuid = Uuid::from_u128(1);
563    const RS2: Uuid = Uuid::from_u128(2);
564
565    /// Build a single-instance topology directly, bypassing `pg::Row` parsing.
566    fn make_topology(name: &str, tier: &str, rs_uuid: Uuid) -> Topology {
567        let inst = Arc::new(Instance::new_for_test(
568            name,
569            "127.0.0.1:3301",
570            tier,
571            rs_uuid,
572            true,
573            3000,
574        ));
575
576        let mut instances = HashMap::new();
577        instances.insert(inst.name.clone(), inst.clone());
578
579        let rs = Replicaset {
580            uuid: rs_uuid,
581            master: inst.clone(),
582            replicas: vec![],
583        };
584        let tier_obj = Tier {
585            name: tier.to_string(),
586            bucket_count: 3000,
587            replicasets: vec![rs],
588            bucket_map: vec![BucketSlot {
589                start: 1,
590                end: 3000,
591                replicaset_idx: 0,
592            }],
593        };
594
595        Topology {
596            instances,
597            pgproto_tiers: vec![tier_obj],
598            other_tiers: HashMap::new(),
599        }
600    }
601
602    /// A range of an unknown replicaset leaves a hole, which must be reported.
603    #[test]
604    fn bucket_map_rejects_an_incomplete_map() {
605        let mut topo = Topology::new();
606        let mut incoming = make_topology("i1", "default", RS1);
607        incoming.pgproto_tiers[0].bucket_map = vec![
608            BucketSlot {
609                start: 1,
610                end: 1500,
611                replicaset_idx: 0,
612            },
613            // Dangling: `replicasets` holds a single entry, so idx 7 is unknown.
614            BucketSlot {
615                start: 1501,
616                end: 3000,
617                replicaset_idx: 7,
618            },
619        ];
620        topo.merge(incoming);
621
622        match topo.snapshot().tiers()[0].bucket_map() {
623            Err(Error::IncompleteBucketMap(m)) => {
624                assert_eq!(m.tier, "default");
625                assert_eq!((m.covered, m.expected), (1500, 3000));
626            }
627            other => panic!("a half-covered tier must not be returned: {other:?}"),
628        }
629    }
630
631    /// A tier without ranges must be reported, not returned empty.
632    #[test]
633    fn bucket_map_rejects_a_tier_with_no_ranges() {
634        let mut topo = Topology::new();
635        let mut incoming = make_topology("i1", "default", RS1);
636        incoming.pgproto_tiers[0].bucket_map.clear();
637        topo.merge(incoming);
638
639        assert!(matches!(
640            topo.snapshot().tiers()[0].bucket_map(),
641            Err(Error::IncompleteBucketMap(ref m)) if m.covered == 0
642        ));
643    }
644
645    /// Every range a replicaset owns must route to that replicaset's master.
646    #[test]
647    fn snapshot_ranges_agree_with_resolve() {
648        let mut topo = Topology::new();
649        let mut incoming = make_topology("i1", "default", RS1);
650        let i2 = Arc::new(Instance::new_for_test(
651            "i2",
652            "127.0.0.1:3302",
653            "default",
654            RS2,
655            true,
656            3000,
657        ));
658        incoming.instances.insert(i2.name.clone(), i2.clone());
659        let tier = &mut incoming.pgproto_tiers[0];
660        tier.replicasets.push(Replicaset {
661            uuid: RS2,
662            master: i2,
663            replicas: vec![],
664        });
665        tier.bucket_map = vec![
666            BucketSlot {
667                start: 1,
668                end: 1000,
669                replicaset_idx: 0,
670            },
671            BucketSlot {
672                start: 1001,
673                end: 2000,
674                replicaset_idx: 1,
675            },
676            BucketSlot {
677                start: 2001,
678                end: 3000,
679                replicaset_idx: 0,
680            },
681        ];
682        topo.merge(incoming);
683
684        let snapshot = topo.snapshot();
685        let tier = &snapshot.tiers()[0];
686        assert_eq!(tier.name(), "default");
687        let owned: Vec<_> = tier
688            .replicasets()
689            .iter()
690            .map(|rs| {
691                (
692                    rs.uuid(),
693                    rs.master().map(view::Instance::name),
694                    rs.ranges().len(),
695                )
696            })
697            .collect();
698        assert_eq!(owned, [(RS1, Some("i1"), 2), (RS2, Some("i2"), 1)]);
699        let bucket_map = tier.bucket_map().expect("map covers the tier");
700        assert_eq!(bucket_map[&RS1], [1..=1000, 2001..=3000]);
701
702        for rs in tier.replicasets() {
703            let expected = topo.instances[rs.master().expect("named master").name()].idx();
704            for r in rs.ranges() {
705                for bucket in [*r.start(), *r.end()] {
706                    assert_eq!(topo.resolve("default", bucket, true), Some(expected));
707                }
708            }
709        }
710    }
711
712    /// Without a named master routing keeps the placeholder, but `master()` is `None`.
713    #[test]
714    fn snapshot_reports_no_master_until_one_is_named() {
715        // What `Topology::from` builds when no row carries is_master.
716        let inst = Arc::new(Instance::new_for_test(
717            "i1",
718            "127.0.0.1:3301",
719            "default",
720            RS1,
721            false,
722            3000,
723        ));
724        let mut incoming = make_topology("i1", "default", RS1);
725        incoming.instances.insert(inst.name.clone(), inst.clone());
726        let rs = &mut incoming.pgproto_tiers[0].replicasets[0];
727        rs.master = inst.clone();
728        rs.replicas = vec![inst];
729        let mut topo = Topology::new();
730        topo.merge(incoming);
731
732        assert_eq!(
733            topo.resolve("default", 1, true),
734            Some(topo.instances["i1"].idx())
735        );
736        let snapshot = topo.snapshot();
737        let rs = &snapshot.tiers()[0].replicasets()[0];
738        assert_eq!(rs.master(), None);
739        let replicas: Vec<_> = rs.replicas().iter().map(view::Instance::name).collect();
740        assert_eq!(replicas, ["i1"]);
741    }
742
743    /// `instances` yields the master first, then the replicas by name.
744    #[test]
745    fn replicaset_lists_the_master_first() {
746        let mut topo = make_topology("m", "default", RS1);
747        topo.pgproto_tiers[0].replicasets[0].replicas = ["b", "a"]
748            .map(|name| {
749                Arc::new(Instance::new_for_test(
750                    name,
751                    "127.0.0.1:3302",
752                    "default",
753                    RS1,
754                    false,
755                    3000,
756                ))
757            })
758            .into();
759
760        let snapshot = topo.snapshot();
761        let rs = &snapshot.tiers()[0].replicasets()[0];
762        let instances: Vec<_> = rs.instances().map(|i| (i.name(), i.is_master())).collect();
763        assert_eq!(instances, [("m", true), ("a", false), ("b", false)]);
764    }
765
766    /// Regression: without `inc_inst.set_idx(existing.idx())` in `merge`, the
767    /// second merge replaces `self.pgproto_tiers` with `incoming.pgproto_tiers` whose
768    /// `Arc<Instance>` objects still carry the default `idx == 0`.  Every
769    /// subsequent `resolve` call would then return `Some(0)` instead of the
770    /// pool slot that was assigned during the first merge.
771    #[test]
772    fn merge_preserves_pool_idx_in_tiers() {
773        // Bump the global counter so the assigned idx is unambiguously non-zero.
774        NEXT_IDX.fetch_add(1, Ordering::Relaxed);
775
776        let mut topo = Topology::new();
777
778        // First merge: "i1" is unknown, so it receives a fresh pool-slot index.
779        topo.merge(make_topology("i1", "default", RS1));
780        let expected_idx = topo.instances["i1"].idx();
781        assert!(expected_idx > 0, "precondition: idx must be non-zero");
782
783        // Second merge: same instance name, but a brand-new Arc<Instance> with
784        // idx == 0.  Before the fix, `self.pgproto_tiers = incoming.pgproto_tiers` made the
785        // tiers point at those fresh objects, so resolve returned Some(0).
786        topo.merge(make_topology("i1", "default", RS1));
787
788        assert_eq!(
789            topo.resolve("default", 1, true),
790            Some(expected_idx),
791            "resolve must return the stable pool slot, not the default 0 from the fresh Arc"
792        );
793    }
794
795    /// Regression: `merge` kept the first-seen Arc in `instances` while the tiers got the
796    /// fresh one, so the pools and routing saw two objects with diverging fields.
797    #[test]
798    fn merge_keeps_one_object_per_instance() {
799        let mut topo = Topology::new();
800        topo.merge(make_topology("i1", "default", RS1));
801        topo.merge(make_topology("i1", "default", RS1));
802
803        let master = &topo.pgproto_tiers[0].replicasets[0].master;
804        assert!(Arc::ptr_eq(&topo.instances["i1"], master));
805    }
806
807    /// Builds a topology holding only `tier`, filed under the `iproto` connection type.
808    fn with_tier(tier: Tier) -> Topology {
809        Topology {
810            instances: HashMap::new(),
811            pgproto_tiers: Vec::new(),
812            other_tiers: HashMap::from([("iproto".to_string(), vec![tier])]),
813        }
814    }
815
816    /// Builds a one-replicaset tier whose master listens on `addr`.
817    fn tier_at(addr: &str) -> Tier {
818        let inst = Arc::new(Instance::new_for_test(
819            "i1", addr, "default", RS1, true, 3000,
820        ));
821        Tier {
822            name: "default".to_string(),
823            bucket_count: 3000,
824            replicasets: vec![Replicaset {
825                uuid: RS1,
826                master: inst,
827                replicas: vec![],
828            }],
829            bucket_map: vec![BucketSlot {
830                start: 1,
831                end: 3000,
832                replicaset_idx: 0,
833            }],
834        }
835    }
836
837    /// An instance is listed once, with the address of each connection type it accepts.
838    #[test]
839    fn snapshot_merges_the_addresses_of_an_instance() {
840        let mut topo = with_tier(tier_at("127.0.0.1:3301"));
841        topo.pgproto_tiers = vec![tier_at("127.0.0.1:5432")];
842
843        let snapshot = topo.snapshot();
844        let rs = &snapshot.tiers()[0].replicasets()[0];
845        assert!(rs.replicas().is_empty());
846        let i1 = rs.master().expect("named master");
847        assert_eq!(i1.address("pgproto"), Some("127.0.0.1:5432"));
848        assert_eq!(i1.address("iproto"), Some("127.0.0.1:3301"));
849    }
850
851    /// An instance without pgproto is still listed, and the buckets it owns still count.
852    #[test]
853    fn snapshot_keeps_an_instance_without_pgproto() {
854        let topo = with_tier(tier_at("127.0.0.1:3301"));
855
856        let snapshot = topo.snapshot();
857        let tier = &snapshot.tiers()[0];
858        let i1 = tier.replicasets()[0].master().expect("named master");
859        assert_eq!(i1.address("pgproto"), None);
860        assert_eq!(i1.address("iproto"), Some("127.0.0.1:3301"));
861        assert!(tier.bucket_map().is_ok());
862    }
863
864    /// `merge` replaces the non-pgproto tiers wholesale.
865    #[test]
866    fn merge_replaces_other_tiers() {
867        let mut topo = with_tier(tier_at("127.0.0.1:3301"));
868        topo.merge(with_tier(tier_at("127.0.0.1:3302")));
869
870        let snapshot = topo.snapshot();
871        let i1 = snapshot.tiers()[0].replicasets()[0]
872            .master()
873            .expect("named master");
874        assert_eq!(i1.address("iproto"), Some("127.0.0.1:3302"));
875    }
876
877    /// Insertion order of replicasets and replicas does not affect the snapshot.
878    #[test]
879    fn snapshot_is_independent_of_insertion_order() {
880        let inst = |name: &str, rs: Uuid, master: bool| {
881            Arc::new(Instance::new_for_test(
882                name,
883                "127.0.0.1:3301",
884                "default",
885                rs,
886                master,
887                3000,
888            ))
889        };
890        let build = |reversed: bool| {
891            let mut sets = vec![
892                (RS1, 1, 1500, vec!["a", "b"]),
893                (RS2, 1501, 3000, vec!["c", "d"]),
894            ];
895            if reversed {
896                sets.reverse();
897            }
898            let mut tier = Tier {
899                name: "default".to_string(),
900                bucket_count: 3000,
901                replicasets: Vec::new(),
902                bucket_map: Vec::new(),
903            };
904            for (idx, (uuid, start, end, mut names)) in sets.into_iter().enumerate() {
905                if reversed {
906                    names.reverse();
907                }
908                tier.replicasets.push(Replicaset {
909                    uuid,
910                    master: inst(&format!("{uuid}-m"), uuid, true),
911                    replicas: names.into_iter().map(|n| inst(n, uuid, false)).collect(),
912                });
913                tier.bucket_map.push(BucketSlot {
914                    start,
915                    end,
916                    replicaset_idx: idx,
917                });
918            }
919            tier.bucket_map.sort_unstable_by_key(|s| s.start);
920            let mut topo = Topology::new();
921            topo.pgproto_tiers = vec![tier];
922            topo.snapshot()
923        };
924
925        assert_eq!(build(false), build(true));
926    }
927}