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}