coven_replication/blob/
mod.rs1pub(crate) mod delete;
8pub(crate) mod progress;
9pub(crate) mod retry;
10pub mod transition;
11
12pub use delete::BlobTombstoneJson;
13pub use transition::{MakeLocalError, MakeRemoteError, MakeRemoteRoot};
14
15#[derive(Debug)]
16pub enum DrainOutcome {
17 Drained {
18 uploaded: usize,
19 yielded_for_publish: bool,
20 failures: UploadFailures,
21 },
22 QueueEmpty,
23 AllInBackoff,
24 Paused,
25}
26
27#[cfg(any(test, feature = "test-utils"))]
28impl DrainOutcome {
29 #[track_caller]
30 fn drained(&self) -> (usize, bool, &UploadFailures) {
31 match self {
32 Self::Drained {
33 uploaded,
34 yielded_for_publish,
35 failures,
36 } => (*uploaded, *yielded_for_publish, failures),
37 other => panic!("expected a drain that attempted queued entries, got {other:?}"),
38 }
39 }
40
41 #[track_caller]
42 pub fn uploaded(&self) -> usize {
43 self.drained().0
44 }
45
46 #[track_caller]
47 pub fn yielded_for_publish(&self) -> bool {
48 self.drained().1
49 }
50
51 #[track_caller]
52 pub fn failures(&self) -> &UploadFailures {
53 self.drained().2
54 }
55
56 #[track_caller]
57 pub fn into_failures(self) -> UploadFailures {
58 match self {
59 Self::Drained { failures, .. } => failures,
60 other => panic!("expected a drain that attempted queued entries, got {other:?}"),
61 }
62 }
63}
64
65#[derive(Debug, thiserror::Error)]
66pub enum UploadFailureCause {
67 #[error("local upload state: {0}")]
68 Database(#[from] coven_database::DbError),
69 #[error("local upload locator: {0}")]
70 Locator(#[from] coven_protocol::blob::locator::BlobLocatorError),
71 #[error("local upload file: {0}")]
72 File(#[from] coven_foundation::atomic_file::FileError),
73 #[error("local upload pin: {0}")]
74 Pin(#[from] coven_foundation::store_dir::StoreBlobFileError),
75 #[error("cancelled cache copy: {0}")]
76 CachedRemoval(#[from] coven_foundation::store_dir::CachedLocatorRemovalError),
77 #[error("blob storage: {0}")]
78 Storage(#[from] coven_protocol::objects::StorageError),
79 #[error("local upload state: {0}")]
80 InvalidState(String),
81}
82
83#[derive(Debug)]
84pub struct UploadFailure {
85 pub entry_id: i64,
86 pub object_key: String,
87 pub cause: UploadFailureCause,
88}
89
90#[derive(Debug)]
91pub struct UploadFailures(Vec<UploadFailure>);
92
93impl UploadFailures {
94 pub fn new(failures: Vec<UploadFailure>) -> Self {
95 Self(failures)
96 }
97
98 pub fn failures(&self) -> &[UploadFailure] {
99 &self.0
100 }
101
102 pub fn has_transport_failure(&self) -> bool {
103 self.0.iter().any(|failure| {
104 matches!(&failure.cause, UploadFailureCause::Storage(error) if error.is_transport())
105 })
106 }
107}
108
109impl std::fmt::Display for UploadFailures {
110 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 write!(formatter, "{} blob upload(s) failed", self.0.len())?;
112 for failure in &self.0 {
113 write!(
114 formatter,
115 "; entry {} {}: {}",
116 failure.entry_id, failure.object_key, failure.cause
117 )?;
118 }
119 Ok(())
120 }
121}
122
123impl std::error::Error for UploadFailures {
124 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
125 self.0.iter().find_map(|failure| match &failure.cause {
126 UploadFailureCause::Storage(error) if error.is_transport() => {
127 Some(error as &(dyn std::error::Error + 'static))
128 }
129 _ => None,
130 })
131 }
132}
133
134#[cfg(test)]
135mod upload_tests;
136
137#[cfg(test)]
138mod transition_tests;
139
140#[cfg(test)]
141mod local_store_tests;
142
143#[cfg(test)]
144mod delete_tests;