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(super) fn shared(&self, id: SerializableTransactionId) -> Option<SerializableParticipant> {
94        let entries = self.0.lock();
95        let position = entries
96            .binary_search_by_key(&key(id), |entry| key(entry.id))
97            .ok()?;
98        entries[position]
99            .lease
100            .upgrade()
101            .map(SerializableParticipant)
102    }
103
104    pub fn new(memory: &MemoryBudget) -> Self {
105        Self(Mutex::new(BudgetedVec::new(memory)))
106    }
107
108    pub fn retain(
109        self: &Arc<Self>,
110        id: SerializableTransactionId,
111        control: &StorageReadControl,
112    ) -> VersionResult<SerializableParticipant> {
113        self.retain_with(id, (), control)
114    }
115
116    /// 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.
117    pub fn retain_with<T: Send + Sync + 'static>(
118        self: &Arc<Self>,
119        id: SerializableTransactionId,
120        retained: T,
121        control: &StorageReadControl,
122    ) -> VersionResult<SerializableParticipant> {
123        control.check()?;
124        let participant = SerializableParticipant::retain_value(
125            id,
126            LocalLeaseOwner {
127                registry: Arc::clone(self),
128                _retained: retained,
129            },
130            control,
131        )?;
132        let mut entries = self.0.lock();
133        prune(&mut entries);
134        if entries.is_empty() {
135            *entries = BudgetedVec::new(control.memory());
136        }
137        let position = entries
138            .binary_search_by_key(&key(id), |entry| key(entry.id))
139            .err()
140            .ok_or(VersionError::InvalidEncoding(
141                "duplicate serializable participant lease",
142            ))?;
143        entries.reserve(1)?;
144        entries.push(LocalEntry {
145            id,
146            lease: Arc::downgrade(&participant.0),
147        })?;
148        entries[position..].rotate_right(1);
149        Ok(participant)
150    }
151
152    pub fn is_alive(&self, id: SerializableTransactionId) -> bool {
153        let entries = self.0.lock();
154        entries
155            .binary_search_by_key(&key(id), |entry| key(entry.id))
156            .is_ok_and(|position| entries[position].lease.strong_count() != 0)
157    }
158
159    pub fn reclaim(&self) {
160        prune(&mut self.0.lock());
161    }
162}
163
164fn prune(entries: &mut BudgetedVec<LocalEntry>) {
165    let mut kept = 0;
166    for index in 0..entries.len() {
167        if entries[index].lease.strong_count() != 0 {
168            entries.swap(kept, index);
169            kept += 1;
170        }
171    }
172    entries.truncate(kept);
173    if entries.is_empty() {
174        *entries = BudgetedVec::new(entries.budget());
175    }
176}
177
178fn key(id: SerializableTransactionId) -> ([u8; 16], [u8; 16], u64) {
179    (id.database().as_bytes(), id.coordinator(), id.allocation())
180}
181
182impl SerializableGraph {
183    pub fn database(&self) -> DatabaseId {
184        self.database
185    }
186    pub fn coordinator(&self) -> [u8; 16] {
187        self.coordinator
188    }
189
190    /// 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.
191    pub fn admit_with_lease(
192        &mut self,
193        read_only: bool,
194        control: &StorageReadControl,
195        retain: impl FnOnce(SerializableTransactionId) -> VersionResult<SerializableParticipant>,
196    ) -> VersionResult<SerializableParticipant> {
197        let id = self.admit(read_only, control)?;
198        let retained = retain(id).and_then(|participant| {
199            if participant.id() != id {
200                return Err(VersionError::InvalidEncoding(
201                    "serializable lease identity mismatch",
202                ));
203            }
204            Ok(participant)
205        });
206        match retained {
207            Ok(participant) => {
208                let position = self.position(id)?;
209                self.transactions[position].owner = ParticipantOwner::Leased;
210                self.checkpoint_changed = true;
211                Ok(participant)
212            }
213            Err(error) => {
214                self.rollback(id)?;
215                Err(error)
216            }
217        }
218    }
219
220    /// 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.
221    pub fn recover_abandoned(
222        &mut self,
223        control: &StorageReadControl,
224        mut is_alive: impl FnMut(SerializableTransactionId) -> VersionResult<bool>,
225        mut finish: impl FnMut(StorageTransactionId) -> VersionResult<CommitStatus>,
226    ) -> VersionResult<()> {
227        for index in 0..self.transactions.len() {
228            control.check()?;
229            let entry = self.transactions[index];
230            if entry.owner != ParticipantOwner::Leased {
231                continue;
232            }
233            let participant =
234                SerializableTransactionId::new(self.database, self.coordinator, entry.id)?;
235            if is_alive(participant)? {
236                continue;
237            }
238            if !entry.live() {
239                self.transactions[index].owner = ParticipantOwner::Manual;
240                self.checkpoint_changed = true;
241                continue;
242            }
243            let Some(publication) = self.publication(participant)? else {
244                self.rollback(participant)?;
245                self.transactions[index].owner = ParticipantOwner::Manual;
246                self.checkpoint_changed = true;
247                continue;
248            };
249            let status = match finish(publication.transaction()) {
250                Ok(status) => status,
251                Err(VersionError::AlreadyCommitted(receipt)) => CommitStatus::Committed(receipt),
252                Err(VersionError::AlreadyAborted(transaction))
253                    if transaction == publication.transaction() =>
254                {
255                    CommitStatus::Aborted
256                }
257                Err(error) => return Err(error),
258            };
259            match self.resolve_publication(publication, status)? {
260                CommitStatus::Unknown => return Err(VersionError::UnknownTransaction),
261                CommitStatus::Pending | CommitStatus::Committed(_) | CommitStatus::Aborted => {}
262            }
263            if !self.transactions[index].live() {
264                self.transactions[index].owner = ParticipantOwner::Manual;
265                self.checkpoint_changed = true;
266            }
267        }
268        Ok(())
269    }
270}
271
272#[cfg(test)]
273mod tests;