coven_sync/last_pass.rs
1//! How the last pass ended (E5), as `coven_sync_status.last_pass` keeps it:
2//! one of the facts the sync state is computed from (§19.1).
3
4use crate::{DeviceIdEvidence, PendingRecord, SyncFailure};
5use coven_database::DbError;
6use coven_storage::{StorageError, StorageFailure};
7use std::time::SystemTime;
8
9/// How the last pass that reached storage, or failed trying, ended (E5), as
10/// `coven_sync_status.last_pass` holds it. It outlives each connection:
11/// opening keeps it, and the running loop writes it as each pass ends. An
12/// error is kept as its text, which names every cause below it with secrets
13/// and URLs already cut, so a pass read after the process that ran it ended
14/// still says why.
15#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
16pub enum LastPass {
17 /// The pass reached its observed end; other subjects may still be pending.
18 Synced,
19 /// The request preventing the pass received no usable provider response.
20 Offline {
21 /// The network failure, as the provider adapter classified it.
22 failure: StorageFailure,
23 /// The failure's text.
24 error: String,
25 },
26 /// This slot invokes §5.4's stuck-device rule.
27 Blocked {
28 /// The slot that cannot apply.
29 #[serde(with = "coven_database::slot_json")]
30 slot: coven_merge::Slot,
31 /// Why it cannot apply.
32 reason: crate::PendingReason,
33 /// The text of the error that blocked the slot: the cause the
34 /// slot's pending record keeps. A device that opens already blocked
35 /// has none until a pass of its own meets the error.
36 error: Option<String>,
37 },
38 /// Storage refused the whole pass, or coven's own operation failed.
39 Failed {
40 /// What failed.
41 failure: StatusFailure,
42 /// The failure's text.
43 error: String,
44 },
45}
46
47/// What stopped a pass as a whole, as `LastPass` keeps it: a `SyncFailure`
48/// without the error objects, whose text it holds beside it (E5).
49#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
50pub enum StatusFailure {
51 /// A schema or stored object format is newer than this implementation.
52 UpdateRequired(crate::RequiredUpdate),
53 /// This device or its member has been removed.
54 Removed,
55 /// The admission that published this joined store was refused (§13.2).
56 NotAdmitted,
57 /// Another copy uses this device's id; this copy sends nothing more (§19.3).
58 DeviceIdInUse(DeviceIdEvidence),
59 /// The provider refused or failed a request, as its adapter classified it.
60 Storage(StorageFailure),
61 /// The pass finished, but this work of coven's own failed its last
62 /// attempt; it retries under backoff.
63 Operation(PendingRecord),
64 /// Any other failure.
65 Other,
66}
67
68impl LastPass {
69 /// How a pass that failed as a whole ended.
70 pub fn failed(failure: impl Into<SyncFailure>) -> Self {
71 let failure = failure.into();
72 let error = failure.to_string();
73 let failure = match failure {
74 SyncFailure::UpdateRequired(update) => StatusFailure::UpdateRequired(update),
75 SyncFailure::Removed => StatusFailure::Removed,
76 SyncFailure::NotAdmitted => StatusFailure::NotAdmitted,
77 SyncFailure::DeviceIdInUse(evidence) => StatusFailure::DeviceIdInUse(evidence),
78 SyncFailure::Storage(storage) => StatusFailure::Storage(storage.failure()),
79 SyncFailure::Operation(record) => StatusFailure::Operation(*record),
80 SyncFailure::Other(_) => StatusFailure::Other,
81 };
82 Self::Failed { failure, error }
83 }
84
85 fn encoded(&self) -> Result<Vec<u8>, DbError> {
86 serde_json::to_vec(self).map_err(DbError::StateEncoding)
87 }
88
89 /// Write this as how the last pass ended when sync failed outside a
90 /// pass's end: starting, stopping, or a local failure or panic of the
91 /// loop's, in a pass or outside one; and that nothing moves, so the
92 /// state shows the failure (IO `failed_worker_shows`). A
93 /// failure fails the store's handle with its cause
94 /// (`DbError::StatePersistence`), so the caller stops the work that
95 /// failed.
96 pub async fn record(&self, database: &coven_database::Database) -> Result<(), DbError> {
97 database.record_last_pass(self.encoded()?).await
98 }
99
100 /// Write this, a store-wide worker's panic, as how the last pass ended,
101 /// halting sync until the store reopens: nothing moves, and no pass
102 /// writes over it (IO `halt`). A failure fails the store's handle, as
103 /// `record`'s does.
104 pub async fn record_halt(&self, database: &coven_database::Database) -> Result<(), DbError> {
105 database.record_halt(self.encoded()?).await
106 }
107
108 /// Write this as how the running pass ended, `now` by the store's clock,
109 /// and that it moves no more. A synced pass's end becomes the last
110 /// synced time, unless that is later. A failure fails the store's
111 /// handle, as `record`'s does.
112 pub async fn record_end(
113 &self,
114 database: &coven_database::Database,
115 now: SystemTime,
116 ) -> Result<(), DbError> {
117 let finished = matches!(self, Self::Synced).then_some(now);
118 database.record_pass_end(self.encoded()?, finished).await
119 }
120
121 /// Follow `coven_sync_status.last_pass` with a live query, as an app's
122 /// SQL does: `None` until a pass ends on this device.
123 #[cfg(any(test, feature = "test-utils"))]
124 pub fn follow(
125 database: &coven_database::Database,
126 ) -> coven_database::test_utils::Followed<Option<Self>, coven_database::DbError> {
127 coven_database::test_utils::Followed::new(database.subscribe(|sql| {
128 Ok(sql.query_row("SELECT last_pass FROM coven_sync_status", [], |row| row.get(0))?)
129 }))
130 }
131}
132
133/// Reads `coven_sync_status.last_pass`.
134impl coven_database::types::FromSql for LastPass {
135 fn column_result(
136 value: coven_database::types::ValueRef<'_>,
137 ) -> coven_database::types::FromSqlResult<Self> {
138 serde_json::from_slice(value.as_blob()?)
139 .map_err(|error| coven_database::types::FromSqlError::Other(Box::new(error)))
140 }
141}
142
143/// Why a pass, or the connection attempt that begins one, failed: how it
144/// ended (`LastPass`, E5).
145#[derive(Clone, Debug)]
146pub enum PassFailure {
147 /// The request preventing the pass received no usable provider response.
148 Offline {
149 /// The network failure and its original cause.
150 error: std::sync::Arc<StorageError>,
151 },
152 /// This slot invokes §5.4's stuck-device rule.
153 Blocked {
154 /// The slot that cannot apply.
155 slot: coven_merge::Slot,
156 /// Why it cannot apply.
157 reason: crate::PendingReason,
158 /// The text of the error that blocked the slot: the cause the slot's
159 /// pending record keeps.
160 error: Option<String>,
161 },
162 /// Storage refused the whole pass, or coven's own operation failed.
163 Failed {
164 /// The failure and its original cause.
165 error: SyncFailure,
166 },
167}
168
169impl From<PassFailure> for LastPass {
170 fn from(failure: PassFailure) -> Self {
171 match failure {
172 PassFailure::Offline { error } => {
173 Self::Offline { failure: error.failure(), error: error.to_string() }
174 }
175 PassFailure::Blocked { slot, reason, error } => Self::Blocked { slot, reason, error },
176 PassFailure::Failed { error } => Self::failed(error),
177 }
178 }
179}