1use std::sync::atomic::{AtomicBool, Ordering};
13use std::sync::Arc;
14
15use tokio::sync::mpsc::error::TrySendError;
16use tracing::debug;
17
18use coven_foundation::clock::ClockRef;
19use coven_foundation::config::Config;
20#[cfg(any(test, feature = "test-utils"))]
21use coven_foundation::store_dir::StoreDir;
22use coven_foundation::store_dir::StoreOpenGuard;
23use coven_protocol::blob::BlobTransitionObserver;
24
25use super::cycle::SyncComponents;
26use super::loop_policy::SyncLoopSuccess;
27use coven_storage::BlobPathScheme;
28
29mod thread;
30#[cfg(test)]
31pub(crate) use thread::current_success_status;
32#[cfg(test)]
33use thread::storage_check_failure_status;
34pub use thread::PreparedSyncLoopRuntime;
35
36#[derive(Debug, thiserror::Error)]
38pub enum SyncLoopError {
39 #[error("failed to spawn sync loop thread: {0}")]
41 ThreadSpawn(std::io::Error),
42 #[error("failed to create sync loop runtime: {0}")]
44 Runtime(Arc<std::io::Error>),
45 #[error("sync loop thread panicked")]
47 ThreadPanicked,
48}
49
50#[derive(Debug, Clone, thiserror::Error)]
51pub enum SyncLoopFailure {
52 #[error("check sync storage: {0}")]
53 Storage(Arc<coven_protocol::objects::StorageError>),
54 #[error("sync cycle: {0}")]
55 Cycle(Arc<crate::sync::cycle::SyncCycleFailure>),
56 #[error("read blocked operations after sync: {0}")]
57 BlockedOperations(Arc<coven_database::DbError>),
58 #[error("sync loop panicked")]
59 Panicked,
60}
61
62pub trait SyncLoopRuntimeFactory: Send + Sync {
64 fn prepare(&self) -> Result<PreparedSyncLoopRuntime, SyncLoopError>;
66}
67
68pub struct SystemSyncLoopRuntimeFactory;
70
71impl SyncLoopRuntimeFactory for SystemSyncLoopRuntimeFactory {
72 fn prepare(&self) -> Result<PreparedSyncLoopRuntime, SyncLoopError> {
73 PreparedSyncLoopRuntime::prepare()
74 }
75}
76
77#[derive(Debug, Clone)]
93pub enum SyncLoopStatus {
94 Offline,
96 CheckingStorage,
98 Publishing,
100 Synchronized(SyncLoopSuccess),
103 Blocked {
106 success: SyncLoopSuccess,
107 operations: Vec<BlockedOperation>,
108 },
109 Failed { error: SyncLoopFailure },
111}
112
113#[derive(Debug, Clone)]
119pub enum BlockedOperation {
120 Write(coven_protocol::write::PendingWrite),
122 CircleOperation(coven_protocol::circle::CircleOperationInfo),
125 Reclaim(coven_database::StuckReclaimOperation),
128}
129
130impl BlockedOperation {
131 pub fn id(&self) -> BlockedOperationId {
133 match self {
134 Self::Write(write) => BlockedOperationId::Write(write.write_id.clone()),
135 Self::CircleOperation(operation) => {
136 BlockedOperationId::CircleOperation(operation.operation_id.clone())
137 }
138 Self::Reclaim(operation) => BlockedOperationId::Reclaim(operation.operation_id),
139 }
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub enum BlockedOperationId {
146 Write(coven_protocol::write::WriteId),
147 CircleOperation(coven_protocol::circle::CircleOperationId),
148 Reclaim(coven_protocol::store_commit::ObjectHash),
149}
150
151#[derive(Debug, thiserror::Error)]
153pub enum RetryStuckReclaimError {
154 #[error("the sync loop is not accepting commands")]
155 CommandChannelClosed,
156 #[error("the sync loop dropped its reply")]
157 ReplyChannelClosed,
158 #[error("{0}")]
159 Database(#[source] Box<coven_database::DbError>),
160}
161
162pub struct SyncLoopHandle {
164 inner: Arc<SyncLoopHandleInner>,
165 trigger_tx: tokio::sync::mpsc::Sender<()>,
166 command_tx: tokio::sync::mpsc::Sender<SyncCommand>,
167 stop_tx: tokio::sync::watch::Sender<bool>,
168 eager_cache_cancel_tx: tokio::sync::watch::Sender<bool>,
169 activate_tx: tokio::sync::watch::Sender<bool>,
170 status_tx: tokio::sync::watch::Sender<SyncLoopStatus>,
174 thread_handle: std::sync::Mutex<Option<std::thread::JoinHandle<()>>>,
175 running: Arc<AtomicBool>,
176}
177
178struct SyncLoopHandleInner {
179 components: SyncComponents,
180 clock: ClockRef,
181 config: Config,
182 observer: Option<Arc<dyn BlobTransitionObserver>>,
183
184 _open_guard: Arc<StoreOpenGuard>,
190}
191
192type CircleReply<T> =
193 tokio::sync::oneshot::Sender<Result<T, crate::sync::store::CircleOperationError>>;
194
195enum SyncCommand {
196 CreateCircle {
197 name: String,
198 reply: CircleReply<coven_protocol::CircleId>,
199 },
200 RenameCircle {
201 circle_id: coven_protocol::CircleId,
202 name: String,
203 reply: CircleReply<()>,
204 },
205 AddCircleMember {
206 circle_id: coven_protocol::CircleId,
207 member_pubkey: String,
208 role: coven_protocol::CircleRole,
209 reply: CircleReply<()>,
210 },
211 RemoveCircleMember {
212 circle_id: coven_protocol::CircleId,
213 member_pubkey: String,
214 reply: CircleReply<coven_protocol::CircleOperationId>,
215 },
216 ResolveCircleControl {
217 circle_id: coven_protocol::CircleId,
218 chosen: coven_protocol::CircleControlCoord,
219 reply: CircleReply<()>,
220 },
221 CancelCircleEpochClose {
222 circle_id: coven_protocol::CircleId,
223 reply: CircleReply<coven_protocol::CircleOperationId>,
224 },
225 ExcludeCircleCloseDevice {
226 circle_id: coven_protocol::CircleId,
227 excluded_device_id: coven_protocol::StoreDeviceId,
228 reply: CircleReply<()>,
229 },
230 DeleteCircle {
231 circle_id: coven_protocol::CircleId,
232 reply: CircleReply<()>,
233 },
234 RetryCircleOperation {
235 operation_id: coven_protocol::CircleOperationId,
236 reply: CircleReply<()>,
237 },
238 DiscardCircleOperation {
239 operation_id: coven_protocol::CircleOperationId,
240 reply: CircleReply<()>,
241 },
242 RetryStuckReclaim {
243 operation_id: coven_protocol::store_commit::ObjectHash,
244 reply: tokio::sync::oneshot::Sender<Result<(), RetryStuckReclaimError>>,
245 },
246}
247
248impl SyncLoopHandle {
249 pub fn new(
250 components: SyncComponents,
251 clock: ClockRef,
252 config: Config,
253 observer: Option<Arc<dyn BlobTransitionObserver>>,
254 open_guard: Arc<StoreOpenGuard>,
255 status_tx: tokio::sync::watch::Sender<SyncLoopStatus>,
256 eager_cache_status_tx: tokio::sync::watch::Sender<super::store::EagerCacheFillStatus>,
257 runtime: Option<PreparedSyncLoopRuntime>,
258 ) -> Self {
259 let (trigger_tx, trigger_rx) = tokio::sync::mpsc::channel(1);
260 let (command_tx, command_rx) = tokio::sync::mpsc::channel(16);
261 let (stop_tx, stop_rx) = tokio::sync::watch::channel(false);
262 let (eager_cache_cancel_tx, eager_cache_cancel_rx) = tokio::sync::watch::channel(false);
263 let (activate_tx, activate_rx) = tokio::sync::watch::channel(false);
264 let inner = Arc::new(SyncLoopHandleInner {
265 components,
266 clock,
267 config,
268 observer,
269 _open_guard: open_guard,
270 });
271 let running = Arc::new(AtomicBool::new(runtime.is_some()));
272 let thread_handle = runtime.map(|runtime| {
273 runtime.install(thread::SyncLoopThread::new(
274 Arc::clone(&inner),
275 trigger_rx,
276 command_rx,
277 stop_rx,
278 eager_cache_cancel_rx,
279 activate_rx,
280 status_tx.clone(),
281 eager_cache_status_tx,
282 Arc::clone(&running),
283 ))
284 });
285 Self {
286 inner,
287 trigger_tx,
288 command_tx,
289 stop_tx,
290 eager_cache_cancel_tx,
291 activate_tx,
292 status_tx,
293 thread_handle: std::sync::Mutex::new(thread_handle),
294 running,
295 }
296 }
297
298 pub fn activate(&self) {
300 self.activate_tx.send_replace(true);
301 }
302
303 pub fn provider_requests(
307 &self,
308 ) -> Option<Arc<dyn coven_foundation::stage_timing::ProviderRequests>> {
309 self.inner.components.provider_requests()
310 }
311
312 pub fn is_running(&self) -> bool {
314 self.running.load(Ordering::Acquire)
315 }
316
317 pub fn stop(&self) {
319 let handle = {
320 let mut guard = self.thread_handle.lock().unwrap();
321 if guard.is_none() && !self.running.load(Ordering::Acquire) {
322 return;
323 }
324 if self.stop_tx.send(true).is_err() {
325 debug!("sync loop stop requested after stop receiver closed");
326 }
327 self.eager_cache_cancel_tx.send_replace(true);
328 self.trigger();
329 guard.take()
330 };
331
332 if let Some(handle) = handle {
333 if handle.join().is_err() {
334 self.running.store(false, Ordering::Release);
335 let failure = SyncLoopFailure::Panicked;
336 self.status_tx
337 .send_replace(SyncLoopStatus::Failed { error: failure });
338 }
339 }
340 self.running.store(false, Ordering::Release);
341 }
342
343 pub fn trigger(&self) {
349 match self.trigger_tx.try_send(()) {
350 Ok(()) | Err(TrySendError::Full(())) => {}
351 Err(TrySendError::Closed(())) => {
352 debug!("Sync trigger channel closed, loop is not running");
353 }
354 }
355 }
356
357 pub fn cancel_eager_cache_fill(&self) {
359 self.eager_cache_cancel_tx.send_replace(true);
360 }
361
362 pub async fn discard_blocked_write(
363 &self,
364 write_id: coven_protocol::write::WriteId,
365 ) -> Result<Vec<coven_protocol::write::WriteId>, crate::sync::store::StoreError> {
366 self.inner.components.discard_blocked_write(write_id).await
367 }
368
369 pub async fn members(
370 &self,
371 ) -> Result<Vec<coven_protocol::membership::MemberInfo>, super::store::MembershipOpsError> {
372 self.inner.components.members().await
373 }
374
375 pub async fn membership_conflict(
376 &self,
377 ) -> Result<Option<coven_protocol::MembershipConflictInfo>, super::store::MembershipOpsError>
378 {
379 self.inner.components.membership_conflict().await
380 }
381
382 pub async fn restore_membership(
383 &self,
384 ) -> Result<super::store::authorization::StoreRestoreMembership, super::store::MembershipOpsError>
385 {
386 self.inner.components.restore_membership().await
387 }
388
389 pub fn host_write_blob_staging(
390 &self,
391 runtime: tokio::runtime::Handle,
392 ) -> crate::sync::store::HostWriteBlobStaging {
393 self.inner.components.host_write_blob_staging(runtime)
394 }
395
396 pub async fn propose_device_exclusion(
397 &self,
398 device_id: coven_protocol::StoreDeviceId,
399 ) -> Result<
400 coven_protocol::store_commit::StoreDeviceExclusionProposalRef,
401 crate::sync::store::StoreDeviceExclusionError,
402 > {
403 self.inner
404 .components
405 .propose_device_exclusion(device_id)
406 .await
407 }
408
409 pub async fn cancel_device_exclusion(
410 &self,
411 proposal: &coven_protocol::store_commit::StoreDeviceExclusionProposalRef,
412 ) -> Result<(), crate::sync::store::StoreDeviceExclusionError> {
413 self.inner
414 .components
415 .cancel_device_exclusion(proposal)
416 .await
417 }
418
419 pub async fn finalize_device_exclusion(
420 &self,
421 proposal: &coven_protocol::store_commit::StoreDeviceExclusionProposalRef,
422 ) -> Result<(), crate::sync::store::StoreDeviceExclusionError> {
423 self.inner
424 .components
425 .finalize_device_exclusion(proposal)
426 .await
427 }
428
429 pub async fn begin_owner_promotion(
430 &self,
431 device_id: coven_protocol::StoreDeviceId,
432 ) -> Result<
433 coven_protocol::store_commit::OwnerPromotionRequest,
434 crate::sync::store::OwnerPromotionError,
435 > {
436 self.inner.components.begin_owner_promotion(device_id).await
437 }
438
439 pub async fn accept_owner_promotion(
440 &self,
441 request: coven_protocol::store_commit::OwnerPromotionRequest,
442 ) -> Result<
443 coven_protocol::store_commit::OwnerPromotionAcceptance,
444 crate::sync::store::OwnerPromotionError,
445 > {
446 self.inner.components.accept_owner_promotion(request).await
447 }
448
449 pub async fn finalize_owner_promotion(
450 &self,
451 acceptance: coven_protocol::store_commit::OwnerPromotionAcceptance,
452 ) -> Result<(), crate::sync::store::OwnerPromotionError> {
453 self.inner
454 .components
455 .finalize_owner_promotion(acceptance)
456 .await
457 }
458
459 pub async fn begin_device_join_bundle(
460 &self,
461 member_pubkey: &str,
462 ) -> Result<crate::sync::DeviceJoinOfferBundle, crate::sync::store::DeviceJoinTransportError>
463 {
464 self.inner
465 .components
466 .begin_device_join_bundle(member_pubkey)
467 .await
468 }
469
470 pub async fn drive_device_join(
471 &self,
472 bundle: &crate::sync::DeviceJoinOfferBundle,
473 policy: crate::sync::DeviceJoinApprovalPolicy<'_>,
474 access_administrator: Option<&dyn crate::sync::DeviceProviderAccessAdministrator>,
475 on_progress: &(dyn Fn(crate::sync::AdmittingDeviceJoinProgress) + Send + Sync),
476 timing: crate::sync::DeviceJoinTransportTiming,
477 ) -> Result<crate::sync::DeviceJoinDriveOutcome, crate::sync::store::DeviceJoinTransportError>
478 {
479 self.inner
480 .components
481 .drive_device_join(bundle, policy, access_administrator, on_progress, timing)
482 .await
483 }
484
485 pub async fn abandon_device_join_transport(
486 &self,
487 bundle: &crate::sync::DeviceJoinOfferBundle,
488 ) -> Result<crate::sync::DeviceJoinAbandonment, crate::sync::store::DeviceJoinTransportError>
489 {
490 self.inner
491 .components
492 .abandon_device_join_transport(bundle)
493 .await
494 }
495
496 pub async fn abort_device_join_transport(
497 &self,
498 bundle: &crate::sync::DeviceJoinOfferBundle,
499 ) -> Result<(), crate::sync::store::DeviceJoinTransportError> {
500 self.inner
501 .components
502 .abort_device_join_transport(bundle)
503 .await
504 }
505
506 pub async fn begin_device_join(
507 &self,
508 member_pubkey: &str,
509 ) -> Result<crate::sync::DeviceJoinOffer, crate::sync::DeviceJoinError> {
510 self.inner.components.begin_device_join(member_pubkey).await
511 }
512
513 pub async fn abandon_device_join(
514 &self,
515 offer: crate::sync::DeviceJoinOffer,
516 ) -> Result<crate::sync::DeviceJoinAbandonment, crate::sync::DeviceJoinError> {
517 self.inner.components.abandon_device_join(offer).await
518 }
519
520 pub async fn authorize_device_provider_access(
521 &self,
522 request: crate::sync::DeviceProviderAccessRequest,
523 access_administrator: Option<&dyn crate::sync::DeviceProviderAccessAdministrator>,
524 ) -> Result<crate::sync::DeviceProviderAdmissionApproval, crate::sync::DeviceJoinError> {
525 self.inner
526 .components
527 .authorize_device_provider_access(request, access_administrator)
528 .await
529 }
530
531 pub async fn accept_device_registration(
532 &self,
533 request: crate::sync::DeviceRegistrationRequest,
534 ) -> Result<crate::sync::ProvisionalDeviceBootstrap, crate::sync::DeviceJoinError> {
535 self.inner
536 .components
537 .accept_device_registration(request)
538 .await
539 }
540
541 pub async fn publish_device_provider_challenge(
542 &self,
543 bootstrap: crate::sync::ProvisionalDeviceBootstrap,
544 ) -> Result<crate::sync::ProviderReadyDeviceBootstrap, crate::sync::DeviceJoinError> {
545 self.inner
546 .components
547 .publish_device_provider_challenge(bootstrap)
548 .await
549 }
550
551 pub async fn complete_device_provider_admission(
552 &self,
553 readiness: crate::sync::DeviceJoinReadiness,
554 ) -> Result<crate::sync::DeviceProviderAdmissionCompletion, crate::sync::DeviceJoinError> {
555 self.inner
556 .components
557 .complete_device_provider_admission(readiness)
558 .await
559 }
560
561 pub async fn finalize_device_join(
562 &self,
563 completion: crate::sync::DeviceProviderAdmissionCompletion,
564 ) -> Result<crate::sync::DeviceJoinActivation, crate::sync::DeviceJoinError> {
565 self.inner.components.finalize_device_join(completion).await
566 }
567
568 pub fn config(&self) -> &Config {
569 &self.inner.config
570 }
571
572 pub fn blob_path_scheme(&self) -> BlobPathScheme {
573 self.inner.components.blob_path_scheme()
574 }
575
576 pub fn is_encrypted(&self) -> bool {
577 self.inner.components.is_encrypted()
578 }
579
580 pub async fn admit_member(
581 &self,
582 public_key_hex: &str,
583 member_email: Option<&str>,
584 role: coven_protocol::membership::MemberRole,
585 store_name: &str,
586 ) -> Result<crate::sync::store::MemberAdmission, super::store::MembershipOpsError> {
587 self.inner
588 .components
589 .admit_member(public_key_hex, member_email, role, store_name)
590 .await
591 }
592
593 pub async fn remove_member(
594 &self,
595 public_key_hex: &str,
596 ) -> Result<String, super::store::MembershipOpsError> {
597 self.inner.components.remove_member(public_key_hex).await
598 }
599
600 pub async fn resolve_membership_conflict(
601 &self,
602 choice: &coven_protocol::MembershipConflictChoice,
603 ) -> Result<(), super::store::MembershipOpsError> {
604 self.inner
605 .components
606 .resolve_membership_conflict(choice)
607 .await
608 }
609
610 pub async fn drain_uploads(
611 &self,
612 ) -> Result<crate::blob::DrainOutcome, super::store::StoreError> {
613 self.inner
614 .components
615 .drain_uploads(self.inner.clock.as_ref(), self.inner.observer.as_deref())
616 .await
617 }
618
619 pub async fn make_remote(
620 &self,
621 root_table: &str,
622 root_id: &str,
623 root_label: &str,
624 pin: bool,
625 refs: Vec<coven_protocol::blob::RowBlobRef>,
626 ) -> Result<(), crate::blob::transition::MakeRemoteError> {
627 self.inner
628 .components
629 .make_remote(root_table, root_id, root_label, pin, refs)
630 .await
631 }
632
633 pub async fn make_remote_batch(
634 &self,
635 root_table: &str,
636 roots: Vec<crate::blob::MakeRemoteRoot>,
637 pin: bool,
638 ) -> Result<(), crate::blob::transition::MakeRemoteError> {
639 self.inner
640 .components
641 .make_remote_batch(root_table, roots, pin)
642 .await
643 }
644
645 pub async fn cancel_make_remote(
646 &self,
647 root_table: &str,
648 root_id: &str,
649 ) -> Result<(), crate::blob::transition::MakeRemoteError> {
650 self.inner
651 .components
652 .cancel_make_remote(root_table, root_id)
653 .await
654 }
655
656 pub async fn make_local(
657 &self,
658 root_table: &str,
659 root_id: &str,
660 dest: &std::collections::HashMap<String, std::path::PathBuf>,
661 cancel: &tokio::sync::watch::Receiver<bool>,
662 ) -> Result<(), crate::blob::transition::MakeLocalError> {
663 self.inner
664 .components
665 .make_local(root_table, root_id, dest, cancel)
666 .await
667 }
668
669 async fn send_circle_command<T>(
672 &self,
673 command: impl FnOnce(CircleReply<T>) -> SyncCommand,
674 ) -> Result<T, crate::sync::store::CircleOperationError> {
675 let (reply, response) = tokio::sync::oneshot::channel();
676 self.command_tx
677 .send(command(reply))
678 .await
679 .map_err(|_| crate::sync::store::CircleOperationError::CommandChannelClosed)?;
680 response
681 .await
682 .map_err(|_| crate::sync::store::CircleOperationError::ReplyChannelClosed)?
683 }
684
685 pub async fn create_circle(
686 &self,
687 name: &str,
688 ) -> Result<coven_protocol::CircleId, crate::sync::store::CircleOperationError> {
689 let name = name.to_string();
690 self.send_circle_command(|reply| SyncCommand::CreateCircle { name, reply })
691 .await
692 }
693
694 pub async fn rename_circle(
695 &self,
696 circle_id: coven_protocol::CircleId,
697 name: &str,
698 ) -> Result<(), crate::sync::store::CircleOperationError> {
699 let name = name.to_string();
700 self.send_circle_command(|reply| SyncCommand::RenameCircle {
701 circle_id,
702 name,
703 reply,
704 })
705 .await
706 }
707
708 pub async fn add_circle_member(
709 &self,
710 circle_id: coven_protocol::CircleId,
711 member_pubkey: String,
712 role: coven_protocol::CircleRole,
713 ) -> Result<(), crate::sync::store::CircleOperationError> {
714 self.send_circle_command(|reply| SyncCommand::AddCircleMember {
715 circle_id,
716 member_pubkey,
717 role,
718 reply,
719 })
720 .await
721 }
722
723 pub async fn remove_circle_member(
724 &self,
725 circle_id: coven_protocol::CircleId,
726 member_pubkey: String,
727 ) -> Result<coven_protocol::CircleOperationId, crate::sync::store::CircleOperationError> {
728 self.send_circle_command(|reply| SyncCommand::RemoveCircleMember {
729 circle_id,
730 member_pubkey,
731 reply,
732 })
733 .await
734 }
735
736 pub async fn resolve_circle_control(
737 &self,
738 circle_id: coven_protocol::CircleId,
739 chosen: coven_protocol::CircleControlCoord,
740 ) -> Result<(), crate::sync::store::CircleOperationError> {
741 self.send_circle_command(|reply| SyncCommand::ResolveCircleControl {
742 circle_id,
743 chosen,
744 reply,
745 })
746 .await
747 }
748
749 pub async fn cancel_circle_epoch_close(
750 &self,
751 circle_id: coven_protocol::CircleId,
752 ) -> Result<coven_protocol::CircleOperationId, crate::sync::store::CircleOperationError> {
753 self.send_circle_command(|reply| SyncCommand::CancelCircleEpochClose { circle_id, reply })
754 .await
755 }
756
757 pub async fn exclude_circle_close_device(
758 &self,
759 circle_id: coven_protocol::CircleId,
760 excluded_device_id: coven_protocol::StoreDeviceId,
761 ) -> Result<(), crate::sync::store::CircleOperationError> {
762 self.send_circle_command(|reply| SyncCommand::ExcludeCircleCloseDevice {
763 circle_id,
764 excluded_device_id,
765 reply,
766 })
767 .await
768 }
769
770 pub async fn delete_circle(
771 &self,
772 circle_id: coven_protocol::CircleId,
773 ) -> Result<(), crate::sync::store::CircleOperationError> {
774 self.send_circle_command(|reply| SyncCommand::DeleteCircle { circle_id, reply })
775 .await
776 }
777
778 pub async fn retry_circle_operation(
779 &self,
780 operation_id: coven_protocol::CircleOperationId,
781 ) -> Result<(), crate::sync::store::CircleOperationError> {
782 self.send_circle_command(|reply| SyncCommand::RetryCircleOperation {
783 operation_id,
784 reply,
785 })
786 .await
787 }
788
789 pub async fn discard_circle_operation(
790 &self,
791 operation_id: coven_protocol::CircleOperationId,
792 ) -> Result<(), crate::sync::store::CircleOperationError> {
793 self.send_circle_command(|reply| SyncCommand::DiscardCircleOperation {
794 operation_id,
795 reply,
796 })
797 .await
798 }
799
800 pub async fn retry_stuck_reclaim(
806 &self,
807 operation_id: coven_protocol::store_commit::ObjectHash,
808 ) -> Result<(), RetryStuckReclaimError> {
809 let (reply, response) = tokio::sync::oneshot::channel();
810 self.command_tx
811 .send(SyncCommand::RetryStuckReclaim {
812 operation_id,
813 reply,
814 })
815 .await
816 .map_err(|_| RetryStuckReclaimError::CommandChannelClosed)?;
817 response
818 .await
819 .map_err(|_| RetryStuckReclaimError::ReplyChannelClosed)?
820 }
821
822 pub async fn circle_close_status(
825 &self,
826 circle_id: coven_protocol::CircleId,
827 ) -> Result<coven_protocol::CircleCloseStatus, crate::sync::store::CircleOperationError> {
828 self.inner.components.circle_close_status(circle_id).await
829 }
830
831 #[cfg(any(test, feature = "test-utils"))]
832 pub fn uses_storage_for_test(
833 &self,
834 expected: &Arc<dyn coven_storage::CloudSyncObjectStorage>,
835 ) -> bool {
836 self.inner.components.uses_storage_for_test(expected)
837 }
838
839 #[cfg(any(test, feature = "test-utils"))]
840 pub fn uses_store_dir_for_test(&self, expected: &StoreDir) -> bool {
841 self.inner.components.uses_store_dir_for_test(expected)
842 }
843
844 #[cfg(any(test, feature = "test-utils"))]
845 pub fn encryption_generation_for_test(&self) -> Option<u64> {
846 self.inner.components.encryption_generation_for_test()
847 }
848
849 #[cfg(any(test, feature = "test-utils"))]
850 pub fn open_sealed_blob_for_test(
851 &self,
852 stored: &[u8],
853 aad_context: &[u8],
854 ) -> Result<
855 (coven_keys::encryption::KeyFingerprint, Vec<u8>),
856 coven_keys::encryption::EncryptionError,
857 > {
858 self.inner
859 .components
860 .open_sealed_blob_for_test(stored, aad_context)
861 }
862
863 #[cfg(any(test, feature = "test-utils"))]
864 pub fn adopt_key_rotation_for_test(
865 &self,
866 encryption: coven_keys::encryption::EncryptionService,
867 ) -> Result<String, coven_keys::keys::KeyError> {
868 self.inner.components.adopt_key_rotation(encryption)
869 }
870}
871
872impl SyncLoopHandleInner {
873 async fn execute_command(&self, command: SyncCommand) {
874 match command {
875 SyncCommand::CreateCircle { name, reply } => {
876 reply_circle_command(reply, self.components.create_circle(&name).await);
877 }
878 SyncCommand::RenameCircle {
879 circle_id,
880 name,
881 reply,
882 } => {
883 reply_circle_command(reply, self.components.rename_circle(circle_id, &name).await);
884 }
885 SyncCommand::AddCircleMember {
886 circle_id,
887 member_pubkey,
888 role,
889 reply,
890 } => {
891 reply_circle_command(
892 reply,
893 self.components
894 .add_circle_member(circle_id, member_pubkey, role)
895 .await,
896 );
897 }
898 SyncCommand::RemoveCircleMember {
899 circle_id,
900 member_pubkey,
901 reply,
902 } => {
903 reply_circle_command(
904 reply,
905 self.components
906 .remove_circle_member(circle_id, member_pubkey)
907 .await,
908 );
909 }
910 SyncCommand::ResolveCircleControl {
911 circle_id,
912 chosen,
913 reply,
914 } => {
915 reply_circle_command(
916 reply,
917 self.components
918 .resolve_circle_control(circle_id, chosen)
919 .await,
920 );
921 }
922 SyncCommand::CancelCircleEpochClose { circle_id, reply } => {
923 reply_circle_command(
924 reply,
925 self.components.cancel_circle_epoch_close(circle_id).await,
926 );
927 }
928 SyncCommand::ExcludeCircleCloseDevice {
929 circle_id,
930 excluded_device_id,
931 reply,
932 } => {
933 reply_circle_command(
934 reply,
935 self.components
936 .exclude_circle_close_device(circle_id, excluded_device_id)
937 .await,
938 );
939 }
940 SyncCommand::DeleteCircle { circle_id, reply } => {
941 reply_circle_command(reply, self.components.delete_circle(circle_id).await);
942 }
943 SyncCommand::RetryCircleOperation {
944 operation_id,
945 reply,
946 } => {
947 reply_circle_command(
948 reply,
949 self.components.retry_circle_operation(&operation_id).await,
950 );
951 }
952 SyncCommand::DiscardCircleOperation {
953 operation_id,
954 reply,
955 } => {
956 reply_circle_command(
957 reply,
958 self.components
959 .discard_circle_operation(&operation_id)
960 .await,
961 );
962 }
963 SyncCommand::RetryStuckReclaim {
964 operation_id,
965 reply,
966 } => {
967 let result = self
968 .components
969 .retry_stuck_reclaim(operation_id)
970 .await
971 .map_err(|error| RetryStuckReclaimError::Database(Box::new(error)));
972 if reply.send(result).is_err() {
973 debug!("stuck reclaim retry caller dropped its reply receiver");
974 }
975 }
976 }
977 }
978
979 async fn run_single_cycle(
980 &self,
981 ) -> Result<super::cycle::SyncCycleResult, super::cycle::SyncCycleFailure> {
982 self.components
983 .run_cycle(self.clock.as_ref(), self.observer.as_deref())
984 .await
985 }
986}
987
988fn reply_circle_command<T>(
989 reply: CircleReply<T>,
990 result: Result<T, crate::sync::store::CircleOperationError>,
991) {
992 if reply.send(result).is_err() {
993 debug!("Circle command caller dropped its reply receiver");
994 }
995}
996
997#[cfg(test)]
998#[path = "sync_loop_tests.rs"]
999mod tests;