Skip to main content

uqa_storage/mvcc/serializable/
liveness.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Participant lifetime and orphan recovery never infer an uncertain durable outcome from process death.
8
9use std::sync::{Arc, Weak};
10
11use parking_lot::Mutex;
12use uqa_core::memory::{BudgetedVec, MemoryBudget, MemoryReservation};
13
14use super::{
15    DatabaseId, ParticipantOwner, SerializableGraph, SerializableTransactionId, VersionError,
16    VersionResult,
17};
18use crate::{
19    mvcc::{CommitStatus, StorageTransactionId},
20    read_control::StorageReadControl,
21};
22
23struct Participant<T> {
24    id: SerializableTransactionId,
25    _retained: T,
26    _memory: MemoryReservation,
27}
28
29trait ParticipantLease: Send + Sync {
30    fn id(&self) -> SerializableTransactionId;
31}
32
33impl<T: Send + Sync> ParticipantLease for Participant<T> {
34    fn id(&self) -> SerializableTransactionId {
35        self.id
36    }
37}
38
39/// All clones retain the original participant's liveness through nested execution and pinned data views. The provider exposes this handle only after admission is retained. Lease release never acquires SSI admission or resolves a logical transaction. A final retained physical owner may subsequently perform its provider's ordinary close.
40#[derive(Clone)]
41pub struct SerializableParticipant(Arc<dyn ParticipantLease>);
42
43impl SerializableParticipant {
44    pub fn retain(
45        id: SerializableTransactionId,
46        lease: Box<dyn Send + Sync>,
47        control: &StorageReadControl,
48    ) -> VersionResult<Self> {
49        Self::retain_value(id, lease, control)
50    }
51
52    fn retain_value<T: Send + Sync + 'static>(
53        id: SerializableTransactionId,
54        lease: T,
55        control: &StorageReadControl,
56    ) -> VersionResult<Self> {
57        let memory = control
58            .memory()
59            .reserve(std::mem::size_of::<Participant<T>>() + 2 * std::mem::size_of::<usize>())?;
60        Ok(Self(Arc::new(Participant {
61            id,
62            _retained: lease,
63            _memory: memory,
64        })))
65    }
66
67    pub fn id(&self) -> SerializableTransactionId {
68        self.0.id()
69    }
70}
71
72struct LocalEntry {
73    id: SerializableTransactionId,
74    lease: Weak<dyn ParticipantLease>,
75}
76
77struct LocalLeaseOwner<T> {
78    registry: Arc<LocalSerializableLeases>,
79    _retained: T,
80}
81
82impl<T> Drop for LocalLeaseOwner<T> {
83    fn drop(&mut self) {
84        // This private list lock never enters SSI/provider admission or closes physical owners. Participant construction occurs outside it, so failed publication can release its lease here too.
85        self.registry.reclaim();
86    }
87}
88
89/// Liveness for an exclusively owned database or one browser/in-memory owner. File-backed multiprocess providers must use native leases instead. The entries and their capacity share the owner's allowance; live handles retain the registry through their lease payload.
90pub struct LocalSerializableLeases(Mutex<BudgetedVec<LocalEntry>>);
91
92impl LocalSerializableLeases {
93    pub fn new(memory: &MemoryBudget) -> Self {
94        Self(Mutex::new(BudgetedVec::new(memory)))
95    }
96
97    pub fn retain(
98        self: &Arc<Self>,
99        id: SerializableTransactionId,
100        control: &StorageReadControl,
101    ) -> VersionResult<SerializableParticipant> {
102        self.retain_with(id, (), control)
103    }
104
105    /// Retain an additional physical/coordinator owner with the original participant, charging the complete lease payload before allocation. Destruction must not reenter SSI admission or resolve a logical transaction. Admission operations must retain the physical owner separately so its final close cannot run while their gate is held.
106    pub fn retain_with<T: Send + Sync + 'static>(
107        self: &Arc<Self>,
108        id: SerializableTransactionId,
109        retained: T,
110        control: &StorageReadControl,
111    ) -> VersionResult<SerializableParticipant> {
112        control.check()?;
113        let participant = SerializableParticipant::retain_value(
114            id,
115            LocalLeaseOwner {
116                registry: Arc::clone(self),
117                _retained: retained,
118            },
119            control,
120        )?;
121        let mut entries = self.0.lock();
122        prune(&mut entries);
123        if entries.is_empty() {
124            *entries = BudgetedVec::new(control.memory());
125        }
126        let position = entries
127            .binary_search_by_key(&key(id), |entry| key(entry.id))
128            .err()
129            .ok_or(VersionError::InvalidEncoding(
130                "duplicate serializable participant lease",
131            ))?;
132        entries.reserve(1)?;
133        entries.push(LocalEntry {
134            id,
135            lease: Arc::downgrade(&participant.0),
136        })?;
137        entries[position..].rotate_right(1);
138        Ok(participant)
139    }
140
141    pub fn is_alive(&self, id: SerializableTransactionId) -> bool {
142        let entries = self.0.lock();
143        entries
144            .binary_search_by_key(&key(id), |entry| key(entry.id))
145            .is_ok_and(|position| entries[position].lease.strong_count() != 0)
146    }
147
148    pub fn reclaim(&self) {
149        prune(&mut self.0.lock());
150    }
151}
152
153fn prune(entries: &mut BudgetedVec<LocalEntry>) {
154    let mut kept = 0;
155    for index in 0..entries.len() {
156        if entries[index].lease.strong_count() != 0 {
157            entries.swap(kept, index);
158            kept += 1;
159        }
160    }
161    entries.truncate(kept);
162    if entries.is_empty() {
163        *entries = BudgetedVec::new(entries.budget());
164    }
165}
166
167fn key(id: SerializableTransactionId) -> ([u8; 16], [u8; 16], u64) {
168    (id.database().as_bytes(), id.coordinator(), id.allocation())
169}
170
171impl SerializableGraph {
172    pub fn database(&self) -> DatabaseId {
173        self.database
174    }
175    pub fn coordinator(&self) -> [u8; 16] {
176        self.coordinator
177    }
178
179    /// Bind admission to a retained provider lease before publishing the actor. Failed lease allocation aborts only the new actor and still consumes its allocation when the owner retains this state. Legacy manually owned actors have no lease binding and are never inferred dead by recovery.
180    pub fn admit_with_lease(
181        &mut self,
182        read_only: bool,
183        control: &StorageReadControl,
184        retain: impl FnOnce(SerializableTransactionId) -> VersionResult<SerializableParticipant>,
185    ) -> VersionResult<SerializableParticipant> {
186        let id = self.admit(read_only, control)?;
187        let retained = retain(id).and_then(|participant| {
188            if participant.id() != id {
189                return Err(VersionError::InvalidEncoding(
190                    "serializable lease identity mismatch",
191                ));
192            }
193            Ok(participant)
194        });
195        match retained {
196            Ok(participant) => {
197                let position = self.position(id)?;
198                self.transactions[position].owner = ParticipantOwner::Leased;
199                self.checkpoint_changed = true;
200                Ok(participant)
201            }
202            Err(error) => {
203                self.rollback(id)?;
204                Err(error)
205            }
206        }
207    }
208
209    /// Retire only participants whose liveness owner is authoritatively gone. Completed participants release their outcome-retention lease without changing their confirmed state. A live unbound participant can abort immediately. A prepared physical publication must be resolved or aborted through its exact durable receipt identity; a missing receipt stops recovery and never proves rollback. Earlier confirmed resolutions survive a later error. The caller retains the resulting state before releasing shared admission, including on failure.
210    pub fn recover_abandoned(
211        &mut self,
212        control: &StorageReadControl,
213        mut is_alive: impl FnMut(SerializableTransactionId) -> VersionResult<bool>,
214        mut finish: impl FnMut(StorageTransactionId) -> VersionResult<CommitStatus>,
215    ) -> VersionResult<()> {
216        for index in 0..self.transactions.len() {
217            control.check()?;
218            let entry = self.transactions[index];
219            if entry.owner != ParticipantOwner::Leased {
220                continue;
221            }
222            let participant =
223                SerializableTransactionId::new(self.database, self.coordinator, entry.id)?;
224            if is_alive(participant)? {
225                continue;
226            }
227            if !entry.live() {
228                self.transactions[index].owner = ParticipantOwner::Manual;
229                self.checkpoint_changed = true;
230                continue;
231            }
232            let Some(publication) = self.publication(participant)? else {
233                self.rollback(participant)?;
234                self.transactions[index].owner = ParticipantOwner::Manual;
235                self.checkpoint_changed = true;
236                continue;
237            };
238            let status = match finish(publication.transaction()) {
239                Ok(status) => status,
240                Err(VersionError::AlreadyCommitted(receipt)) => CommitStatus::Committed(receipt),
241                Err(VersionError::AlreadyAborted(transaction))
242                    if transaction == publication.transaction() =>
243                {
244                    CommitStatus::Aborted
245                }
246                Err(error) => return Err(error),
247            };
248            match self.resolve_publication(publication, status)? {
249                CommitStatus::Unknown => return Err(VersionError::UnknownTransaction),
250                CommitStatus::Pending | CommitStatus::Committed(_) | CommitStatus::Aborted => {}
251            }
252            if !self.transactions[index].live() {
253                self.transactions[index].owner = ParticipantOwner::Manual;
254                self.checkpoint_changed = true;
255            }
256        }
257        Ok(())
258    }
259}
260
261#[cfg(test)]
262mod tests;