Skip to main content

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}