coven_replication/sync/
error.rs1use coven_database::DbError;
6use coven_keys::keys::KeyError;
7use coven_storage::cloud::setup::{SetupError, StorageSetupError};
8use coven_storage::cloud::CloudHomeError;
9
10use super::cycle::InitSyncError;
11use super::sync_loop::SyncLoopError;
12
13#[derive(Debug, thiserror::Error)]
14pub enum SyncError {
15 #[error("sync is not configured")]
16 NotConfigured,
17 #[error("sync loop is not running")]
18 LoopNotRunning,
19 #[error("sharing requires an encrypted cloud home")]
20 NotEncryptedHome,
21 #[error("no master key is established for this opaque store (locked, or never initialized)")]
22 MasterKeyNotEstablished,
23 #[error("failed to build cloud home: {0}")]
24 CloudHome(#[from] CloudHomeError),
25 #[error("failed to create sync storage: {0}")]
26 StorageSetup(#[source] StorageSetupError),
27 #[error("key error: {0}")]
28 Key(#[from] KeyError),
29 #[error("sync initialization error: {0}")]
30 Init(#[source] Box<InitSyncError>),
31 #[error("Store operation: {0}")]
32 Store(#[source] Box<crate::sync::store::StoreError>),
33 #[error("{0}")]
34 Setup(#[from] SetupError),
35 #[error("membership error: {0}")]
36 Membership(#[source] Box<crate::sync::store::MembershipOpsError>),
37 #[error("circle operation: {0}")]
38 Circle(#[source] Box<crate::sync::store::CircleOperationError>),
39 #[error("stuck reclaim operation: {0}")]
40 StuckReclaim(#[from] super::sync_loop::RetryStuckReclaimError),
41 #[error("device join: {0}")]
42 DeviceJoin(#[source] Box<crate::sync::DeviceJoinError>),
43 #[error("device join transport: {0}")]
44 DeviceJoinTransport(#[source] Box<crate::sync::store::DeviceJoinTransportError>),
45 #[error("invalid Store membership operation code: {0}")]
46 InvalidMembershipOperationCode(#[source] coven_foundation::code_envelope::EnvelopeError),
47 #[error("Store device exclusion: {0}")]
48 DeviceExclusion(#[source] Box<crate::sync::store::StoreDeviceExclusionError>),
49 #[error("Store Owner promotion: {0}")]
50 OwnerPromotion(#[source] Box<crate::sync::store::OwnerPromotionError>),
51 #[error("{0}")]
52 Database(#[source] Box<DbError>),
53 #[error("row routing key: {0}")]
54 RoutingEncryption(#[from] coven_keys::keys::RoutingEncryptionError),
55 #[error("blob upload drain failed: {0}")]
56 BlobUpload(#[source] Box<crate::sync::store::StoreError>),
57 #[error("sync loop error: {0}")]
58 Loop(#[source] SyncLoopError),
59}
60
61impl SyncError {
62 pub fn is_retryable(&self) -> bool {
65 error_chain_contains_transport(self)
66 }
67}
68
69pub(crate) fn error_chain_contains_transport(error: &(dyn std::error::Error + 'static)) -> bool {
70 let mut current = Some(error);
71 while let Some(source) = current {
72 if source
73 .downcast_ref::<coven_protocol::objects::StorageError>()
74 .is_some_and(coven_protocol::objects::StorageError::is_transport)
75 || source
76 .downcast_ref::<CloudHomeError>()
77 .is_some_and(|error| {
78 matches!(
79 error,
80 CloudHomeError::Transport(_)
81 | CloudHomeError::Backend {
82 kind: coven_protocol::objects::StorageBackendFailure::Transport,
83 ..
84 }
85 | CloudHomeError::Io(_)
86 )
87 })
88 {
89 return true;
90 }
91 current = source.source();
92 }
93 false
94}
95
96impl From<crate::sync::store::MembershipOpsError> for SyncError {
97 fn from(error: crate::sync::store::MembershipOpsError) -> Self {
98 Self::Membership(Box::new(error))
99 }
100}
101
102impl From<InitSyncError> for SyncError {
103 fn from(error: InitSyncError) -> Self {
104 Self::Init(Box::new(error))
105 }
106}
107
108impl From<crate::sync::store::StoreError> for SyncError {
109 fn from(error: crate::sync::store::StoreError) -> Self {
110 Self::Store(Box::new(error))
111 }
112}
113
114impl From<crate::sync::store::CircleOperationError> for SyncError {
115 fn from(error: crate::sync::store::CircleOperationError) -> Self {
116 Self::Circle(Box::new(error))
117 }
118}
119
120impl From<crate::sync::DeviceJoinError> for SyncError {
121 fn from(error: crate::sync::DeviceJoinError) -> Self {
122 Self::DeviceJoin(Box::new(error))
123 }
124}
125
126impl From<crate::sync::store::DeviceJoinTransportError> for SyncError {
127 fn from(error: crate::sync::store::DeviceJoinTransportError) -> Self {
128 Self::DeviceJoinTransport(Box::new(error))
129 }
130}
131
132impl From<crate::sync::store::StoreDeviceExclusionError> for SyncError {
133 fn from(error: crate::sync::store::StoreDeviceExclusionError) -> Self {
134 Self::DeviceExclusion(Box::new(error))
135 }
136}
137
138impl From<crate::sync::store::OwnerPromotionError> for SyncError {
139 fn from(error: crate::sync::store::OwnerPromotionError) -> Self {
140 Self::OwnerPromotion(Box::new(error))
141 }
142}
143
144impl From<DbError> for SyncError {
145 fn from(error: DbError) -> Self {
146 Self::Database(Box::new(error))
147 }
148}
149
150impl From<coven_database::DeviceJoinJournalError> for SyncError {
151 fn from(error: coven_database::DeviceJoinJournalError) -> Self {
152 crate::sync::DeviceJoinError::from(error).into()
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn sync_error_fits_below_clippys_large_result_threshold() {
162 let size = std::mem::size_of::<super::SyncError>();
163 assert!(size <= 128, "SyncError occupies {size} bytes");
164 }
165
166 #[test]
167 fn nested_store_initialization_transport_is_retryable() {
168 let storage = coven_protocol::objects::StorageError::from(CloudHomeError::Transport(
169 "provider unavailable".to_string(),
170 ));
171 let probe = coven_protocol::provider::ProviderProbeError::Storage(storage);
172 let root = crate::sync::store::protocol_root::StoreProtocolRootError::ProviderProbe(probe);
173 let initialization = crate::sync::store::StoreInitializationError::ProtocolRoot(root);
174 let error = SyncError::from(InitSyncError::Initialization(initialization));
175
176 assert!(error.is_retryable());
177 }
178}