1use rusqlite::OptionalExtension;
2
3use super::*;
4use crate::{
5 insert_store_reclaim_operation_on, load_remote_object_on, load_store_reclaim_operation_on,
6 parse_store_reclaim_operation, persist_exact_remote_object_on,
7 record_reclaimed_store_package_on, store_reclaim_journal_error,
8 update_store_reclaim_operation_on, ActiveStorePublication, ActiveStorePublicationOwner,
9};
10use coven_protocol::remote_object::{remote_object_id, RetainedReplayOwner};
11use coven_protocol::store_commit::{ObjectHash, StoreBatchCommitRef, StorePackageRef};
12
13pub mod journal;
14
15impl VerifiedStoreTransaction<'_, '_, '_, '_> {
16 pub(super) fn advance_snapshot_replay_baseline(
19 &mut self,
20 root: &coven_protocol::store_commit::StoreRootRef,
21 snapshot_authority: &coven_protocol::store_commit::RetainedReplaySnapshotAuthority,
22 schema_version: u32,
23 routing_hash: ObjectHash,
24 routing_encryption: Option<&coven_keys::encryption::EncryptionService>,
25 ) -> Result<Option<(crate::AdvancedReplayBaseline, bool)>, DbError> {
26 let cut = &snapshot_authority.metadata.coverage;
27 if !crate::store::store_session::replay_baseline_advances_on(
28 crate::store::store_session::StoreRecords::new(
29 self.store.transaction,
30 self.store.store_dir,
31 ),
32 &snapshot_authority.snapshot,
33 cut,
34 )? {
35 return Ok(None);
36 }
37 let current_cut = coven_protocol::store_commit::CommitFrontier::from_refs(
38 crate::store::materialized_commit_index::materialized_frontier_on(
39 self.store.transaction,
40 None,
41 )?,
42 )
43 .map_err(DbError::from)?;
44 if !current_cut.covers(cut) {
45 return Err(DbError::Message(
46 "accepted Store snapshot is ahead of the locally reconstructable frontier"
47 .to_string(),
48 ));
49 }
50 let installed = super::retained_replay::load_replay_baseline_metadata_on(
51 StoreRecords::new(self.store.transaction, self.store.store_dir),
52 )?
53 .ok_or_else(|| {
54 DbError::Message("snapshot advance has no installed baseline".to_string())
55 })?;
56 let changes_publication_base = !matches!(&installed.authority,
57 crate::RetainedReplayAuthority::InstalledSnapshot(previous) if previous.snapshot == snapshot_authority.snapshot);
58 let (image, folded) = self.capture_replay_baseline_at_cut(
59 root,
60 cut,
61 ¤t_cut,
62 snapshot_authority.snapshot.snapshot_hash,
63 routing_encryption,
64 )?;
65 let advanced = self.store.install_advanced_replay_baseline(
66 self.authority,
67 root,
68 schema_version,
69 routing_hash,
70 snapshot_authority.clone(),
71 image,
72 &folded,
73 self.blob_decls,
74 self.synced_tables,
75 )?;
76 self.authority.forget_superseded_replay_baseline();
77 Ok(Some((advanced, changes_publication_base)))
78 }
79}
80
81impl StoreSession<'_> {
82 fn begin_store_reclaim_operation(
83 &mut self,
84 operation: DurableStoreReclaimOperation,
85 remotes: Vec<coven_protocol::remote_object::ClosedRemoteObject>,
86 ) -> Result<DurableStoreReclaimOperation, DbError> {
87 let conn = self.conn;
88 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
89 let operation_id = operation.operation_id();
90 let candidate = operation.candidate().ok_or_else(|| {
91 DbError::Message("Store reclaim operation has no publication candidate".to_string())
92 })?;
93 let active_publication = ActiveStorePublication::for_commit(
94 ActiveStorePublicationOwner::Reclaim(operation_id),
95 candidate,
96 )?;
97 if let Some(existing) = load_store_reclaim_operation_on(&tx, operation_id)? {
98 if existing != operation {
99 return Err(DbError::Message(format!(
100 "Store reclaim operation {operation_id} already has different durable state"
101 )));
102 }
103 if !super::active_store_publication::load_active_store_publication_on(&tx)?
104 .is_some_and(|existing| existing.same_commit_reservation(&active_publication))
105 {
106 return Err(DbError::Message(format!(
107 "Store reclaim operation {operation_id} differs from its active publication"
108 )));
109 }
110 return Ok(existing);
111 }
112 match super::active_store_publication::claim_active_store_publication_on(
113 &tx,
114 &active_publication,
115 )? {
116 super::active_store_publication::ActiveStorePublicationClaim::Acquired => {}
117 super::active_store_publication::ActiveStorePublicationClaim::AlreadyOwned => {
118 return Err(DbError::Message(
119 "Store reclaim owns publication before its journal".to_string(),
120 ));
121 }
122 super::active_store_publication::ActiveStorePublicationClaim::Occupied(owner) => {
123 return Err(DbError::Message(format!(
124 "another local Store operation owns publication: {owner:?}"
125 )));
126 }
127 }
128 for remote in &remotes {
129 persist_exact_remote_object_on(
130 &tx,
131 self.store_dir,
132 remote,
133 "Store reclaim candidate object",
134 )?;
135 }
136 insert_store_reclaim_operation_on(&tx, &operation)?;
137 tx.commit().map_err(DbError::from)?;
138 Ok(operation)
139 }
140
141 fn advance_snapshot_replay_baseline(
143 &mut self,
144 root: &coven_protocol::store_commit::StoreRootRef,
145 snapshot_authority: coven_protocol::store_commit::RetainedReplaySnapshotAuthority,
146 routing_encryption: Option<&coven_keys::encryption::EncryptionService>,
147 membership: coven_protocol::membership::LocalStoreMembership,
148 ) -> Result<Option<crate::AdvancedReplayBaseline>, DbError> {
149 let schema_version = self.schema_version;
150 let routing_hash = self.sync_routing_hash;
151 self.verified_store_transaction(|transaction| {
152 let current = super::observed_store_publication::load_store_current_publication_on(
153 transaction.store.transaction,
154 )?;
155 let accepted = current
156 .record()
157 .latest_snapshot()
158 .filter(|accepted| accepted.snapshot == snapshot_authority.snapshot)
159 .ok_or_else(|| {
160 DbError::Message(
161 "replay baseline must adopt the current accepted snapshot".to_string(),
162 )
163 })?;
164 let Some((advanced, changes_publication_base)) = transaction
165 .advance_snapshot_replay_baseline(
166 root,
167 &snapshot_authority,
168 schema_version,
169 routing_hash,
170 routing_encryption,
171 )?
172 else {
173 return Ok(StoreTransactionOutcome::Rollback(None));
174 };
175 let routing_key = if transaction.gates.has_scoped_graph() {
176 let encryption = routing_encryption.ok_or_else(|| {
177 DbError::Message(
178 "scoped write rebase requires Store routing encryption".to_string(),
179 )
180 })?;
181 Some(coven_protocol::circle::derive_row_routing_key(
182 encryption,
183 root.store_root_hash,
184 )?)
185 } else {
186 None
187 };
188 if changes_publication_base {
189 transaction.rebase_unpublished_store_writes(
190 accepted,
191 routing_key.as_ref(),
192 membership,
193 )?;
194 }
195 super::observed_store_publication::retire_store_publication_prefix_before_snapshot_on(
196 transaction.store,
197 transaction.authority,
198 &accepted.publication,
199 )?;
200 Ok(StoreTransactionOutcome::Commit(Some(advanced)))
201 })
202 }
203
204 fn store_package_is_retained_for_replay(
205 &mut self,
206 root: &coven_protocol::store_commit::StoreRootRef,
207 target: &StorePackageRef,
208 activation: &StoreBatchCommitRef,
209 ) -> Result<bool, DbError> {
210 let object_id = remote_object_id(&target.object);
211 let exists: bool = self
212 .conn
213 .query_row(
214 "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
215 [object_id.to_string()],
216 |row| row.get(0),
217 )
218 .map_err(DbError::from)?;
219 if !exists {
220 return Ok(false);
221 }
222 let remote = load_remote_object_on(self.conn, object_id)?;
223 let retained = remote
224 .store_package_is_retained_for_replay(target, activation)
225 .map_err(|error| {
226 DbError::context(
227 format!("validate Store package {object_id} replay ownership"),
228 error,
229 )
230 })?;
231 if !retained {
232 return Ok(false);
233 }
234 for owner in remote.retained_replay_owners() {
235 let RetainedReplayOwner::Commit { commit, input_hash } = owner;
236 let retained = self
237 .verified_store_authority
238 .validate_retained_materialization_by_ref_on(
239 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
240 commit,
241 )?;
242 if retained.root() != root || retained.input_hash() != *input_hash {
243 return Err(DbError::Message(
244 "Store package replay owner differs from retained materialization".to_string(),
245 ));
246 }
247 }
248 Ok(true)
249 }
250
251 fn circle_package_is_retained_for_replay(
252 &mut self,
253 root: &coven_protocol::store_commit::StoreRootRef,
254 target: &coven_protocol::store_commit::CirclePackageRef,
255 activation: &StoreBatchCommitRef,
256 ) -> Result<bool, DbError> {
257 let object_id = remote_object_id(&target.package.object);
258 let exists: bool = self
259 .conn
260 .query_row(
261 "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
262 [object_id.to_string()],
263 |row| row.get(0),
264 )
265 .map_err(DbError::from)?;
266 if !exists {
267 return Ok(false);
268 }
269 let remote = load_remote_object_on(self.conn, object_id)?;
270 let retained = remote
271 .circle_package_is_retained_for_replay(target, activation)
272 .map_err(|error| {
273 DbError::context(
274 format!("validate Circle package {object_id} replay ownership"),
275 error,
276 )
277 })?;
278 if !retained {
279 return Ok(false);
280 }
281 for owner in remote.retained_replay_owners() {
282 let RetainedReplayOwner::Commit { commit, input_hash } = owner;
283 let retained = self
284 .verified_store_authority
285 .validate_retained_materialization_by_ref_on(
286 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
287 commit,
288 )?;
289 if retained.root() != root || retained.input_hash() != *input_hash {
290 return Err(DbError::Message(
291 "Circle package replay owner differs from retained materialization".to_string(),
292 ));
293 }
294 }
295 Ok(true)
296 }
297
298 fn circle_image_is_retained_for_replay(
299 &self,
300 circle_id: coven_protocol::circle::CircleId,
301 image: &coven_protocol::store_commit::SnapshotImageRef,
302 ) -> Result<bool, DbError> {
303 let row: Option<Vec<u8>> = self
304 .conn
305 .query_row(
306 "SELECT bootstrap_ref FROM circle_bootstrap_coverage WHERE circle_id = ?1",
307 [circle_id.to_string()],
308 |row| row.get(0),
309 )
310 .optional()
311 .map_err(DbError::from)?;
312 let Some(bootstrap_ref) = row else {
313 return Ok(false);
314 };
315 let bootstrap: coven_protocol::circle::CircleBootstrapRef =
316 serde_json::from_slice(&bootstrap_ref).map_err(|error| {
317 DbError::context("parse retained Circle bootstrap reference", error)
318 })?;
319 Ok(bootstrap.image == *image)
320 }
321
322 fn stored_blob_reclaim_candidates(
323 &self,
324 ) -> Result<
325 Vec<(
326 coven_protocol::blob::locator::StoredBlobRef,
327 Vec<StoreBatchCommitRef>,
328 )>,
329 DbError,
330 > {
331 let conn = self.conn;
332 let mut statement = conn
333 .prepare("SELECT remote_object_id FROM blob_locators ORDER BY remote_object_id")
334 .map_err(DbError::from)?;
335 let object_ids = statement
336 .query_map([], |row| row.get::<_, String>(0))
337 .map_err(DbError::from)?
338 .collect::<Result<Vec<_>, _>>()
339 .map_err(DbError::from)?;
340 drop(statement);
341 let mut candidates = Vec::new();
342 for object_id in object_ids {
343 let parsed = object_id.parse().map_err(|error| {
344 DbError::context(format!("stored blob object id {object_id:?}"), error)
345 })?;
346 let remote = load_remote_object_on(conn, parsed)?;
347 if !remote.is_activated_stored_blob() {
348 continue;
349 }
350 let Some(locator_bytes) = remote.payloads().carried_locator_bytes() else {
351 return Err(DbError::Message(format!(
352 "stored blob {object_id} carries no locator"
353 )));
354 };
355 let locator = coven_protocol::blob::locator::BlobLocator::parse(locator_bytes)
356 .map_err(|error| {
357 DbError::context(format!("stored blob {object_id} locator"), error)
358 })?;
359 let stored =
360 coven_protocol::blob::locator::StoredBlobRef::new(locator, remote.object().clone())
361 .map_err(|error| {
362 DbError::context(format!("stored blob {object_id} reference"), error)
363 })?;
364 candidates.push((stored, remote.stored_blob_commit_owners()));
365 }
366 Ok(candidates)
367 }
368
369 fn stored_blob_is_row_orphaned(
370 &self,
371 stored: &coven_protocol::blob::locator::StoredBlobRef,
372 ) -> Result<bool, DbError> {
373 match crate::Database::stored_blob_reference_state_on(
374 self.conn,
375 self.gates,
376 self.synced_tables,
377 stored,
378 )? {
379 crate::StoredBlobReferenceState::NotLiveRemote => Ok(true),
380 crate::StoredBlobReferenceState::LiveRemote => Ok(false),
381 crate::StoredBlobReferenceState::Unresolved => Err(DbError::Message(format!(
382 "stored blob {} has a live reference whose locality is unresolved",
383 remote_object_id(stored.object())
384 ))),
385 }
386 }
387
388 fn audience_blob_is_retained_for_replay(
389 &self,
390 stored: &coven_protocol::blob::locator::StoredBlobRef,
391 ) -> Result<bool, DbError> {
392 let conn = self.conn;
393 let object_id = remote_object_id(stored.object());
394 let exists: bool = conn
395 .query_row(
396 "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
397 [object_id.to_string()],
398 |row| row.get(0),
399 )
400 .map_err(DbError::from)?;
401 if exists {
402 let remote = load_remote_object_on(conn, object_id)?;
403 if remote.snapshot_owners().next().is_some()
404 || remote.retained_replay_owners().next().is_some()
405 {
406 return Ok(true);
407 }
408 }
409 let mut statement = conn
410 .prepare("SELECT bootstrap_ref FROM circle_bootstrap_coverage")
411 .map_err(DbError::from)?;
412 let coverages = statement
413 .query_map([], |row| row.get::<_, Vec<u8>>(0))
414 .map_err(DbError::from)?
415 .collect::<Result<Vec<_>, _>>()
416 .map_err(DbError::from)?;
417 drop(statement);
418 for bytes in coverages {
419 let bootstrap: coven_protocol::circle::CircleBootstrapRef =
420 serde_json::from_slice(&bytes).map_err(|error| {
421 DbError::context("parse retained Circle bootstrap reference", error)
422 })?;
423 if bootstrap
424 .blobs
425 .iter()
426 .any(|blob| blob.stored() == Some(stored))
427 {
428 return Ok(true);
429 }
430 }
431 Ok(false)
432 }
433
434 fn package_is_retained_by_pending_blob_reclaim(
444 &self,
445 package: &coven_protocol::objects::ExactObjectRef,
446 ) -> Result<bool, DbError> {
447 let package_id = remote_object_id(package);
448 Ok(self.store_reclaim_operations()?.iter().any(|operation| {
449 if matches!(operation, DurableStoreReclaimOperation::Completed { .. }) {
450 return false;
451 }
452 match operation.authorization().target() {
453 coven_protocol::reclaim::ReclaimTarget::AudienceBlob(
454 coven_protocol::reclaim::AudienceBlobReclaimTarget::Circle { source, .. },
455 ) => remote_object_id(&source.package.package.object) == package_id,
456 _ => false,
457 }
458 }))
459 }
460
461 fn store_reclaim_journal(
466 &self,
467 ) -> Result<Vec<(DurableStoreReclaimOperation, Option<String>)>, DbError> {
468 let mut statement = self
469 .conn
470 .prepare(
471 "SELECT authorization_hash, state, stuck_error FROM store_reclaim_operations
472 ORDER BY authorization_hash",
473 )
474 .map_err(DbError::from)?;
475 let rows = statement
476 .query_map([], |row| {
477 Ok((
478 row.get::<_, String>(0)?,
479 row.get::<_, String>(1)?,
480 row.get::<_, Option<String>>(2)?,
481 ))
482 })
483 .map_err(DbError::from)?
484 .collect::<Result<Vec<_>, _>>()
485 .map_err(DbError::from)?;
486 rows.into_iter()
487 .map(|(raw_id, raw, stuck_error)| {
488 let id = raw_id
489 .parse()
490 .map_err(|error| DbError::context("Store reclaim operation id", error))?;
491 Ok((parse_store_reclaim_operation(id, &raw)?, stuck_error))
492 })
493 .collect()
494 }
495
496 fn store_reclaim_operations(&self) -> Result<Vec<DurableStoreReclaimOperation>, DbError> {
497 Ok(self
498 .store_reclaim_journal()?
499 .into_iter()
500 .map(|(operation, _)| operation)
501 .collect())
502 }
503
504 fn runnable_store_reclaim_operations(
505 &self,
506 ) -> Result<Vec<DurableStoreReclaimOperation>, DbError> {
507 Ok(self
508 .store_reclaim_journal()?
509 .into_iter()
510 .filter_map(|(operation, stuck_error)| stuck_error.is_none().then_some(operation))
511 .collect())
512 }
513
514 fn stuck_reclaim_operations(&self) -> Result<Vec<StuckReclaimOperation>, DbError> {
515 Ok(self
516 .store_reclaim_journal()?
517 .into_iter()
518 .filter_map(|(operation, stuck_error)| {
519 stuck_error.map(|error| StuckReclaimOperation {
520 operation_id: operation.operation_id(),
521 target: operation.authorization().target().clone(),
522 error,
523 })
524 })
525 .collect())
526 }
527
528 fn mark_store_reclaim_operation_stuck(
529 &mut self,
530 operation_id: ObjectHash,
531 error: String,
532 ) -> Result<(), DbError> {
533 crate::mark_store_reclaim_operation_stuck_on(self.conn, operation_id, &error)
534 }
535
536 fn retry_stuck_reclaim_operation(&mut self, operation_id: ObjectHash) -> Result<(), DbError> {
537 crate::clear_store_reclaim_operation_stuck_on(self.conn, operation_id)
538 }
539
540 fn begin_store_reclaim_receipt(
541 &mut self,
542 expected: DurableStoreReclaimOperation,
543 next: DurableStoreReclaimOperation,
544 remotes: Vec<coven_protocol::remote_object::ClosedRemoteObject>,
545 ) -> Result<DurableStoreReclaimOperation, DbError> {
546 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
547 let current = load_store_reclaim_operation_on(&tx, expected.operation_id())?
548 .ok_or_else(|| DbError::Message("Store reclaim operation disappeared".to_string()))?;
549 if current != expected {
550 return Err(DbError::Message(
551 "Store reclaim operation changed before receipt preparation".to_string(),
552 ));
553 }
554 let candidate = next.candidate().ok_or_else(|| {
555 DbError::Message("Store reclaim receipt has no publication candidate".to_string())
556 })?;
557 let active_publication = ActiveStorePublication::for_commit(
558 ActiveStorePublicationOwner::Reclaim(next.operation_id()),
559 candidate,
560 )?;
561 match super::active_store_publication::claim_active_store_publication_on(
562 &tx,
563 &active_publication,
564 )? {
565 super::active_store_publication::ActiveStorePublicationClaim::Acquired => {}
566 super::active_store_publication::ActiveStorePublicationClaim::AlreadyOwned => {
567 return Err(DbError::Message(
568 "Store reclaim receipt owns publication before its journal".to_string(),
569 ));
570 }
571 super::active_store_publication::ActiveStorePublicationClaim::Occupied(owner) => {
572 return Err(DbError::Message(format!(
573 "another local Store operation owns publication: {owner:?}"
574 )));
575 }
576 }
577 for remote in &remotes {
578 persist_exact_remote_object_on(
579 &tx,
580 self.store_dir,
581 remote,
582 "Store reclaim receipt candidate",
583 )?;
584 }
585 update_store_reclaim_operation_on(&tx, &expected, &next)?;
586 tx.commit().map_err(DbError::from)?;
587 Ok(next)
588 }
589
590 fn mark_store_reclaim_target_absent(
591 &mut self,
592 expected: DurableStoreReclaimOperation,
593 next: DurableStoreReclaimOperation,
594 reclaimed: ReclaimedStorePackage,
595 ) -> Result<DurableStoreReclaimOperation, DbError> {
596 let root = self.required_root_authority()?;
597 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
598 let current = load_store_reclaim_operation_on(&tx, expected.operation_id())?
599 .ok_or_else(|| DbError::Message("Store reclaim operation disappeared".to_string()))?;
600 if current != expected {
601 return Err(DbError::Message(
602 "Store reclaim operation changed before absence recording".to_string(),
603 ));
604 }
605 record_reclaimed_store_package_on(&tx, Some(root.store_root_hash), &reclaimed)?;
606 update_store_reclaim_operation_on(&tx, &expected, &next)?;
607 tx.commit().map_err(DbError::from)?;
608 Ok(next)
609 }
610
611 #[cfg(any(test, feature = "test-utils"))]
612 fn stored_blob_has_snapshot_owner_for_test(
613 &self,
614 stored: &coven_protocol::blob::locator::StoredBlobRef,
615 ) -> Result<bool, DbError> {
616 let remote = load_remote_object_on(self.conn, remote_object_id(stored.object()))?;
617 let pinned = remote.snapshot_owners().next().is_some();
618 Ok(pinned)
619 }
620}
621
622impl StoreDatabase {
623 pub async fn replay_baseline_would_advance(
626 &self,
627 snapshot: coven_protocol::store_commit::StoreSnapshotRef,
628 cut: coven_protocol::store_commit::CommitFrontier,
629 ) -> Result<bool, DbError> {
630 self.call_store(move |session| {
631 crate::store::store_session::replay_baseline_advances_on(
632 crate::store::store_session::StoreRecords::new(session.conn, session.store_dir),
633 &snapshot,
634 &cut,
635 )
636 })
637 .await
638 }
639
640 pub async fn begin_store_reclaim_operation(
641 &self,
642 operation: DurableStoreReclaimOperation,
643 ) -> Result<DurableStoreReclaimOperation, DbError> {
644 operation.validate().map_err(store_reclaim_journal_error)?;
645 let DurableStoreReclaimOperation::AuthorizationCandidate { .. } = &operation else {
646 return Err(DbError::Message(
647 "a new Store reclaim operation must own an activation candidate".to_string(),
648 ));
649 };
650 let remotes = match &operation {
651 DurableStoreReclaimOperation::AuthorizationCandidate { object, candidate } => object
652 .remote_objects(candidate)
653 .map_err(store_reclaim_journal_error)?,
654 _ => unreachable!("matched reclaim candidate"),
655 };
656 self.call_store(move |session| session.begin_store_reclaim_operation(operation, remotes))
657 .await
658 }
659
660 pub async fn advance_snapshot_replay_baseline(
667 &self,
668 root: coven_protocol::store_commit::StoreRootRef,
669 authority: crate::VerifiedStoreSnapshotAuthority,
670 routing_encryption: Option<coven_keys::encryption::EncryptionService>,
671 membership: coven_protocol::membership::LocalStoreMembership,
672 ) -> Result<Option<crate::AdvancedReplayBaseline>, DbError> {
673 let authority = authority.into_authority();
674 self.call_store(move |session| {
675 session.advance_snapshot_replay_baseline(
676 &root,
677 authority,
678 routing_encryption.as_ref(),
679 membership,
680 )
681 })
682 .await
683 }
684
685 pub async fn store_package_is_retained_for_replay(
686 &self,
687 root: coven_protocol::store_commit::StoreRootRef,
688 target: StorePackageRef,
689 activation: StoreBatchCommitRef,
690 ) -> Result<bool, DbError> {
691 self.call_store(move |session| {
692 session.store_package_is_retained_for_replay(&root, &target, &activation)
693 })
694 .await
695 }
696
697 pub async fn circle_package_is_retained_for_replay(
698 &self,
699 root: coven_protocol::store_commit::StoreRootRef,
700 target: coven_protocol::store_commit::CirclePackageRef,
701 activation: StoreBatchCommitRef,
702 ) -> Result<bool, DbError> {
703 self.call_store(move |session| {
704 session.circle_package_is_retained_for_replay(&root, &target, &activation)
705 })
706 .await
707 }
708
709 pub async fn circle_bootstrap_image_is_retained_for_replay(
715 &self,
716 coverage: coven_protocol::circle::CircleBootstrapCoverageRef,
717 ) -> Result<bool, DbError> {
718 self.circle_image_is_retained_for_replay(
719 coverage.circle_id,
720 coverage.bootstrap.image.clone(),
721 )
722 .await
723 }
724
725 pub async fn circle_image_is_retained_for_replay(
731 &self,
732 circle_id: coven_protocol::circle::CircleId,
733 image: coven_protocol::store_commit::SnapshotImageRef,
734 ) -> Result<bool, DbError> {
735 self.call_store(move |session| {
736 session.circle_image_is_retained_for_replay(circle_id, &image)
737 })
738 .await
739 }
740
741 pub async fn stored_blob_reclaim_candidates(
746 &self,
747 ) -> Result<
748 Vec<(
749 coven_protocol::blob::locator::StoredBlobRef,
750 Vec<StoreBatchCommitRef>,
751 )>,
752 DbError,
753 > {
754 self.call_store(|session| session.stored_blob_reclaim_candidates())
755 .await
756 }
757
758 pub async fn stored_blob_is_row_orphaned(
764 &self,
765 stored: coven_protocol::blob::locator::StoredBlobRef,
766 ) -> Result<bool, DbError> {
767 self.call_store(move |session| session.stored_blob_is_row_orphaned(&stored))
768 .await
769 }
770
771 pub async fn audience_blob_is_retained_for_replay(
782 &self,
783 stored: coven_protocol::blob::locator::StoredBlobRef,
784 ) -> Result<bool, DbError> {
785 self.call_store(move |session| session.audience_blob_is_retained_for_replay(&stored))
786 .await
787 }
788
789 pub async fn package_is_retained_by_pending_blob_reclaim(
792 &self,
793 package: coven_protocol::objects::ExactObjectRef,
794 ) -> Result<bool, DbError> {
795 self.call_store(move |session| {
796 session.package_is_retained_by_pending_blob_reclaim(&package)
797 })
798 .await
799 }
800
801 pub async fn store_reclaim_operations(
805 &self,
806 ) -> Result<Vec<DurableStoreReclaimOperation>, DbError> {
807 self.call_store(|session| session.store_reclaim_operations())
808 .await
809 }
810
811 pub async fn runnable_store_reclaim_operations(
814 &self,
815 ) -> Result<Vec<DurableStoreReclaimOperation>, DbError> {
816 self.call_store(|session| session.runnable_store_reclaim_operations())
817 .await
818 }
819
820 pub async fn stuck_reclaim_operations(&self) -> Result<Vec<StuckReclaimOperation>, DbError> {
823 self.call_store(|session| session.stuck_reclaim_operations())
824 .await
825 }
826
827 pub async fn mark_store_reclaim_operation_stuck(
830 &self,
831 operation_id: ObjectHash,
832 error: String,
833 ) -> Result<(), DbError> {
834 self.call_store(move |session| {
835 session.mark_store_reclaim_operation_stuck(operation_id, error)
836 })
837 .await
838 }
839
840 pub async fn retry_stuck_reclaim_operation(
843 &self,
844 operation_id: ObjectHash,
845 ) -> Result<(), DbError> {
846 self.call_store(move |session| session.retry_stuck_reclaim_operation(operation_id))
847 .await
848 }
849
850 pub async fn begin_store_reclaim_receipt(
851 &self,
852 expected: DurableStoreReclaimOperation,
853 object: DurableStoreReclaimObject,
854 candidate: coven_protocol::prepared_commit::PreparedStoreOperationCommit,
855 ) -> Result<DurableStoreReclaimOperation, DbError> {
856 let DurableStoreReclaimOperation::AbsentVerified {
857 authorization,
858 authorization_activation,
859 ..
860 } = &expected
861 else {
862 return Err(DbError::Message(
863 "only an authorized reclaim can prepare a receipt".to_string(),
864 ));
865 };
866 let next = DurableStoreReclaimOperation::ReceiptCandidate {
867 authorization: authorization.clone(),
868 authorization_activation: authorization_activation.clone(),
869 object: Box::new(object),
870 candidate: Box::new(candidate),
871 };
872 next.validate().map_err(store_reclaim_journal_error)?;
873 let remotes = match &next {
874 DurableStoreReclaimOperation::ReceiptCandidate {
875 object, candidate, ..
876 } => object
877 .remote_objects(candidate)
878 .map_err(store_reclaim_journal_error)?,
879 _ => unreachable!("constructed receipt candidate"),
880 };
881 self.call_store(move |session| session.begin_store_reclaim_receipt(expected, next, remotes))
882 .await
883 }
884
885 pub async fn mark_store_reclaim_target_absent(
886 &self,
887 expected: DurableStoreReclaimOperation,
888 target: coven_protocol::reclaim::ReclaimTarget,
889 ) -> Result<DurableStoreReclaimOperation, DbError> {
890 let DurableStoreReclaimOperation::Authorized {
891 authorization,
892 activation,
893 } = &expected
894 else {
895 return Err(DbError::Message(
896 "only an authorized reclaim can record target absence".to_string(),
897 ));
898 };
899 if &target != authorization.target() {
900 return Err(DbError::Message(
901 "verified reclaim target differs from its signed exact reference".to_string(),
902 ));
903 }
904 let next = DurableStoreReclaimOperation::AbsentVerified {
905 authorization: authorization.clone(),
906 authorization_activation: activation.clone(),
907 target,
908 };
909 let reclaimed =
910 ReclaimedStorePackage::absent_verified(authorization.clone(), activation.clone())
911 .map_err(store_reclaim_journal_error)?;
912 next.validate().map_err(store_reclaim_journal_error)?;
913 self.call_store(move |session| {
914 session.mark_store_reclaim_target_absent(expected, next, reclaimed)
915 })
916 .await
917 }
918
919 #[cfg(any(test, feature = "test-utils"))]
922 pub async fn stored_blob_has_snapshot_owner_for_test(
923 &self,
924 stored: coven_protocol::blob::locator::StoredBlobRef,
925 ) -> Result<bool, DbError> {
926 self.call_store(move |session| session.stored_blob_has_snapshot_owner_for_test(&stored))
927 .await
928 }
929
930 #[cfg(any(test, feature = "test-utils"))]
931 pub async fn stored_blob_reclaim_candidates_for_test(
932 &self,
933 ) -> Result<
934 Vec<(
935 coven_protocol::blob::locator::StoredBlobRef,
936 Vec<StoreBatchCommitRef>,
937 )>,
938 DbError,
939 > {
940 self.stored_blob_reclaim_candidates().await
941 }
942}