Skip to main content

uqa_storage/mvcc/serializable/
checkpoint.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Versioned streaming checkpoints preserve the common SSI state across physical coordination owners.
8
9mod io;
10mod nodes;
11mod predicates;
12pub(super) mod records;
13#[cfg(test)]
14mod tests;
15
16use std::io::{Read, Write};
17
18use super::{DatabaseId, Edge, SerializableGraph, Transaction, VersionError, VersionResult};
19use crate::read_control::StorageReadControl;
20use io::{invalid, Decoder, Encoder};
21
22const MAGIC: &[u8; 8] = b"UQASER03";
23
24impl SerializableGraph {
25    /// Whether retained state needs a durable checkpoint. A new coordinator does; a fully restored checkpoint does not until an operation changes its contents, including changes preceding an error. Encoding alone cannot acknowledge provider durability, so it never clears this flag. Check only while holding the same exclusive admission that loaded this graph.
26    pub fn checkpoint_changed(&self) -> bool {
27        self.checkpoint_changed
28    }
29
30    /// Stream one complete coordinator checkpoint, including predicates and prepared physical receipt bindings. The provider owns atomic replacement, encryption and shared admission. A checksum detects incomplete/corrupt state; it does not replace the provider's authentication or durability. No complete encoded-state buffer is allocated.
31    pub fn write_checkpoint(
32        &self,
33        output: &mut dyn Write,
34        control: &StorageReadControl,
35    ) -> VersionResult<()> {
36        let mut encoder = Encoder::new(output, control);
37        encoder.bytes(MAGIC)?;
38        encoder.bytes(&self.database.as_bytes())?;
39        encoder.bytes(&self.coordinator)?;
40        for value in [self.last_allocation, self.clock, self.pending_finishes] {
41            encoder.number(value)?;
42        }
43        for count in [
44            self.transactions.len(),
45            self.outgoing.len(),
46            self.predicates.reads.len(),
47            self.predicates.writes.len(),
48        ] {
49            encoder.count(count)?;
50        }
51        for &entry in &*self.transactions {
52            nodes::write(&mut encoder, entry)?;
53        }
54        for &Edge(reader, writer) in &*self.outgoing {
55            encoder.number(reader)?;
56            encoder.number(writer)?;
57        }
58        for entries in [&self.predicates.reads, &self.predicates.writes] {
59            for entry in &**entries {
60                predicates::write(&mut encoder, entry)?;
61            }
62        }
63        encoder.finish()
64    }
65
66    /// Compute the exact serialized length for bounded physical BLOB allocation without creating an encoded-state copy.
67    pub fn checkpoint_length(&self, control: &StorageReadControl) -> VersionResult<u64> {
68        let mut count = CountBytes(0);
69        self.write_checkpoint(&mut count, control)?;
70        Ok(count.0)
71    }
72
73    /// Restore a complete checkpoint for the expected database and coordinator incarnation. Decoded participants, both edge orientations and owned predicate keys share the supplied allowance. On any format, checksum, I/O, cancellation or resource failure, no partially restored graph is returned.
74    pub fn read_checkpoint(
75        database: DatabaseId,
76        coordinator: [u8; 16],
77        input: &mut dyn Read,
78        control: &StorageReadControl,
79    ) -> VersionResult<Self> {
80        let mut decoder = Decoder::new(input, control);
81        let magic = decoder.array::<8>()?;
82        if magic != *MAGIC && magic != *b"UQASER02" && magic != *b"UQASER01" {
83            return Err(invalid());
84        }
85        if decoder.array::<16>()? != database.as_bytes() {
86            return Err(VersionError::WrongDatabase);
87        }
88        if decoder.array::<16>()? != coordinator {
89            return Err(VersionError::WrongSerializableCoordinator);
90        }
91        let mut graph = Self::new(database, coordinator, control.memory())?;
92        graph.last_allocation = decoder.number()?;
93        graph.clock = decoder.number()?;
94        graph.pending_finishes = decoder.number()?;
95        if graph.clock.checked_add(graph.pending_finishes).is_none()
96            || (graph.clock == 0 && graph.last_allocation != 0)
97        {
98            return Err(invalid());
99        }
100        let transaction_count = decoder.count()?;
101        let edge_count = decoder.count()?;
102        let read_count = decoder.count()?;
103        let write_count = decoder.count()?;
104        nodes::restore(
105            &mut graph,
106            transaction_count,
107            magic != *b"UQASER01",
108            magic == *MAGIC,
109            &mut decoder,
110        )?;
111        graph.outgoing.reserve(edge_count)?;
112        graph.incoming.reserve(edge_count)?;
113        for _ in 0..edge_count {
114            let edge = Edge(decoder.number()?, decoder.number()?);
115            if edge.0 == edge.1
116                || graph
117                    .outgoing
118                    .last()
119                    .is_some_and(|previous| *previous >= edge)
120            {
121                return Err(invalid());
122            }
123            lookup(&graph, edge.0)?;
124            if lookup(&graph, edge.1)?.read_only {
125                return Err(invalid());
126            }
127            graph.outgoing.push(edge)?;
128            graph.incoming.push(Edge(edge.1, edge.0))?;
129        }
130        graph.incoming.sort_unstable();
131        for (writing, count) in [(false, read_count), (true, write_count)] {
132            let entries = if writing {
133                &mut graph.predicates.writes
134            } else {
135                &mut graph.predicates.reads
136            };
137            entries.reserve(count)?;
138            for _ in 0..count {
139                let mut observed = predicates::read(&mut decoder, writing)?;
140                observed.fingerprint =
141                    records::SerializableCheckpointRecord::predicate(&graph, &observed, writing)
142                        .fingerprint(control)?;
143                let owner = lookup(&graph, observed.owner)?;
144                if owner.aborted
145                    || (writing
146                        && (owner.read_only
147                            || observed.write == 0
148                            || observed.write > owner.writes))
149                    || (!writing && observed.write != 0)
150                {
151                    return Err(invalid());
152                }
153                let entries = if writing {
154                    &mut graph.predicates.writes
155                } else {
156                    &mut graph.predicates.reads
157                };
158                if entries
159                    .last()
160                    .is_some_and(|previous| previous.predicate.object > observed.predicate.object)
161                {
162                    return Err(invalid());
163                }
164                entries.push(observed)?;
165            }
166        }
167        decoder.finish()?;
168        for entries in [&mut graph.predicates.reads, &mut graph.predicates.writes] {
169            entries.sort_unstable_by_key(|entry| (entry.predicate.object, entry.fingerprint));
170        }
171        control.check()?;
172        graph.checkpoint_changed = false;
173        Ok(graph)
174    }
175}
176
177fn lookup(graph: &SerializableGraph, allocation: u64) -> VersionResult<&Transaction> {
178    let position = graph
179        .transactions
180        .binary_search_by_key(&allocation, |entry| entry.id)
181        .map_err(|_| invalid())?;
182    Ok(&graph.transactions[position])
183}
184
185struct CountBytes(u64);
186
187impl Write for CountBytes {
188    fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
189        self.0 = self
190            .0
191            .checked_add(
192                u64::try_from(bytes.len())
193                    .map_err(|_| std::io::Error::other("checkpoint length overflow"))?,
194            )
195            .ok_or_else(|| std::io::Error::other("checkpoint length overflow"))?;
196        Ok(bytes.len())
197    }
198
199    fn flush(&mut self) -> std::io::Result<()> {
200        Ok(())
201    }
202}