Skip to main content

coven_database/
database_snapshots.rs

1//! Snapshot streams and atomic reload through the database owner.
2
3use super::Database;
4use crate::DbError;
5use coven_foundation::progress::ProgressReport;
6
7impl Database {
8    /// The sealed bytes a new device replays for `audience` after a
9    /// snapshot's coverage (§16.2): each applied writes slot carrying one of
10    /// its parts, and for the store every admin, key-copy and announcement
11    /// slot too. Local originals without a slot do not request a snapshot of
12    /// unpublished data, and slots this installation loaded inside a
13    /// snapshot instead of reading count nothing.
14    pub async fn snapshot_slot_bytes(
15        &self,
16        audience: coven_merge::Audience,
17        after: u64,
18    ) -> Result<u64, DbError> {
19        self.call(move |inner| {
20            inner.access.with_committed(|db| {
21                let too_large = || DbError::TooLarge {
22                    field: "snapshot slot bytes", actual: u64::MAX, maximum: u64::MAX - 1,
23                };
24                let mut bytes = 0u64;
25                db.for_each("SELECT s.size FROM _coven_write_slot_audiences a JOIN _coven_write_slots s ON s.slot=a.slot WHERE a.audience=?1 AND a.slot>?2",
26                    rusqlite::params![audience, after.to_be_bytes()], |row| {
27                        let size = u64::try_from(row.get::<_, i64>(0)?).map_err(|_negative| DbError::DamagedDatabase)?;
28                        bytes = bytes.checked_add(size).ok_or_else(too_large)?;
29                        Ok::<_, DbError>(())
30                    })?;
31                if audience == coven_merge::Audience::Store {
32                    let records: i64 = db.query_row("SELECT coalesce(sum(length(record)),0) FROM _coven_slot_records WHERE slot>?1",
33                        [after.to_be_bytes()], |row| row.get(0))?;
34                    bytes = u64::try_from(records).ok().and_then(|records| bytes.checked_add(records)).ok_or_else(too_large)?;
35                }
36                Ok(bytes)
37            })
38        }).await
39    }
40
41    /// Headers of waiting plaintext writes, including fixed upload attempts.
42    /// Snapshot growth counts these parts before their eventual publication.
43    pub async fn waiting_snapshot_headers(
44        &self,
45    ) -> Result<Vec<coven_format::write_stream::WriteHeaderFrame>, DbError> {
46        self.call(move |inner| {
47            inner.access.with_committed(|db| {
48                let mut headers = Vec::new();
49                crate::upload::each_plaintext(db, |header, _| {
50                    headers.push(header);
51                    Ok(())
52                })?;
53                Ok(headers)
54            })
55        })
56        .await
57    }
58
59    /// Run the snapshot loader's schema and merge checks over a staged
60    /// snapshot in a transaction that always rolls back, returning the uploaded
61    /// files it names. File retention uses this for a snapshot no reload has
62    /// read; a damaged snapshot cannot leave rows, metadata or observations
63    /// behind. Supply the staging of a body that authenticated: the check is
64    /// its last phase, and removes its records whether it passes or fails
65    /// (§16.2).
66    pub async fn validate_snapshot(
67        &self,
68        id: coven_format::admin::SnapshotId,
69        staging: crate::SnapshotStaging,
70        author: coven_foundation::id_source::MemberId,
71        authorship: std::sync::Arc<dyn crate::Authorship>,
72    ) -> Result<
73        std::collections::BTreeSet<(
74            coven_foundation::id_source::DeviceId,
75            coven_foundation::id_source::ObjectId,
76        )>,
77        DbError,
78    > {
79        if !staging.staged_by(self) {
80            return Err(
81                super::input_database::staging::refuse(vec![staging], STAGED_ELSEWHERE).await
82            );
83        }
84        let mut stagings = [staging];
85        self.call(move |inner| {
86            inner.inputs.read_staged(&mut stagings, |inputs, [staging]| {
87                // A failure reading the body names the snapshot.
88                let body = |error: DbError| crate::SnapshotError::Body {
89                    snapshot: id.clone(),
90                    error: Box::new(error),
91                    staged: None,
92                };
93                let prepared = staging.prepare(inputs, authorship.as_ref()).map_err(&body)?;
94                inner.access.with_writer(|writer| {
95                    writer.discarded_transaction(|writer| {
96                        let schema = &inner.access.write_schema;
97                        crate::snapshot_state::create_tables(writer)?;
98                        crate::snapshot_install::create_tables(writer, schema)?;
99                        // Validating runs installing's passes where no
100                        // host draws them.
101                        let mut progress = ProgressReport::unwatched(inner.clock.clone());
102                        crate::snapshot_load::begin_installing(schema, 1, &mut progress);
103                        crate::snapshot_load::read(
104                            writer,
105                            schema,
106                            prepared,
107                            Some(author),
108                            &mut progress,
109                        )
110                        .map_err(&body)?;
111                        super::file_retention::snapshot_references(writer, schema, &id.audience)
112                    })
113                })
114            })
115        })
116        .await
117    }
118
119    /// Stream an audience's plaintext snapshot frames from one committed reader
120    /// transaction. `begin` receives metadata before any frame, so the consumer
121    /// can write the sealed prefix and seal each subsequent frame as it arrives;
122    /// commits on the writer connection continue throughout this call. While
123    /// this device's raise of the audience waits, it refuses with
124    /// [`DbError::RaiseWaiting`]: the snapshot would be written at a schema
125    /// version the history doesn't hold yet (§16.2). Returns the snapshot's
126    /// breaking version, for its announcement (§16.3).
127    pub async fn write_snapshot<B, F, E>(
128        &self,
129        id: coven_format::admin::SnapshotId,
130        begin: B,
131        emit: F,
132    ) -> Result<u32, crate::SnapshotWriteError<E>>
133    where
134        B: FnOnce(&coven_format::snapshot::SnapshotHeader) -> Result<(), E> + Send + 'static,
135        F: FnMut(Vec<u8>) -> Result<(), E> + Send + 'static,
136        E: coven_foundation::task::CarriesBug + Send + 'static,
137    {
138        self.call(move |inner| {
139            inner.access.with_committed(|reader| {
140                // In the transaction that reads the schema version the
141                // snapshot is written at.
142                if crate::raise_capture::provisional(reader)?
143                    .iter()
144                    .any(|(audience, _, _)| *audience == id.audience)
145                {
146                    return Err(DbError::RaiseWaiting.into());
147                }
148                crate::write_schema::prepare_evaluators(reader, &inner.access.write_schema)?;
149                crate::snapshot_write::write(reader, &inner.access.write_schema, id, begin, emit)
150            })
151        })
152        .await
153    }
154
155    /// The version of the newest breaking migration this installation ran,
156    /// zero if none: its snapshots' breaking version (§16.3).
157    pub async fn breaking_version(&self) -> Result<u32, DbError> {
158        self.call(move |inner| {
159            let reader = inner.access.readers.acquire_reader();
160            reader.with_reader(crate::raise_capture::breaking_version)
161        })
162        .await
163    }
164
165    /// The raise snapshots a breaking migration captured through its marker,
166    /// one per audience this device read then (§18.3, §11.5).
167    pub async fn raise_captures(&self) -> Result<Vec<crate::RaiseCapture>, DbError> {
168        self.call(move |inner| {
169            let reader = inner.access.readers.acquire_reader();
170            reader.with_reader(|reader| crate::raise_capture::captures(reader, inner.access.device))
171        })
172        .await
173    }
174
175    /// The capture to seal for an audience's raise, right after a completed
176    /// catch-up: kept if that catch-up applied nothing past it, otherwise
177    /// captured again from the current state through this device's newest
178    /// write, which then joins the prefix to confirm (§16.3, §18.5). None when
179    /// a later migration replaced the capture.
180    pub async fn prepare_raise_capture(
181        &self,
182        audience: coven_merge::Audience,
183        version: u32,
184    ) -> Result<Option<crate::RaiseCapture>, DbError> {
185        self.call(move |inner| {
186            let device = inner.access.device;
187            inner.access.with_writer(|writer| {
188                writer.transaction(|writer| {
189                    crate::raise_capture::prepare(
190                        writer,
191                        &inner.access.write_schema,
192                        device,
193                        &audience,
194                        version,
195                    )
196                })
197            })
198        })
199        .await
200    }
201
202    /// The first own write of a snapshot's captured prefix, through its
203    /// own position, not yet confirmed stored. Any snapshot, ordinary, raise
204    /// or reset, is sent only once this is `None` (§16.1); a raise starts
205    /// only then (§18.5).
206    pub async fn unconfirmed_own_prefix(
207        &self,
208        through: coven_merge::WriteId,
209    ) -> Result<Option<coven_merge::WriteId>, DbError> {
210        self.call(move |inner| {
211            let reader = inner.access.readers.acquire_reader();
212            reader.with_reader(|reader| crate::upload::unconfirmed(reader, through))
213        })
214        .await
215    }
216
217    /// Stream an audience's captured raise snapshot under its published
218    /// identity, like `write_snapshot` but from the migration's capture.
219    pub async fn write_raise_snapshot<B, F, E>(
220        &self,
221        id: coven_format::admin::SnapshotId,
222        version: u32,
223        begin: B,
224        emit: F,
225    ) -> Result<u32, crate::SnapshotWriteError<E>>
226    where
227        B: FnOnce(&coven_format::snapshot::SnapshotHeader) -> Result<(), E> + Send + 'static,
228        F: FnMut(Vec<u8>) -> Result<(), E> + Send + 'static,
229        E: coven_foundation::task::CarriesBug + Send + 'static,
230    {
231        self.call(move |inner| {
232            inner.access.with_committed(|reader| {
233                crate::write_schema::prepare_evaluators(reader, &inner.access.write_schema)?;
234                crate::raise_capture::write(
235                    reader,
236                    &inner.access.write_schema,
237                    id,
238                    version,
239                    begin,
240                    emit,
241                )
242            })
243        })
244        .await
245    }
246
247    /// Forget a capture whose raise is published or superseded.
248    pub async fn discard_raise_capture(
249        &self,
250        audience: coven_merge::Audience,
251        version: u32,
252    ) -> Result<(), DbError> {
253        self.call(move |inner| {
254            inner.access.with_writer(|writer| {
255                writer
256                    .transaction(|writer| crate::raise_capture::discard(writer, &audience, version))
257            })
258        })
259        .await
260    }
261
262    /// Load authenticated snapshots and gap writes supplied by sync. Each stored
263    /// snapshot supplies its identity and its staging, of a body whose stream
264    /// checked out to its end; their write evidence is authenticated, and write
265    /// parts are staged on the input connection, before taking the application
266    /// writer. Inputs name a fixed target and consecutive retained slots.
267    ///
268    /// Replaces the selected audiences and replays uncovered parts and waiting
269    /// uploads in one transaction. The load is each staging's last phase: it
270    /// reads each staged snapshot once and removes its records, whether it
271    /// commits or fails. Every audience finishes at the common target slot;
272    /// missing history, read errors and failed checks roll the whole load back,
273    /// a body's failure as `SnapshotError::Body` naming that snapshot. Returns
274    /// the uploaded files each stored snapshot names.
275    /// Supply each readable audience's parts, including those needed by waiting
276    /// writes and the device's own earlier writes. Other audiences retain their
277    /// merge history. Upload records, numbers and sealing key ids stay unchanged.
278    /// `progress`, when given, hears the `Installing` and `InstallingSlots`
279    /// steps as they run (§13.4).
280    pub async fn load_snapshots<P: crate::PendingValue, W>(
281        &self,
282        reload: crate::SnapshotReload<crate::SnapshotStaging, crate::DownloadedWriteStream<W>>,
283        progress: Option<ProgressReport<crate::LoadStep>>,
284    ) -> Result<Vec<crate::LoadedSnapshot>, DbError>
285    where
286        W: std::io::Read + Send + 'static,
287    {
288        // Each stored source names its staging by its place among them.
289        let mut stagings = Vec::new();
290        let sources: Vec<crate::SnapshotSource<usize>> = reload
291            .snapshots
292            .into_iter()
293            .map(|source| match source {
294                crate::SnapshotSource::Stored { id, input, author } => {
295                    stagings.push(input);
296                    crate::SnapshotSource::Stored { id, input: stagings.len() - 1, author }
297                }
298                crate::SnapshotSource::Empty(audience) => crate::SnapshotSource::Empty(audience),
299                crate::SnapshotSource::Current(audience) => {
300                    crate::SnapshotSource::Current(audience)
301                }
302            })
303            .collect();
304        if !stagings.iter().all(|staging| staging.staged_by(self)) {
305            return Err(super::input_database::staging::refuse(stagings, STAGED_ELSEWHERE).await);
306        }
307        let authorship = reload.authorship;
308        self.call(move |inner| {
309            let started = inner.clock.elapsed();
310            let mut progress =
311                progress.unwrap_or_else(|| ProgressReport::unwatched(inner.clock.clone()));
312            inner.inputs.read_staged(&mut stagings, |inputs, stagings| {
313                let snapshots = sources
314                    .into_iter()
315                    .map(|source| {
316                        Ok(match source {
317                            crate::SnapshotSource::Stored { id, input, author } => {
318                                let input = stagings[input]
319                                    .prepare(inputs, authorship.as_ref())
320                                    .map_err(|error| crate::SnapshotError::Body {
321                                        snapshot: id.clone(),
322                                        error: Box::new(error),
323                                        staged: None,
324                                    })?;
325                                crate::SnapshotSource::Stored { id, input, author }
326                            }
327                            crate::SnapshotSource::Empty(audience) => {
328                                crate::SnapshotSource::Empty(audience)
329                            }
330                            crate::SnapshotSource::Current(audience) => {
331                                crate::SnapshotSource::Current(audience)
332                            }
333                        })
334                    })
335                    .collect::<Result<_, DbError>>()?;
336                let reload = crate::SnapshotReload {
337                    slots: reload
338                        .slots
339                        .into_iter()
340                        .map(|slot| slot.prepare(inputs))
341                        .collect::<Result<_, _>>()?,
342                    snapshots,
343                    target: reload.target,
344                    state: reload.state,
345                    provider_access: reload.provider_access,
346                    cutoffs: reload.cutoffs,
347                    boundaries: reload.boundaries,
348                    operation: reload.operation,
349                    authorship,
350                };
351                let prepared = inner.clock.elapsed();
352                // Reserve a reader before locking the writer: snapshot consumers
353                // may be waiting for a commit before releasing their readers.
354                let reader = inner.access.readers.acquire_reader();
355                let loaded = inner.with_files("load a snapshot", |writer, files| {
356                    reader.with_reader(|reader| {
357                        reader.read_transaction(|| {
358                            crate::write_schema::prepare_evaluators(
359                                reader,
360                                &inner.access.write_schema,
361                            )?;
362                            crate::snapshot_load::load::<P>(
363                                writer,
364                                reader,
365                                &inner.access.write_schema,
366                                reload,
367                                files,
368                                &mut progress,
369                            )
370                        })
371                    })
372                });
373                tracing::info!(
374                    preparing = ?(prepared - started),
375                    installing = ?(inner.clock.elapsed() - prepared),
376                    "installed snapshots"
377                );
378                loaded
379            })
380        })
381        .await
382    }
383}
384
385/// A staging another store made: its records are on that store's input
386/// connection, not this one's.
387const STAGED_ELSEWHERE: DbError =
388    DbError::Invariant { what: "a snapshot is loaded by the store that staged it" };