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 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 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 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 #[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 #[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}