Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_steel/src/srv/admin/persist.rs

3.9 KiB, 21 runs

created by r1870400018:10972, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Ozone persistence of admin dashboard state.
2//!
3//! Dashboard history (the Overview sparkline strip) lives in an in-memory
4//! ring that is wiped on every Steel restart. That's fine for the live
5//! process but makes charts empty after every deploy and makes it harder
6//! for the operator to notice slow-moving trends through a restart.
7//!
8//! This module writes the derived sparkline points to the default vhost's
9//! ozone database on a fixed cadence and reads them back at start-up so
10//! the Overview strip picks up where the previous run left off.
11//!
12//! The encoded payload is a [`Dat::Vek`] of [`Dat::Tup5`] rows,
13//! `(t_secs: u64, cpu_pct: f64, mem_pct: f64, disk_bps: f64, net_bps: f64)`,
14//! stored under a fixed string key with a version suffix so future format
15//! changes can migrate gracefully.
16//!
17//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
18//! Anthropic Claude
19
20use crate::srv::admin::host_sampler::DerivedHostPoint;
21
22use oxedyne_fe2o3_core::prelude::*;
23use oxedyne_fe2o3_iop_crypto::enc::Encrypter;
24use oxedyne_fe2o3_iop_db::api::Database;
25use oxedyne_fe2o3_iop_hash::api::Hasher;
26use oxedyne_fe2o3_jdat::{
27 prelude::*,
28 id::NumIdDat,
29 try_extract_tup5dat,
30 tup5dat,
31};
32
33pub const HOST_HISTORY_KEY: &str = "admin.history.host.v1"; // bump to .v2 on a format change
34
35pub fn encode_host_points(points: &[DerivedHostPoint]) -> Dat {
36 let mut v = Vec::with_capacity(points.len());
37 for p in points {
38 v.push(tup5dat![
39 Dat::U64(p.t_secs),
40 dat!(p.cpu_pct),
41 dat!(p.mem_pct),
42 dat!(p.disk_bps),
43 dat!(p.net_bps),
44 ]);
45 }
46 Dat::Vek(Vek(v))
47}
48
49/// Malformed rows are skipped, so a stale or partially valid payload degrades to
50/// a shorter history rather than a hard load failure.
51pub fn decode_host_points(dat: Dat) -> Outcome<Vec<DerivedHostPoint>> {
52 let vek = try_extract_dat!(dat, Vek);
53 let mut out = Vec::with_capacity(vek.len());
54 for item in vek {
55 // Use a local closure so a single bad row does not abort the
56 // whole load. Operators never want the first sample after a
57 // redeploy to be "dashboard is blank because one row had a
58 // typo somewhere".
59 let row_result: Outcome<DerivedHostPoint> = (|| {
60 let arr = try_extract_tup5dat!(item);
61 let t = try_extract_dat!(arr[0].clone(), U64);
62 let cpu = try_extract_dat!(arr[1].clone(), F64);
63 let mem = try_extract_dat!(arr[2].clone(), F64);
64 let dsk = try_extract_dat!(arr[3].clone(), F64);
65 let net = try_extract_dat!(arr[4].clone(), F64);
66 Ok(DerivedHostPoint {
67 t_secs: t,
68 cpu_pct: *cpu,
69 mem_pct: *mem,
70 disk_bps: *dsk,
71 net_bps: *net,
72 })
73 })();
74 match row_result {
75 Ok(p) => out.push(p),
76 Err(e) => {
77 warn!("persist: skipping malformed host history row: {}", e);
78 },
79 }
80 }
81 Ok(out)
82}
83
84pub fn save_host_points<
85 const UIDL: usize,
86 UID: NumIdDat<UIDL>,
87 ENC: Encrypter,
88 KH: Hasher,
89 DB: Database<UIDL, UID, ENC, KH>,
90>(
91 db: &DB,
92 user: UID,
93 points: &[DerivedHostPoint],
94)
95 -> Outcome<()>
96{
97 let key = Dat::Str(HOST_HISTORY_KEY.to_string());
98 let val = encode_host_points(points);
99 let _ = res!(db.insert(key, val, user, None));
100 Ok(())
101}
102
103/// A missing key yields an empty vector, which is what the first run after this
104/// feature shipped sees.
105pub fn load_host_points<
106 const UIDL: usize,
107 UID: NumIdDat<UIDL>,
108 ENC: Encrypter,
109 KH: Hasher,
110 DB: Database<UIDL, UID, ENC, KH>,
111>(
112 db: &DB,
113)
114 -> Outcome<Vec<DerivedHostPoint>>
115{
116 let key = Dat::Str(HOST_HISTORY_KEY.to_string());
117 match res!(db.get(&key, None)) {
118 Some((dat, _meta)) => decode_host_points(dat),
119 None => Ok(Vec::new()),
120 }
121}