Skip to main content

yggdrasil/tasks/
log_purge.rs

1//! 运行日志保留策略后台任务(server-only)。
2//!
3//! 每小时执行一次(含启动时立即执行),读取 settings 表两键
4//! (缺键回退默认值,trash 模式):
5//! - `logs_retention_days`(默认 7):删除 `ts < now() - 保留天数` 的日志;
6//! - `logs_max_rows`(默认 100000):按 id 从新到旧保留前 N 行,其余删除。
7//!
8//! 任何错误只记日志,不中断循环(与 post_purge 同一模式)。
9
10use std::time::Duration;
11
12use tokio::time::interval;
13
14use crate::db::pool::get_conn;
15use crate::models::log::{LogSettings, DEFAULT_LOGS_MAX_ROWS, DEFAULT_LOGS_RETENTION_DAYS};
16
17/// 裁剪间隔:每小时。
18const PURGE_INTERVAL: Duration = Duration::from_secs(3600);
19
20/// 启动日志保留策略循环(serve() 内 spawn 一次)。
21pub async fn run_purge() {
22    let mut ticker = interval(PURGE_INTERVAL);
23    loop {
24        match get_conn().await {
25            Ok(client) => {
26                if let Err(e) = purge_once(&client).await {
27                    tracing::error!("Log purge error: {:?}", e);
28                }
29            }
30            Err(e) => tracing::error!("Failed to get DB connection for log purge: {:?}", e),
31        }
32        ticker.tick().await;
33    }
34}
35
36/// 读取配置并执行一次龄期 + 行数裁剪。
37async fn purge_once(client: &tokio_postgres::Client) -> Result<(), tokio_postgres::Error> {
38    // 读取配置,缺键/非法值时回退默认值。
39    let retention_days: i32 = client
40        .query_opt(
41            "SELECT value FROM settings WHERE key = 'logs_retention_days'",
42            &[],
43        )
44        .await?
45        .and_then(|r| r.get::<_, String>("value").parse().ok())
46        .unwrap_or(DEFAULT_LOGS_RETENTION_DAYS);
47
48    let max_rows: i32 = client
49        .query_opt(
50            "SELECT value FROM settings WHERE key = 'logs_max_rows'",
51            &[],
52        )
53        .await?
54        .and_then(|r| r.get::<_, String>("value").parse().ok())
55        .unwrap_or(DEFAULT_LOGS_MAX_ROWS);
56
57    let retention_days = LogSettings::clamp_retention(retention_days);
58    let max_rows = LogSettings::clamp_max_rows(max_rows) as i64;
59
60    // 1) 按龄期删除。
61    let aged = client
62        .execute(
63            "DELETE FROM logs WHERE ts < now() - make_interval(days => $1)",
64            &[&retention_days],
65        )
66        .await?;
67
68    // 2) 按行数裁剪:子查询找到「第 max_rows+1 新」的 id,
69    //    删除所有 id <= 它的行。表不足 max_rows 行时子查询返回 NULL,
70    //    `id <= NULL` 恒不为真,一行不删。
71    let trimmed = client
72        .execute(
73            "DELETE FROM logs WHERE id <= (\
74                 SELECT id FROM logs ORDER BY id DESC OFFSET $1 LIMIT 1\
75             )",
76            &[&max_rows],
77        )
78        .await?;
79
80    if aged + trimmed > 0 {
81        tracing::info!(
82            aged,
83            trimmed,
84            retention_days,
85            max_rows,
86            "Log purge: removed expired/excess log rows"
87        );
88    }
89    Ok(())
90}