Skip to main content

orch/store/
kv.rs

1use super::*;
2use crate::cluster::{Command, Reply};
3use sqlx::SqliteConnection;
4
5/// Key prefixes that stay on this node: metrics and the node's own Raft position
6const LOCAL_PREFIXES: [&str; 2] = ["metrics:", "raft:"];
7
8pub(crate) fn is_local(key: &str) -> bool {
9    LOCAL_PREFIXES.iter().any(|p| key.starts_with(p))
10}
11
12/// One replicated kv row: key, value and `updated_at`
13pub(crate) type Row = (String, String, String);
14
15pub(crate) async fn set(
16    conn: &mut SqliteConnection,
17    now: &str,
18    key: &str,
19    value: &str,
20) -> anyhow::Result<()> {
21    sqlx::query(
22        "INSERT INTO kv (key, value, updated_at) VALUES (?, ?, ?)
23         ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at",
24    )
25    .bind(key)
26    .bind(value)
27    .bind(now)
28    .execute(conn)
29    .await?;
30    Ok(())
31}
32
33/// A single statement, so the read-modify-write cannot race
34pub(crate) async fn increment(
35    conn: &mut SqliteConnection,
36    now: &str,
37    key: &str,
38) -> anyhow::Result<u64> {
39    let row: (i64,) = sqlx::query_as(
40        "INSERT INTO kv (key, value, updated_at) VALUES (?1, '1', ?2)
41         ON CONFLICT(key) DO UPDATE
42           SET value = CAST(value AS INTEGER) + 1,
43               updated_at = ?2
44         RETURNING CAST(value AS INTEGER)",
45    )
46    .bind(key)
47    .bind(now)
48    .fetch_one(conn)
49    .await?;
50    debug_assert!(
51        row.0 >= 1,
52        "kv_increment returned unexpected value {}",
53        row.0
54    );
55    Ok(row.0 as u64)
56}
57
58/// Whoever lands first wins, and every caller reads back the stored winner
59pub(crate) async fn insert_if_absent(
60    conn: &mut SqliteConnection,
61    now: &str,
62    key: &str,
63    value: &str,
64) -> anyhow::Result<String> {
65    sqlx::query("INSERT OR IGNORE INTO kv (key, value, updated_at) VALUES (?, ?, ?)")
66        .bind(key)
67        .bind(value)
68        .bind(now)
69        .execute(&mut *conn)
70        .await?;
71    let (stored,): (String,) = sqlx::query_as("SELECT value FROM kv WHERE key = ?")
72        .bind(key)
73        .fetch_one(conn)
74        .await?;
75    Ok(stored)
76}
77
78pub(crate) async fn delete(conn: &mut SqliteConnection, key: &str) -> anyhow::Result<()> {
79    sqlx::query("DELETE FROM kv WHERE key = ?")
80        .bind(key)
81        .execute(conn)
82        .await?;
83    Ok(())
84}
85
86pub(crate) async fn replicated_rows(pool: &SqlitePool) -> anyhow::Result<Vec<Row>> {
87    let rows: Vec<Row> = sqlx::query_as("SELECT key, value, updated_at FROM kv ORDER BY key")
88        .fetch_all(pool)
89        .await?;
90    Ok(rows.into_iter().filter(|r| !is_local(&r.0)).collect())
91}
92
93/// Swap every replicated row for `rows`, leaving local keys alone
94pub(crate) async fn replace_replicated(
95    conn: &mut SqliteConnection,
96    rows: &[Row],
97) -> anyhow::Result<()> {
98    let keys: Vec<(String,)> = sqlx::query_as("SELECT key FROM kv")
99        .fetch_all(&mut *conn)
100        .await?;
101    for (key,) in keys.iter().filter(|(k,)| !is_local(k)) {
102        delete(conn, key).await?;
103    }
104
105    for (key, value, updated_at) in rows.iter().filter(|(k, _, _)| !is_local(k)) {
106        set(conn, updated_at, key, value).await?;
107    }
108
109    Ok(())
110}
111
112impl TaskStore {
113    pub async fn kv_get(&self, key: &str) -> anyhow::Result<Option<String>> {
114        let result: Option<(String,)> = sqlx::query_as("SELECT value FROM kv WHERE key = ?")
115            .bind(key)
116            .fetch_optional(&self.pool)
117            .await?;
118        Ok(result.map(|r| r.0))
119    }
120
121    /// Set a value in the KV store (upsert).
122    pub async fn kv_set(&self, key: &str, value: &str) -> anyhow::Result<()> {
123        let cmd = Command::KvSet {
124            key: key.to_string(),
125            value: value.to_string(),
126        };
127        self.write_kv(key, cmd).await.map(|_| ())
128    }
129
130    /// Atomically increment an integer counter in the KV store, inserting it as 1 if absent.
131    /// Returns the new value after the increment.
132    pub async fn kv_increment(&self, key: &str) -> anyhow::Result<u64> {
133        let cmd = Command::KvIncrement {
134            key: key.to_string(),
135        };
136        match self.write_kv(key, cmd).await? {
137            Reply::Count(n) => Ok(n),
138            other => anyhow::bail!("kv_increment got {other:?}"),
139        }
140    }
141
142    /// Insert a value only if the key is absent, then return the stored winner.
143    pub async fn kv_insert_if_absent(&self, key: &str, value: &str) -> anyhow::Result<String> {
144        let cmd = Command::KvInsertIfAbsent {
145            key: key.to_string(),
146            value: value.to_string(),
147        };
148        match self.write_kv(key, cmd).await? {
149            Reply::Value(v) => Ok(v),
150            other => anyhow::bail!("kv_insert_if_absent got {other:?}"),
151        }
152    }
153
154    /// Delete a key from the KV store.
155    pub async fn kv_delete(&self, key: &str) -> anyhow::Result<()> {
156        let cmd = Command::KvDelete {
157            key: key.to_string(),
158        };
159        self.write_kv(key, cmd).await.map(|_| ())
160    }
161
162    /// Keys local to this node skip the Raft log
163    async fn write_kv(&self, key: &str, cmd: Command) -> anyhow::Result<Reply> {
164        if is_local(key) {
165            return self.apply_direct(cmd).await;
166        }
167
168        self.write(cmd).await
169    }
170
171    /// List all (key, value) pairs where the key starts with `prefix`.
172    ///
173    /// The prefix is escaped so that literal `%` and `_` characters are treated as
174    /// plain text rather than SQLite LIKE wildcards.
175    pub async fn kv_list_prefix(&self, prefix: &str) -> anyhow::Result<Vec<(String, String)>> {
176        // Escape LIKE metacharacters so a prefix containing '%' or '_' (or the chosen
177        // escape char '\') is matched literally.
178        let escaped = prefix
179            .replace('\\', "\\\\")
180            .replace('%', "\\%")
181            .replace('_', "\\_");
182        let pattern = format!("{escaped}%");
183        let rows: Vec<(String, String)> =
184            sqlx::query_as("SELECT key, value FROM kv WHERE key LIKE ? ESCAPE '\\'")
185                .bind(&pattern)
186                .fetch_all(&self.pool)
187                .await?;
188        Ok(rows)
189    }
190}