Skip to main content

yggdrasil/tasks/
backup.rs

1//! 自动定时备份后台任务。
2//!
3//! 仅在 `server` feature 启用时编译。每天定点(UTC,见 settings 表 `backup_time_utc`)
4//! 执行一次应用内备份(pg_dump + uploads 打包 + 自动轮转,见
5//! [`crate::api::database::backup::run_auto_backup`]),执行结果落库
6//! `backup_last_run_*` 键供面板展示。
7//!
8//! 调度语义:
9//! - 每次循环重读 DB 配置;关闭时挂起等待,不跑空转 tick。
10//! - 面板保存设置后经 [`crate::tasks::backup::notify_settings_changed`] 立即唤醒重排,
11//!   无需等原定的下次触发。
12//! - 任何错误只记录日志,不中断循环(与 post_purge 等任务一致)。
13
14use std::sync::LazyLock;
15use std::time::Duration;
16
17use chrono::Utc;
18use tokio::sync::Notify;
19
20use crate::api::database::backup::{self, BackupRunOutcome};
21use crate::api::settings::{load_backup_settings, save_last_backup_run};
22use crate::db::pool::get_conn;
23use crate::models::settings::LastBackupRun;
24
25/// 设置变更通知。`Notify` 单 permit 语义足够:多次保存折叠为一次重排。
26static SETTINGS_CHANGED: LazyLock<Notify> = LazyLock::new(Notify::new);
27
28/// 唤醒调度器立即重读设置并重排(`update_backup_settings` 调用)。
29pub(crate) fn notify_settings_changed() {
30    SETTINGS_CHANGED.notify_waiters();
31}
32
33/// 启动自动备份调度循环。
34pub async fn run_scheduler() {
35    // 关闭态/读配置失败时的兜底自醒间隔(防丢通知导致永久挂起)。
36    const IDLE_RETRY: Duration = Duration::from_secs(3600);
37
38    loop {
39        let settings = match get_conn().await {
40            Ok(conn) => match load_backup_settings(&conn).await {
41                Ok(s) => s,
42                Err(e) => {
43                    tracing::error!("Auto-backup: failed to load settings: {e:?}");
44                    tokio::select! {
45                        _ = SETTINGS_CHANGED.notified() => {}
46                        _ = tokio::time::sleep(IDLE_RETRY) => {}
47                    }
48                    continue;
49                }
50            },
51            Err(e) => {
52                tracing::error!("Auto-backup: failed to get DB connection: {e:?}");
53                tokio::time::sleep(IDLE_RETRY).await;
54                continue;
55            }
56        };
57
58        if !settings.auto_enabled {
59            tokio::select! {
60                _ = SETTINGS_CHANGED.notified() => {}
61                _ = tokio::time::sleep(IDLE_RETRY) => {}
62            }
63            continue;
64        }
65
66        let now = Utc::now();
67        let Some(next) = settings.next_run_after(now) else {
68            // time_utc 非法(面板/API 已规范化,此处仅是纵深防御)。
69            tracing::error!(
70                "Auto-backup: invalid backup_time_utc {:?}",
71                settings.time_utc
72            );
73            tokio::select! {
74                _ = SETTINGS_CHANGED.notified() => {}
75                _ = tokio::time::sleep(IDLE_RETRY) => {}
76            }
77            continue;
78        };
79        let wait = (next - now).to_std().unwrap_or(Duration::ZERO);
80        tracing::info!("Auto-backup: next run scheduled at {}", next.to_rfc3339());
81
82        // 睡眠期间设置变更 → 立即重排;否则睡到触发点执行。
83        tokio::select! {
84            _ = SETTINGS_CHANGED.notified() => continue,
85            _ = tokio::time::sleep(wait) => {}
86        }
87
88        tracing::info!("Auto-backup: starting scheduled backup");
89        let outcome = backup::run_auto_backup().await;
90        persist_last_run(&outcome).await;
91        match &outcome {
92            Ok(o) => tracing::info!(
93                "Auto-backup: done (sql={}, uploads={:?}, warning={:?})",
94                o.sql_filename,
95                o.uploads_filename,
96                o.warning
97            ),
98            Err(e) => tracing::error!("Auto-backup: failed: {e}"),
99        }
100    }
101}
102
103/// 落库最近一次自动备份结果(面板展示用)。写库失败只记日志。
104async fn persist_last_run(outcome: &Result<BackupRunOutcome, String>) {
105    let run = match outcome {
106        Ok(o) => LastBackupRun {
107            at: Utc::now().to_rfc3339(),
108            ok: true,
109            file: Some(o.sql_filename.clone()),
110            error: o.warning.clone(),
111        },
112        Err(e) => LastBackupRun {
113            at: Utc::now().to_rfc3339(),
114            ok: false,
115            file: None,
116            error: Some(e.clone()),
117        },
118    };
119    match get_conn().await {
120        Ok(conn) => {
121            if let Err(e) = save_last_backup_run(&conn, &run).await {
122                tracing::error!("Auto-backup: failed to persist last_run: {e:?}");
123            }
124        }
125        Err(e) => tracing::error!("Auto-backup: failed to get DB connection for last_run: {e:?}"),
126    }
127}