use std::collections::HashMap;
use std::fs;
use std::io::{BufWriter, Write};
use std::path::Path;
use std::sync::mpsc;
use crate::core::NormalizedPath;
use super::super::event_log::open_append;
pub(super) const JOURNAL_MAX_SIZE: u64 = 50 * 1024 * 1024;
pub(super) const JOURNAL_MAX_FILES: usize = 3;
const JOURNAL_BUF_CAPACITY: usize = 64 * 1024;
const JOURNAL_BATCH_CAP: usize = 64;
pub(super) enum JournalMessage {
Entry {
line: String,
session_path: Option<NormalizedPath>,
},
CloseSession { path: NormalizedPath },
}
type GlobalWriter = BufWriter<std::fs::File>;
type SessionWriter = BufWriter<std::fs::File>;
pub(super) fn journal_thread(
rx: mpsc::Receiver<JournalMessage>,
global_path: NormalizedPath,
global_file: std::fs::File,
) {
let mut session_files: HashMap<NormalizedPath, SessionWriter> = HashMap::new();
let mut current_size: u64 = global_path.metadata().map(|m| m.len()).unwrap_or(0);
let mut global_file: GlobalWriter = BufWriter::with_capacity(JOURNAL_BUF_CAPACITY, global_file);
let mut batch: Vec<JournalMessage> = Vec::with_capacity(JOURNAL_BATCH_CAP);
while let Ok(msg) = rx.recv() {
batch.push(msg);
while batch.len() < JOURNAL_BATCH_CAP {
match rx.try_recv() {
Ok(more) => batch.push(more),
Err(_) => break,
}
}
let mut touched_global = false;
let mut touched_sessions: Vec<NormalizedPath> = Vec::new();
for msg in batch.drain(..) {
match msg {
JournalMessage::Entry { line, session_path } => {
if current_size > JOURNAL_MAX_SIZE {
let _ = global_file.flush();
if let Some((new_file, new_size)) = rotate_journal(&global_path) {
global_file = BufWriter::with_capacity(JOURNAL_BUF_CAPACITY, new_file);
current_size = new_size;
}
}
let line_bytes = line.len() as u64 + 1; if writeln!(global_file, "{line}").is_err() {
if let Ok(f) = open_append(&global_path) {
global_file = BufWriter::with_capacity(JOURNAL_BUF_CAPACITY, f);
let _ = writeln!(global_file, "{line}");
}
}
current_size += line_bytes;
touched_global = true;
if let Some(path) = session_path {
let file = session_files.entry(path.clone()).or_insert_with(|| {
match open_append(&path) {
Ok(f) => BufWriter::with_capacity(JOURNAL_BUF_CAPACITY, f),
Err(e) => {
tracing::debug!("session journal open error: {e}");
#[expect(
clippy::expect_used,
reason = "NUL on Windows / /dev/null on Unix must exist; their absence indicates a fundamentally broken host where the daemon cannot recover"
)]
let fallback = open_append(&path).unwrap_or_else(|_| {
std::fs::File::open(if cfg!(windows) {
"NUL"
} else {
"/dev/null"
})
.expect("cannot open null device")
});
BufWriter::with_capacity(JOURNAL_BUF_CAPACITY, fallback)
}
}
});
let _ = writeln!(file, "{line}");
if !touched_sessions.contains(&path) {
touched_sessions.push(path);
}
}
}
JournalMessage::CloseSession { path } => {
if let Some(mut writer) = session_files.remove(&path) {
let _ = writer.flush();
}
touched_sessions.retain(|p| p != &path);
}
}
}
if touched_global {
let _ = global_file.flush();
}
for path in &touched_sessions {
if let Some(writer) = session_files.get_mut(path) {
let _ = writer.flush();
}
}
}
let _ = global_file.flush();
for writer in session_files.values_mut() {
let _ = writer.flush();
}
}
pub(super) fn rotate_journal(path: &Path) -> Option<(std::fs::File, u64)> {
let ts =
super::super::event_log::format_timestamp(std::time::SystemTime::now()).replace(':', "-");
let rotated = path.with_file_name(format!("compile_journal.jsonl.{ts}"));
if fs::rename(path, &rotated).is_err() {
return None;
}
let file = open_append(path).ok()?;
gc_journal_files(path);
Some((file, 0))
}
pub(super) fn gc_journal_files(path: &Path) {
let dir = match path.parent() {
Some(d) => d,
None => return,
};
let mut rotated: Vec<NormalizedPath> = fs::read_dir(dir)
.into_iter()
.flatten()
.flatten()
.filter_map(|e| {
let name = e.file_name().to_string_lossy().into_owned();
if name.starts_with("compile_journal.jsonl.") {
Some(e.path().into())
} else {
None
}
})
.collect();
if rotated.len() <= JOURNAL_MAX_FILES {
return;
}
rotated.sort();
let to_remove = rotated.len() - JOURNAL_MAX_FILES;
for p in rotated.into_iter().take(to_remove) {
let _ = fs::remove_file(p);
}
}