Skip to main content

coven_database/
database_retry.rs

1//! Persisted retry waits through the owning database (§6.7).
2
3use super::Database;
4use crate::{
5    retry::{self, RetrySubject, RetryWait},
6    DbError,
7};
8
9impl Database {
10    /// Persist a failed pass's connection condition and retry deadline together.
11    /// A failure already owned by a slot or operation supplies no connection record.
12    pub async fn record_sync_retry<P: crate::PendingValue>(
13        &self,
14        connection: P::Subject,
15        record: Option<P>,
16        wait: Option<RetryWait>,
17    ) -> Result<(), DbError>
18    where
19        P::Subject: Send + 'static,
20    {
21        self.call(move |inner| {
22            inner.access.with_writer(|writer| {
23                writer.state_transaction(|db| {
24                    if let Some(record) = record {
25                        crate::pending::save(db, &record)?;
26                    } else {
27                        crate::pending::clear(db, &connection)?;
28                    }
29                    retry::set(db, RetrySubject::Sync, wait.as_ref())
30                })
31            })
32        })
33        .await
34    }
35
36    /// A successful pass settles its connection condition and wait atomically.
37    pub async fn clear_sync_retry<S: serde::Serialize + Send + 'static>(
38        &self,
39        connection: S,
40    ) -> Result<(), DbError> {
41        self.call(move |inner| {
42            let changed = inner.access.readers.acquire_reader().with_reader(|db| {
43                Ok::<_, DbError>(
44                    retry::find(db, RetrySubject::Sync)?.is_some()
45                        || crate::pending::recorded(db, &connection)?,
46                )
47            })?;
48            if !changed {
49                return Ok(());
50            }
51            inner.access.with_writer(|writer| {
52                writer.state_transaction(|db| {
53                    retry::clear(db, RetrySubject::Sync)?;
54                    crate::pending::clear(db, &connection)
55                })
56            })
57        })
58        .await
59    }
60
61    /// Persist an operation's failure and its automatic retry deadline together.
62    pub async fn record_operation_retry<P: crate::PendingValue>(
63        &self,
64        record: P,
65        wait: Option<RetryWait>,
66    ) -> Result<(), DbError> {
67        self.call(move |inner| {
68            let id = record
69                .operation()
70                .ok_or(DbError::Invariant { what: "an operation retry names its journal row" })?;
71            inner.access.with_writer(|writer| {
72                writer.state_transaction(|db| {
73                    retry::save_failure(db, &record, RetrySubject::Operation(id), wait.as_ref())
74                })
75            })
76        })
77        .await
78    }
79
80    /// Persist an operation's failure and its automatic retry deadline
81    /// together, while the operation is still at `step`, the step the failed
82    /// attempt read, and return whether it did. A writer outside the attempt,
83    /// such as the app's write deciding an invite or the invite's expiry, may
84    /// have moved the operation on since, or ended it: the failure then
85    /// belongs to a step the operation has left, and recording it would hold
86    /// the operation's next step back behind a wait it never met.
87    pub async fn record_operation_retry_at<P: crate::PendingValue>(
88        &self,
89        record: P,
90        wait: Option<RetryWait>,
91        step: u32,
92    ) -> Result<bool, DbError> {
93        self.call(move |inner| {
94            let id = record
95                .operation()
96                .ok_or(DbError::Invariant { what: "an operation retry names its journal row" })?;
97            inner.access.with_writer(|writer| {
98                writer.state_transaction(|db| {
99                    if crate::operation::find(db, id)?.is_none_or(|row| row.last_step != step) {
100                        return Ok(false);
101                    }
102                    retry::save_failure(db, &record, RetrySubject::Operation(id), wait.as_ref())
103                        .map(|()| true)
104                })
105            })
106        })
107        .await
108    }
109
110    /// Every persisted wait, for arming on reopen (§6.7).
111    pub async fn retry_waits(&self) -> Result<Vec<(RetrySubject, RetryWait)>, DbError> {
112        self.call(move |inner| inner.access.readers.acquire_reader().with_reader(retry::read_all))
113            .await
114    }
115
116    /// The subject's persisted wait, if one is armed.
117    pub async fn retry_wait(&self, subject: RetrySubject) -> Result<Option<RetryWait>, DbError> {
118        self.call(move |inner| {
119            inner.access.readers.acquire_reader().with_reader(|db| retry::find(db, subject))
120        })
121        .await
122    }
123
124    /// The same wait, live (E4), for a test waiting for a failure to arm it.
125    #[cfg(any(test, feature = "test-utils"))]
126    pub fn subscribe_retry_wait(
127        &self,
128        subject: RetrySubject,
129    ) -> crate::LiveQuery<Option<RetryWait>, crate::DbError> {
130        self.subscribe(move |sql| sql.retry_wait(subject))
131    }
132
133    /// Persist `T` and `W` before the subject waits. A failure here fails the
134    /// initiating work; it is never permission to retry immediately.
135    pub async fn record_retry(
136        &self,
137        subject: RetrySubject,
138        wait: RetryWait,
139    ) -> Result<(), DbError> {
140        self.call(move |inner| {
141            inner
142                .access
143                .with_writer(|writer| writer.transaction(|db| retry::save(db, subject, &wait)))
144        })
145        .await
146    }
147
148    /// End, in one transaction, the persisted waits `select` picks after
149    /// reading every wait, every pending record, the upload queue and the pin
150    /// requests as one committed state. Each ended wait keeps its failure count; its deadline
151    /// becomes `now` and its full wait zero, so reopening arms none (§6.7).
152    /// Returns the ended waits.
153    pub async fn end_retry_waits<P: crate::PendingValue>(
154        &self,
155        now: std::time::SystemTime,
156        select: impl FnOnce(
157                &[(RetrySubject, RetryWait)],
158                &[P],
159                &[crate::FileUpload],
160                &[crate::PinRequest],
161            ) -> Vec<RetrySubject>
162            + Send
163            + 'static,
164    ) -> Result<Vec<(RetrySubject, RetryWait)>, DbError> {
165        self.call(move |inner| {
166            #[cfg(any(test, feature = "test-utils"))]
167            let injected = inner
168                .retry_end_failures
169                .fetch_update(
170                    std::sync::atomic::Ordering::SeqCst,
171                    std::sync::atomic::Ordering::SeqCst,
172                    |left| left.checked_sub(1),
173                )
174                .is_ok();
175            inner.access.with_writer(|writer| {
176                writer.transaction(|db| {
177                    #[cfg(any(test, feature = "test-utils"))]
178                    if injected {
179                        return Err(DbError::Sqlite(rusqlite::Error::SqliteFailure(
180                            rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_IOERR),
181                            Some("injected retry end failure".into()),
182                        )));
183                    }
184                    let waits = retry::read_all(db)?;
185                    let pending = crate::pending::read::<P>(db)?;
186                    let uploads = crate::file_queue::read(db)?;
187                    let pins = crate::file_pins::read(db)?;
188                    let mut ended = Vec::new();
189                    for subject in select(&waits, &pending, &uploads, &pins) {
190                        let Some((_, wait)) = waits.iter().find(|(held, _)| *held == subject)
191                        else {
192                            return Err(DbError::Invariant {
193                                what: "an ended retry wait is persisted",
194                            });
195                        };
196                        retry::end(db, subject, now)?;
197                        ended.push((
198                            subject,
199                            RetryWait {
200                                failures: wait.failures,
201                                not_before: now,
202                                wait: std::time::Duration::ZERO,
203                            },
204                        ));
205                    }
206                    Ok(ended)
207                })
208            })
209        })
210        .await
211    }
212
213    /// Fail the next `count` `end_retry_waits` transactions with a disk error.
214    #[cfg(any(test, feature = "test-utils"))]
215    pub async fn fail_retry_ends(&self, count: usize) {
216        self.call(move |inner| {
217            inner.retry_end_failures.store(count, std::sync::atomic::Ordering::SeqCst);
218            Ok::<_, DbError>(())
219        })
220        .await
221        .expect("an open store arms its retry-end failures");
222    }
223
224    /// Forget a wait once its work succeeds or no longer retries automatically.
225    pub async fn clear_retry(&self, subject: RetrySubject) -> Result<(), DbError> {
226        self.call(move |inner| {
227            inner.access.with_writer(|writer| writer.transaction(|db| retry::clear(db, subject)))
228        })
229        .await
230    }
231}