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}