coven_database/
internal_schema_retry_effect.rs1use crate::stored_millis::{since_epoch, stored_time, Round};
23use crate::DbError;
24use std::time::SystemTime;
25
26pub(crate) const RETRY_EFFECT: &str = "coven_retry_effect";
28
29pub(super) const TABLE: &str = "CREATE TABLE coven_retry_effect (
31 singleton INTEGER PRIMARY KEY NOT NULL CHECK(singleton=1),
32 syncing INTEGER NOT NULL CHECK(syncing IN (0,1)),
33 held INTEGER NOT NULL CHECK(held IN (0,1)),
34 cooling_until INTEGER,
35 effect TEXT NOT NULL GENERATED ALWAYS AS (CASE
36 WHEN syncing=0 THEN 'not_syncing'
37 WHEN held=1 THEN 'failed'
38 WHEN cooling_until IS NOT NULL THEN 'cooling_down'
39 ELSE 'pass' END) STORED,
40 CHECK(syncing=1 OR cooling_until IS NULL)
41 ) STRICT;
42 INSERT INTO coven_retry_effect(singleton,syncing,held,cooling_until) VALUES(1,0,0,NULL);";
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub struct RetryState {
47 pub sync: RetrySync,
50 pub held: bool,
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum RetrySync {
58 Stopped,
60 Started {
62 cooling_until: Option<SystemTime>,
65 },
66}
67
68impl RetryState {
69 pub(crate) const OPENED: Self = Self { sync: RetrySync::Stopped, held: false };
72}
73
74fn millis(time: SystemTime) -> Result<i64, DbError> {
78 since_epoch(time, Round::Up).ok_or(DbError::Invariant {
79 what: "a cooldown ends between the Unix epoch and SQLite's last millisecond",
80 })
81}
82
83pub(crate) fn read(db: &crate::sqlite::DatabaseConnection) -> Result<RetryState, DbError> {
85 let (syncing, held, cooling_until) = db.query_row(
86 "SELECT syncing,held,cooling_until FROM coven_retry_effect WHERE singleton=1",
87 [],
88 |row| Ok((row.get::<_, bool>(0)?, row.get::<_, bool>(1)?, row.get::<_, Option<i64>>(2)?)),
89 )?;
90 let sync = if syncing {
91 RetrySync::Started { cooling_until: cooling_until.map(stored_time).transpose()? }
92 } else {
93 RetrySync::Stopped
94 };
95 Ok(RetryState { sync, held })
96}
97
98pub(crate) fn save(
101 db: &crate::sqlite::DatabaseConnection,
102 state: RetryState,
103) -> Result<(), DbError> {
104 let held = state.held;
105 let (syncing, cooling_until) = match state.sync {
106 RetrySync::Stopped => (false, None),
107 RetrySync::Started { cooling_until } => (true, cooling_until.map(millis).transpose()?),
108 };
109 db.internal_execute(
110 "UPDATE coven_retry_effect SET syncing=?1,held=?2,cooling_until=?3
111 WHERE singleton=1 AND (syncing IS NOT ?1 OR held IS NOT ?2 OR cooling_until IS NOT ?3)",
112 rusqlite::params![syncing, held, cooling_until],
113 )?;
114 Ok(())
115}
116
117pub(crate) fn stored(state: RetryState) -> Result<RetryState, DbError> {
120 let sync = match state.sync {
121 RetrySync::Stopped => RetrySync::Stopped,
122 RetrySync::Started { cooling_until } => RetrySync::Started {
123 cooling_until: cooling_until
124 .map(|until| millis(until).and_then(stored_time))
125 .transpose()?,
126 },
127 };
128 Ok(RetryState { sync, ..state })
129}
130
131#[cfg(test)]
132#[path = "internal_schema_retry_effect_tests.rs"]
133mod tests;