laterite_core/parameters/
mod.rs1use std::collections::HashSet;
18use std::sync::atomic::{AtomicU64, Ordering};
19use std::sync::{Arc, Mutex};
20use std::time::Duration;
21
22use chrono::{SecondsFormat, Utc};
23use serde::de::DeserializeOwned;
24use serde::Serialize;
25
26use crate::error::{CoreError, CoreResult};
27use crate::query::{bind_values, build, AnyRowExt};
28use crate::{Cache, Db, Module, ModuleId};
29use sea_query::{Expr, Iden, OnConflict, Query};
30
31pub mod migrations;
32
33#[derive(Iden)]
35pub(crate) enum ParametersTable {
36 #[iden = "parameters"]
37 Table,
38 Namespace,
39 Group,
40 Item,
41 Value,
42 UpdatedAt,
43}
44
45pub struct ParametersModule;
47
48impl Module for ParametersModule {
49 fn id(&self) -> ModuleId {
50 ModuleId::new(migrations::MODULE_ID)
51 }
52 fn migrations(&self) -> crate::MigrationSet {
53 migrations::migrations()
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct Key<'a> {
61 pub namespace: &'a str,
62 pub group: &'a str,
63 pub item: &'a str,
64}
65
66impl<'a> Key<'a> {
67 pub fn parse(key: &'a str) -> CoreResult<Self> {
70 let malformed = || CoreError::Validation(format!("malformed parameter key `{key}`"));
71 let (namespace, rest) = key.split_once("::").ok_or_else(malformed)?;
72 let (group, item) = rest.split_once('.').ok_or_else(malformed)?;
73 if namespace.is_empty() || group.is_empty() || item.is_empty() {
74 return Err(malformed());
75 }
76 Ok(Self {
77 namespace,
78 group,
79 item,
80 })
81 }
82}
83
84pub async fn get<T: DeserializeOwned>(db: &Db, key: &str) -> CoreResult<Option<T>> {
86 match get_value(db, key).await? {
87 Some(value) => Ok(Some(serde_json::from_value(value).map_err(|e| {
88 CoreError::Internal(format!(
89 "parameter `{key}` holds a value of another shape: {e}"
90 ))
91 })?)),
92 None => Ok(None),
93 }
94}
95
96pub async fn get_value(db: &Db, key: &str) -> CoreResult<Option<serde_json::Value>> {
98 let parts = Key::parse(key)?;
99 let (sql, values) = build(
100 db.backend,
101 Query::select()
102 .column(ParametersTable::Value)
103 .from(ParametersTable::Table)
104 .and_where(Expr::col(ParametersTable::Namespace).eq(parts.namespace))
105 .and_where(Expr::col(ParametersTable::Group).eq(parts.group))
106 .and_where(Expr::col(ParametersTable::Item).eq(parts.item))
107 .to_owned(),
108 );
109 let row = bind_values(sqlx::query(&sql), values)
110 .fetch_optional(&db.pool)
111 .await?;
112 match row {
113 Some(r) => {
114 let text = r.get_text("value")?;
115 Ok(Some(serde_json::from_str(&text).map_err(|e| {
116 CoreError::Internal(format!("parameter `{key}` holds invalid JSON: {e}"))
117 })?))
118 }
119 None => Ok(None),
120 }
121}
122
123pub async fn set<T: Serialize + ?Sized>(db: &Db, key: &str, value: &T) -> CoreResult<()> {
125 let parts = Key::parse(key)?;
126 let json = serde_json::to_string(value)
127 .map_err(|e| CoreError::Internal(format!("parameter `{key}` cannot serialize: {e}")))?;
128 let now = Utc::now().to_rfc3339_opts(SecondsFormat::Micros, true);
129 let (sql, values) = build(
130 db.backend,
131 Query::insert()
132 .into_table(ParametersTable::Table)
133 .columns([
134 ParametersTable::Namespace,
135 ParametersTable::Group,
136 ParametersTable::Item,
137 ParametersTable::Value,
138 ParametersTable::UpdatedAt,
139 ])
140 .values_panic([
141 parts.namespace.into(),
142 parts.group.into(),
143 parts.item.into(),
144 json.into(),
145 now.into(),
146 ])
147 .on_conflict(
148 OnConflict::columns([
149 ParametersTable::Namespace,
150 ParametersTable::Group,
151 ParametersTable::Item,
152 ])
153 .update_columns([ParametersTable::Value, ParametersTable::UpdatedAt])
154 .to_owned(),
155 )
156 .to_owned(),
157 );
158 bind_values(sqlx::query(&sql), values)
159 .execute(&db.pool)
160 .await?;
161 Ok(())
162}
163
164pub async fn reset(db: &Db, key: &str) -> CoreResult<bool> {
166 let parts = Key::parse(key)?;
167 let (sql, values) = build(
168 db.backend,
169 Query::delete()
170 .from_table(ParametersTable::Table)
171 .and_where(Expr::col(ParametersTable::Namespace).eq(parts.namespace))
172 .and_where(Expr::col(ParametersTable::Group).eq(parts.group))
173 .and_where(Expr::col(ParametersTable::Item).eq(parts.item))
174 .to_owned(),
175 );
176 let done = bind_values(sqlx::query(&sql), values)
177 .execute(&db.pool)
178 .await?;
179 Ok(done.rows_affected() > 0)
180}
181
182pub const NAMESPACE: &str = "laterite.parameters::values";
185
186#[derive(Clone)]
191pub struct Parameters {
192 db: Db,
193 cache: Cache,
194 ttl: Duration,
195 stale: Arc<Mutex<HashSet<String>>>,
197 epoch: Arc<AtomicU64>,
200}
201
202impl std::fmt::Debug for Parameters {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 f.debug_struct("Parameters")
205 .field("backend", &self.db.backend)
206 .field("ttl", &self.ttl)
207 .finish_non_exhaustive()
208 }
209}
210
211pub const DEFAULT_TTL: Duration = Duration::from_secs(300);
213
214impl Parameters {
215 pub fn new(db: Db) -> Self {
217 Self::with_cache(db, Cache::local())
218 }
219
220 pub fn with_cache(db: Db, cache: Cache) -> Self {
222 cache.allow(NAMESPACE);
223 Self {
224 db,
225 cache,
226 ttl: DEFAULT_TTL,
227 stale: Arc::new(Mutex::new(HashSet::new())),
228 epoch: Arc::new(AtomicU64::new(0)),
229 }
230 }
231
232 pub fn with_ttl(mut self, ttl: Duration) -> Self {
234 self.ttl = ttl;
235 self
236 }
237
238 pub fn db(&self) -> &Db {
240 &self.db
241 }
242
243 pub fn cache(&self) -> &Cache {
245 &self.cache
246 }
247
248 fn cache_key(&self, key: &str) -> String {
249 format!(
250 "laterite.parameters::{}:{key}",
251 self.epoch.load(Ordering::Relaxed)
252 )
253 }
254
255 fn take_stale(&self, key: &str) -> bool {
256 self.stale
257 .lock()
258 .unwrap_or_else(|e| e.into_inner())
259 .remove(key)
260 }
261
262 pub async fn get<T: DeserializeOwned>(&self, key: &str) -> CoreResult<Option<T>> {
264 let cache_key = self.cache_key(key);
265 let value: Option<serde_json::Value> = if self.take_stale(key) {
266 let value = get_value(&self.db, key).await?;
267 self.cache
268 .put_json(&cache_key, &value, &[NAMESPACE], self.ttl)
269 .await?;
270 value
271 } else {
272 self.cache
273 .remember(&cache_key, self.ttl, &[NAMESPACE], || {
274 get_value(&self.db, key)
275 })
276 .await?
277 };
278 match value {
279 Some(value) => Ok(Some(serde_json::from_value(value).map_err(|e| {
280 CoreError::Internal(format!(
281 "parameter `{key}` holds a value of another shape: {e}"
282 ))
283 })?)),
284 None => Ok(None),
285 }
286 }
287
288 pub async fn get_or<T: DeserializeOwned>(&self, key: &str, default: T) -> CoreResult<T> {
290 Ok(self.get(key).await?.unwrap_or(default))
291 }
292
293 pub async fn set<T: Serialize + ?Sized>(&self, key: &str, value: &T) -> CoreResult<()> {
295 set(&self.db, key, value).await?;
296 let json = serde_json::to_value(value)
297 .map_err(|e| CoreError::Internal(format!("parameter `{key}` cannot serialize: {e}")))?;
298 self.cache.bump(NAMESPACE).await?;
299 self.cache
300 .put_json(&self.cache_key(key), &Some(json), &[NAMESPACE], self.ttl)
301 .await
302 }
303
304 pub async fn reset(&self, key: &str) -> CoreResult<bool> {
306 let done = reset(&self.db, key).await?;
307 self.cache.bump(NAMESPACE).await?;
308 self.cache
309 .put_json(
310 &self.cache_key(key),
311 &None::<serde_json::Value>,
312 &[NAMESPACE],
313 self.ttl,
314 )
315 .await?;
316 Ok(done)
317 }
318
319 pub fn forget(&self, key: &str) {
321 self.stale
322 .lock()
323 .unwrap_or_else(|e| e.into_inner())
324 .insert(key.to_string());
325 }
326
327 pub fn clear(&self) {
330 self.epoch.fetch_add(1, Ordering::Relaxed);
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn a_key_splits_into_namespace_group_and_item() {
340 let key = Key::parse("acme.blog::sync.cursor").unwrap();
341 assert_eq!(key.namespace, "acme.blog");
342 assert_eq!(key.group, "sync");
343 assert_eq!(key.item, "cursor");
344 let dotted = Key::parse("laterite.backend::setup.completed.at").unwrap();
345 assert_eq!(dotted.group, "setup");
346 assert_eq!(dotted.item, "completed.at");
347 }
348
349 #[test]
350 fn a_malformed_key_is_refused() {
351 for bad in [
352 "",
353 "sync.cursor",
354 "acme::cursor",
355 "::sync.cursor",
356 "acme::.cursor",
357 "acme::sync.",
358 ] {
359 assert!(
360 matches!(Key::parse(bad), Err(CoreError::Validation(_))),
361 "{bad}"
362 );
363 }
364 }
365}