Skip to main content

uqa_storage/mvcc/serializable/
identity.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Serializable participation is independent of physical publication and its durable receipts.
8
9use super::{DatabaseId, VersionError, VersionResult};
10
11/// One logical participant in a database and coordinator incarnation. Read-only participants require no physical transaction allocation. This identity must never be passed to a provider's commit-status/abort API; a publishing participant retains its separately allocated `StorageTransactionId` when preparing a durable commit.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct SerializableTransactionId {
14    database: DatabaseId,
15    coordinator: [u8; 16],
16    allocation: u64,
17}
18
19impl SerializableTransactionId {
20    /// Decode a retained participant reference. Admission allocates these identities through `SerializableGraph::admit`; constructing a reference does not admit a participant.
21    pub fn new(
22        database: DatabaseId,
23        coordinator: [u8; 16],
24        allocation: u64,
25    ) -> VersionResult<Self> {
26        validate_coordinator(coordinator)?;
27        if allocation == 0 {
28            return Err(VersionError::InvalidTransactionId);
29        }
30        Ok(Self {
31            database,
32            coordinator,
33            allocation,
34        })
35    }
36
37    pub const fn database(self) -> DatabaseId {
38        self.database
39    }
40
41    pub const fn coordinator(self) -> [u8; 16] {
42        self.coordinator
43    }
44
45    pub const fn allocation(self) -> u64 {
46        self.allocation
47    }
48}
49
50pub(super) fn validate_coordinator(coordinator: [u8; 16]) -> VersionResult<()> {
51    if coordinator == [0; 16] {
52        return Err(VersionError::InvalidEncoding(
53            "serializable coordinator identity must be nonzero",
54        ));
55    }
56    Ok(())
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use crate::{mvcc::SerializableGraph, read_control::StorageReadControl};
63
64    const DATABASE: DatabaseId = DatabaseId::from_bytes([17; 16]);
65
66    #[test]
67    fn read_only_participants_allocate_without_physical_transactions_and_never_reuse_ids() {
68        let control = StorageReadControl::with_limit(64 * 1024);
69        let mut graph = SerializableGraph::new(DATABASE, [1; 16], control.memory()).unwrap();
70        let first = graph.admit(true, &control).unwrap();
71        assert_eq!(first.allocation(), 1);
72        graph.prepare_commit(first, &control).unwrap();
73        graph.commit(first).unwrap();
74        graph.reclaim();
75        assert_eq!(control.memory().used(), 0);
76        let second = graph.admit(true, &control).unwrap();
77        assert_eq!(second.allocation(), 2);
78        assert!(matches!(
79            graph.check_active(first),
80            Err(VersionError::UnknownTransaction)
81        ));
82        graph.check_active(second).unwrap();
83    }
84
85    #[test]
86    fn a_new_coordinator_rejects_old_participants_even_when_allocations_match() {
87        let control = StorageReadControl::with_limit(64 * 1024);
88        let mut previous = SerializableGraph::new(DATABASE, [1; 16], control.memory()).unwrap();
89        let old = previous.admit(false, &control).unwrap();
90        let old_mark = previous.write_mark(old).unwrap();
91        let mut replacement = SerializableGraph::new(DATABASE, [2; 16], control.memory()).unwrap();
92        let current = replacement.admit(false, &control).unwrap();
93        assert_eq!(old.allocation(), current.allocation());
94        assert_ne!(old, current);
95        assert!(matches!(
96            replacement.check_active(old),
97            Err(VersionError::WrongSerializableCoordinator)
98        ));
99        assert!(matches!(
100            replacement.rollback(old),
101            Err(VersionError::WrongSerializableCoordinator)
102        ));
103        assert!(matches!(
104            replacement.rollback_writes(old_mark),
105            Err(VersionError::WrongSerializableCoordinator)
106        ));
107        assert!(matches!(
108            replacement.observe_rw(current, current, old, &control),
109            Err(VersionError::WrongSerializableCoordinator)
110        ));
111        replacement.check_active(current).unwrap();
112    }
113
114    #[test]
115    fn invalid_identity_or_failed_admission_cannot_consume_a_participant_number() {
116        let control = StorageReadControl::with_limit(64 * 1024);
117        assert!(matches!(
118            SerializableGraph::new(DATABASE, [0; 16], control.memory()),
119            Err(VersionError::InvalidEncoding(_))
120        ));
121        assert!(matches!(
122            SerializableTransactionId::new(DATABASE, [0; 16], 1),
123            Err(VersionError::InvalidEncoding(_))
124        ));
125        assert!(matches!(
126            SerializableTransactionId::new(DATABASE, [1; 16], 0),
127            Err(VersionError::InvalidTransactionId)
128        ));
129        let mut graph = SerializableGraph::new(DATABASE, [1; 16], control.memory()).unwrap();
130        let occupied = control.memory().reserve(control.memory().limit()).unwrap();
131        assert!(matches!(
132            graph.admit(true, &control),
133            Err(VersionError::Memory(_))
134        ));
135        drop(occupied);
136        assert_eq!(graph.admit(true, &control).unwrap().allocation(), 1);
137    }
138
139    #[test]
140    fn exhausted_participant_numbers_do_not_prevent_prepared_completion() {
141        let control = StorageReadControl::with_limit(64 * 1024);
142        let mut graph = SerializableGraph::new(DATABASE, [1; 16], control.memory()).unwrap();
143        graph.last_allocation = u64::MAX - 1;
144        let last = graph.admit(false, &control).unwrap();
145        graph.prepare_commit(last, &control).unwrap();
146        assert!(matches!(
147            graph.admit(false, &control),
148            Err(VersionError::SequenceExhausted)
149        ));
150        graph.commit(last).unwrap();
151        graph.reclaim();
152        assert!(matches!(
153            graph.admit(false, &control),
154            Err(VersionError::SequenceExhausted)
155        ));
156        assert_eq!(control.memory().used(), 0);
157    }
158}