1use coven_protocol::objects::StoreObjectError;
2use coven_protocol::store_commit::{StoreBatchCommitRef, StoreDeviceId};
3
4#[derive(Debug, thiserror::Error)]
5pub enum BlobPreparationCleanupError {
6 #[error("prepared blob spool is absent: {}", path.display())]
7 MissingSpool { path: std::path::PathBuf },
8 #[error("prepared blob file: {0}")]
9 File(#[from] coven_foundation::atomic_file::FileError),
10}
11
12#[derive(Debug)]
13pub struct BlobPreparationRollback {
14 operation: Box<StoreError>,
15 cleanup: Vec<BlobPreparationCleanupError>,
16}
17
18impl BlobPreparationRollback {
19 pub(crate) fn new(operation: StoreError, cleanup: Vec<BlobPreparationCleanupError>) -> Self {
20 Self {
21 operation: Box::new(operation),
22 cleanup,
23 }
24 }
25}
26
27impl std::fmt::Display for BlobPreparationRollback {
28 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 write!(formatter, "blob preparation failed: {}", self.operation)?;
30 for cleanup in &self.cleanup {
31 write!(formatter, "; cleanup failed: {cleanup}")?;
32 }
33 Ok(())
34 }
35}
36
37impl std::error::Error for BlobPreparationRollback {
38 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
39 Some(self.operation.as_ref())
40 }
41}
42
43#[derive(Debug)]
44pub enum StorePreparationError {
45 Database(coven_database::DbError),
46 Gate(String),
47 AssetScan(String),
48 AssetScanFile(coven_foundation::store_dir::LocalBlobStoreError),
49 AssetUpload(String),
50 Storage {
51 operation: &'static str,
52 source: coven_protocol::objects::StorageError,
53 },
54 LocalUserBlob {
55 namespace: String,
56 id: String,
57 },
58 MissingPreparedBlob {
59 namespace: String,
60 id: String,
61 },
62}
63
64impl std::fmt::Display for StorePreparationError {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 match self {
67 Self::Database(error) => write!(f, "database error: {error}"),
68 Self::Gate(error) => write!(f, "gate error: {error}"),
69 Self::AssetScan(error) => write!(f, "asset scan error: {error}"),
70 Self::AssetScanFile(error) => write!(f, "asset scan error: {error}"),
71 Self::AssetUpload(error) => write!(f, "asset upload error: {error}"),
72 Self::Storage { operation, source } => write!(f, "{operation}: {source}"),
73 Self::LocalUserBlob { namespace, id } => {
74 write!(
75 f,
76 "user-provided blob {namespace}/{id} still has a local external ref"
77 )
78 }
79 Self::MissingPreparedBlob { namespace, id } => {
80 write!(
81 f,
82 "blob {namespace}/{id} has no prepared exact publication object"
83 )
84 }
85 }
86 }
87}
88
89impl std::error::Error for StorePreparationError {
90 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
91 match self {
92 Self::Database(source) => Some(source),
93 Self::Storage { source, .. } => Some(source),
94 Self::AssetScanFile(source) => Some(source),
95 Self::Gate(_)
96 | Self::AssetScan(_)
97 | Self::AssetUpload(_)
98 | Self::LocalUserBlob { .. }
99 | Self::MissingPreparedBlob { .. } => None,
100 }
101 }
102}
103
104#[derive(Debug, thiserror::Error)]
105pub enum StoreError {
106 #[error("database: {0}")]
107 Database(#[from] coven_database::DbError),
108 #[error("local file: {0}")]
109 File(#[from] coven_foundation::atomic_file::FileError),
110 #[error("inspect host blob source {}: {source}", path.display())]
111 InspectBlobSource {
112 path: std::path::PathBuf,
113 #[source]
114 source: std::io::Error,
115 },
116 #[error("blob cache: {0}")]
117 BlobCache(#[from] crate::sync::BlobCacheError),
118 #[error("{0}")]
119 Object(#[from] StoreObjectError),
120 #[error("Store protocol: {0}")]
121 Protocol(#[from] coven_protocol::store_commit::StoreProtocolError),
122 #[error("Store JSON: {0}")]
123 Json(#[from] serde_json::Error),
124 #[error("Store changeset: {0}")]
125 Changeset(#[from] coven_database::ChangesetError),
126 #[error("Store writer authorization: {0}")]
127 WriterAuthorization(#[source] Box<crate::sync::store::StoreWriterAuthorizationError>),
128 #[error("Store sync cycle: {0}")]
129 SyncCycle(#[source] Box<crate::sync::cycle::SyncCycleFailure>),
130 #[error("Store membership chain: {0}")]
131 AnchoredChain(#[source] Box<crate::sync::store::AnchoredChainError>),
132 #[error("Store protocol root: {0}")]
133 ProtocolRoot(#[source] Box<crate::sync::store::protocol_root::StoreProtocolRootError>),
134 #[error("Store audience package: {0}")]
135 AudiencePackage(#[from] coven_protocol::audience_package::AudiencePackageError),
136 #[error("Store blob path: {0}")]
137 BlobPath(#[from] coven_foundation::store_dir::PathTokenError),
138 #[error("Store remote object: {0}")]
139 RemoteObject(#[from] coven_protocol::remote_object::RemoteObjectRecordError),
140 #[error("Store blob locator: {0}")]
141 BlobLocator(#[from] coven_protocol::blob::locator::BlobLocatorError),
142 #[error("Store prepared commit: {0}")]
143 PreparedCommit(#[source] coven_protocol::prepared_commit::PreparedCommitError),
144 #[error("Store membership preparation: {0}")]
145 MembershipPreparation(
146 #[source] coven_protocol::membership_mutation::MembershipPreparationError,
147 ),
148 #[error("Store keyring: {0}")]
149 Keyring(#[source] Box<crate::sync::store::MembershipMutationError>),
150 #[error("Store row routing key: {0}")]
151 RowRoutingKey(#[from] coven_protocol::circle::RowRoutingKeyError),
152 #[error("Store Circle package: {0}")]
153 CirclePackage(#[source] Box<crate::sync::store::CirclePackageReadError>),
154 #[error("Store protocol state {key:?} is absent")]
155 MissingState { key: &'static str },
156 #[error("Store protocol state {key:?} is invalid: {reason}")]
157 InvalidState { key: &'static str, reason: String },
158 #[error("outbound Store row is invalid: {0}")]
159 InvalidOutbound(String),
160 #[error("another writer activated first; this Store operation persisted nothing")]
166 ActivationConflict,
167 #[error("outbound Store preparation failed: {0}")]
168 Preparation(#[source] StorePreparationError),
169 #[error("{0}")]
170 BlobPreparationRollback(#[from] BlobPreparationRollback),
171 #[error("blob preparation cleanup: {0}")]
172 BlobPreparationCleanup(#[from] BlobPreparationCleanupError),
173 #[error("outbound blob {namespace}/{id} is local and cannot be published")]
174 LocalUserBlob { namespace: String, id: String },
175 #[error("outbound blob {namespace}/{id} is absent from storage")]
176 MissingBlob { namespace: String, id: String },
177 #[error("checking outbound blob {namespace}/{id}: {source}")]
178 BlobStorage {
179 namespace: String,
180 id: String,
181 source: coven_protocol::objects::StorageError,
182 },
183 #[error("Store pull: {0}")]
184 Pull(#[from] crate::sync::store::pull::StorePullError),
185 #[error("Store publication waits for accepted history: {0:?}")]
186 PublicationHeld(Vec<crate::sync::store::pull::HeldStorePosition>),
187 #[error("Store publication response was uncertain ({publication}); verifying its outcome failed: {verification}")]
188 PublicationSettlement {
189 publication: coven_protocol::objects::StorageError,
190 #[source]
191 verification: Box<StoreError>,
192 },
193 #[error("Store sequence {current} has no representable successor")]
194 SequenceExhausted { current: u64 },
195 #[error("published Store write count has no representable successor")]
196 PublishCountExhausted,
197 #[error("write {write_id} was not marked blocked ({status}) after its operation failed ({operation})")]
201 WriteBlockNotRecorded {
202 write_id: coven_protocol::write::WriteId,
203 #[source]
204 operation: Box<StoreError>,
205 status: coven_database::DbError,
206 },
207 #[error("Store author {device_id} was excluded before candidate activation")]
208 AuthorExcluded { device_id: StoreDeviceId },
209 #[error("Merge announcement selected {actual:?}, not candidate {expected:?}")]
210 MergeAnnouncementOccupied {
211 expected: Box<StoreBatchCommitRef>,
212 actual: Box<StoreBatchCommitRef>,
213 },
214 #[error("{0}")]
215 CirclePublicationBlocked(coven_protocol::circle::CirclePublicationBlocked),
216}
217
218impl StoreError {
219 pub(crate) fn prepared_object(error: coven_protocol::objects::StorageError) -> Self {
222 match error {
223 coven_protocol::objects::StorageError::PreparedObjectMismatch(key) => {
224 Self::InvalidOutbound(format!(
225 "prepared exact object {key} differs from its signed bytes"
226 ))
227 }
228 error => StoreObjectError::from(error).into(),
229 }
230 }
231
232 pub(crate) fn write_block(
233 &self,
234 attempted_write: &coven_protocol::write::WriteId,
235 ) -> Option<(
236 coven_protocol::write::WriteId,
237 coven_protocol::write::WriteBlock,
238 )> {
239 let mut cause: &(dyn std::error::Error + 'static) = self;
240 loop {
241 if let Some(database) = cause.downcast_ref::<coven_database::DbError>() {
242 if let Some(conflict) = database.write_rebase_conflict() {
243 return Some((
244 conflict.write_id.clone(),
245 coven_protocol::write::WriteBlock::RebaseConflict(conflict.clone()),
246 ));
247 }
248 break;
251 }
252 let Some(source) = cause.source() else {
253 break;
254 };
255 cause = source;
256 }
257 let block = match self {
258 Self::Preparation(StorePreparationError::Database(_)) => Some(
259 coven_protocol::write::WriteBlock::InvalidPackage { reason: self.to_string() },
260 ),
261 Self::Database(_)
262 | Self::File(_)
263 | Self::InspectBlobSource { .. }
264 | Self::BlobCache(_)
265 | Self::BlobPreparationRollback(_)
266 | Self::BlobPreparationCleanup(_)
267 | Self::WriteBlockNotRecorded { .. }
268 | Self::BlobStorage { .. }
269 | Self::ActivationConflict
272 | Self::Pull(_)
273 | Self::PublicationHeld(_)
274 | Self::PublicationSettlement { .. }
275 | Self::SyncCycle(_) => None,
276 Self::MergeAnnouncementOccupied { .. }
277 | Self::SequenceExhausted { .. }
278 | Self::PublishCountExhausted
279 | Self::AuthorExcluded { .. } => Some(coven_protocol::write::WriteBlock::InvalidProtocolState {
280 reason: self.to_string(),
281 }),
282 Self::CirclePublicationBlocked(
283 coven_protocol::circle::CirclePublicationBlocked::RotationRequired {
284 circle_id,
285 removed_members,
286 },
287 ) => Some(coven_protocol::write::WriteBlock::RotationRequired {
288 circle_id: *circle_id,
289 removed_members: removed_members.clone(),
290 }),
291 Self::Object(StoreObjectError::Storage(_)) => None,
292 Self::MissingBlob { namespace, id } => Some(coven_protocol::write::WriteBlock::MissingBlob {
293 namespace: namespace.clone(),
294 id: id.clone(),
295 }),
296 Self::LocalUserBlob { namespace, id } => Some(coven_protocol::write::WriteBlock::LocalUserBlob {
297 namespace: namespace.clone(),
298 id: id.clone(),
299 }),
300 Self::MissingState { key } => Some(coven_protocol::write::WriteBlock::InvalidProtocolState {
301 reason: format!("Store protocol state {key:?} is absent"),
302 }),
303 Self::InvalidState { key, reason } => Some(coven_protocol::write::WriteBlock::InvalidProtocolState {
304 reason: format!("Store protocol state {key:?} is invalid: {reason}"),
305 }),
306 Self::InvalidOutbound(_)
307 | Self::Object(_)
308 | Self::Protocol(_)
309 | Self::Json(_)
310 | Self::Changeset(_)
311 | Self::WriterAuthorization(_)
312 | Self::AnchoredChain(_)
313 | Self::ProtocolRoot(_)
314 | Self::AudiencePackage(_)
315 | Self::BlobPath(_)
316 | Self::RemoteObject(_)
317 | Self::BlobLocator(_)
318 | Self::PreparedCommit(_)
319 | Self::MembershipPreparation(_)
320 | Self::Keyring(_)
321 | Self::RowRoutingKey(_)
322 | Self::CirclePackage(_) => {
323 Some(coven_protocol::write::WriteBlock::InvalidPackage {
324 reason: self.to_string(),
325 })
326 }
327 Self::Preparation(StorePreparationError::LocalUserBlob { namespace, id }) => {
328 Some(coven_protocol::write::WriteBlock::LocalUserBlob {
329 namespace: namespace.clone(),
330 id: id.clone(),
331 })
332 }
333 Self::Preparation(StorePreparationError::MissingPreparedBlob { namespace, id }) => {
334 Some(coven_protocol::write::WriteBlock::MissingBlob {
335 namespace: namespace.clone(),
336 id: id.clone(),
337 })
338 }
339 Self::Preparation(StorePreparationError::Gate(_))
340 | Self::Preparation(StorePreparationError::AssetScan(_))
341 | Self::Preparation(StorePreparationError::AssetScanFile(_)) => {
342 Some(coven_protocol::write::WriteBlock::InvalidPackage {
343 reason: self.to_string(),
344 })
345 }
346 Self::Preparation(StorePreparationError::AssetUpload(_))
347 | Self::Preparation(StorePreparationError::Storage { .. }) => None,
348 };
349 block.map(|block| (attempted_write.clone(), block))
350 }
351}
352
353impl From<crate::sync::store::CirclePackageReadError> for StoreError {
354 fn from(error: crate::sync::store::CirclePackageReadError) -> Self {
355 Self::CirclePackage(Box::new(error))
356 }
357}
358
359impl From<coven_protocol::prepared_commit::PreparedCommitError> for StoreError {
360 fn from(error: coven_protocol::prepared_commit::PreparedCommitError) -> Self {
361 StoreError::PreparedCommit(error)
362 }
363}
364
365impl From<coven_protocol::membership_mutation::MembershipPreparationError> for StoreError {
366 fn from(error: coven_protocol::membership_mutation::MembershipPreparationError) -> Self {
367 StoreError::MembershipPreparation(error)
368 }
369}
370
371impl From<crate::sync::store::MembershipMutationError> for StoreError {
372 fn from(error: crate::sync::store::MembershipMutationError) -> Self {
373 Self::Keyring(Box::new(error))
374 }
375}
376
377impl From<crate::sync::store::StoreWriterAuthorizationError> for StoreError {
378 fn from(error: crate::sync::store::StoreWriterAuthorizationError) -> Self {
379 Self::WriterAuthorization(Box::new(error))
380 }
381}
382
383impl From<crate::sync::cycle::SyncCycleFailure> for StoreError {
384 fn from(error: crate::sync::cycle::SyncCycleFailure) -> Self {
385 Self::SyncCycle(Box::new(error))
386 }
387}
388
389impl From<crate::sync::store::AnchoredChainError> for StoreError {
390 fn from(error: crate::sync::store::AnchoredChainError) -> Self {
391 Self::AnchoredChain(Box::new(error))
392 }
393}
394
395impl From<crate::sync::store::protocol_root::StoreProtocolRootError> for StoreError {
396 fn from(error: crate::sync::store::protocol_root::StoreProtocolRootError) -> Self {
397 Self::ProtocolRoot(Box::new(error))
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404 use coven_protocol::write::{
405 AffectedRow, WriteBlock, WriteId, WriteRebaseConflict, WriteRebaseConflictReason,
406 };
407
408 #[test]
409 fn write_rebase_conflicts_remain_typed_at_both_publication_boundaries() {
410 let conflict = WriteRebaseConflict {
411 write_id: WriteId::from_generated("pending-write".into()),
412 affected_rows: vec![AffectedRow {
413 table: "notes".into(),
414 primary_key: "private-note".into(),
415 }],
416 reason: WriteRebaseConflictReason::PrivateShared,
417 };
418 let wrapped = || {
419 coven_database::DbError::context(
420 "rebase pending writes",
421 coven_database::DbError::from(conflict.clone()),
422 )
423 };
424 let attempted = WriteId::from_generated("attempted-write".into());
425 for error in [
426 StoreError::Database(wrapped()),
427 StoreError::Preparation(StorePreparationError::Database(wrapped())),
428 ] {
429 assert_eq!(
430 error.write_block(&attempted),
431 Some((
432 conflict.write_id.clone(),
433 WriteBlock::RebaseConflict(conflict.clone())
434 ))
435 );
436 }
437 }
438
439 #[test]
440 fn write_rebase_conflicts_remain_typed_through_pull_and_cycle_causes() {
441 let conflict = WriteRebaseConflict {
442 write_id: WriteId::from_generated("dependent-write".into()),
443 affected_rows: vec![AffectedRow {
444 table: "notes".into(),
445 primary_key: "conflicting-note".into(),
446 }],
447 reason: WriteRebaseConflictReason::Constraint {
448 message: "CHECK constraint failed".into(),
449 },
450 };
451 let pull = || {
452 crate::sync::store::StorePullError::context(
453 "install accepted snapshot",
454 coven_database::DbError::context(
455 "rebase pending suffix",
456 coven_database::DbError::from(conflict.clone()),
457 ),
458 )
459 };
460 let attempted = WriteId::from_generated("attempted-write".into());
461 for error in [
462 StoreError::Pull(pull()),
463 StoreError::from(crate::sync::cycle::SyncCycleFailure::operation(
464 "install publication winner",
465 pull(),
466 )),
467 StoreError::PublicationSettlement {
468 publication: coven_protocol::objects::StorageError::Storage(
469 "publication response unavailable".into(),
470 ),
471 verification: Box::new(StoreError::Pull(pull())),
472 },
473 ] {
474 assert_eq!(
475 error.write_block(&attempted),
476 Some((
477 conflict.write_id.clone(),
478 WriteBlock::RebaseConflict(conflict.clone())
479 )),
480 "{error}"
481 );
482 }
483 }
484
485 #[test]
486 fn transient_pull_and_cycle_causes_do_not_block_a_write() {
487 let attempted = WriteId::from_generated("attempted-write".into());
488 let pull = || {
489 crate::sync::store::StorePullError::context(
490 "load publication winner",
491 crate::sync::store::StorePullError::Storage(
492 coven_protocol::objects::StorageError::Storage("provider unavailable".into()),
493 ),
494 )
495 };
496 for error in [
497 StoreError::Pull(pull()),
498 StoreError::from(crate::sync::cycle::SyncCycleFailure::operation(
499 "install publication winner",
500 pull(),
501 )),
502 StoreError::PublicationSettlement {
503 publication: coven_protocol::objects::StorageError::Storage(
504 "publication response unavailable".into(),
505 ),
506 verification: Box::new(StoreError::Pull(pull())),
507 },
508 ] {
509 assert_eq!(error.write_block(&attempted), None, "{error}");
510 }
511 }
512
513 #[test]
514 fn unattributed_write_fault_blocks_the_attempted_write() {
515 let attempted = WriteId::from_generated("attempted-write".into());
516 let error = StoreError::MissingBlob {
517 namespace: "documents".into(),
518 id: "missing-content".into(),
519 };
520 assert_eq!(
521 error.write_block(&attempted),
522 Some((
523 attempted,
524 WriteBlock::MissingBlob {
525 namespace: "documents".into(),
526 id: "missing-content".into(),
527 }
528 ))
529 );
530 }
531}