Skip to main content

uqa_storage/mvcc/serializable/
observations.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Bounded logical observations, atomic dependency registration and savepoint write undo.
8
9#[cfg(test)]
10mod tests;
11
12use uqa_core::memory::{BudgetedVec, MemoryBudget};
13
14use super::{
15    conflicts::DependencyAction, predicates::OwnedPredicate, SerializableGraph,
16    SerializablePredicate, SerializableTransactionId, Transaction, VersionError, VersionResult,
17};
18use crate::read_control::StorageReadControl;
19
20/// A transaction-owned position in its successful write observations. Undo removes subsequent write intents while retaining every read observation and previously established dependency.
21#[derive(Debug, Clone, Copy)]
22pub struct SerializableWriteMark {
23    transaction: SerializableTransactionId,
24    writes: u64,
25}
26
27pub(super) struct Observation {
28    pub(super) owner: u64,
29    pub(super) write: u64,
30    pub(super) predicate: OwnedPredicate,
31    pub(super) fingerprint: [u8; 32],
32}
33
34pub(super) struct Observations {
35    pub(super) reads: BudgetedVec<Observation>,
36    pub(super) writes: BudgetedVec<Observation>,
37}
38
39impl Observations {
40    pub(super) fn new(memory: &MemoryBudget) -> Self {
41        Self {
42            reads: BudgetedVec::new(memory),
43            writes: BudgetedVec::new(memory),
44        }
45    }
46
47    pub(super) fn clear(&mut self) -> bool {
48        let changed = !self.reads.is_empty() || !self.writes.is_empty();
49        self.reads = BudgetedVec::new(self.reads.budget());
50        self.writes = BudgetedVec::new(self.writes.budget());
51        changed
52    }
53
54    pub(super) fn forget(&mut self, owner: u64) -> bool {
55        let reads = retain(&mut self.reads, |entry| entry.owner != owner);
56        let writes = retain(&mut self.writes, |entry| entry.owner != owner);
57        reads || writes
58    }
59
60    pub(super) fn reclaim(&mut self, transactions: &[Transaction], oldest: u64) -> bool {
61        let retained = |entry: &Observation| {
62            transactions
63                .binary_search_by_key(&entry.owner, |transaction| transaction.id)
64                .is_ok_and(|position| transactions[position].retains_history(oldest))
65        };
66        let reads = retain(&mut self.reads, retained);
67        let writes = retain(&mut self.writes, retained);
68        reads || writes
69    }
70
71    fn selected(&self, writing: bool) -> &BudgetedVec<Observation> {
72        if writing {
73            &self.writes
74        } else {
75            &self.reads
76        }
77    }
78
79    fn selected_mut(&mut self, writing: bool) -> &mut BudgetedVec<Observation> {
80        if writing {
81            &mut self.writes
82        } else {
83            &mut self.reads
84        }
85    }
86}
87
88impl SerializableGraph {
89    /// Register before a logical read can race with a write. This includes empty point/range results, index-only and cached reads. Check existing write intents as well as committed writers invisible to the reader's fixed snapshot; later writers inspect the retained read. The caller coordinates this boundary with snapshot capture and publication.
90    pub fn observe_read(
91        &mut self,
92        transaction: SerializableTransactionId,
93        predicate: SerializablePredicate<'_>,
94        control: &StorageReadControl,
95    ) -> VersionResult<()> {
96        self.observe_predicate(transaction, predicate, false, control)
97    }
98
99    /// Register before staging a logical write. Supply the changed row and every affected old/new index key separately, or the entire object for an object-wide change. Physical posting clusters and counters are not logical keys. This checks existing readers and retains an intent for later reads. A failed observation publishes neither its intent nor any dependencies or peer victims.
100    pub fn observe_write(
101        &mut self,
102        transaction: SerializableTransactionId,
103        predicate: SerializablePredicate<'_>,
104        control: &StorageReadControl,
105    ) -> VersionResult<()> {
106        self.observe_predicate(transaction, predicate, true, control)
107    }
108
109    pub fn write_mark(
110        &self,
111        transaction: SerializableTransactionId,
112    ) -> VersionResult<SerializableWriteMark> {
113        self.check_active(transaction)?;
114        Ok(SerializableWriteMark {
115            transaction,
116            writes: self.transactions[self.position(transaction)?].writes,
117        })
118    }
119
120    /// Cancel successful write intents after a statement/savepoint mark. This allocation-free cleanup remains usable for cancelled or peer-doomed transactions, but cannot erase their reads or serialization failure. Prepared or finished transactions cannot undo their publication inputs.
121    pub fn rollback_writes(&mut self, mark: SerializableWriteMark) -> VersionResult<()> {
122        let entry = self.transactions[self.position(mark.transaction)?];
123        if !entry.live() {
124            return Err(VersionError::TransactionFinished);
125        }
126        if entry.prepared.is_some() {
127            return Err(VersionError::TransactionSealed);
128        }
129        if mark.writes > entry.writes {
130            return Err(VersionError::InvalidEncoding(
131                "invalid serializable write checkpoint",
132            ));
133        }
134        self.checkpoint_changed |= retain(&mut self.predicates.writes, |observed| {
135            observed.owner != entry.id || observed.write <= mark.writes
136        });
137        Ok(())
138    }
139
140    fn observe_predicate(
141        &mut self,
142        transaction: SerializableTransactionId,
143        predicate: SerializablePredicate<'_>,
144        writing: bool,
145        control: &StorageReadControl,
146    ) -> VersionResult<()> {
147        control.check()?;
148        self.check_active(transaction)?;
149        predicate.validate(writing)?;
150        let position = self.position(transaction)?;
151        let entry = self.transactions[position];
152        if writing && entry.read_only {
153            return Err(VersionError::InvalidEncoding(
154                "read-only serializable transaction cannot observe writes",
155            ));
156        }
157        if predicate.is_empty() {
158            return Ok(());
159        }
160        for observed in object_entries(self.predicates.selected(writing), predicate.object) {
161            control.check()?;
162            if observed.owner == entry.id && observed.predicate.borrowed() == predicate {
163                return Ok(());
164            }
165        }
166        let write = if writing {
167            entry
168                .writes
169                .checked_add(1)
170                .ok_or(VersionError::SequenceExhausted)?
171        } else {
172            0
173        };
174        let owned = OwnedPredicate::new(predicate, self.transactions.budget())?;
175        let mut actions = BudgetedVec::<DependencyAction>::new(self.transactions.budget());
176        for observed in object_entries(self.predicates.selected(!writing), predicate.object) {
177            control.check()?;
178            if observed.owner == entry.id {
179                continue;
180            }
181            let other = observed.predicate.borrowed();
182            let matches = if writing {
183                other.overlaps_write(predicate)
184            } else {
185                predicate.overlaps_write(other)
186            };
187            if !matches {
188                continue;
189            }
190            let peer =
191                SerializableTransactionId::new(self.database, self.coordinator, observed.owner)?;
192            let (reader, writer) = if writing {
193                (peer, transaction)
194            } else {
195                (transaction, peer)
196            };
197            if let Some(action) = self.plan_dependency(transaction, reader, writer, control)? {
198                if let Err(index) =
199                    actions.binary_search_by_key(&action.edge, |planned| planned.edge)
200                {
201                    actions.push(action)?;
202                    actions[index..].rotate_right(1);
203                }
204            }
205        }
206        let mut observed = Observation {
207            owner: entry.id,
208            write,
209            predicate: owned,
210            fingerprint: [0; 32],
211        };
212        observed.fingerprint = super::checkpoint::records::SerializableCheckpointRecord::predicate(
213            self, &observed, writing,
214        )
215        .fingerprint(control)?;
216        let index = self
217            .predicates
218            .selected(writing)
219            .binary_search_by_key(&(predicate.object, observed.fingerprint), |entry| {
220                (entry.predicate.object, entry.fingerprint)
221            })
222            .err()
223            .ok_or(VersionError::InvalidEncoding(
224                "serializable predicate fingerprint collision",
225            ))?;
226        // Every action has the same reader or writer; new edges cannot change another action's incoming/outgoing checks. Validate and reserve the complete observation before publishing any action.
227        self.predicates.selected_mut(writing).reserve(1)?;
228        self.reserve_dependencies(
229            actions
230                .iter()
231                .filter(|action| action.victim.is_none())
232                .count(),
233        )?;
234        control.check()?;
235        let observations = self.predicates.selected_mut(writing);
236        observations.push(observed)?;
237        self.checkpoint_changed = true;
238        observations[index..].rotate_right(1);
239        for &action in &*actions {
240            self.publish_dependency(action)?;
241        }
242        if writing {
243            self.transactions[position].writes = write;
244        }
245        Ok(())
246    }
247}
248
249fn object_entries(observations: &[Observation], object: [u8; 16]) -> &[Observation] {
250    let start = observations.partition_point(|entry| entry.predicate.object < object);
251    let end = observations.partition_point(|entry| entry.predicate.object <= object);
252    &observations[start..end]
253}
254
255fn retain(
256    values: &mut BudgetedVec<Observation>,
257    mut predicate: impl FnMut(&Observation) -> bool,
258) -> bool {
259    let previous = values.len();
260    let mut kept = 0;
261    for index in 0..values.len() {
262        if predicate(&values[index]) {
263            values.swap(kept, index);
264            kept += 1;
265        }
266    }
267    values.truncate(kept);
268    if values.is_empty() {
269        *values = BudgetedVec::new(values.budget());
270    }
271    kept != previous
272}