use crate::config_model::ServerConfig;
use kodegen_mcp_schema::McpError;
use parking_lot::RwLock;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicUsize, AtomicU64, AtomicBool, Ordering};
use tempfile::NamedTempFile;
use std::io::Write;
static CONFIG_WRITE_COUNT: AtomicUsize = AtomicUsize::new(0);
static CONFIG_WRITE_START: OnceLock<std::time::Instant> = OnceLock::new();
pub(crate) static CONFIG_SAVE_ERRORS: AtomicUsize = AtomicUsize::new(0);
pub(crate) async fn save_to_disk(
config: &Arc<RwLock<ServerConfig>>,
config_path: &PathBuf,
) -> Result<(), McpError> {
let start_time = CONFIG_WRITE_START.get_or_init(std::time::Instant::now);
let count = CONFIG_WRITE_COUNT.fetch_add(1, Ordering::Relaxed);
if count.is_multiple_of(10) {
let elapsed = start_time.elapsed().as_secs();
let rate = if elapsed > 0 {
f64::from(u32::try_from(count).unwrap_or(u32::MAX)) / elapsed as f64 * 60.0
} else {
0.0
};
log::info!("Config writes: {count} total ({rate:.2}/min)");
}
let json = {
let config = config.read();
serde_json::to_string_pretty(&*config)?
};
if let Some(parent) = config_path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
rotate_backups(config_path, 3).await?;
let config_path_clone = config_path.clone();
let json_clone = json.clone();
tokio::task::spawn_blocking(move || {
let parent = config_path_clone.parent().ok_or_else(|| {
McpError::Other(anyhow::anyhow!("Config path has no parent directory"))
})?;
let mut temp_file = NamedTempFile::new_in(parent)?;
temp_file.write_all(json_clone.as_bytes())?;
temp_file.as_file().sync_all()?;
temp_file.persist(&config_path_clone)?;
Ok::<(), anyhow::Error>(())
})
.await
.map_err(|e| McpError::Other(anyhow::anyhow!("Spawn blocking failed: {}", e)))??;
Ok(())
}
async fn rotate_backups(config_path: &PathBuf, max_backups: usize) -> Result<(), McpError> {
for i in (1..max_backups).rev() {
let old_backup = backup_path(config_path, Some(i));
let new_backup = backup_path(config_path, Some(i + 1));
if old_backup.exists() {
let _ = tokio::fs::rename(&old_backup, &new_backup).await;
}
}
let primary_backup = backup_path(config_path, None);
let backup_1 = backup_path(config_path, Some(1));
if primary_backup.exists() {
let _ = tokio::fs::rename(&primary_backup, &backup_1).await;
}
if config_path.exists() {
tokio::fs::copy(config_path, &primary_backup).await?;
}
Ok(())
}
pub(crate) fn backup_path(config_path: &std::path::Path, index: Option<usize>) -> PathBuf {
match index {
None => {
let mut path = config_path.as_os_str().to_os_string();
path.push(".backup");
PathBuf::from(path)
}
Some(n) => {
let mut path = config_path.as_os_str().to_os_string();
path.push(format!(".backup.{}", n));
PathBuf::from(path)
}
}
}
pub(crate) struct SaveRequest {
pub min_generation: u64,
}
pub(crate) fn start_background_saver(
config: Arc<RwLock<ServerConfig>>,
config_path: PathBuf,
mut save_receiver: tokio::sync::mpsc::UnboundedReceiver<SaveRequest>,
generation: Arc<AtomicU64>,
saving: Arc<AtomicBool>,
) {
tokio::spawn(async move {
const DEBOUNCE_MS: u64 = 300;
let mut last_persisted_gen: u64 = 0;
let mut pending_gen: Option<u64> = None;
let mut last_request_time = std::time::Instant::now();
loop {
tokio::select! {
Some(req) = save_receiver.recv() => {
pending_gen = Some(
pending_gen
.map(|g| g.max(req.min_generation))
.unwrap_or(req.min_generation)
);
last_request_time = std::time::Instant::now();
log::trace!("Save requested for generation {}", req.min_generation);
}
() = tokio::time::sleep(std::time::Duration::from_millis(100)) => {
if let Some(required_gen) = pending_gen {
if last_request_time.elapsed().as_millis() >= u128::from(DEBOUNCE_MS) {
if required_gen > last_persisted_gen {
let (json, read_gen) = {
let cfg = config.read();
let current_gen = generation.load(Ordering::SeqCst);
let json = serde_json::to_string_pretty(&*cfg)
.unwrap_or_else(|e| {
log::error!("Failed to serialize config: {}", e);
String::new()
});
(json, current_gen)
};
if json.is_empty() {
pending_gen = None;
continue; }
saving.store(true, Ordering::SeqCst);
if let Err(e) = save_to_disk(&config, &config_path).await {
log::error!("Failed to save config: {}", e);
CONFIG_SAVE_ERRORS.fetch_add(1, Ordering::Relaxed);
} else {
last_persisted_gen = read_gen;
log::debug!("Saved config generation {} to disk", read_gen);
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
saving.store(false, Ordering::SeqCst);
} else {
log::trace!(
"Skipping save: generation {} already persisted",
required_gen
);
}
pending_gen = None;
}
}
}
else => {
log::info!("Background saver shutting down");
if let Some(required_gen) = pending_gen
&& required_gen > last_persisted_gen {
let _ = save_to_disk(&config, &config_path).await;
}
break;
}
}
}
});
}
#[must_use]
pub fn get_save_error_count() -> usize {
CONFIG_SAVE_ERRORS.load(Ordering::Relaxed)
}