Skip to main content

vantage_vista/
transaction.rs

1mod direct;
2mod joining;
3mod participant;
4mod shell;
5mod slot;
6mod wrap;
7
8use std::any::Any;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::{Arc, Mutex, MutexGuard, PoisonError, Weak};
11
12use futures_core::future::BoxFuture;
13use indexmap::{IndexMap, IndexSet};
14use vantage_core::{Result, VantageError, error};
15
16pub use direct::direct;
17pub use participant::{TxKey, TxParticipant};
18pub use slot::{TxBackend, TxOpen, TxSlot};
19
20use joining::Joined;
21
22type Deferred = Box<dyn FnOnce() -> BoxFuture<'static, Result<()>> + Send>;
23
24/// The error-context key under which a failed commit names the tables that
25/// had already committed, as a JSON array.
26const COMMITTED: &str = "committed";
27
28/// What a successful commit reports.
29#[derive(Debug)]
30#[non_exhaustive]
31pub struct Committed {
32    /// Tables written through the transaction, in first-written order.
33    pub written: Vec<String>,
34    /// Tables written directly, outside the transaction, by shells over a
35    /// datasource without transactions ([`direct`]). Their writes landed
36    /// when made and no rollback undoes them.
37    pub unprotected: Vec<String>,
38}
39
40#[derive(Default)]
41struct State {
42    typed: IndexMap<TxKey, Arc<dyn Any + Send + Sync>>,
43    ordered: Vec<(TxKey, Arc<dyn TxParticipant>)>,
44    written: IndexSet<String>,
45    /// The tables written through each participant, so a failed commit can
46    /// name the ones that landed.
47    tables_of: IndexMap<TxKey, IndexSet<String>>,
48    /// Kept past `finish`: a failed block still reports them.
49    unprotected: IndexSet<String>,
50    deferred: Vec<Deferred>,
51}
52
53/// What `finish` takes out of the state.
54struct Taken {
55    participants: Vec<(TxKey, Arc<dyn TxParticipant>)>,
56    deferred: Vec<Deferred>,
57    written: IndexSet<String>,
58    tables_of: IndexMap<TxKey, IndexSet<String>>,
59}
60
61#[derive(Default)]
62struct Inner {
63    state: Mutex<State>,
64    finished: AtomicBool,
65}
66
67/// A unit of work shared by every backend it touches. Commit runs backend by
68/// backend, so it's atomic per backend, not across them: if one fails part-way,
69/// the backends before it stay committed and the rest roll back.
70#[derive(Clone)]
71pub struct Transaction(Arc<Inner>);
72
73/// A [`Transaction`] handle that doesn't keep it alive.
74#[derive(Clone)]
75pub struct WeakTransaction(Weak<Inner>);
76
77impl Default for Transaction {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl Transaction {
84    pub fn new() -> Transaction {
85        Transaction(Arc::new(Inner::default()))
86    }
87
88    pub fn downgrade(&self) -> WeakTransaction {
89        WeakTransaction(Arc::downgrade(&self.0))
90    }
91
92    pub fn same(&self, other: &Transaction) -> bool {
93        Arc::ptr_eq(&self.0, &other.0)
94    }
95
96    fn state(&self) -> MutexGuard<'_, State> {
97        self.0.state.lock().unwrap_or_else(PoisonError::into_inner)
98    }
99
100    /// The participant for `key`, made by `make` on first use; later calls return the same Arc.
101    /// Err if the one stored under `key` is another type.
102    pub fn participant<P: TxParticipant>(
103        &self,
104        key: TxKey,
105        make: impl FnOnce() -> P,
106    ) -> Result<Arc<P>> {
107        let mut state = self.state();
108        // Checked under the state lock, where `finish` sets it: a participant
109        // added now would never be committed or rolled back.
110        if self.is_finished() {
111            return Err(error!("transaction finished"));
112        }
113        let joined = match state.typed.get(&key) {
114            Some(existing) => existing
115                .clone()
116                .downcast::<P>()
117                .map_err(|_| error!("transaction participant under this key is another type"))?,
118            None => {
119                let made = Arc::new(make());
120                state.typed.insert(key, made.clone());
121                state.ordered.push((key, made.clone()));
122                made
123            }
124        };
125        drop(state);
126        // Every rebind that reaches the participant is credited, not only the
127        // first: each shell's writes are attributed to it.
128        joining::note_key(self, key);
129        Ok(joined)
130    }
131
132    /// Run after a successful commit, in order. Dropped on rollback.
133    pub fn defer(&self, after: Deferred) {
134        self.state().deferred.push(after);
135    }
136
137    /// Record a table written through this transaction (the written-tables report).
138    pub fn note_written(&self, table: &str) {
139        self.state().written.insert(table.to_string());
140    }
141
142    /// Record a write to `table` by a shell whose rebind joined `joined`.
143    fn note_written_by(&self, table: &str, joined: &Joined) {
144        let mut state = self.state();
145        if joined.direct {
146            state.unprotected.insert(table.to_string());
147            return;
148        }
149        state.written.insert(table.to_string());
150        for key in &joined.keys {
151            state
152                .tables_of
153                .entry(*key)
154                .or_default()
155                .insert(table.to_string());
156        }
157    }
158
159    /// The tables written directly so far ([`Committed::unprotected`]),
160    /// also after a rollback or a failed commit.
161    pub fn unprotected(&self) -> Vec<String> {
162        self.state().unprotected.iter().cloned().collect()
163    }
164
165    /// The tables whose participants committed before the failure, in commit
166    /// order, read from an error [`commit`](Self::commit) returned. Empty
167    /// for any other error, and when nothing had committed.
168    pub fn committed_before_failure(err: &VantageError) -> Vec<String> {
169        err.context
170            .get(COMMITTED)
171            .and_then(|tables| serde_json::from_str(tables).ok())
172            .unwrap_or_default()
173    }
174
175    pub fn is_finished(&self) -> bool {
176        self.0.finished.load(Ordering::SeqCst)
177    }
178
179    pub fn check_open(&self) -> Result<()> {
180        if self.is_finished() {
181            return Err(error!("transaction finished"));
182        }
183        Ok(())
184    }
185
186    /// Commits participants in join order. On the first failure the rest are
187    /// rolled back and the error returned; the ones already committed stay
188    /// committed, and the error names their tables
189    /// ([`committed_before_failure`](Self::committed_before_failure)).
190    /// Deferred closures run only after a full success, and their failures
191    /// are logged, not returned.
192    pub async fn commit(&self) -> Result<Committed> {
193        let Taken {
194            participants,
195            deferred,
196            written,
197            tables_of,
198        } = self.finish()?;
199
200        for (done, (_, participant)) in participants.iter().enumerate() {
201            if let Err(mut err) = participant.commit().await {
202                Self::roll_back_all(&participants[done + 1..]).await;
203                let landed: IndexSet<&String> = participants[..done]
204                    .iter()
205                    .filter_map(|(key, _)| tables_of.get(key))
206                    .flatten()
207                    .collect();
208                if !landed.is_empty() {
209                    let landed: Vec<&String> = landed.into_iter().collect();
210                    let landed = serde_json::to_string(&landed).unwrap_or_default();
211                    err.context.insert(COMMITTED.to_string(), landed);
212                }
213                return Err(err);
214            }
215        }
216
217        for after in deferred {
218            if let Err(err) = after().await {
219                tracing::warn!("deferred work after commit failed: {err}");
220            }
221        }
222        Ok(Committed {
223            written: written.into_iter().collect(),
224            unprotected: self.unprotected(),
225        })
226    }
227
228    pub async fn rollback(&self) -> Result<()> {
229        // Rolling back a finished transaction is a no-op, so cleanup paths can call it freely.
230        if let Ok(taken) = self.finish() {
231            Self::roll_back_all(&taken.participants).await;
232        }
233        Ok(())
234    }
235
236    fn finish(&self) -> Result<Taken> {
237        let mut state = self.state();
238        if self.0.finished.swap(true, Ordering::SeqCst) {
239            return Err(error!("transaction finished"));
240        }
241        state.typed.clear();
242        Ok(Taken {
243            participants: std::mem::take(&mut state.ordered),
244            deferred: std::mem::take(&mut state.deferred),
245            written: std::mem::take(&mut state.written),
246            tables_of: std::mem::take(&mut state.tables_of),
247        })
248    }
249
250    async fn roll_back_all(participants: &[(TxKey, Arc<dyn TxParticipant>)]) {
251        for (_, participant) in participants {
252            if let Err(err) = participant.rollback().await {
253                tracing::warn!("transaction rollback failed: {err}");
254            }
255        }
256    }
257}
258
259impl WeakTransaction {
260    pub fn upgrade(&self) -> Option<Transaction> {
261        self.0.upgrade().map(Transaction)
262    }
263}
264
265/// The refusal of a table whose shell can't rebind to a transaction. Marked
266/// `Unsupported`: [`Transaction::wrap`] renames any `Unsupported` rebind
267/// refusal to the wrapped Vista's name.
268pub fn cant_take_part(table: &str) -> VantageError {
269    error!(format!("\"{table}\" can't take part in a transaction")).mark_unsupported()
270}
271
272/// The default `rebind` refusal; a shell doesn't know the name its Vista goes by.
273pub(crate) fn refuse_rebind() -> VantageError {
274    error!("this shell can't take part in a transaction").mark_unsupported()
275}
276
277pub(crate) fn refuse_lock() -> VantageError {
278    error!("this shell can't lock rows").mark_unsupported()
279}