Skip to main content

coven_database/
database_operations.rs

1//! Operation transactions, borrowing the database owner's connections.
2
3use super::Database;
4use crate::{DbError, NewOperation, OperationId, OperationRecord, OperationUpdate};
5use coven_foundation::id_source::DeviceId;
6use std::collections::BTreeSet;
7
8impl Database {
9    /// Read every unfinished operation, including permanently failed ones.
10    pub async fn operations(&self) -> Result<Vec<OperationRecord>, DbError> {
11        self.call(move |inner| inner.access.with_committed(crate::operation::read)).await
12    }
13
14    /// Read one committed journal row without loading unrelated operation data.
15    /// Absence means the operation has finished or never existed.
16    pub async fn operation(&self, id: OperationId) -> Result<Option<OperationRecord>, DbError> {
17        self.call(move |inner| {
18            inner.access.with_committed(|writer| crate::operation::find(writer, id))
19        })
20        .await
21    }
22
23    /// Read the earliest unfinished row of a sync-owned kind, including failures.
24    pub async fn first_operation(&self, kind: &str) -> Result<Option<OperationRecord>, DbError> {
25        let kind = kind.to_owned();
26        self.call(move |inner| {
27            inner.access.with_committed(|writer| crate::operation::first(writer, &kind))
28        })
29        .await
30    }
31
32    /// Commit the operation before its first step.
33    pub async fn start_operation(&self, operation: NewOperation) -> Result<OperationId, DbError> {
34        self.call(move |inner| {
35            inner.access.with_writer(|writer| {
36                writer.transaction(|db| crate::operation::insert(db, &operation))
37            })
38        })
39        .await
40    }
41
42    /// Commit an operation a call of the app's starts, before its first
43    /// step, listed in `coven_operations` as `kind` names it from this
44    /// commit until a write acknowledges its outcome (§11.4, §11.6).
45    pub async fn start_listed_operation(
46        &self,
47        operation: NewOperation,
48        kind: String,
49    ) -> Result<OperationId, DbError> {
50        self.call(move |inner| {
51            inner.access.with_writer(|writer| {
52                writer.transaction(|db| crate::operation::insert_for_app(db, &operation, &kind))
53            })
54        })
55        .await
56    }
57
58    /// Commit a new operation and a step of another that names it, in one
59    /// transaction: `update` builds the step from the new operation's id. A
60    /// parent that journals a child this way never loses track of it, nor
61    /// starts a second one, whenever it stops.
62    pub async fn start_operation_advancing<E: crate::WriteFailure + Send + 'static>(
63        &self,
64        operation: NewOperation,
65        update: impl FnOnce(OperationId) -> Result<OperationUpdate, E> + Send + 'static,
66    ) -> Result<OperationId, E> {
67        self.call(move |inner| {
68            inner.access.with_writer(|writer| {
69                writer.transaction_as(|db| {
70                    let id = crate::operation::insert(db, &operation)?;
71                    crate::operation::advance(db, &update(id)?)?;
72                    Ok(id)
73                })
74            })
75        })
76        .await
77    }
78
79    /// Reserve the path number of a snapshot attempt about to be prepared:
80    /// the next of this device's snapshot sequence, never reused, which §19.3
81    /// compares with its stored snapshots.
82    pub async fn reserve_snapshot_number(&self) -> Result<std::num::NonZeroU64, DbError> {
83        self.call(move |inner| {
84            inner
85                .access
86                .with_writer(|writer| writer.transaction(crate::operation::reserve_snapshot))
87        })
88        .await
89    }
90
91    /// Commit a local step's state after an idempotent storage effect.
92    pub async fn advance_operation(&self, update: OperationUpdate) -> Result<(), DbError> {
93        self.call(move |inner| {
94            inner.access.with_writer(|writer| {
95                writer.transaction(|db| crate::operation::advance(db, &update))
96            })
97        })
98        .await
99    }
100
101    /// Read the journal's rows of `kind` and commit the transitions `step`
102    /// makes from them, in one transaction on the writer: no other write
103    /// comes between the read and the transitions. `step` also returns what
104    /// it concluded from the rows. A failure fails this handle with its
105    /// cause, as losing a pending record does: the rows would go on showing
106    /// the app a state their owner has left.
107    pub async fn advance_operations<T: Send + 'static>(
108        &self,
109        kind: &str,
110        step: impl FnOnce(&[OperationRecord]) -> Result<(Vec<OperationUpdate>, T), DbError>
111            + Send
112            + 'static,
113    ) -> Result<T, DbError> {
114        let kind = kind.to_owned();
115        self.call(move |inner| {
116            inner.access.with_writer(|writer| {
117                writer.state_transaction(|db| {
118                    let (updates, concluded) = step(&crate::operation::of_kind(db, &kind)?)?;
119                    for update in &updates {
120                        crate::operation::advance(db, update)?;
121                    }
122                    Ok(concluded)
123                })
124            })
125        })
126        .await
127    }
128
129    /// Record member pairs this installation holds, or is about to keep in
130    /// custody, before custody keeps them: applying a binding of its member
131    /// to a pair not recorded stops it for good (§12.3).
132    pub async fn hold_member_keys(
133        &self,
134        keys: Vec<coven_crypto::MemberPublicKeys>,
135    ) -> Result<(), DbError> {
136        self.call(move |inner| {
137            inner.access.with_writer(|writer| {
138                writer.transaction(|db| crate::operation::hold_keys(db, &keys))
139            })
140        })
141        .await
142    }
143
144    /// Read the pending condition associated with a journal row.
145    pub async fn operation_pending<P: crate::PendingValue>(
146        &self,
147        id: OperationId,
148    ) -> Result<Option<P>, DbError> {
149        self.call(move |inner| inner.access.with_writer(|db| crate::pending::operation(db, id)))
150            .await
151    }
152
153    /// Clear a condition for an explicit retry; completed steps stay recorded.
154    #[cfg(test)]
155    pub async fn retry_operation(&self, id: OperationId) -> Result<(), DbError> {
156        self.call(move |inner| {
157            inner.access.with_writer(|writer| {
158                writer.transaction(|db| crate::pending::clear_operation(db, id))
159            })
160        })
161        .await
162    }
163
164    /// Commit completed work and its first remaining condition in one transaction.
165    #[cfg(test)]
166    pub async fn advance_operation_pending<P: crate::PendingValue>(
167        &self,
168        update: OperationUpdate,
169        pending: P,
170    ) -> Result<(), DbError> {
171        self.call(move |inner| {
172            inner.access.with_writer(|writer| {
173                if pending.operation() != Some(update.id) {
174                    return Err(DbError::Invariant {
175                        what: "an operation's condition names its journal row",
176                    });
177                }
178                writer.transaction(|db| {
179                    crate::operation::advance(db, &update)?;
180                    crate::pending::save(db, &pending)
181                })
182            })
183        })
184        .await
185    }
186
187    /// Retire a finished operation with its outcome, as sync encodes it,
188    /// which `coven_operations` lists for an operation the app's write
189    /// journaled, in the same transaction: an outcome nobody awaits is
190    /// never lost (§11.4).
191    pub async fn finish_operation_with_outcome(
192        &self,
193        id: OperationId,
194        outcome: crate::OperationOutcome,
195    ) -> Result<(), DbError> {
196        self.call(move |inner| {
197            inner.access.with_writer(|writer| {
198                writer.transaction(|db| crate::operation::finish_with_outcome(db, id, &outcome))
199            })
200        })
201        .await
202    }
203
204    /// Retire an ended invite in one commit with the operations it started
205    /// that are still journaled, its `children`, and with the deletion it
206    /// leaves owed: its marker's, once that has passed (§13.2). An `outcome`
207    /// the invite ends with, with its `kind` as `coven_operations` lists it,
208    /// is listed there until the app acknowledges it. An invite, or a child,
209    /// that has ended already fails with `OperationChanged`.
210    pub async fn finish_operation_with_removal(
211        &self,
212        id: OperationId,
213        children: Vec<OperationId>,
214        removal: crate::ObjectRemoval,
215        outcome: Option<(String, crate::OperationOutcome)>,
216    ) -> Result<(), DbError> {
217        self.call(move |inner| {
218            inner.access.with_writer(|writer| {
219                writer.transaction(|db| {
220                    // The invite's outcome is listed in `coven_operations`
221                    // until the app acknowledges it, the journal row's end
222                    // requiring it written first.
223                    if let Some((kind, outcome)) = &outcome {
224                        crate::operation::list_outcome(db, id, kind, outcome)?;
225                    }
226                    for id in children.iter().copied().chain([id]) {
227                        if crate::operation::finish(db, id)? != 1 {
228                            return Err(DbError::OperationChanged(id));
229                        }
230                    }
231                    crate::object_removals::record(db, &removal)
232                })
233            })
234        })
235        .await
236    }
237
238    /// End an operation another started, with its outcome, in the
239    /// transaction that advances its parent past it: the parent's step and
240    /// the child's end commit together, so no restart finds one without the
241    /// other. An outcome `coven_operations` lists is written as
242    /// `finish_operation_with_outcome` writes it. A parent that has moved on
243    /// since, or a child already ended, fails with `OperationChanged`.
244    pub async fn finish_operation_advancing(
245        &self,
246        child: OperationId,
247        outcome: crate::OperationOutcome,
248        parent: OperationUpdate,
249    ) -> Result<(), DbError> {
250        self.call(move |inner| {
251            inner.access.with_writer(|writer| {
252                writer.transaction(|db| {
253                    crate::operation::advance(db, &parent)?;
254                    crate::operation::finish_with_outcome(db, child, &outcome)
255                })
256            })
257        })
258        .await
259    }
260
261    /// End every operation the app's writes journaled that still runs, and
262    /// every invite this device made, as the transaction applying a stop for
263    /// good does, for a stop no slot applies: history shows another copy of
264    /// this installation using its device id (§19.3). A failure fails this
265    /// handle with its cause: they would otherwise run, listed, forever.
266    pub async fn end_for_good(&self, stop: crate::DeviceStop) -> Result<(), DbError> {
267        self.call(move |inner| {
268            inner.access.with_writer(|writer| {
269                writer.state_transaction(|db| crate::operation::end_for_good(db, stop))
270            })
271        })
272        .await
273    }
274
275    /// How the applied state stops this installation for good, if it does:
276    /// its device removed or replaced, its member removed, or its member
277    /// bound to a pair it doesn't hold (§14.1). `None` while the applied
278    /// state doesn't name its device. A restore ending without its store
279    /// reads it for the facts (§13.1).
280    pub async fn device_stop(&self) -> Result<Option<crate::DeviceStop>, DbError> {
281        self.call(|inner| inner.access.with_committed(crate::operation::stopped)).await
282    }
283
284    /// The paths of objects whose time to go has passed by `now`, in seconds
285    /// since the Unix epoch.
286    pub async fn due_object_removals(&self, now: u64) -> Result<Vec<String>, DbError> {
287        let now = i64::try_from(now).unwrap_or(i64::MAX);
288        self.call(move |inner| {
289            inner.access.with_committed(|db| {
290                db.query(
291                    "SELECT path FROM _coven_object_removals WHERE after<=?1 ORDER BY after",
292                    [now],
293                    |r| r.get(0),
294                )
295            })
296        })
297        .await
298    }
299
300    /// The object at `path` is gone from storage.
301    pub async fn object_removed(&self, path: String) -> Result<(), DbError> {
302        self.call(move |inner| {
303            inner.access.with_writer(|writer| {
304                writer.transaction(|db| crate::object_removals::removed(db, &path))
305            })
306        })
307        .await
308    }
309
310    /// The devices gone for good whose join requests this device ended
311    /// (§13.2).
312    pub async fn requests_ended(&self) -> Result<BTreeSet<DeviceId>, DbError> {
313        self.call(move |inner| {
314            inner.access.with_committed(|db| {
315                db.query("SELECT device FROM _coven_requests_ended", [], |r| {
316                    r.get::<_, DeviceId>(0)
317                })
318            })
319        })
320        .await
321        .map(|devices| devices.into_iter().collect())
322    }
323
324    /// This device ended `device`'s join requests: its gone marker is in
325    /// storage, and nothing else was under its requests when listed (§13.2).
326    pub async fn record_requests_ended(&self, device: DeviceId) -> Result<(), DbError> {
327        self.call(move |inner| {
328            inner.access.with_writer(|writer| {
329                writer.transaction(|db| {
330                    db.internal_execute(
331                        "INSERT INTO _coven_requests_ended(device) VALUES(?1)",
332                        [device],
333                    )
334                    .map(drop)
335                })
336            })
337        })
338        .await
339    }
340
341    /// The devices gone for good whose storage this device cleared once
342    /// (§16.6, §17.10).
343    pub async fn gone_storage_cleared(&self) -> Result<BTreeSet<DeviceId>, DbError> {
344        self.call(move |inner| {
345            inner.access.with_committed(|db| {
346                db.query("SELECT device FROM _coven_gone_storage_cleared", [], |r| {
347                    r.get::<_, DeviceId>(0)
348                })
349            })
350        })
351        .await
352        .map(|devices| devices.into_iter().collect())
353    }
354
355    /// This device ended the uploads `device` left, listed its snapshot
356    /// folders and deleted every body there that no announcement names
357    /// (§16.6, §17.10). Recording it again, after clearing repeated because
358    /// its operation hadn't finished, changes nothing.
359    pub async fn record_gone_storage_cleared(&self, device: DeviceId) -> Result<(), DbError> {
360        self.call(move |inner| {
361            inner.access.with_writer(|writer| {
362                writer.transaction(|db| {
363                    db.internal_execute(
364                        "INSERT INTO _coven_gone_storage_cleared(device) VALUES(?1) ON CONFLICT DO NOTHING",
365                        [device],
366                    )
367                    .map(drop)
368                })
369            })
370        })
371        .await
372    }
373
374    /// Retire a finished operation. The caller must publish every reserved entry
375    /// before discarding; removing this row cannot retire the publication queue.
376    pub async fn finish_operation(&self, id: OperationId) -> Result<(), DbError> {
377        self.call(move |inner| {
378            inner.access.with_writer(|writer| {
379                writer.transaction(|db| crate::operation::finish(db, id).map(drop))
380            })
381        })
382        .await
383    }
384}