Skip to main content

nodedb_graph/csr/
local_node_id.rs

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