1use tracing::{debug, info, warn};
12
13use crate::blob::DrainOutcome;
14use coven_foundation::changeset::RowChange;
15use coven_foundation::store_dir::StoreDir;
16use coven_protocol::blob::BlobTransitionObserver;
17
18use super::status::DeviceActivity;
19use super::store::HeldStorePosition;
20use super::store::{AuthorizedWriterOperation, Store};
21use coven_foundation::stage_timing::StageTimings;
22use coven_protocol::objects::RotationPending;
23use coven_storage::{
24 BlobPathScheme, CloudSyncCipherStateAccess, CloudSyncConnection, CloudSyncObjectStorage,
25 CloudSyncRotationStateAccess,
26};
27
28#[derive(Debug)]
30pub struct SyncCycleResult {
31 pub changesets_applied: u64,
33 pub held_positions: Vec<HeldStorePosition>,
38 pub device_activity: Vec<DeviceActivity>,
42 pub sync_time: String,
44 pub local_blob_cleanup_pending: bool,
49 pub row_changes: Vec<RowChange>,
51 pub resume_drain_promptly: bool,
56 pub rotation_pending: Option<RotationPending>,
61}
62
63mod failure;
64#[cfg(test)]
65pub(crate) use failure::SyncCycleCause;
66pub use failure::SyncCycleFailure;
67
68struct PreparedCycle {
69 sync_time: String,
70 resume_drain_promptly: bool,
71 rotation_pending: Option<RotationPending>,
72}
73
74struct CompletedPullCycle {
75 store_pull: super::store::StorePullResult,
76 local_blob_cleanup_pending: bool,
77 sync_time: String,
78 resume_drain_promptly: bool,
79 rotation_pending: Option<RotationPending>,
80}
81
82struct AuthorizedSyncCycle<'cycle, 'store> {
83 device_id: &'cycle str,
84 snapshot_commit_threshold: std::num::NonZeroU64,
85 clock: &'cycle dyn coven_foundation::clock::Clock,
86 cipher: &'cycle dyn CloudSyncCipherStateAccess,
87 pending_rotation: &'cycle dyn CloudSyncRotationStateAccess,
88 master_keys: Option<&'cycle dyn coven_keys::keys::MasterKeyCustody>,
89 routing_encryption: Option<&'cycle coven_keys::encryption::EncryptionService>,
90 local_blob_access: &'cycle super::store::blob::LocalStoreBlobAccess,
91 observer: Option<&'cycle dyn BlobTransitionObserver>,
92 settled: &'cycle super::store::SettledCycle,
93 authorization: AuthorizedWriterOperation<'store>,
94}
95
96impl AuthorizedSyncCycle<'_, '_> {
97 async fn run(mut self) -> Result<SyncCycleResult, SyncCycleFailure> {
98 let mut timings =
101 StageTimings::counting("sync cycle", self.authorization.provider_requests());
102 let outcome = Box::pin(self.run_stages(&mut timings)).await;
103 timings.report();
104 outcome
105 }
106
107 async fn run_stages(
108 &mut self,
109 timings: &mut StageTimings,
110 ) -> Result<SyncCycleResult, SyncCycleFailure> {
111 timings
112 .stage(
113 "resume operations",
114 self.authorization
115 .resume_operations(self.routing_encryption),
116 )
117 .await?;
118 let prepared = Box::pin(self.prepare_before_pull(timings)).await?;
119 let store_pull = timings
120 .stage("pull", self.authorization.pull(self.routing_encryption))
121 .await?;
122 let completed = Box::pin(self.complete_after_pull(prepared, store_pull, timings)).await?;
123 if completed.rotation_pending.is_none() {
124 timings
125 .stage(
126 "publish epoch-close responses",
127 self.authorization
128 .circles()
129 .publish_circle_epoch_close_responses(),
130 )
131 .await
132 .map_err(|error| {
133 SyncCycleFailure::operation("publish Circle epoch-close responses", error)
134 })?;
135 if let Some(routing_encryption) = self.routing_encryption {
136 timings
137 .stage(
138 "finalize epoch closes",
139 self.authorization
140 .circles()
141 .finalize_ready_circle_epoch_closes(routing_encryption),
142 )
143 .await
144 .map_err(|error| {
145 SyncCycleFailure::operation("finalize Circle epoch closes", error)
146 })?;
147 }
148 let routing_encryption = self.routing_encryption;
149 timings
150 .stage(
151 "advance replay baseline",
152 Box::pin(self.stand_on_accepted_snapshot(routing_encryption)),
153 )
154 .await?;
155 timings
156 .stage(
157 "publish acknowledgements",
158 Box::pin(
159 self.authorization
160 .acknowledgements()
161 .stage_and_publish(&completed.sync_time),
162 ),
163 )
164 .await?;
165 timings
166 .stage(
167 "retire arrived device joins",
168 Box::pin(self.authorization.retire_arrived_device_joins()),
169 )
170 .await
171 .map(|retired| {
172 if retired > 0 {
173 info!(
174 retired,
175 "Device joins reached their arrival and were retired"
176 );
177 }
178 })
179 .map_err(|error| {
180 SyncCycleFailure::operation("retire arrived device joins", error)
181 })?;
182 timings
183 .stage("reclaim packages", Box::pin(self.reclaim_packages()))
184 .await?;
185 }
186 Ok(SyncCycleResult {
187 changesets_applied: completed.store_pull.changesets_applied,
188 held_positions: completed.store_pull.held_positions,
189 device_activity: super::status::other_device_activity(
190 &completed.store_pull.visible_commits,
191 self.device_id,
192 ),
193 sync_time: completed.sync_time,
194 local_blob_cleanup_pending: completed.local_blob_cleanup_pending,
195 row_changes: completed.store_pull.row_changes,
196 resume_drain_promptly: completed.resume_drain_promptly,
197 rotation_pending: completed.rotation_pending,
198 })
199 }
200
201 async fn prepare_before_pull(
202 &mut self,
203 timings: &mut StageTimings,
204 ) -> Result<PreparedCycle, SyncCycleFailure> {
205 timings
215 .stage(
216 "refresh authorization",
217 self.authorization.refresh_authorization_state(
218 self.cipher,
219 self.pending_rotation,
220 self.master_keys,
221 ),
222 )
223 .await?;
224
225 let rotation_pending = self
236 .pending_rotation
237 .check(self.cipher.current_generation())
238 .err();
239 if let Some(pending) = &rotation_pending {
240 warn!(
241 rotation_state = ?pending.state,
242 live_generation = pending.live_generation,
243 "sync paused: store-key rotation work is incomplete; sealing nothing new for the cloud"
244 );
245 }
246
247 if rotation_pending.is_none() {
248 let drained = timings
249 .stage(
250 "drain tombstones",
251 self.authorization.drain_tombstones(self.clock),
252 )
253 .await
254 .map_err(|error| {
255 SyncCycleFailure::operation("drain queued blob tombstones", error)
256 })?;
257 if drained > 0 {
258 info!(count = drained, "Drained blob tombstones");
259 }
260 }
261 let reclaimed = timings
262 .stage(
263 "collect tombstones",
264 self.authorization.gc_tombstones(self.clock),
265 )
266 .await
267 .map_err(|error| {
268 SyncCycleFailure::operation("garbage-collect blob tombstones", error)
269 })?;
270 if reclaimed > 0 {
271 info!(count = reclaimed, "Reclaimed tombstoned blobs");
272 }
273
274 let local_seq = timings
275 .stage(
276 "read local position",
277 self.authorization.latest_local_store_position(),
278 )
279 .await
280 .map_err(|error| SyncCycleFailure::operation("read local Store position", error))?
281 .map_or(0, |reference| reference.coord.sequence());
282 timings
283 .stage(
284 "drain blob drop intents",
285 self.local_blob_access
286 .drain_published_blob_drop_intents(local_seq),
287 )
288 .await
289 .map_err(|error| {
290 SyncCycleFailure::operation("drain published blob drop intents", error)
291 })?;
292
293 let sync_time = self.clock.now().to_rfc3339();
297
298 let mut resume_drain_promptly = false;
299 if rotation_pending.is_none() {
300 let outcome = timings
301 .stage(
302 "drain blob uploads",
303 self.authorization.drain_uploads(
304 self.clock,
305 self.routing_encryption,
306 self.observer,
307 ),
308 )
309 .await
310 .map_err(|error| SyncCycleFailure::operation("drain queued blob uploads", error))?;
311 Self::record_upload_outcome(outcome, &mut resume_drain_promptly)?;
312 }
313
314 if rotation_pending.is_none() {
315 let published = timings
316 .stage(
317 "publish prepared writes",
318 self.authorization
319 .publish_prepared_store_writes(self.routing_encryption),
320 )
321 .await?;
322 if published > 0 {
323 info!(published, "Published queued Store writes");
324 }
325 }
326
327 Ok(PreparedCycle {
328 sync_time,
329 resume_drain_promptly,
330 rotation_pending,
331 })
332 }
333
334 async fn complete_after_pull(
335 &mut self,
336 prepared: PreparedCycle,
337 store_pull: super::store::StorePullResult,
338 timings: &mut StageTimings,
339 ) -> Result<CompletedPullCycle, SyncCycleFailure> {
340 let PreparedCycle {
341 sync_time,
342 mut resume_drain_promptly,
343 rotation_pending,
344 } = prepared;
345 if rotation_pending.is_none() {
346 let upload_authorization = self.authorization.blob_upload_lane();
351 let lanes = timings
352 .stage("publish pending writes and drain next blob root", async {
353 tokio::join!(
354 self.authorization
355 .publish_pending_store_writes(self.routing_encryption),
356 upload_authorization.drain_uploads(
357 self.clock,
358 self.routing_encryption,
359 self.observer,
360 ),
361 )
362 })
363 .await;
364 let (published, drained) = match lanes {
365 (Ok(published), Ok(drained)) => (published, drained),
366 (Err(first), Err(second)) => {
367 return Err(SyncCycleFailure::concurrent(
368 first,
369 SyncCycleFailure::operation("drain queued blob uploads", second),
370 ));
371 }
372 (Err(error), Ok(_)) => return Err(error),
373 (Ok(_), Err(error)) => {
374 return Err(SyncCycleFailure::operation(
375 "drain queued blob uploads",
376 error,
377 ));
378 }
379 };
380 if published > 0 {
381 info!(published, "Published Store writes");
382 }
383 Self::record_upload_outcome(drained, &mut resume_drain_promptly)?;
384 }
385
386 let local_seq = timings
387 .stage(
388 "read local position",
389 self.authorization.latest_local_store_position(),
390 )
391 .await
392 .map_err(|error| {
393 SyncCycleFailure::operation("read local Store position after publish", error)
394 })?
395 .map_or(0, |position| position.coord.sequence());
396 timings
397 .stage(
398 "drain blob drop intents",
399 self.local_blob_access
400 .drain_published_blob_drop_intents(local_seq),
401 )
402 .await
403 .map_err(|error| {
404 SyncCycleFailure::operation("drain published blob drop intents", error)
405 })?;
406 let local_blob_cleanup_pending = timings
407 .stage(
408 "drain local blob cleanup",
409 self.authorization.drain_local_blob_cleanup(),
410 )
411 .await
412 .map_err(|error| {
413 SyncCycleFailure::operation(
414 "drain local blob cleanup after Store publication",
415 error,
416 )
417 })?
418 || store_pull.local_blob_cleanup_pending;
419
420 timings
425 .stage(
426 "persist clock high-water",
427 self.authorization.persist_hlc_high_water(),
428 )
429 .await
430 .map_err(|error| SyncCycleFailure::operation("persist HLC high-water mark", error))?;
431
432 timings
433 .stage(
434 "publish snapshots",
435 self.authorization.snapshots().publish_due_snapshots(
436 &sync_time,
437 self.routing_encryption,
438 rotation_pending.is_some(),
439 self.snapshot_commit_threshold,
440 ),
441 )
442 .await?;
443
444 Ok(CompletedPullCycle {
445 store_pull,
446 local_blob_cleanup_pending,
447 sync_time,
448 resume_drain_promptly,
449 rotation_pending,
450 })
451 }
452
453 fn record_upload_outcome(
454 outcome: DrainOutcome,
455 resume_drain_promptly: &mut bool,
456 ) -> Result<(), SyncCycleFailure> {
457 match outcome {
458 DrainOutcome::Drained {
459 uploaded,
460 yielded_for_publish,
461 failures,
462 } => {
463 if failures.has_transport_failure() {
464 return Err(SyncCycleFailure::operation("upload queued blobs", failures));
465 }
466 *resume_drain_promptly |= yielded_for_publish;
467 if uploaded > 0 {
468 info!(count = uploaded, "Drained blob uploads");
469 }
470 }
471 DrainOutcome::QueueEmpty => {}
472 DrainOutcome::AllInBackoff => {
473 debug!("Every queued blob upload is inside its retry backoff");
474 }
475 DrainOutcome::Paused => {
476 debug!("Blob uploads are paused by the host; nothing was admitted");
477 }
478 }
479 Ok(())
480 }
481
482 async fn stand_on_accepted_snapshot(
491 &mut self,
492 routing_encryption: Option<&coven_keys::encryption::EncryptionService>,
493 ) -> Result<(), SyncCycleFailure> {
494 use super::store::{ReplayBaselineAdvance, ReplayBaselineDecline};
495
496 let outcome = self
497 .authorization
498 .acknowledgements()
499 .stand_on_accepted_snapshot(routing_encryption)
500 .await
501 .map_err(|error| {
502 SyncCycleFailure::operation("advance the Store replay baseline", error)
503 })?;
504 match outcome {
505 ReplayBaselineAdvance::Advanced(advanced) => info!(
506 commits = advanced.retired_commits,
507 pins = advanced.released_pins,
508 writes = advanced.folded_writes,
509 "Advanced the replay baseline over an accepted snapshot"
510 ),
511 ReplayBaselineAdvance::Declined(decline) => info!(
512 declined = decline.as_str(),
513 snapshot = ?decline.snapshot(),
514 accepted_snapshot = !matches!(decline, ReplayBaselineDecline::NoAcceptedSnapshot),
515 "Did not advance the replay baseline"
516 ),
517 }
518 Ok(())
519 }
520
521 async fn reclaim_packages(&mut self) -> Result<(), SyncCycleFailure> {
531 use super::store::StorePackageReclaimCoverage;
532
533 let result = match self.authorization.reclaim_packages(self.settled).await {
534 Ok(result) => result,
535 Err(error) => return Err(SyncCycleFailure::operation("reclaim Store packages", error)),
536 };
537 let store = &result.store_packages;
538 let coverage = match &store.coverage {
539 StorePackageReclaimCoverage::Snapshot { snapshot } => {
540 format!(
541 "accepted snapshot {} at publication {}",
542 snapshot.snapshot.snapshot_hash,
543 snapshot.publication.position.get()
544 )
545 }
546 StorePackageReclaimCoverage::NoSnapshot => "no accepted Store snapshot".to_string(),
547 StorePackageReclaimCoverage::NotOwner => {
548 "this device is not the current owner".to_string()
549 }
550 StorePackageReclaimCoverage::InputsUnchanged => {
551 "inputs unchanged since the last evaluation".to_string()
552 }
553 };
554 info!(
555 %coverage,
556 considered = store.targets_considered,
557 retained_for_replay = store.retained_for_replay,
558 already_authorized = store.already_authorized,
559 authorized = store.authorized,
560 packages = result.packages_deleted,
561 copies = result.physical_copies_deleted,
562 stuck = result.stuck,
563 "Reclaimed snapshot-covered Store packages"
564 );
565 Ok(())
566 }
567}
568
569#[derive(Debug, thiserror::Error)]
570pub enum InitSyncError {
571 #[error("no synced tables configured; pass a non-empty synced-table set before sync starts")]
572 NoSyncedTables,
573 #[error("cloud cipher and blob path scheme describe different storage modes")]
574 IncoherentStorageRepresentation,
575 #[error("Store row routing initialization failed: {0}")]
576 RowRouting(coven_database::DbError),
577 #[error("Store initialization failed: {0}")]
578 Initialization(#[from] crate::sync::store::StoreInitializationError),
579 #[error("restoring the persisted pending rotation failed: {0}")]
580 PendingRotationRestore(#[source] coven_database::DbError),
581 #[error("prepared sync identity differs from its storage identity")]
582 StorageIdentityMismatch,
583 #[error("unlock requires an existing Store root")]
584 ExistingStoreRequired,
585}
586
587#[derive(Debug, Clone)]
590pub enum StoreInitialization {
591 CreateStore,
592 OpenStore {
593 expected_store_root: coven_protocol::store_commit::StoreRootRef,
594 },
595}
596
597pub(crate) trait CloudSyncCycleConnection:
602 CloudSyncObjectStorage + CloudSyncCipherStateAccess + CloudSyncRotationStateAccess
603{
604}
605
606impl CloudSyncCycleConnection for CloudSyncConnection {}
607
608pub struct PreparedSyncComponents {
611 database: coven_database::StoreDatabase,
612 store_dir: StoreDir,
613 local_blob_access: super::store::blob::LocalStoreBlobAccess,
614 storage: std::sync::Arc<CloudSyncConnection>,
615 identity: coven_keys::keys::UserKeypair,
616 initialization: StoreInitialization,
617 store_id: String,
618 routing_encryption: Option<coven_keys::encryption::EncryptionService>,
619 master_keys: std::sync::Arc<dyn coven_keys::keys::MasterKeyCustody>,
620}
621
622impl PreparedSyncComponents {
623 pub async fn prepare(
624 database: coven_database::StoreDatabase,
625 store_dir: StoreDir,
626 storage: impl Into<std::sync::Arc<CloudSyncConnection>>,
627 identity: coven_keys::keys::UserKeypair,
628 initialization: StoreInitialization,
629 routing_encryption: Option<coven_keys::encryption::EncryptionService>,
630 master_keys: std::sync::Arc<dyn coven_keys::keys::MasterKeyCustody>,
631 ) -> Result<Self, InitSyncError> {
632 #[cfg(any(test, feature = "test-utils"))]
633 database.assert_owns_payload_directory_for_test(&store_dir);
634 let storage = storage.into();
635 if !storage.uses_identity(&identity) {
636 return Err(InitSyncError::StorageIdentityMismatch);
637 }
638 if !database.has_synced_tables() {
643 return Err(InitSyncError::NoSyncedTables);
644 }
645 database
646 .validate_store_write_routing(routing_encryption.as_ref())
647 .map_err(InitSyncError::RowRouting)?;
648
649 let cipher_is_plaintext = storage.is_plaintext();
650 let representation_is_coherent = matches!(
651 (cipher_is_plaintext, storage.blob_path_scheme()),
652 (true, BlobPathScheme::Plain) | (false, BlobPathScheme::Hashed)
653 );
654 if !representation_is_coherent {
655 return Err(InitSyncError::IncoherentStorageRepresentation);
656 }
657
658 if !cipher_is_plaintext {
662 let gate = database
663 .load_rotation_gate()
664 .await
665 .map_err(InitSyncError::PendingRotationRestore)?;
666 storage.install_durable_gate(gate);
667 }
668
669 let store_id = storage.store_id().to_string();
670 let local_blob_access = super::store::blob::LocalStoreBlobAccess::new(
671 database.clone(),
672 store_dir.clone(),
673 super::store::blob::StoreBlobCache::new(database.clone(), store_dir.clone()),
674 );
675 Ok(Self {
676 database,
677 store_dir,
678 local_blob_access,
679 storage,
680 identity,
681 initialization,
682 store_id,
683 routing_encryption,
684 master_keys,
685 })
686 }
687
688 pub async fn initialize(
689 self,
690 observer: Option<std::sync::Arc<dyn BlobTransitionObserver>>,
691 ) -> Result<SyncComponents, InitSyncError> {
692 let storage: std::sync::Arc<dyn CloudSyncCycleConnection> = self.storage;
693 let store_storage: std::sync::Arc<dyn coven_storage::CloudSyncObjectStorage> =
694 storage.clone();
695 let initialized = match self.initialization {
696 StoreInitialization::CreateStore => {
697 Store::create(
698 self.database.clone(),
699 store_storage.clone(),
700 self.store_dir.clone(),
701 &self.database.stamp(),
702 &self.identity,
703 self.routing_encryption.clone(),
704 )
705 .await
706 }
707 StoreInitialization::OpenStore {
708 expected_store_root,
709 } => {
710 Store::open(
711 self.database.clone(),
712 store_storage.clone(),
713 self.store_dir.clone(),
714 &expected_store_root,
715 &self.identity,
716 self.routing_encryption.clone(),
717 )
718 .await
719 }
720 }
721 .map_err(InitSyncError::Initialization)?;
722
723 let (store, device_id) = initialized.into_parts();
724 let blob_access = std::sync::Arc::new(super::store::blob::RemoteStoreBlobAccess::new(
725 self.local_blob_access.clone(),
726 super::store::blob::CurrentRemoteBlobSource::current(
727 self.database.clone(),
728 store_storage,
729 ),
730 ));
731 let blob_transitions = crate::blob::transition::ConnectedBlobTransitions::new(
732 crate::blob::transition::LocalBlobTransitions::new(
733 self.database.clone(),
734 self.store_dir.clone(),
735 ),
736 blob_access.clone(),
737 self.routing_encryption.clone(),
738 observer,
739 );
740 info!("Sync initialized (device: {})", device_id);
741 Ok(SyncComponents {
742 store: std::sync::Arc::new(store),
743 database: self.database,
744 local_blob_access: self.local_blob_access,
745 storage,
746 store_id: self.store_id,
747 device_id,
748 routing_encryption: self.routing_encryption,
749 master_keys: self.master_keys,
750 blob_transitions,
751 blob_access,
752 eager_fill_wanted: std::sync::Arc::default(),
753 settled: std::sync::Arc::default(),
754 })
755 }
756
757 pub async fn verify_open_store_key(&self) -> Result<(), InitSyncError> {
758 let StoreInitialization::OpenStore {
759 expected_store_root,
760 } = &self.initialization
761 else {
762 return Err(InitSyncError::ExistingStoreRequired);
763 };
764 super::store::protocol_root::verify_store_key_confirmation(
765 &self.database,
766 self.storage.as_ref(),
767 expected_store_root,
768 )
769 .await
770 .map_err(crate::sync::store::StoreInitializationError::from)
771 .map_err(InitSyncError::Initialization)
772 }
773}
774
775pub struct SyncComponents {
781 store: std::sync::Arc<Store>,
782 database: coven_database::StoreDatabase,
783 local_blob_access: super::store::blob::LocalStoreBlobAccess,
784 storage: std::sync::Arc<dyn CloudSyncCycleConnection>,
785 store_id: String,
789 device_id: String,
790 routing_encryption: Option<coven_keys::encryption::EncryptionService>,
791 master_keys: std::sync::Arc<dyn coven_keys::keys::MasterKeyCustody>,
792 blob_transitions: crate::blob::transition::ConnectedBlobTransitions,
793 blob_access: std::sync::Arc<super::store::blob::RemoteStoreBlobAccess>,
794 eager_fill_wanted: std::sync::Arc<tokio::sync::Notify>,
799 settled: std::sync::Arc<super::store::SettledCycle>,
803}
804
805impl SyncComponents {
806 pub(crate) async fn fill_eager_cache(
807 &self,
808 cancel: tokio::sync::watch::Receiver<bool>,
809 status: &tokio::sync::watch::Sender<super::store::blob::eager_cache::EagerCacheFillStatus>,
810 ) -> Result<(), std::sync::Arc<super::store::blob::eager_cache::EagerCacheFillError>> {
811 super::store::blob::eager_cache::run(
812 &self.database,
813 self.blob_access.as_ref(),
814 cancel,
815 status,
816 )
817 .await
818 }
819
820 pub(crate) fn eager_fill_wanted(&self) -> &tokio::sync::Notify {
824 &self.eager_fill_wanted
825 }
826
827 pub(crate) async fn probe_storage(&self) -> Result<(), coven_protocol::objects::StorageError> {
828 self.storage.probe_provider().await
829 }
830
831 async fn pending_blocked_writes(
832 &self,
833 ) -> Result<Vec<coven_protocol::write::PendingWrite>, coven_database::DbError> {
834 Ok(self
835 .database
836 .pending_writes()
837 .await?
838 .into_iter()
839 .filter(|write| {
840 matches!(
841 write.status,
842 coven_protocol::write::WriteStatus::Blocked(_)
843 | coven_protocol::write::WriteStatus::LocalOnlyBlocked(_)
844 )
845 })
846 .collect())
847 }
848
849 pub(crate) async fn blocked_operations(
854 &self,
855 ) -> Result<Vec<super::sync_loop::BlockedOperation>, coven_database::DbError> {
856 use super::sync_loop::BlockedOperation;
857
858 let mut blocked: Vec<BlockedOperation> = self
859 .pending_blocked_writes()
860 .await?
861 .into_iter()
862 .map(BlockedOperation::Write)
863 .collect();
864 blocked.extend(
865 self.database
866 .get_circle_operations()
867 .await?
868 .into_iter()
869 .filter(|operation| {
870 matches!(
871 operation.state,
872 coven_protocol::circle::CircleOperationState::Blocked { .. }
873 )
874 })
875 .map(BlockedOperation::CircleOperation),
876 );
877 blocked.extend(
878 self.database
879 .stuck_reclaim_operations()
880 .await?
881 .into_iter()
882 .map(BlockedOperation::Reclaim),
883 );
884 Ok(blocked)
885 }
886
887 pub(crate) async fn retry_stuck_reclaim(
890 &self,
891 operation_id: coven_protocol::store_commit::ObjectHash,
892 ) -> Result<(), coven_database::DbError> {
893 self.database
894 .retry_stuck_reclaim_operation(operation_id)
895 .await
896 }
897
898 pub(crate) async fn discard_blocked_write(
899 &self,
900 write_id: coven_protocol::write::WriteId,
901 ) -> Result<Vec<coven_protocol::write::WriteId>, super::store::StoreError> {
902 self.store
903 .discard_blocked_write(write_id, self.routing_encryption.as_ref())
904 .await
905 }
906
907 pub(crate) async fn members(
908 &self,
909 ) -> Result<Vec<coven_protocol::membership::MemberInfo>, super::store::MembershipOpsError> {
910 self.store.members().await
911 }
912
913 pub(crate) async fn restore_membership(
914 &self,
915 ) -> Result<super::store::authorization::StoreRestoreMembership, super::store::MembershipOpsError>
916 {
917 self.store.restore_membership().await
918 }
919
920 pub(crate) fn host_write_blob_staging(
921 &self,
922 runtime: tokio::runtime::Handle,
923 ) -> super::store::HostWriteBlobStaging {
924 self.store.host_write_blob_staging(runtime)
925 }
926
927 pub(crate) async fn propose_device_exclusion(
928 &self,
929 device_id: coven_protocol::StoreDeviceId,
930 ) -> Result<
931 coven_protocol::store_commit::StoreDeviceExclusionProposalRef,
932 super::store::StoreDeviceExclusionError,
933 > {
934 self.store
935 .propose_device_exclusion_for_device(device_id)
936 .await
937 }
938
939 pub(crate) async fn cancel_device_exclusion(
940 &self,
941 proposal: &coven_protocol::store_commit::StoreDeviceExclusionProposalRef,
942 ) -> Result<(), super::store::StoreDeviceExclusionError> {
943 self.store.cancel_device_exclusion_proposal(proposal).await
944 }
945
946 pub(crate) async fn finalize_device_exclusion(
947 &self,
948 proposal: &coven_protocol::store_commit::StoreDeviceExclusionProposalRef,
949 ) -> Result<(), super::store::StoreDeviceExclusionError> {
950 self.store
951 .finalize_device_exclusion_proposal(proposal)
952 .await
953 }
954
955 pub(crate) async fn begin_owner_promotion(
956 &self,
957 device_id: coven_protocol::StoreDeviceId,
958 ) -> Result<
959 coven_protocol::store_commit::OwnerPromotionRequest,
960 super::store::OwnerPromotionError,
961 > {
962 self.store.begin_owner_promotion_for_device(device_id).await
963 }
964
965 pub(crate) async fn accept_owner_promotion(
966 &self,
967 request: coven_protocol::store_commit::OwnerPromotionRequest,
968 ) -> Result<
969 coven_protocol::store_commit::OwnerPromotionAcceptance,
970 super::store::OwnerPromotionError,
971 > {
972 self.store.accept_owner_promotion(request).await
973 }
974
975 pub(crate) async fn finalize_owner_promotion(
976 &self,
977 acceptance: coven_protocol::store_commit::OwnerPromotionAcceptance,
978 ) -> Result<(), super::store::OwnerPromotionError> {
979 let encryption = self
980 .routing_encryption
981 .as_ref()
982 .ok_or(super::store::OwnerPromotionError::EncryptionRequired)?;
983 self.store
984 .finalize_owner_promotion(encryption, acceptance)
985 .await
986 .map(|_| ())
987 }
988
989 pub(crate) async fn begin_device_join_bundle(
990 &self,
991 member_pubkey: &str,
992 ) -> Result<crate::sync::DeviceJoinOfferBundle, super::store::DeviceJoinTransportError> {
993 self.store.begin_device_join_bundle(member_pubkey).await
994 }
995
996 pub(crate) async fn drive_device_join(
997 &self,
998 bundle: &crate::sync::DeviceJoinOfferBundle,
999 policy: crate::sync::DeviceJoinApprovalPolicy<'_>,
1000 access_administrator: Option<&dyn crate::sync::DeviceProviderAccessAdministrator>,
1001 on_progress: &(dyn Fn(crate::sync::AdmittingDeviceJoinProgress) + Send + Sync),
1002 timing: crate::sync::DeviceJoinTransportTiming,
1003 ) -> Result<crate::sync::DeviceJoinDriveOutcome, super::store::DeviceJoinTransportError> {
1004 self.store
1005 .device_join_transport()
1006 .drive(bundle, policy, access_administrator, on_progress, timing)
1007 .await
1008 }
1009
1010 pub(crate) async fn abandon_device_join_transport(
1011 &self,
1012 bundle: &crate::sync::DeviceJoinOfferBundle,
1013 ) -> Result<crate::sync::DeviceJoinAbandonment, super::store::DeviceJoinTransportError> {
1014 self.store.device_join_transport().abandon(bundle).await
1015 }
1016
1017 pub(crate) async fn abort_device_join_transport(
1018 &self,
1019 bundle: &crate::sync::DeviceJoinOfferBundle,
1020 ) -> Result<(), super::store::DeviceJoinTransportError> {
1021 self.store.device_join_transport().abort(bundle).await
1022 }
1023
1024 pub(crate) async fn begin_device_join(
1025 &self,
1026 member_pubkey: &str,
1027 ) -> Result<crate::sync::DeviceJoinOffer, crate::sync::DeviceJoinError> {
1028 self.store.begin_device_join(member_pubkey).await
1029 }
1030
1031 pub(crate) async fn abandon_device_join(
1032 &self,
1033 offer: crate::sync::DeviceJoinOffer,
1034 ) -> Result<crate::sync::DeviceJoinAbandonment, crate::sync::DeviceJoinError> {
1035 self.store.abandon_device_join(offer).await
1036 }
1037
1038 pub(crate) async fn authorize_device_provider_access(
1039 &self,
1040 request: crate::sync::DeviceProviderAccessRequest,
1041 access_administrator: Option<&dyn crate::sync::DeviceProviderAccessAdministrator>,
1042 ) -> Result<crate::sync::DeviceProviderAdmissionApproval, crate::sync::DeviceJoinError> {
1043 self.store
1044 .authorize_device_provider_access(request, access_administrator)
1045 .await
1046 }
1047
1048 pub(crate) async fn accept_device_registration(
1049 &self,
1050 request: crate::sync::DeviceRegistrationRequest,
1051 ) -> Result<crate::sync::ProvisionalDeviceBootstrap, crate::sync::DeviceJoinError> {
1052 self.store.accept_device_registration_request(request).await
1053 }
1054
1055 pub(crate) async fn publish_device_provider_challenge(
1056 &self,
1057 bootstrap: crate::sync::ProvisionalDeviceBootstrap,
1058 ) -> Result<crate::sync::ProviderReadyDeviceBootstrap, crate::sync::DeviceJoinError> {
1059 self.store
1060 .publish_device_provider_challenge(bootstrap)
1061 .await
1062 }
1063
1064 pub(crate) async fn complete_device_provider_admission(
1065 &self,
1066 readiness: crate::sync::DeviceJoinReadiness,
1067 ) -> Result<crate::sync::DeviceProviderAdmissionCompletion, crate::sync::DeviceJoinError> {
1068 self.store
1069 .complete_device_provider_admission(readiness)
1070 .await
1071 }
1072
1073 pub(crate) async fn finalize_device_join(
1074 &self,
1075 completion: crate::sync::DeviceProviderAdmissionCompletion,
1076 ) -> Result<crate::sync::DeviceJoinActivation, crate::sync::DeviceJoinError> {
1077 self.store.finalize_device_join(completion).await
1078 }
1079
1080 pub(crate) fn blob_path_scheme(&self) -> BlobPathScheme {
1081 self.store.blob_path_scheme()
1082 }
1083
1084 pub(crate) fn is_encrypted(&self) -> bool {
1085 !self.storage.is_plaintext()
1086 }
1087
1088 pub(crate) async fn drain_uploads(
1089 &self,
1090 clock: &dyn coven_foundation::clock::Clock,
1091 observer: Option<&dyn BlobTransitionObserver>,
1092 ) -> Result<crate::blob::DrainOutcome, crate::sync::store::StoreError> {
1093 self.store
1094 .authorize_writer()
1095 .await
1096 .map_err(crate::sync::store::StoreError::from)?
1097 .drain_uploads(clock, self.routing_encryption.as_ref(), observer)
1098 .await
1099 .map_err(crate::sync::store::StoreError::from)
1100 }
1101
1102 pub(crate) async fn make_remote(
1103 &self,
1104 root_table: &str,
1105 root_id: &str,
1106 root_label: &str,
1107 pin: bool,
1108 refs: Vec<coven_protocol::blob::RowBlobRef>,
1109 ) -> Result<(), crate::blob::transition::MakeRemoteError> {
1110 self.blob_transitions
1111 .make_remote(root_table, root_id, root_label, pin, refs)
1112 .await
1113 }
1114
1115 pub(crate) async fn make_remote_batch(
1116 &self,
1117 root_table: &str,
1118 roots: Vec<crate::blob::MakeRemoteRoot>,
1119 pin: bool,
1120 ) -> Result<(), crate::blob::transition::MakeRemoteError> {
1121 self.blob_transitions
1122 .make_remote_batch(root_table, roots, pin)
1123 .await
1124 }
1125
1126 pub(crate) async fn cancel_make_remote(
1127 &self,
1128 root_table: &str,
1129 root_id: &str,
1130 ) -> Result<(), crate::blob::transition::MakeRemoteError> {
1131 self.blob_transitions
1132 .cancel_make_remote(root_table, root_id)
1133 .await
1134 }
1135
1136 pub(crate) async fn make_local(
1137 &self,
1138 root_table: &str,
1139 root_id: &str,
1140 dest: &std::collections::HashMap<String, std::path::PathBuf>,
1141 cancel: &tokio::sync::watch::Receiver<bool>,
1142 ) -> Result<(), crate::blob::transition::MakeLocalError> {
1143 self.blob_transitions
1144 .make_local(root_table, root_id, dest, cancel)
1145 .await
1146 }
1147
1148 pub(crate) async fn admit_member(
1149 &self,
1150 public_key_hex: &str,
1151 member_email: Option<&str>,
1152 role: coven_protocol::membership::MemberRole,
1153 store_name: &str,
1154 ) -> Result<crate::sync::store::MemberAdmission, super::store::MembershipOpsError> {
1155 let encryption = self
1156 .routing_encryption
1157 .as_ref()
1158 .ok_or(super::store::MembershipOpsError::NotEncryptedHome)?;
1159 self.store
1160 .admit_member(
1161 public_key_hex,
1162 member_email,
1163 role,
1164 encryption,
1165 &self.store_id,
1166 store_name,
1167 )
1168 .await
1169 }
1170
1171 pub(crate) async fn remove_member(
1172 &self,
1173 public_key_hex: &str,
1174 ) -> Result<String, super::store::MembershipOpsError> {
1175 let encryption = self
1176 .routing_encryption
1177 .as_ref()
1178 .ok_or(super::store::MembershipOpsError::NotEncryptedHome)?;
1179 self.store
1180 .remove_member(
1181 public_key_hex,
1182 encryption,
1183 self.master_keys.as_ref(),
1184 self.storage.as_ref(),
1185 self.storage.as_ref(),
1186 )
1187 .await
1188 }
1189
1190 pub(crate) async fn create_circle(
1191 &self,
1192 name: &str,
1193 ) -> Result<coven_protocol::circle::CircleId, super::store::CircleOperationError> {
1194 self.store
1195 .circles()
1196 .create_circle(&self.database.stamp(), name)
1197 .await
1198 }
1199
1200 pub(crate) async fn rename_circle(
1201 &self,
1202 circle_id: coven_protocol::circle::CircleId,
1203 name: &str,
1204 ) -> Result<(), super::store::CircleOperationError> {
1205 self.store
1206 .circles()
1207 .rename_circle(&self.database.stamp(), circle_id, name)
1208 .await
1209 }
1210
1211 pub(crate) async fn resolve_circle_control(
1212 &self,
1213 circle_id: coven_protocol::circle::CircleId,
1214 chosen: coven_protocol::circle::CircleControlCoord,
1215 ) -> Result<(), super::store::CircleOperationError> {
1216 self.store
1217 .circles()
1218 .resolve_circle_control(circle_id, chosen)
1219 .await
1220 }
1221
1222 pub(crate) async fn delete_circle(
1223 &self,
1224 circle_id: coven_protocol::circle::CircleId,
1225 ) -> Result<(), super::store::CircleOperationError> {
1226 self.store.circles().delete_circle(circle_id).await
1227 }
1228
1229 pub(crate) async fn add_circle_member(
1230 &self,
1231 circle_id: coven_protocol::circle::CircleId,
1232 member_pubkey: String,
1233 role: coven_protocol::circle::CircleRole,
1234 ) -> Result<(), super::store::CircleOperationError> {
1235 use super::store::CircleOperationError;
1236 let routing_encryption = self
1240 .routing_encryption
1241 .as_ref()
1242 .ok_or(CircleOperationError::BrowsableStorage)?;
1243 let mut authorization = self
1244 .store
1245 .authorize_writer()
1246 .await
1247 .map_err(CircleOperationError::from)?;
1248 authorization
1249 .publish_pending_store_writes(Some(routing_encryption))
1250 .await
1251 .map_err(CircleOperationError::from)?;
1252 let bootstrap = authorization
1253 .circles()
1254 .snapshots()
1255 .capture_circle_snapshot_cut(routing_encryption, circle_id)
1256 .await?;
1257 let routing_key = coven_protocol::circle::derive_row_routing_key(
1258 routing_encryption,
1259 self.store.store_root().store_root_hash,
1260 )
1261 .map_err(CircleOperationError::from)?;
1262 authorization
1263 .circles()
1264 .add_circle_member(circle_id, member_pubkey, role, bootstrap, &routing_key)
1265 .await
1266 }
1267
1268 pub(crate) async fn remove_circle_member(
1269 &self,
1270 circle_id: coven_protocol::circle::CircleId,
1271 member_pubkey: String,
1272 ) -> Result<coven_protocol::circle::CircleOperationId, super::store::CircleOperationError> {
1273 self.store
1274 .circles()
1275 .remove_circle_member(circle_id, member_pubkey)
1276 .await
1277 }
1278
1279 pub(crate) async fn cancel_circle_epoch_close(
1280 &self,
1281 circle_id: coven_protocol::circle::CircleId,
1282 ) -> Result<coven_protocol::circle::CircleOperationId, super::store::CircleOperationError> {
1283 self.store
1284 .circles()
1285 .cancel_circle_epoch_close(circle_id)
1286 .await
1287 }
1288
1289 pub(crate) async fn exclude_circle_close_device(
1290 &self,
1291 circle_id: coven_protocol::circle::CircleId,
1292 excluded_device_id: coven_protocol::store_commit::StoreDeviceId,
1293 ) -> Result<(), super::store::CircleOperationError> {
1294 self.store
1295 .circles()
1296 .exclude_circle_close_device(circle_id, excluded_device_id)
1297 .await
1298 }
1299
1300 pub(crate) async fn retry_circle_operation(
1301 &self,
1302 operation_id: &coven_protocol::circle::CircleOperationId,
1303 ) -> Result<(), super::store::CircleOperationError> {
1304 self.store
1305 .circles()
1306 .retry_circle_operation(operation_id, self.routing_encryption.as_ref())
1307 .await
1308 }
1309
1310 pub(crate) async fn discard_circle_operation(
1311 &self,
1312 operation_id: &coven_protocol::circle::CircleOperationId,
1313 ) -> Result<(), super::store::CircleOperationError> {
1314 self.store
1315 .circles()
1316 .discard_circle_operation(operation_id)
1317 .await
1318 }
1319
1320 pub(crate) async fn circle_close_status(
1321 &self,
1322 circle_id: coven_protocol::circle::CircleId,
1323 ) -> Result<coven_protocol::circle::CircleCloseStatus, super::store::CircleOperationError> {
1324 self.store.circles().circle_close_status(circle_id).await
1325 }
1326
1327 pub(crate) fn provider_requests(
1330 &self,
1331 ) -> Option<std::sync::Arc<dyn coven_foundation::stage_timing::ProviderRequests>> {
1332 self.storage.provider_requests()
1333 }
1334
1335 pub async fn run_cycle(
1336 &self,
1337 clock: &dyn coven_foundation::clock::Clock,
1338 observer: Option<&dyn BlobTransitionObserver>,
1339 snapshot_commit_threshold: std::num::NonZeroU64,
1340 ) -> Result<SyncCycleResult, SyncCycleFailure> {
1341 let authorization =
1342 self.store.authorize_writer().await.map_err(|error| {
1343 SyncCycleFailure::operation("authorize local Store writer", error)
1344 })?;
1345 AuthorizedSyncCycle {
1346 device_id: &self.device_id,
1347 snapshot_commit_threshold,
1348 clock,
1349 cipher: self.storage.as_ref(),
1350 pending_rotation: self.storage.as_ref(),
1351 master_keys: Some(self.master_keys.as_ref()),
1352 routing_encryption: self.routing_encryption.as_ref(),
1353 local_blob_access: &self.local_blob_access,
1354 observer,
1355 settled: self.settled.as_ref(),
1356 authorization,
1357 }
1358 .run()
1359 .await
1360 .inspect(|result| {
1361 if result.changesets_applied > 0 {
1362 self.eager_fill_wanted.notify_one();
1363 }
1364 })
1365 }
1366
1367 #[cfg(any(test, feature = "test-utils"))]
1368 #[allow(clippy::too_many_arguments)]
1369 pub(crate) fn from_retained_test_device<S>(
1370 store: std::sync::Arc<Store>,
1371 database: coven_database::StoreDatabase,
1372 store_dir: StoreDir,
1373 storage: std::sync::Arc<S>,
1374 store_id: String,
1375 device_id: String,
1376 master_keys: std::sync::Arc<dyn coven_keys::keys::MasterKeyCustody>,
1377 settled: std::sync::Arc<super::store::SettledCycle>,
1382 ) -> Self
1383 where
1384 S: CloudSyncCycleConnection + 'static,
1385 {
1386 database.assert_owns_payload_directory_for_test(&store_dir);
1387 let storage: std::sync::Arc<dyn CloudSyncCycleConnection> = storage;
1388 let store_storage: std::sync::Arc<dyn coven_storage::CloudSyncObjectStorage> =
1389 storage.clone();
1390 let local_blob_access = super::store::blob::LocalStoreBlobAccess::new(
1391 database.clone(),
1392 store_dir.clone(),
1393 super::store::blob::StoreBlobCache::new(database.clone(), store_dir.clone()),
1394 );
1395 let blob_access = std::sync::Arc::new(super::store::blob::RemoteStoreBlobAccess::new(
1396 local_blob_access.clone(),
1397 super::store::blob::CurrentRemoteBlobSource::current(database.clone(), store_storage),
1398 ));
1399 let blob_transitions = crate::blob::transition::ConnectedBlobTransitions::new(
1400 crate::blob::transition::LocalBlobTransitions::new(database.clone(), store_dir.clone()),
1401 blob_access.clone(),
1402 None,
1403 None,
1404 );
1405 Self {
1406 store,
1407 database,
1408 local_blob_access,
1409 store_id,
1410 storage,
1411 device_id,
1412 routing_encryption: None,
1413 master_keys,
1414 blob_transitions,
1415 blob_access,
1416 eager_fill_wanted: std::sync::Arc::default(),
1417 settled,
1418 }
1419 }
1420
1421 #[cfg(any(test, feature = "test-utils"))]
1422 pub async fn list_storage_objects_for_test(
1423 &self,
1424 prefix: &str,
1425 ) -> Result<Vec<String>, coven_protocol::objects::StorageError> {
1426 self.storage.list_provider_keys_for_test(prefix).await
1427 }
1428
1429 #[cfg(any(test, feature = "test-utils"))]
1430 pub fn uses_storage_for_test(
1431 &self,
1432 expected: &std::sync::Arc<dyn coven_storage::CloudSyncObjectStorage>,
1433 ) -> bool {
1434 let actual: std::sync::Arc<dyn coven_storage::CloudSyncObjectStorage> =
1435 self.storage.clone();
1436 std::sync::Arc::ptr_eq(&actual, expected)
1437 }
1438
1439 #[cfg(any(test, feature = "test-utils"))]
1440 pub fn uses_store_dir_for_test(&self, expected: &StoreDir) -> bool {
1441 self.local_blob_access.uses_store_dir_for_test(expected)
1442 }
1443
1444 #[cfg(any(test, feature = "test-utils"))]
1445 pub fn encryption_generation_for_test(&self) -> Option<u64> {
1446 self.storage.current_generation()
1447 }
1448
1449 #[cfg(any(test, feature = "test-utils"))]
1450 pub fn open_sealed_blob_for_test(
1451 &self,
1452 stored: &[u8],
1453 aad_context: &[u8],
1454 ) -> Result<
1455 (coven_keys::encryption::KeyFingerprint, Vec<u8>),
1456 coven_keys::encryption::EncryptionError,
1457 > {
1458 self.storage.open_sealed_blob_for_test(stored, aad_context)
1459 }
1460
1461 #[cfg(any(test, feature = "test-utils"))]
1462 pub fn adopt_key_rotation(
1463 &self,
1464 encryption: coven_keys::encryption::EncryptionService,
1465 ) -> Result<String, coven_keys::keys::KeyError> {
1466 CloudSyncCipherStateAccess::adopt_key_rotation(
1467 self.storage.as_ref(),
1468 &encryption,
1469 self.master_keys.as_ref(),
1470 )
1471 .map(|adopted| adopted.fingerprint().to_string())
1472 }
1473}