1use crate::*;
2use coven_protocol::write::{PendingWrite, WriteId, WriteResolution, WriteStatus};
3use std::sync::Arc;
4
5use super::*;
6
7#[derive(Debug, PartialEq, Eq)]
8pub enum BlockedWriteDiscard {
9 Discarded(Vec<coven_protocol::write::WriteId>),
10 RemoteResolutionRequired,
11}
12
13impl StoreSession<'_> {
14 fn pending_writes(&self) -> Result<Vec<PendingWrite>, DbError> {
15 let mut statement = self
16 .conn
17 .prepare(
18 "SELECT write_id, status, affected_rows FROM store_writes
19 WHERE status IN ('\"pending\"', '\"publishing\"')
20 OR json_extract(status, '$.blocked') IS NOT NULL
21 OR json_extract(status, '$.local_only_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 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 #[cfg(any(test, feature = "test-utils"))]
59 fn published_write_commits(
60 &self,
61 ) -> Result<Vec<coven_protocol::store_commit::StoreBatchCommitRef>, DbError> {
62 use coven_protocol::write::PublishedWrite;
63 let rows = crate::query_mapped_rows(
64 self.conn,
65 "SELECT status FROM store_writes ORDER BY ordinal",
66 [],
67 |row| row.get::<_, String>(0),
68 )?;
69 let mut commits = Vec::new();
70 for raw in rows {
71 let status: WriteStatus = serde_json::from_str(&raw)
72 .map_err(|error| DbError::context("published write status", error))?;
73 if let WriteStatus::Published(position) = status {
74 match *position {
75 PublishedWrite::Commit(position) => commits.push(position.commit),
76 PublishedWrite::Snapshot(_) => {
77 return Err(DbError::Message(
78 "published-write commit lookup encountered a snapshot-covered receipt"
79 .into(),
80 ));
81 }
82 }
83 }
84 }
85 Ok(commits)
86 }
87
88 fn set_write_status(&self, write_id: &WriteId, status: &WriteStatus) -> Result<(), DbError> {
89 Database::set_write_status_on(self.conn, write_id, status)
90 }
91
92 fn block_write_if_unresolved(
93 &self,
94 write_id: &WriteId,
95 block: coven_protocol::write::WriteBlock,
96 ) -> Result<Option<WriteStatus>, DbError> {
97 let raw: String = self
98 .conn
99 .query_row(
100 "SELECT status FROM store_writes WHERE write_id = ?1",
101 [write_id.as_str()],
102 |row| row.get(0),
103 )
104 .map_err(DbError::from)?;
105 let current: WriteStatus = serde_json::from_str(&raw).map_err(|error| {
106 DbError::context(format!("write {write_id} status before blocking"), error)
107 })?;
108 match current {
109 WriteStatus::Resolved(_) => Ok(None),
110 WriteStatus::Pending | WriteStatus::Publishing | WriteStatus::Blocked(_) => {
111 let blocked = WriteStatus::Blocked(block);
112 Database::set_write_status_on(self.conn, write_id, &blocked)?;
113 Ok(Some(blocked))
114 }
115 WriteStatus::LocalOnly | WriteStatus::LocalOnlyBlocked(_) => {
116 let blocked = WriteStatus::LocalOnlyBlocked(block);
117 Database::set_write_status_on(self.conn, write_id, &blocked)?;
118 Ok(Some(blocked))
119 }
120 state @ WriteStatus::Published(_) => Err(DbError::Message(format!(
121 "write {write_id} cannot become blocked from {state:?}",
122 ))),
123 }
124 }
125
126 fn retry_blocked_write(
127 &mut self,
128 write_id: WriteId,
129 ) -> Result<Vec<(WriteId, WriteStatus)>, DbError> {
130 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
131 if let Some(active) =
132 super::active_store_publication::load_active_store_publication_on(&tx)?
133 {
134 if active.owner() == &ActiveStorePublicationOwner::StoreWrite(write_id.clone())
135 && active.is_discarding()
136 {
137 return Err(DbError::Message(format!(
138 "discard of write {write_id} has begun and publication cannot resume"
139 )));
140 }
141 }
142 let (raw_status, prepared): (String, Option<String>) = tx
143 .query_row(
144 "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
145 [write_id.as_str()],
146 |row| Ok((row.get(0)?, row.get(1)?)),
147 )
148 .map_err(DbError::from)?;
149 let status: WriteStatus = serde_json::from_str(&raw_status)
150 .map_err(|error| DbError::context(format!("blocked write {write_id} status"), error))?;
151 if !matches!(
152 status,
153 WriteStatus::Blocked(_) | WriteStatus::LocalOnlyBlocked(_)
154 ) {
155 return Err(DbError::Message(format!("write {write_id} is not blocked")));
156 }
157 let local_only = matches!(status, WriteStatus::LocalOnlyBlocked(_));
158 if local_only && prepared.is_some() {
159 return Err(DbError::Message(format!(
160 "private-only write {write_id} carries a publication candidate"
161 )));
162 }
163 let next = if local_only {
164 WriteStatus::LocalOnly
165 } else if prepared.is_some() {
166 WriteStatus::Publishing
167 } else {
168 WriteStatus::Pending
169 };
170 let next_json = serde_json::to_string(&next)
171 .map_err(|error| DbError::context("serialize retry status", error))?;
172 let updated = tx
173 .execute(
174 "UPDATE store_writes SET status = ?2
175 WHERE write_id = ?1 AND status = ?3",
176 rusqlite::params![write_id.as_str(), next_json, raw_status],
177 )
178 .map_err(DbError::from)?;
179 if updated != 1 {
180 return Err(DbError::Message(format!(
181 "blocked write {write_id} changed during retry"
182 )));
183 }
184 let retried = vec![(write_id, next)];
185 tx.commit().map_err(DbError::from)?;
186 Ok(retried)
187 }
188
189 fn discard_blocked_write(&mut self, write_id: WriteId) -> Result<BlockedWriteDiscard, DbError> {
190 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
191 let (raw_status, target_ordinal): (String, i64) = tx
192 .query_row(
193 "SELECT status, ordinal FROM store_writes WHERE write_id = ?1",
194 [write_id.as_str()],
195 |row| Ok((row.get(0)?, row.get(1)?)),
196 )
197 .map_err(DbError::from)?;
198 let target_status: WriteStatus = serde_json::from_str(&raw_status)
199 .map_err(|error| DbError::context(format!("blocked write {write_id} status"), error))?;
200 if !matches!(
201 target_status,
202 WriteStatus::Blocked(_) | WriteStatus::LocalOnlyBlocked(_)
203 ) {
204 return Err(DbError::Message(format!("write {write_id} is not blocked")));
205 }
206
207 let mut statement = tx
208 .prepare(
209 "SELECT write_id, status, changeset_hash FROM store_writes
210 WHERE ordinal >= ?1
211 AND json_extract(status, '$.published') IS NULL
212 AND json_extract(status, '$.resolved') IS NULL
213 ORDER BY ordinal",
214 )
215 .map_err(DbError::from)?;
216 let rows = statement
217 .query_map([target_ordinal], |row| {
218 Ok((
219 row.get::<_, String>(0)?,
220 row.get::<_, String>(1)?,
221 row.get::<_, Option<String>>(2)?,
222 ))
223 })
224 .map_err(DbError::from)?;
225 let mut discarded = Vec::new();
226 for row in rows {
227 let (stored_id, raw_status, changeset_hash) = row.map_err(DbError::from)?;
228 let changeset_hash = changeset_hash.ok_or_else(|| {
232 DbError::Message(format!("unpublished write {stored_id} has been folded"))
233 })?;
234 let status: WriteStatus = serde_json::from_str(&raw_status)
235 .map_err(|error| DbError::context("discard write status", error))?;
236 if !matches!(
237 status,
238 WriteStatus::LocalOnly
239 | WriteStatus::LocalOnlyBlocked(_)
240 | WriteStatus::Pending
241 | WriteStatus::Blocked(_)
242 ) {
243 return Err(DbError::Message(format!(
244 "write {stored_id} after blocked write {write_id} has non-discardable status {status:?}"
245 )));
246 }
247 let discarded_id = WriteId::from_generated(stored_id);
248 let actual_hash =
249 match StoreRecords::new(&tx, self.store_dir).rebased_store_write(&discarded_id)? {
250 Some(rebased) => rebased.changeset_hash,
251 None => changeset_hash.parse::<coven_protocol::store_commit::ObjectHash>()?,
252 };
253 discarded.push((discarded_id, actual_hash));
254 }
255 drop(statement);
256 if discarded.first().map(|(stored_id, _)| stored_id) != Some(&write_id) {
257 return Err(DbError::Message(format!(
258 "blocked write {write_id} is absent from its unpublished suffix"
259 )));
260 }
261 for (discarded_id, _) in &discarded {
262 if !crate::store::store_session::StoreTransaction::new(&tx, self.store_dir)
263 .unpublished_write_cleanup_is_complete(
264 self.verified_store_authority,
265 discarded_id,
266 )?
267 {
268 return Ok(BlockedWriteDiscard::RemoteResolutionRequired);
269 }
270 }
271 let schema = Arc::new(crate::TableSchema::for_apply(
272 &tx,
273 self.synced_tables,
274 self.gates,
275 )?);
276 let store_transaction =
277 crate::store::store_session::StoreTransaction::new(&tx, self.store_dir);
278 let mut inverses = Vec::with_capacity(discarded.len());
279 let mut restored_blobs = Vec::new();
280 for (_, changeset_hash) in discarded.iter().rev() {
281 let changeset = store_transaction.payload(*changeset_hash)?;
282 let inverse = StoreDatabase::invert_changeset(&changeset)?;
283 for change in crate::walk_changeset(&inverse).map_err(DbError::Changeset)? {
284 if let Some(blob) = self
285 .blob_decls
286 .ref_from_change(&change)
287 .map_err(DbError::from)?
288 {
289 restored_blobs.push(blob);
290 }
291 }
292 let inverse = crate::ValidatedChangeset::new(inverse, schema.clone())
293 .map_err(|error| DbError::context("invalid blocked-write inverse", error))?;
294 inverses.push(inverse);
295 }
296 let suspended_cleanup =
297 super::local_blob_cleanup::suspend_leased_blob_cleanup_for_restoration_on(
298 &tx,
299 &restored_blobs,
300 )?;
301 for inverse in inverses {
302 MergeMaterializationTransaction::from_store(
303 crate::store::store_session::StoreTransaction::new(&tx, self.store_dir),
304 )
305 .apply_changeset_strict(inverse, self.blob_decls)
306 .map_err(|error| DbError::context("reverse blocked-write suffix", error))?;
307 }
308 super::local_blob_cleanup::reevaluate_suspended_blob_cleanup_on(
309 &tx,
310 self.blob_decls,
311 &suspended_cleanup,
312 )?;
313 let discarded_ids: Vec<_> = discarded
314 .into_iter()
315 .map(|(write_id, _)| write_id)
316 .collect();
317 let resolution = WriteResolution::Discarded;
318 crate::store::store_session::StoreTransaction::new(&tx, self.store_dir)
319 .resolve_unpublished_writes(
320 self.verified_store_authority,
321 &discarded_ids,
322 &resolution,
323 )?;
324 tx.commit().map_err(DbError::from)?;
325 Ok(BlockedWriteDiscard::Discarded(discarded_ids))
326 }
327}
328
329impl StoreDatabase {
330 #[doc(hidden)]
331 pub async fn pending_writes(&self) -> Result<Vec<PendingWrite>, DbError> {
332 self.call_store(|session| session.pending_writes()).await
333 }
334
335 #[cfg(any(test, feature = "test-utils"))]
336 pub async fn published_write_commits(
337 &self,
338 ) -> Result<Vec<coven_protocol::store_commit::StoreBatchCommitRef>, DbError> {
339 self.call_store(|session| session.published_write_commits())
340 .await
341 }
342
343 #[doc(hidden)]
344 pub async fn blocked_writes(&self) -> Result<Vec<PendingWrite>, DbError> {
345 Ok(self
346 .pending_writes()
347 .await?
348 .into_iter()
349 .filter(|write| {
350 matches!(
351 write.status,
352 WriteStatus::Blocked(_) | WriteStatus::LocalOnlyBlocked(_)
353 )
354 })
355 .collect())
356 }
357
358 #[doc(hidden)]
359 pub async fn subscribe_write_status(
360 &self,
361 write_id: &WriteId,
362 ) -> Result<tokio::sync::watch::Receiver<WriteStatus>, DbError> {
363 let write_id = write_id.clone();
364 let current = self
365 .call_store({
366 let write_id = write_id.clone();
367 move |session| session.write_status(&write_id)
368 })
369 .await?;
370 Ok(self.subscribe_store_write_status(write_id, current))
371 }
372
373 pub async fn set_write_status(
374 &self,
375 write_id: &WriteId,
376 status: WriteStatus,
377 ) -> Result<(), DbError> {
378 let stored_id = write_id.clone();
379 let stored_status = status.clone();
380 self.call_store(move |session| session.set_write_status(&stored_id, &stored_status))
381 .await?;
382 self.notify_write_status(write_id.clone(), status);
383 Ok(())
384 }
385
386 pub async fn block_write_if_unresolved(
387 &self,
388 write_id: &WriteId,
389 block: coven_protocol::write::WriteBlock,
390 ) -> Result<bool, DbError> {
391 let write_id = write_id.clone();
392 let notified_write_id = write_id.clone();
393 let outcome = self
394 .call_store(move |session| session.block_write_if_unresolved(&write_id, block))
395 .await?;
396 if let Some(status) = outcome {
397 self.notify_write_status(notified_write_id, status);
398 Ok(true)
399 } else {
400 Ok(false)
401 }
402 }
403
404 #[doc(hidden)]
408 pub async fn retry_blocked_write(&self, write_id: &WriteId) -> Result<Vec<WriteId>, DbError> {
409 let write_id = write_id.clone();
410 let retried = self
411 .call_store(move |session| session.retry_blocked_write(write_id))
412 .await?;
413 let retried_ids = retried
414 .iter()
415 .map(|(write_id, _)| write_id.clone())
416 .collect();
417 for (write_id, status) in retried {
418 self.notify_write_status(write_id, status);
419 }
420 Ok(retried_ids)
421 }
422
423 #[doc(hidden)]
426 pub async fn discard_blocked_write(
427 &self,
428 write_id: &WriteId,
429 ) -> Result<BlockedWriteDiscard, DbError> {
430 let write_id = write_id.clone();
431 let discarded_ids = self
432 .call_store(move |session| session.discard_blocked_write(write_id))
433 .await?;
434 if let BlockedWriteDiscard::Discarded(discarded_ids) = &discarded_ids {
435 let status = WriteStatus::Resolved(WriteResolution::Discarded);
436 for discarded_id in discarded_ids {
437 self.notify_write_status(discarded_id.clone(), status.clone());
438 }
439 }
440 Ok(discarded_ids)
441 }
442}