1use std::collections::BTreeMap;
31
32use rusqlite::Connection;
33
34use super::schema::{bucket_of, BUCKET_COLS};
35
36pub const GRANULARITIES_MS: [i64; 4] = [60_000, 300_000, 3_600_000, 86_400_000];
39
40fn cursor_key(gran: usize) -> String {
43 format!("g{gran}_cursor_ms")
44}
45
46pub fn cursor(conn: &Connection, gran: usize) -> i64 {
48 conn.query_row(
49 "SELECT value FROM meta WHERE key = ?1",
50 [cursor_key(gran)],
51 |r| r.get::<_, String>(0),
52 )
53 .ok()
54 .and_then(|v| v.parse().ok())
55 .unwrap_or(0)
56}
57
58fn set_cursor_tx(conn: &Connection, gran: usize, frontier: i64) -> rusqlite::Result<()> {
59 conn.execute(
60 "INSERT INTO meta (key, value) VALUES (?1, ?2)
61 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
62 rusqlite::params![cursor_key(gran), frontier.to_string()],
63 )?;
64 Ok(())
65}
66
67pub fn window_of(ts_ms: i64, gran_ms: i64) -> i64 {
69 (ts_ms / gran_ms) * gran_ms
70}
71
72pub fn roll_raw_to_1m(conn: &Connection, now_ms: i64, grace_ms: i64) -> rusqlite::Result<usize> {
77 let gran_ms = GRANULARITIES_MS[0];
78 let frontier = window_of(now_ms.saturating_sub(grace_ms), gran_ms);
79 let cur = cursor(conn, 0);
80 if frontier <= cur {
81 return Ok(0);
82 }
83 let tx = conn.unchecked_transaction()?;
84 let n = roll_raw_range(&tx, cur, frontier)?;
85 set_cursor_tx(&tx, 0, frontier)?;
86 tx.commit()?;
87 Ok(n)
88}
89
90pub fn roll_raw_range(conn: &Connection, lo: i64, hi: i64) -> rusqlite::Result<usize> {
94 let windows = roll_raw_fixed(conn, lo, hi)?;
95 roll_raw_dims(conn, lo, hi)?;
96 Ok(windows)
97}
98
99fn roll_raw_fixed(conn: &Connection, lo: i64, hi: i64) -> rusqlite::Result<usize> {
103 let before: i64 = conn.query_row(
104 "SELECT COUNT(DISTINCT (ts_ms / 60000)) FROM raw_all WHERE ts_ms >= ?1 AND ts_ms < ?2",
105 [lo, hi],
106 |r| r.get(0),
107 )?;
108 conn.execute(
109 "INSERT OR REPLACE INTO rollup_fixed (
110 gran, window_start, listener, route, upstream, consumer,
111 method, status_class, requests, errors, rate_limited, shed,
112 duration_sum_ms, duration_max_ms,
113 b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12
114 )
115 SELECT 0,
116 (ts_ms / 60000) * 60000,
117 listener, route, upstream, consumer, method, status_class,
118 COUNT(*),
119 SUM(CASE WHEN status >= 500 THEN 1 ELSE 0 END),
120 SUM(rate_limited), SUM(shed),
121 SUM(duration_ms), MAX(duration_ms),
122 SUM(CASE WHEN duration_ms <= 1.0 THEN 1 ELSE 0 END),
123 SUM(CASE WHEN duration_ms > 1.0 AND duration_ms <= 2.0 THEN 1 ELSE 0 END),
124 SUM(CASE WHEN duration_ms > 2.0 AND duration_ms <= 5.0 THEN 1 ELSE 0 END),
125 SUM(CASE WHEN duration_ms > 5.0 AND duration_ms <= 10.0 THEN 1 ELSE 0 END),
126 SUM(CASE WHEN duration_ms > 10.0 AND duration_ms <= 25.0 THEN 1 ELSE 0 END),
127 SUM(CASE WHEN duration_ms > 25.0 AND duration_ms <= 50.0 THEN 1 ELSE 0 END),
128 SUM(CASE WHEN duration_ms > 50.0 AND duration_ms <= 100.0 THEN 1 ELSE 0 END),
129 SUM(CASE WHEN duration_ms > 100.0 AND duration_ms <= 250.0 THEN 1 ELSE 0 END),
130 SUM(CASE WHEN duration_ms > 250.0 AND duration_ms <= 500.0 THEN 1 ELSE 0 END),
131 SUM(CASE WHEN duration_ms > 500.0 AND duration_ms <= 1000.0 THEN 1 ELSE 0 END),
132 SUM(CASE WHEN duration_ms > 1000.0 AND duration_ms <= 2500.0 THEN 1 ELSE 0 END),
133 SUM(CASE WHEN duration_ms > 2500.0 AND duration_ms <= 5000.0 THEN 1 ELSE 0 END),
134 SUM(CASE WHEN duration_ms > 5000.0 THEN 1 ELSE 0 END)
135 FROM raw_all
136 WHERE ts_ms >= ?1 AND ts_ms < ?2
137 GROUP BY (ts_ms / 60000), listener, route, upstream, consumer,
138 method, status_class",
139 [lo, hi],
140 )?;
141 Ok(before as usize)
142}
143
144fn roll_raw_dims(conn: &Connection, lo: i64, hi: i64) -> rusqlite::Result<()> {
150 #[derive(Default)]
151 struct Acc {
152 requests: i64,
153 errors: i64,
154 duration_sum_ms: f64,
155 buckets: Vec<i64>,
156 }
157 let mut groups: BTreeMap<(i64, String, String), Acc> = BTreeMap::new();
158 {
159 let mut stmt = conn.prepare(
160 "SELECT ts_ms, dims, status, duration_ms FROM raw_all
161 WHERE ts_ms >= ?1 AND ts_ms < ?2",
162 )?;
163 let mut rows = stmt.query([lo, hi])?;
164 while let Some(row) = rows.next()? {
165 let ts: i64 = row.get(0)?;
166 let dims_json: String = row.get(1)?;
167 let status: i64 = row.get(2)?;
168 let duration_ms: f64 = row.get(3)?;
169 let Ok(map) =
170 serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&dims_json)
171 else {
172 continue;
173 };
174 let minute = window_of(ts, 60_000);
175 for (dim, value) in map {
176 let Some(value) = value.as_str().map(str::to_string) else {
177 continue;
178 };
179 let acc = groups
180 .entry((minute, dim.clone(), value))
181 .or_insert_with(|| Acc {
182 buckets: vec![0; BUCKET_COLS],
183 ..Acc::default()
184 });
185 acc.requests += 1;
186 if status >= 500 {
187 acc.errors += 1;
188 }
189 acc.duration_sum_ms += duration_ms;
190 acc.buckets[bucket_of(duration_ms)] += 1;
191 }
192 }
193 }
194 let mut stmt = conn.prepare(
195 "INSERT OR REPLACE INTO rollup_dim (
196 gran, window_start, dim, value, requests, errors,
197 duration_sum_ms, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9,
198 b10, b11, b12
199 ) VALUES (0, ?1, ?2, ?3, ?4, ?5, ?6,
200 ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17,
201 ?18, ?19)",
202 )?;
203 for ((minute, dim, value), acc) in groups {
204 let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![
205 Box::new(minute),
206 Box::new(dim),
207 Box::new(value),
208 Box::new(acc.requests),
209 Box::new(acc.errors),
210 Box::new(acc.duration_sum_ms),
211 ];
212 for b in &acc.buckets {
213 params.push(Box::new(*b));
214 }
215 stmt.execute(rusqlite::params_from_iter(
216 params.iter().map(|p| p.as_ref()),
217 ))?;
218 }
219 Ok(())
220}
221
222pub fn roll_cascade(
226 conn: &Connection,
227 from: usize,
228 now_ms: i64,
229 grace_ms: i64,
230) -> rusqlite::Result<usize> {
231 debug_assert!(from + 1 < GRANULARITIES_MS.len());
232 let coarse = GRANULARITIES_MS[from + 1];
233 let frontier = window_of(now_ms.saturating_sub(grace_ms), coarse);
234 let cur = cursor(conn, from + 1);
235 if frontier <= cur {
236 return Ok(0);
237 }
238 let tx = conn.unchecked_transaction()?;
239 let n = cascade_range(&tx, from, cur, frontier)?;
240 set_cursor_tx(&tx, from + 1, frontier)?;
241 tx.commit()?;
242 Ok(n)
243}
244
245pub fn cascade_range(conn: &Connection, from: usize, lo: i64, hi: i64) -> rusqlite::Result<usize> {
250 let coarse = GRANULARITIES_MS[from + 1];
251 let before: i64 = conn.query_row(
252 "SELECT COUNT(DISTINCT (window_start / ?3)) FROM rollup_fixed
253 WHERE gran = ?1 AND window_start >= ?2 AND window_start < ?4",
254 rusqlite::params![from as i64, lo, coarse, hi],
255 |r| r.get(0),
256 )?;
257 conn.execute(
258 "INSERT OR REPLACE INTO rollup_fixed (
259 gran, window_start, listener, route, upstream, consumer,
260 method, status_class, requests, errors, rate_limited, shed,
261 duration_sum_ms, duration_max_ms,
262 b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12
263 )
264 SELECT ?4,
265 (window_start / ?5) * ?5,
266 listener, route, upstream, consumer, method, status_class,
267 SUM(requests), SUM(errors), SUM(rate_limited), SUM(shed),
268 SUM(duration_sum_ms), MAX(duration_max_ms),
269 SUM(b0), SUM(b1), SUM(b2), SUM(b3), SUM(b4), SUM(b5),
270 SUM(b6), SUM(b7), SUM(b8), SUM(b9), SUM(b10), SUM(b11),
271 SUM(b12)
272 FROM rollup_fixed
273 WHERE gran = ?1 AND window_start >= ?2 AND window_start < ?3
274 GROUP BY (window_start / ?5), listener, route, upstream,
275 consumer, method, status_class",
276 rusqlite::params![from as i64, lo, hi, (from + 1) as i64, coarse],
277 )?;
278 conn.execute(
281 "INSERT OR REPLACE INTO rollup_dim (
282 gran, window_start, dim, value, requests, errors,
283 duration_sum_ms, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9,
284 b10, b11, b12
285 )
286 SELECT ?4,
287 (window_start / ?5) * ?5,
288 dim, value,
289 SUM(requests), SUM(errors), SUM(duration_sum_ms),
290 SUM(b0), SUM(b1), SUM(b2), SUM(b3), SUM(b4), SUM(b5),
291 SUM(b6), SUM(b7), SUM(b8), SUM(b9), SUM(b10), SUM(b11),
292 SUM(b12)
293 FROM rollup_dim
294 WHERE gran = ?1 AND window_start >= ?2 AND window_start < ?3
295 GROUP BY (window_start / ?5), dim, value",
296 rusqlite::params![from as i64, lo, hi, (from + 1) as i64, coarse],
297 )?;
298 Ok(before as usize)
299}
300
301pub fn sweep_retention(
308 conn: &Connection,
309 raw_keep_ms: i64,
310 gran_keep_ms: [i64; 4],
311 now_ms: i64,
312 vacuum_pages: u32,
313) -> rusqlite::Result<usize> {
314 let mut deleted = 0usize;
315 let tx = conn.unchecked_transaction()?;
316 deleted += tx.execute(
319 "DELETE FROM raw WHERE ts_ms < ?1",
320 [now_ms.saturating_sub(raw_keep_ms)],
321 )?;
322 for (gran, keep) in gran_keep_ms.iter().enumerate() {
323 let cutoff = now_ms.saturating_sub(*keep);
324 deleted += tx.execute(
325 "DELETE FROM rollup_fixed WHERE gran = ?1 AND window_start < ?2",
326 rusqlite::params![gran as i64, cutoff],
327 )?;
328 deleted += tx.execute(
329 "DELETE FROM rollup_dim WHERE gran = ?1 AND window_start < ?2",
330 rusqlite::params![gran as i64, cutoff],
331 )?;
332 }
333 tx.commit()?;
334 let _ = conn.execute_batch(&format!("PRAGMA incremental_vacuum={vacuum_pages};"));
336 Ok(deleted)
337}