Skip to main content

laterite_core/cache/
signals.rs

1//! Generations and locks: in this process alone ([`LocalSignals`]) or in
2//! the database, shared by every process ([`DbSignals`]).
3
4use std::collections::HashMap;
5use std::sync::{Mutex, RwLock};
6use std::time::{Duration, Instant};
7
8use async_trait::async_trait;
9use chrono::{SecondsFormat, Utc};
10use sea_query::{Expr, Iden, Query};
11
12use super::Signals;
13use crate::error::CoreResult;
14use crate::query::{bind_values, build, on_conflict_ignore, AnyRowExt};
15use crate::Db;
16use sqlx::Row;
17
18/// A fresh generation: random and never zero, so a namespace seeded again
19/// after a reset never repeats a value a store still holds.
20fn seed() -> u64 {
21    u64::from(rand::random::<u32>()) + 1
22}
23
24/// Generations and locks held by this process alone.
25#[derive(Default)]
26pub struct LocalSignals {
27    generations: RwLock<HashMap<String, u64>>,
28    locks: Mutex<HashMap<String, (String, Instant)>>,
29}
30
31impl LocalSignals {
32    pub fn new() -> Self {
33        Self::default()
34    }
35}
36
37#[async_trait]
38impl Signals for LocalSignals {
39    fn name(&self) -> &'static str {
40        "local"
41    }
42    fn current(&self, namespace: &str) -> Option<u64> {
43        self.generations
44            .read()
45            .unwrap_or_else(|e| e.into_inner())
46            .get(namespace)
47            .copied()
48    }
49    async fn ensure(&self, namespace: &str) -> CoreResult<u64> {
50        let mut all = self.generations.write().unwrap_or_else(|e| e.into_inner());
51        Ok(*all.entry(namespace.to_string()).or_insert_with(seed))
52    }
53    async fn bump(&self, namespace: &str) -> CoreResult<u64> {
54        let mut all = self.generations.write().unwrap_or_else(|e| e.into_inner());
55        let next = all.entry(namespace.to_string()).or_insert_with(seed);
56        *next += 1;
57        Ok(*next)
58    }
59    async fn refresh(&self) -> CoreResult<()> {
60        Ok(())
61    }
62    async fn known(&self) -> CoreResult<Vec<(String, u64)>> {
63        let all = self.generations.read().unwrap_or_else(|e| e.into_inner());
64        let mut out: Vec<(String, u64)> = all.iter().map(|(k, v)| (k.clone(), *v)).collect();
65        out.sort();
66        Ok(out)
67    }
68    async fn lock(&self, name: &str, owner: &str, ttl: Duration) -> CoreResult<bool> {
69        let mut locks = self.locks.lock().unwrap_or_else(|e| e.into_inner());
70        let now = Instant::now();
71        if locks.get(name).is_some_and(|(_, until)| *until > now) {
72            return Ok(false);
73        }
74        locks.insert(name.to_string(), (owner.to_string(), now + ttl));
75        Ok(true)
76    }
77    async fn unlock(&self, name: &str, owner: &str) -> CoreResult<bool> {
78        let mut locks = self.locks.lock().unwrap_or_else(|e| e.into_inner());
79        if locks.get(name).is_some_and(|(held, _)| held == owner) {
80            locks.remove(name);
81            Ok(true)
82        } else {
83            Ok(false)
84        }
85    }
86}
87
88#[derive(Iden)]
89pub(crate) enum CacheGenerations {
90    #[iden = "cache_generations"]
91    Table,
92    Namespace,
93    Generation,
94}
95
96#[derive(Iden)]
97pub(crate) enum CacheLocks {
98    #[iden = "cache_locks"]
99    Table,
100    Name,
101    Owner,
102    ExpiresAt,
103}
104
105fn now_text() -> String {
106    Utc::now().to_rfc3339_opts(SecondsFormat::Micros, true)
107}
108
109fn until_text(ttl: Duration) -> String {
110    let ttl = chrono::Duration::from_std(ttl).unwrap_or(chrono::Duration::MAX);
111    (Utc::now() + ttl).to_rfc3339_opts(SecondsFormat::Micros, true)
112}
113
114/// Generations and locks in the database, with a replica of the generations
115/// in this process.
116pub struct DbSignals {
117    db: Db,
118    replica: RwLock<HashMap<String, u64>>,
119}
120
121impl DbSignals {
122    pub fn new(db: Db) -> Self {
123        Self {
124            db,
125            replica: RwLock::new(HashMap::new()),
126        }
127    }
128
129    fn remember(&self, namespace: &str, generation: u64) {
130        self.replica
131            .write()
132            .unwrap_or_else(|e| e.into_inner())
133            .insert(namespace.to_string(), generation);
134    }
135
136    async fn read(&self, namespace: &str) -> CoreResult<Option<u64>> {
137        let (sql, values) = build(
138            self.db.backend,
139            Query::select()
140                .column(CacheGenerations::Generation)
141                .from(CacheGenerations::Table)
142                .and_where(Expr::col(CacheGenerations::Namespace).eq(namespace))
143                .to_owned(),
144        );
145        let row = bind_values(sqlx::query(&sql), values)
146            .fetch_optional(&self.db.pool)
147            .await?;
148        Ok(match row {
149            Some(r) => Some(r.try_get::<i64, _>("generation")?.max(0) as u64),
150            None => None,
151        })
152    }
153
154    async fn insert_if_new(&self, namespace: &str) -> CoreResult<()> {
155        let (sql, values) = build(
156            self.db.backend,
157            Query::insert()
158                .into_table(CacheGenerations::Table)
159                .columns([CacheGenerations::Namespace, CacheGenerations::Generation])
160                .values_panic([namespace.into(), (seed() as i64).into()])
161                .on_conflict(on_conflict_ignore([CacheGenerations::Namespace]))
162                .to_owned(),
163        );
164        bind_values(sqlx::query(&sql), values)
165            .execute(&self.db.pool)
166            .await?;
167        Ok(())
168    }
169}
170
171#[async_trait]
172impl Signals for DbSignals {
173    fn name(&self) -> &'static str {
174        "database"
175    }
176    fn current(&self, namespace: &str) -> Option<u64> {
177        self.replica
178            .read()
179            .unwrap_or_else(|e| e.into_inner())
180            .get(namespace)
181            .copied()
182    }
183    async fn ensure(&self, namespace: &str) -> CoreResult<u64> {
184        if let Some(current) = self.current(namespace) {
185            return Ok(current);
186        }
187        let generation = match self.read(namespace).await? {
188            Some(generation) => generation,
189            None => {
190                self.insert_if_new(namespace).await?;
191                self.read(namespace).await?.unwrap_or_else(seed)
192            }
193        };
194        self.remember(namespace, generation);
195        Ok(generation)
196    }
197    async fn bump(&self, namespace: &str) -> CoreResult<u64> {
198        self.insert_if_new(namespace).await?;
199        let (sql, values) = build(
200            self.db.backend,
201            Query::update()
202                .table(CacheGenerations::Table)
203                .value(
204                    CacheGenerations::Generation,
205                    Expr::col(CacheGenerations::Generation).add(1),
206                )
207                .and_where(Expr::col(CacheGenerations::Namespace).eq(namespace))
208                .to_owned(),
209        );
210        bind_values(sqlx::query(&sql), values)
211            .execute(&self.db.pool)
212            .await?;
213        let generation = self.read(namespace).await?.unwrap_or_else(seed);
214        self.remember(namespace, generation);
215        Ok(generation)
216    }
217    async fn refresh(&self) -> CoreResult<()> {
218        let known = self.known().await?;
219        let mut replica = self.replica.write().unwrap_or_else(|e| e.into_inner());
220        for (namespace, generation) in known {
221            replica.insert(namespace, generation);
222        }
223        Ok(())
224    }
225    async fn known(&self) -> CoreResult<Vec<(String, u64)>> {
226        let (sql, values) = build(
227            self.db.backend,
228            Query::select()
229                .columns([CacheGenerations::Namespace, CacheGenerations::Generation])
230                .from(CacheGenerations::Table)
231                .order_by(CacheGenerations::Namespace, sea_query::Order::Asc)
232                .to_owned(),
233        );
234        let rows = bind_values(sqlx::query(&sql), values)
235            .fetch_all(&self.db.pool)
236            .await?;
237        let mut out = Vec::with_capacity(rows.len());
238        for r in rows {
239            out.push((
240                r.get_text("namespace")?,
241                r.try_get::<i64, _>("generation")?.max(0) as u64,
242            ));
243        }
244        Ok(out)
245    }
246    async fn lock(&self, name: &str, owner: &str, ttl: Duration) -> CoreResult<bool> {
247        // A lapsed lock goes first, then the insert either lands or finds
248        // the row held; the read says whose it is.
249        let (sql, values) = build(
250            self.db.backend,
251            Query::delete()
252                .from_table(CacheLocks::Table)
253                .and_where(Expr::col(CacheLocks::Name).eq(name))
254                .and_where(Expr::col(CacheLocks::ExpiresAt).lt(now_text()))
255                .to_owned(),
256        );
257        bind_values(sqlx::query(&sql), values)
258            .execute(&self.db.pool)
259            .await?;
260        let (sql, values) = build(
261            self.db.backend,
262            Query::insert()
263                .into_table(CacheLocks::Table)
264                .columns([CacheLocks::Name, CacheLocks::Owner, CacheLocks::ExpiresAt])
265                .values_panic([name.into(), owner.into(), until_text(ttl).into()])
266                .on_conflict(on_conflict_ignore([CacheLocks::Name]))
267                .to_owned(),
268        );
269        bind_values(sqlx::query(&sql), values)
270            .execute(&self.db.pool)
271            .await?;
272        let (sql, values) = build(
273            self.db.backend,
274            Query::select()
275                .column(CacheLocks::Owner)
276                .from(CacheLocks::Table)
277                .and_where(Expr::col(CacheLocks::Name).eq(name))
278                .to_owned(),
279        );
280        let row = bind_values(sqlx::query(&sql), values)
281            .fetch_optional(&self.db.pool)
282            .await?;
283        Ok(match row {
284            Some(r) => r.get_text("owner")? == owner,
285            None => false,
286        })
287    }
288    async fn unlock(&self, name: &str, owner: &str) -> CoreResult<bool> {
289        let (sql, values) = build(
290            self.db.backend,
291            Query::delete()
292                .from_table(CacheLocks::Table)
293                .and_where(Expr::col(CacheLocks::Name).eq(name))
294                .and_where(Expr::col(CacheLocks::Owner).eq(owner))
295                .to_owned(),
296        );
297        let done = bind_values(sqlx::query(&sql), values)
298            .execute(&self.db.pool)
299            .await?;
300        Ok(done.rows_affected() > 0)
301    }
302}