Skip to main content

coven_replication/sync/
error.rs

1//! Why connecting, running, or commanding sync failed: the composition
2//! boundary's error vocabulary, wrapping the storage, key, and initialization
3//! refusals below it.
4
5use 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    /// Whether retrying the same operation may succeed because its error chain
63    /// contains a transient cloud transport or I/O failure.
64    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}