Skip to main content

coven_database/store/store_session/
write_lifecycle.rs

1use crate::*;
2use coven_protocol::write::{PendingWrite, WriteId, WriteResolution, WriteStatus};
3use std::sync::Arc;
4
5use super::publication_state::PreparedStoreWriteState;
6use super::*;
7
8#[derive(Debug, PartialEq, Eq)]
9pub enum BlockedWriteDiscard {
10    Discarded(Vec<coven_protocol::write::WriteId>),
11    RemoteResolutionRequired,
12}
13
14impl StoreSession<'_> {
15    fn pending_writes(&self) -> Result<Vec<PendingWrite>, DbError> {
16        let mut statement = self
17            .conn
18            .prepare(
19                "SELECT write_id, status, affected_rows FROM store_writes
20                 WHERE status IN ('\"pending\"', '\"publishing\"')
21                    OR json_extract(status, '$.blocked') IS NOT NULL
22                 ORDER BY ordinal",
23            )
24            .map_err(DbError::from)?;
25        let rows = statement
26            .query_map([], |row| {
27                Ok((
28                    row.get::<_, String>(0)?,
29                    row.get::<_, String>(1)?,
30                    row.get::<_, Option<String>>(2)?,
31                ))
32            })
33            .map_err(DbError::from)?;
34        rows.map(|row| {
35            let (write_id, status, affected_rows) = row.map_err(DbError::from)?;
36            // Only a folded write has no affected rows, and the fold stops at
37            // the first write that is not settled — which every write selected
38            // here is not.
39            let affected_rows = affected_rows.ok_or_else(|| {
40                DbError::Message(format!("unpublished write {write_id} has been folded"))
41            })?;
42            Ok(PendingWrite {
43                write_id: WriteId::from_generated(write_id),
44                status: serde_json::from_str(&status)
45                    .map_err(|error| DbError::context("pending write status", error))?,
46                affected_rows: serde_json::from_str(&affected_rows)
47                    .map_err(|error| DbError::context("pending affected rows", error))?,
48            })
49        })
50        .collect()
51    }
52
53    /// Where each published write landed, in publication order.
54    ///
55    /// The device's own record of its writes, which survives a replay-baseline
56    /// advance — the per-position `materialized_commits` rows do not, because
57    /// the advance retires the retained rows they name.
58    #[cfg(any(test, feature = "test-utils"))]
59    fn published_write_commits(
60        &self,
61    ) -> Result<Vec<coven_protocol::store_commit::StoreBatchCommitRef>, DbError> {
62        let rows = crate::query_mapped_rows(
63            self.conn,
64            "SELECT status FROM store_writes ORDER BY ordinal",
65            [],
66            |row| row.get::<_, String>(0),
67        )?;
68        let mut commits = Vec::new();
69        for raw in rows {
70            let status: WriteStatus = serde_json::from_str(&raw)
71                .map_err(|error| DbError::context("published write status", error))?;
72            if let WriteStatus::Published(position) = status {
73                commits.push(position.commit().clone());
74            }
75        }
76        Ok(commits)
77    }
78
79    fn set_write_status(&self, write_id: &WriteId, status: &WriteStatus) -> Result<(), DbError> {
80        Database::set_write_status_on(self.conn, write_id, status)
81    }
82
83    fn block_write_if_unresolved(
84        &self,
85        write_id: &WriteId,
86        block: coven_protocol::write::WriteBlock,
87    ) -> Result<Option<WriteStatus>, DbError> {
88        let raw: String = self
89            .conn
90            .query_row(
91                "SELECT status FROM store_writes WHERE write_id = ?1",
92                [write_id.as_str()],
93                |row| row.get(0),
94            )
95            .map_err(DbError::from)?;
96        let current: WriteStatus = serde_json::from_str(&raw).map_err(|error| {
97            DbError::context(format!("write {write_id} status before blocking"), error)
98        })?;
99        match current {
100            WriteStatus::Resolved(_) => Ok(None),
101            WriteStatus::Pending | WriteStatus::Publishing | WriteStatus::Blocked(_) => {
102                let blocked = WriteStatus::Blocked(block);
103                Database::set_write_status_on(self.conn, write_id, &blocked)?;
104                Ok(Some(blocked))
105            }
106            state @ (WriteStatus::LocalOnly | WriteStatus::Published(_)) => Err(DbError::Message(
107                format!("write {write_id} cannot become blocked from {state:?}"),
108            )),
109        }
110    }
111
112    fn retry_blocked_write(
113        &mut self,
114        write_id: WriteId,
115    ) -> Result<Vec<(WriteId, WriteStatus)>, DbError> {
116        let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
117        let (raw_status, prepared): (String, Option<String>) = tx
118            .query_row(
119                "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
120                [write_id.as_str()],
121                |row| Ok((row.get(0)?, row.get(1)?)),
122            )
123            .map_err(DbError::from)?;
124        let status: WriteStatus = serde_json::from_str(&raw_status)
125            .map_err(|error| DbError::context(format!("blocked write {write_id} status"), error))?;
126        if !matches!(status, WriteStatus::Blocked(_)) {
127            return Err(DbError::Message(format!("write {write_id} is not blocked")));
128        }
129        if let Some(raw_prepared) = prepared.as_deref() {
130            let prepared: PreparedStoreWriteState =
131                serde_json::from_str(raw_prepared).map_err(|error| {
132                    DbError::context(format!("blocked write {write_id} preparation"), error)
133                })?;
134            let candidate = crate::store::store_session::StoreTransaction::new(&tx, self.store_dir)
135                .prepared_merge_candidate(self.verified_store_authority, &prepared)?
136                .reference;
137            let remote = load_remote_object_on(&tx, remote_object_id(&candidate.object))?;
138            if matches!(
139                remote,
140                RemoteObjectRecord::CandidateCommit(
141                    coven_protocol::remote_object::CandidateCommitRecord {
142                        state:
143                            coven_protocol::remote_object::CandidateCommitState::CleanupPending {
144                                proof: coven_protocol::remote_object::CandidateNonactivationProof::MergeWinner { .. }
145                            }
146                            | coven_protocol::remote_object::CandidateCommitState::AbsentVerified {
147                                proof: coven_protocol::remote_object::CandidateNonactivationProof::MergeWinner { .. }
148                            },
149                        ..
150                    }
151                )
152            ) {
153                return Err(DbError::Message(format!(
154                    "Merge write {write_id} has an irreversible winner and cannot be retried"
155                )));
156            }
157        }
158        let next = if prepared.is_some() {
159            WriteStatus::Publishing
160        } else {
161            WriteStatus::Pending
162        };
163        let next_json = serde_json::to_string(&next)
164            .map_err(|error| DbError::context("serialize retry status", error))?;
165        let updated = tx
166            .execute(
167                "UPDATE store_writes SET status = ?2
168                 WHERE write_id = ?1 AND json_extract(status, '$.blocked') IS NOT NULL",
169                rusqlite::params![write_id.as_str(), next_json],
170            )
171            .map_err(DbError::from)?;
172        if updated != 1 {
173            return Err(DbError::Message(format!(
174                "blocked write {write_id} changed during retry"
175            )));
176        }
177        let retried = vec![(write_id, next)];
178        tx.commit().map_err(DbError::from)?;
179        Ok(retried)
180    }
181
182    fn discard_blocked_write(&mut self, write_id: WriteId) -> Result<BlockedWriteDiscard, DbError> {
183        let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
184        let (raw_status, target_ordinal): (String, i64) = tx
185            .query_row(
186                "SELECT status, ordinal FROM store_writes WHERE write_id = ?1",
187                [write_id.as_str()],
188                |row| Ok((row.get(0)?, row.get(1)?)),
189            )
190            .map_err(DbError::from)?;
191        let target_status: WriteStatus = serde_json::from_str(&raw_status)
192            .map_err(|error| DbError::context(format!("blocked write {write_id} status"), error))?;
193        if !matches!(target_status, WriteStatus::Blocked(_)) {
194            return Err(DbError::Message(format!("write {write_id} is not blocked")));
195        }
196
197        let mut statement = tx
198            .prepare(
199                "SELECT write_id, status, changeset_hash FROM store_writes
200                 WHERE ordinal >= ?1
201                   AND json_extract(status, '$.published') IS NULL
202                   AND json_extract(status, '$.resolved') IS NULL
203                 ORDER BY ordinal",
204            )
205            .map_err(DbError::from)?;
206        let rows = statement
207            .query_map([target_ordinal], |row| {
208                Ok((
209                    row.get::<_, String>(0)?,
210                    row.get::<_, String>(1)?,
211                    row.get::<_, Option<String>>(2)?,
212                ))
213            })
214            .map_err(DbError::from)?;
215        let mut discarded = Vec::new();
216        for row in rows {
217            let (stored_id, raw_status, changeset_hash) = row.map_err(DbError::from)?;
218            // The changeset is what reversing an unpublished write needs, and
219            // only a folded write is without one. The fold stops at the first
220            // unsettled write, so no write in this suffix can have been folded.
221            let changeset_hash = changeset_hash.ok_or_else(|| {
222                DbError::Message(format!("unpublished write {stored_id} has been folded"))
223            })?;
224            let status: WriteStatus = serde_json::from_str(&raw_status)
225                .map_err(|error| DbError::context("discard write status", error))?;
226            if !matches!(
227                status,
228                WriteStatus::LocalOnly | WriteStatus::Pending | WriteStatus::Blocked(_)
229            ) {
230                return Err(DbError::Message(format!(
231                    "write {stored_id} after blocked write {write_id} has non-discardable status {status:?}"
232                )));
233            }
234            discarded.push((
235                WriteId::from_generated(stored_id),
236                changeset_hash.parse::<coven_protocol::store_commit::ObjectHash>()?,
237            ));
238        }
239        drop(statement);
240        if discarded.first().map(|(stored_id, _)| stored_id) != Some(&write_id) {
241            return Err(DbError::Message(format!(
242                "blocked write {write_id} is absent from its unpublished suffix"
243            )));
244        }
245        for (discarded_id, _) in &discarded {
246            if !crate::store::store_session::StoreTransaction::new(&tx, self.store_dir)
247                .unpublished_write_cleanup_is_complete(
248                    self.verified_store_authority,
249                    discarded_id,
250                )?
251            {
252                return Ok(BlockedWriteDiscard::RemoteResolutionRequired);
253            }
254        }
255        let schema = Arc::new(crate::TableSchema::for_apply(
256            &tx,
257            self.synced_tables,
258            self.gates,
259        )?);
260        let store_transaction =
261            crate::store::store_session::StoreTransaction::new(&tx, self.store_dir);
262        let mut inverses = Vec::with_capacity(discarded.len());
263        let mut restored_blobs = Vec::new();
264        for (_, changeset_hash) in discarded.iter().rev() {
265            let changeset = store_transaction.payload(*changeset_hash)?;
266            let inverse = StoreDatabase::invert_changeset(&changeset)?;
267            for change in crate::walk_changeset(&inverse).map_err(DbError::Changeset)? {
268                if let Some(blob) = self
269                    .blob_decls
270                    .ref_from_change(&change)
271                    .map_err(DbError::from)?
272                {
273                    restored_blobs.push(blob);
274                }
275            }
276            let inverse = crate::ValidatedChangeset::new(inverse, schema.clone())
277                .map_err(|error| DbError::context("invalid blocked-write inverse", error))?;
278            inverses.push(inverse);
279        }
280        let suspended_cleanup =
281            super::local_blob_cleanup::suspend_leased_blob_cleanup_for_restoration_on(
282                &tx,
283                &restored_blobs,
284            )?;
285        for inverse in inverses {
286            MergeMaterializationTransaction::from_store(
287                crate::store::store_session::StoreTransaction::new(&tx, self.store_dir),
288            )
289            .apply_changeset_strict(inverse, self.blob_decls)
290            .map_err(|error| DbError::context("reverse blocked-write suffix", error))?;
291        }
292        super::local_blob_cleanup::reevaluate_suspended_blob_cleanup_on(
293            &tx,
294            self.blob_decls,
295            &suspended_cleanup,
296        )?;
297        let discarded_ids: Vec<_> = discarded
298            .into_iter()
299            .map(|(write_id, _)| write_id)
300            .collect();
301        let resolution = WriteResolution::Discarded;
302        crate::store::store_session::StoreTransaction::new(&tx, self.store_dir)
303            .resolve_unpublished_writes(
304                self.verified_store_authority,
305                &discarded_ids,
306                &resolution,
307            )?;
308        tx.commit().map_err(DbError::from)?;
309        Ok(BlockedWriteDiscard::Discarded(discarded_ids))
310    }
311}
312
313impl StoreDatabase {
314    #[doc(hidden)]
315    pub async fn pending_writes(&self) -> Result<Vec<PendingWrite>, DbError> {
316        self.call_store(|session| session.pending_writes()).await
317    }
318
319    #[cfg(any(test, feature = "test-utils"))]
320    pub async fn published_write_commits(
321        &self,
322    ) -> Result<Vec<coven_protocol::store_commit::StoreBatchCommitRef>, DbError> {
323        self.call_store(|session| session.published_write_commits())
324            .await
325    }
326
327    #[doc(hidden)]
328    pub async fn blocked_writes(&self) -> Result<Vec<PendingWrite>, DbError> {
329        Ok(self
330            .pending_writes()
331            .await?
332            .into_iter()
333            .filter(|write| matches!(write.status, WriteStatus::Blocked(_)))
334            .collect())
335    }
336
337    #[doc(hidden)]
338    pub async fn subscribe_write_status(
339        &self,
340        write_id: &WriteId,
341    ) -> Result<tokio::sync::watch::Receiver<WriteStatus>, DbError> {
342        let write_id = write_id.clone();
343        let current = self
344            .call_store({
345                let write_id = write_id.clone();
346                move |session| session.write_status(&write_id)
347            })
348            .await?;
349        Ok(self.subscribe_store_write_status(write_id, current))
350    }
351
352    pub async fn set_write_status(
353        &self,
354        write_id: &WriteId,
355        status: WriteStatus,
356    ) -> Result<(), DbError> {
357        let stored_id = write_id.clone();
358        let stored_status = status.clone();
359        self.call_store(move |session| session.set_write_status(&stored_id, &stored_status))
360            .await?;
361        self.notify_write_status(write_id.clone(), status);
362        Ok(())
363    }
364
365    pub async fn block_write_if_unresolved(
366        &self,
367        write_id: &WriteId,
368        block: coven_protocol::write::WriteBlock,
369    ) -> Result<bool, DbError> {
370        let write_id = write_id.clone();
371        let notified_write_id = write_id.clone();
372        let outcome = self
373            .call_store(move |session| session.block_write_if_unresolved(&write_id, block))
374            .await?;
375        if let Some(status) = outcome {
376            self.notify_write_status(notified_write_id, status);
377            Ok(true)
378        } else {
379            Ok(false)
380        }
381    }
382
383    /// Return one blocked write to publication. The next preparation attempt
384    /// revalidates every captured fact; another semantic failure records a fresh
385    /// `Blocked` status.
386    #[doc(hidden)]
387    pub async fn retry_blocked_write(&self, write_id: &WriteId) -> Result<Vec<WriteId>, DbError> {
388        let write_id = write_id.clone();
389        let retried = self
390            .call_store(move |session| session.retry_blocked_write(write_id))
391            .await?;
392        let retried_ids = retried
393            .iter()
394            .map(|(write_id, _)| write_id.clone())
395            .collect();
396        for (write_id, status) in retried {
397            self.notify_write_status(write_id, status);
398        }
399        Ok(retried_ids)
400    }
401
402    /// Atomically reverse a blocked write and every later unpublished shared
403    /// write whose working-row state depends on it.
404    #[doc(hidden)]
405    pub async fn discard_blocked_write(
406        &self,
407        write_id: &WriteId,
408    ) -> Result<BlockedWriteDiscard, DbError> {
409        let write_id = write_id.clone();
410        let discarded_ids = self
411            .call_store(move |session| session.discard_blocked_write(write_id))
412            .await?;
413        if let BlockedWriteDiscard::Discarded(discarded_ids) = &discarded_ids {
414            let status = WriteStatus::Resolved(WriteResolution::Discarded);
415            for discarded_id in discarded_ids {
416                self.notify_write_status(discarded_id.clone(), status.clone());
417            }
418        }
419        Ok(discarded_ids)
420    }
421}