Skip to main content

nodedb_graph/csr/
local_node_id.rs

1//! Partition-tagged node identifier for cross-partition safety.
2//!
3//! Each `CsrIndex` is assigned a unique `partition_tag` at construction
4//! from a process-global atomic counter. A `LocalNodeId` carries both
5//! a dense node index and the tag of the partition that produced it;
6//! using one from partition A with a method on partition B panics.
7
8use std::sync::atomic::{AtomicU32, Ordering};
9
10static PARTITION_COUNTER: AtomicU32 = AtomicU32::new(1);
11
12/// Allocate the next unique partition tag. Called once per `CsrIndex`
13/// construction.
14pub(crate) fn next_partition_tag() -> u32 {
15    PARTITION_COUNTER.fetch_add(1, Ordering::Relaxed)
16}
17
18/// A dense node index bound to the partition that produced it.
19///
20/// Constructed only by `CsrIndex` / `CsrSnapshot` read APIs. The
21/// partition tag is checked at every consuming API; passing an ID
22/// from a different partition panics.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct LocalNodeId {
25    raw: u32,
26    partition: u32,
27}
28
29impl LocalNodeId {
30    /// Construct a tagged node id.
31    ///
32    /// Callers outside `nodedb-graph` that need to mint a `LocalNodeId`
33    /// (e.g. `CsrSnapshot`) must pass the partition tag they inherited
34    /// from the source `CsrIndex`. Using a tag from one partition with
35    /// the API of another will panic on the first `.raw(expected)` call.
36    #[inline]
37    pub fn new(raw: u32, partition: u32) -> Self {
38        Self { raw, partition }
39    }
40
41    /// Partition tag this id belongs to.
42    #[inline]
43    pub fn partition(self) -> u32 {
44        self.partition
45    }
46
47    /// Unwrap to the raw dense index, asserting the id was produced by
48    /// the expected partition. Panics on tag mismatch — this catches
49    /// cross-partition id leakage at the call site.
50    #[inline]
51    #[track_caller]
52    pub fn raw(self, expected_partition: u32) -> u32 {
53        assert_eq!(
54            self.partition, expected_partition,
55            "LocalNodeId from partition {} used on partition {}",
56            self.partition, expected_partition
57        );
58        self.raw
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn partition_counter_is_monotonic() {
68        let a = next_partition_tag();
69        let b = next_partition_tag();
70        assert!(b > a);
71    }
72
73    #[test]
74    fn raw_with_matching_partition_returns_id() {
75        let id = LocalNodeId::new(42, 7);
76        assert_eq!(id.raw(7), 42);
77    }
78
79    #[test]
80    #[should_panic(expected = "partition 7 used on partition 9")]
81    fn raw_with_wrong_partition_panics() {
82        let id = LocalNodeId::new(42, 7);
83        let _ = id.raw(9);
84    }
85
86    #[test]
87    fn tagged_ids_are_copy_and_eq() {
88        let id = LocalNodeId::new(1, 1);
89        let copy = id;
90        assert_eq!(id, copy);
91    }
92}