fix(#1957): persist hivectl-start running-agents snapshot in the broker kv table
This commit is contained in:
parent
e5da5b900e
commit
f7d84f4847
2 changed files with 110 additions and 5 deletions
|
|
@ -36,6 +36,11 @@ CREATE TABLE IF NOT EXISTS reminders (
|
|||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_reminders_due
|
||||
ON reminders (agent, due_at) WHERE sent_at IS NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kv (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
";
|
||||
|
||||
/// Capacity of the live event channel. Slow subscribers (e.g. an idle browser)
|
||||
|
|
@ -197,6 +202,52 @@ impl Broker {
|
|||
self.events.subscribe()
|
||||
}
|
||||
|
||||
/// Set a small persistent key/value pair (upsert). The `kv` table is a
|
||||
/// general single-value store for state that must survive a hive-c0re
|
||||
/// restart but isn't worth a dedicated table — currently just the
|
||||
/// `hivectl start` running-agents snapshot (see
|
||||
/// `Coordinator::set_last_stopped_running`).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the sqlite upsert fails.
|
||||
pub fn kv_set(&self, key: &str, value: &str) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO kv (key, value) VALUES (?1, ?2)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||
params![key, value],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read a persistent key/value pair. `None` when the key is absent.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the sqlite query fails.
|
||||
pub fn kv_get(&self, key: &str) -> Result<Option<String>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let value: Option<String> = conn
|
||||
.query_row("SELECT value FROM kv WHERE key = ?1", params![key], |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.optional()?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Delete a persistent key/value pair. Idempotent — deleting an absent
|
||||
/// key is a no-op.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the sqlite delete fails.
|
||||
pub fn kv_delete(&self, key: &str) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute("DELETE FROM kv WHERE key = ?1", params![key])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send(&self, message: &Message) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let now = now_unix();
|
||||
|
|
@ -1216,6 +1267,24 @@ mod tests {
|
|||
batch.pop()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kv_set_get_delete_roundtrip() {
|
||||
let tb = open_broker();
|
||||
let b = &tb.broker;
|
||||
// Absent key -> None.
|
||||
assert_eq!(b.kv_get("k").unwrap(), None);
|
||||
// Set then get.
|
||||
b.kv_set("k", "v1").unwrap();
|
||||
assert_eq!(b.kv_get("k").unwrap(), Some("v1".to_owned()));
|
||||
// Upsert overwrites.
|
||||
b.kv_set("k", "v2").unwrap();
|
||||
assert_eq!(b.kv_get("k").unwrap(), Some("v2".to_owned()));
|
||||
// Delete clears; deleting again is a no-op.
|
||||
b.kv_delete("k").unwrap();
|
||||
assert_eq!(b.kv_get("k").unwrap(), None);
|
||||
b.kv_delete("k").unwrap();
|
||||
}
|
||||
|
||||
/// Happy path: send → recv → `ack_turn` drains the in-memory list
|
||||
/// and marks the row `acked_at IS NOT NULL`. A second recv finds
|
||||
/// nothing pending (the row stays in the table for vacuum).
|
||||
|
|
|
|||
Loading…
Reference in a new issue