Files
qiming/qiming-rcoder/crates/rcoder/src/config_watcher.rs
2026-06-01 13:54:52 +08:00

206 lines
6.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 配置文件监控模块
//!
//! 本模块使用 `notify` crate 实现配置文件的实时监控和热更新。
//! 当 `config.yml` 文件被修改时,自动重新加载 API Key 配置,无需重启服务。
//!
//! # 功能特性
//!
//! - 实时监控配置文件变化(基于文件系统事件)
//! - 自动重载 API Key 配置
//! - 配置验证(防止空 API Key)
//! - 详细的变更日志记录
//! - 线程安全的配置更新(使用 RwLock)
//!
//! # 使用示例
//!
//! ```no_run
//! use std::sync::{Arc, RwLock};
//! use std::path::PathBuf;
//! # use crate::config::ApiKeyAuthConfig;
//! # use crate::config_watcher::ConfigWatcher;
//!
//! let api_key_config = Arc::new(ArcSwap::from_pointee(ApiKeyAuthConfig::default()));
//! let config_path = PathBuf::from("config.yml");
//!
//! match ConfigWatcher::new(config_path, api_key_config) {
//! Ok(watcher) => {
//! println!("config watcher already started");
//! // watcher 必须保持存活,否则监控会停止
//! }
//! Err(e) => {
//! eprintln!("config watcher start failed: {}", e);
//! }
//! }
//! ```
use anyhow::Result;
use arc_swap::ArcSwap;
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use tokio::sync::mpsc;
use tracing::{error, info, warn};
use crate::config::{ApiKeyAuthConfig, load_api_key_config_from_file};
/// 配置文件监控器
///
/// 内部持有 `RecommendedWatcher` 以保持文件监控活跃。
/// 一旦 ConfigWatcher 被 drop,文件监控将停止。
///
/// # 注意
///
/// `config_path` 和 `api_key_config` 字段虽然未直接读取,
/// 但它们被 spawn 的异步任务闭包捕获,因此必须保留。
pub struct ConfigWatcher {
/// 文件系统监控器(必须保持存活)
_watcher: RecommendedWatcher,
/// 配置文件路径(未直接访问,但被闭包捕获)
#[allow(dead_code)]
config_path: PathBuf,
/// API Key 配置的共享引用(未直接访问,但被闭包捕获)
#[allow(dead_code)]
api_key_config: Arc<ArcSwap<ApiKeyAuthConfig>>,
}
impl ConfigWatcher {
/// 创建新的配置监控器
pub fn new(
config_path: PathBuf,
api_key_config: Arc<ArcSwap<ApiKeyAuthConfig>>,
) -> Result<Self> {
let (tx, mut rx) = mpsc::channel(100);
let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
if let Ok(event) = res {
let _ = tx.blocking_send(event);
}
})?;
watcher.watch(&config_path, RecursiveMode::NonRecursive)?;
info!(
"📁 [CONFIG_WATCHER] Starting config file watch: {:?}",
config_path
);
// 克隆必要的数据以在 tokio 任务中使用
let config_path_clone = config_path.clone();
let api_key_config_clone = Arc::clone(&api_key_config);
// 启动配置监控任务
tokio::spawn(async move {
loop {
if let Some(event) = rx.recv().await {
// 只处理修改事件
if matches!(event.kind, EventKind::Modify(_)) {
// 添加短暂延迟,避免文件未完全写入
tokio::time::sleep(Duration::from_millis(100)).await;
if let Err(e) = Self::reload_config(
&config_path_clone,
Arc::clone(&api_key_config_clone),
)
.await
{
warn!(" [CONFIG_WATCHER] config reload failed: {}", e);
}
}
}
}
});
Ok(Self {
_watcher: watcher,
config_path,
api_key_config,
})
}
/// 重新加载配置(使用 ArcSwap 无锁更新)
async fn reload_config(
config_path: &Path,
api_key_config: Arc<ArcSwap<ApiKeyAuthConfig>>,
) -> Result<()> {
match load_api_key_config_from_file(config_path) {
Ok(new_config) => {
// 验证配置有效性
if new_config.enabled && new_config.api_key.trim().is_empty() {
error!("[CONFIG_WATCHER] API Key is empty");
return Err(anyhow::anyhow!("API Key cannot be empty string"));
}
// 🚀 使用 ArcSwap 原子更新配置(无锁,不阻塞读取)
let old_config = api_key_config.load();
let old_enabled = old_config.enabled;
let key_changed = old_config.api_key != new_config.api_key;
// 提前保存新配置状态(用于日志)
let new_enabled = new_config.enabled;
// 原子替换配置(移动所有权,避免 clone
api_key_config.store(Arc::new(new_config));
// 记录配置变更
if old_enabled != new_enabled {
info!(
"🔄 [CONFIG_WATCHER] API Key auth status updated: {} -> {}",
old_enabled, new_enabled
);
}
if key_changed {
info!("[CONFIG_WATCHER] API Key alreadyupdated");
}
if !old_enabled && !new_enabled && !key_changed {
// 配置未实际变化,不记录日志
return Ok(());
}
info!("[CONFIG_WATCHER] Config update succeeded");
Ok(())
}
Err(e) => {
error!("[CONFIG_WATCHER] Config file reload failed: {}", e);
Err(e)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[tokio::test]
async fn test_config_watcher_creation() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.yml");
// 创建测试配置文件
fs::write(
&config_path,
r#"
api_key_auth:
enabled: false
api_key: "sk-test123"
"#,
)
.unwrap();
let api_key_config = Arc::new(ArcSwap::from_pointee(ApiKeyAuthConfig {
enabled: false,
api_key: "sk-test123".to_string(),
}));
let watcher = ConfigWatcher::new(config_path, api_key_config);
assert!(watcher.is_ok());
}
}