Codex Handbook
rollout/src/compression.rs 1046 lines
use std::ffi::OsStr;use std::fs::File;use std::fs::Permissions;use std::io;use std::io::Read;use std::io::Write;use std::path::Path;use std::path::PathBuf;use std::sync::atomic::AtomicU64;use std::sync::atomic::Ordering;use std::time::Duration;#[cfg(unix)]use std::os::unix::fs::OpenOptionsExt;#[cfg(unix)]use std::os::unix::fs::PermissionsExt;const COMPRESSED_SUFFIX: &str = ".zst";const MAX_NOT_FOUND_RETRIES: usize = 3;const OPEN_ROLLOUT_LINE_READER_RETRY_DELAY: Duration = Duration::from_millis(50);const TEMP_SUFFIX: &str = ".tmp";static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);/// Starts a best-effort background job that compresses cold local rollout files.////// The worker is fire-and-forget: failures are logged, startup is not blocked,/// and a run marker under `codex_home` prevents overlapping or too-frequent/// compression runs from the same local store.pub fn spawn_rollout_compression_worker(codex_home: PathBuf) {    worker::spawn(codex_home)}/// Returns the modified time for the existing plain or compressed rollout file.pub(crate) async fn file_modified_time(path: &Path) -> io::Result<Option<time::OffsetDateTime>> {    let Some(path) = path::existing_rollout_path(path).await else {        return Ok(None);    };    let meta = tokio::fs::metadata(path).await?;    let modified = meta.modified().ok();    Ok(modified.map(time::OffsetDateTime::from))}/// Opens a rollout line reader that transparently handles plain `.jsonl` and `.jsonl.zst` files.////// If the requested path disappears during a representation transition, this briefly retries/// resolution so callers do not need to know which representation is on disk.pub async fn open_rollout_line_reader(path: &Path) -> io::Result<RolloutLineReader> {    for _ in 0..MAX_NOT_FOUND_RETRIES {        match reader::open_once(path).await {            Ok(reader) => return Ok(reader),            Err(err) if err.kind() == io::ErrorKind::NotFound => {                tokio::time::sleep(OPEN_ROLLOUT_LINE_READER_RETRY_DELAY).await;            }            Err(err) => return Err(err),        }    }    reader::open_once(path).await}/// Returns the compressed `.jsonl.zst` path for a rollout path.#[cfg(test)]pub(crate) fn compressed_rollout_path(path: &Path) -> PathBuf {    path::compressed_rollout_path(path)}/// Materializes a compressed rollout back to plain `.jsonl` for async append paths.pub(crate) async fn materialize_rollout_for_append(path: &Path) -> io::Result<PathBuf> {    let path = path.to_path_buf();    tokio::task::spawn_blocking(move || materialize_rollout_for_append_blocking(path.as_path()))        .await        .map_err(io::Error::other)?}/// Materializes a compressed rollout back to plain `.jsonl` for blocking append paths.pub(crate) fn materialize_rollout_for_append_blocking(path: &Path) -> io::Result<PathBuf> {    let plain_path = plain_rollout_path(path);    if plain_path.exists() {        metrics::materialize("plain_exists");        return Ok(plain_path);    }    let compressed_path = path::compressed_rollout_path(plain_path.as_path());    if !compressed_path.exists() {        metrics::materialize("missing");        return Ok(plain_path);    }    let temp_path = temp_path_for(plain_path.as_path(), "decompress");    if let Some(parent) = plain_path.parent() {        std::fs::create_dir_all(parent)?;    }    let result: io::Result<()> = (|| {        let permissions = std::fs::metadata(compressed_path.as_path())?.permissions();        {            let input = File::open(compressed_path.as_path())?;            let mut decoder = zstd::stream::read::Decoder::new(input)?;            let mut output = create_file_with_permissions(temp_path.as_path(), &permissions)?;            io::copy(&mut decoder, &mut output)?;            output.flush()?;            output.sync_all()?;        }        match std::fs::hard_link(temp_path.as_path(), plain_path.as_path()) {            Ok(()) => {}            Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}            Err(_) => persist_temp_file_noclobber(temp_path.as_path(), plain_path.as_path())?,        }        let _ = std::fs::remove_file(temp_path.as_path());        match std::fs::remove_file(compressed_path.as_path()) {            Ok(()) => {}            Err(err) if err.kind() == io::ErrorKind::NotFound => {}            Err(err) => return Err(err),        }        Ok(())    })();    if result.is_err() {        let _ = std::fs::remove_file(temp_path.as_path());        metrics::materialize("failed");    }    result?;    metrics::materialize("decompressed");    Ok(plain_path)}fn persist_temp_file_noclobber(temp_path: &Path, destination: &Path) -> io::Result<()> {    let temp_path = tempfile::TempPath::try_from_path(temp_path)?;    match temp_path.persist_noclobber(destination) {        Ok(()) => Ok(()),        Err(err) if err.error.kind() == io::ErrorKind::AlreadyExists => Ok(()),        Err(err) => Err(err.error),    }}/// Returns the plain `.jsonl` path for a plain or compressed rollout path.pub fn plain_rollout_path(path: &Path) -> PathBuf {    path::plain_rollout_path(path)}/// Parses a rollout file name, returning its plain `.jsonl` name when valid.pub(crate) fn parse_rollout_file_name(name: &str) -> Option<&str> {    file_name::parse_rollout_file_name(name)}/// A discovered rollout file, represented by exactly one physical path.////// This keeps directory walkers from reimplementing the plain/compressed/// precedence rules. The physical path may point at either `.jsonl` or/// `.jsonl.zst`, while `plain_file_name` is always the canonical `.jsonl`/// filename used for timestamp and id parsing.#[derive(Debug, Clone, PartialEq, Eq)]pub(crate) struct RolloutFile {    path: PathBuf,    plain_file_name: String,}impl RolloutFile {    /// Creates a logical rollout file from a physical path found during discovery.    ///    /// Returns `None` for non-rollout names and for compressed siblings hidden by    /// an existing plain `.jsonl` file.    pub(crate) fn from_path(path: PathBuf) -> Option<Self> {        let file_name = path.file_name().and_then(|name| name.to_str())?;        let plain_file_name = file_name::parse_rollout_file_name(file_name)?.to_string();        if path::should_skip_compressed_sibling(path.as_path()) {            return None;        }        Some(Self {            path,            plain_file_name,        })    }    /// Returns the physical path that should be opened for reads.    pub(crate) fn path(&self) -> &Path {        self.path.as_path()    }    /// Returns the canonical `.jsonl` filename for timestamp and id parsing.    pub(crate) fn plain_file_name(&self) -> &str {        self.plain_file_name.as_str()    }    /// Returns whether the physical path is the compressed representation.    pub(crate) fn is_compressed(&self) -> bool {        path::is_compressed_rollout_path(self.path.as_path())    }    /// Consumes the entry and returns the physical path that should be read.    pub(crate) fn into_path(self) -> PathBuf {        self.path    }}/// Line-oriented rollout reader returned by [`open_rollout_line_reader`].pub struct RolloutLineReader {    inner: RolloutLineReaderInner,}enum RolloutLineReaderInner {    Plain(tokio::io::Lines<tokio::io::BufReader<tokio::fs::File>>),    Blocking(Option<BlockingLineReader>),}impl RolloutLineReader {    /// Reads the next JSONL record from the rollout.    pub async fn next_line(&mut self) -> io::Result<Option<String>> {        match &mut self.inner {            RolloutLineReaderInner::Plain(lines) => lines.next_line().await,            RolloutLineReaderInner::Blocking(slot) => {                let Some(mut reader) = slot.take() else {                    return Err(io::Error::other("compressed rollout reader is busy"));                };                let (line, reader) =                    tokio::task::spawn_blocking(move || (reader.next().transpose(), reader))                        .await                        .map_err(io::Error::other)?;                *slot = Some(reader);                line            }        }    }}type BlockingLineReader = std::io::Lines<std::io::BufReader<Box<dyn Read + Send>>>;mod worker {    use std::ffi::OsStr;    use std::fs::File;    use std::fs::FileTimes;    use std::fs::Permissions;    use std::io;    use std::io::Write;    use std::path::Path;    use std::path::PathBuf;    use std::time::Duration;    use std::time::Instant;    use std::time::SystemTime;    use tracing::debug;    use tracing::info;    use tracing::warn;    use tokio::task::JoinSet;    use crate::ARCHIVED_SESSIONS_SUBDIR;    use crate::SESSIONS_SUBDIR;    use super::RolloutFile;    use super::metrics;    use super::path;    const TEMP_SUFFIX: &str = ".tmp";    const COMPRESSION_LEVEL: i32 = 3;    const MIN_ROLLOUT_AGE: Duration = Duration::from_secs(7 * 24 * 60 * 60);    const RUN_MARKER_STALE_AFTER: Duration = Duration::from_secs(6 * 60 * 60);    const TEMP_FILE_STALE_AFTER: Duration = RUN_MARKER_STALE_AFTER;    const WORKER_MAX_RUNTIME: Duration = Duration::from_secs(5 * 60 * 60);    const RUN_MARKER_FILE_NAME: &str = "rollout-compression.lock";    const MAX_CONCURRENT_COMPRESSION_JOBS: usize = 2;    #[derive(Default)]    struct CompressionStats {        scanned: usize,        compressed: usize,        skipped: usize,        failed: usize,    }    pub(super) struct CompressionRunMarker {        path: PathBuf,        remove_on_drop: bool,    }    impl CompressionRunMarker {        pub(super) fn try_claim(codex_home: &Path) -> io::Result<Option<Self>> {            let marker_dir = codex_home.join(".tmp");            std::fs::create_dir_all(marker_dir.as_path())?;            let path = marker_dir.join(RUN_MARKER_FILE_NAME);            match create_run_marker_file(path.as_path()) {                Ok(()) => return Ok(Some(Self::new(path))),                Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}                Err(err) => return Err(err),            }            let stale = std::fs::metadata(path.as_path())                .and_then(|metadata| metadata.modified())                .ok()                .and_then(|modified| SystemTime::now().duration_since(modified).ok())                .is_some_and(|age| age >= RUN_MARKER_STALE_AFTER);            if !stale {                return Ok(None);            }            match std::fs::remove_file(path.as_path()) {                Ok(()) => {}                Err(err) if err.kind() == io::ErrorKind::NotFound => {}                Err(err) => return Err(err),            }            match create_run_marker_file(path.as_path()) {                Ok(()) => Ok(Some(Self::new(path))),                Err(err) if err.kind() == io::ErrorKind::AlreadyExists => Ok(None),                Err(err) => Err(err),            }        }        fn new(path: PathBuf) -> Self {            Self {                path,                remove_on_drop: true,            }        }        pub(super) fn persist(mut self) {            self.remove_on_drop = false;        }    }    impl Drop for CompressionRunMarker {        fn drop(&mut self) {            if self.remove_on_drop {                let _ = std::fs::remove_file(self.path.as_path());            }        }    }    pub(super) fn spawn(codex_home: PathBuf) {        let Ok(handle) = tokio::runtime::Handle::try_current() else {            metrics::run("skipped_no_runtime");            warn!(                "failed to start rollout compression worker for {}: no Tokio runtime",                codex_home.display()            );            return;        };        handle.spawn(async move {            if let Err(err) = run(codex_home.clone()).await {                warn!(                    "rollout compression worker failed for {}: {err}",                    codex_home.display()                );            }        });    }    pub(super) async fn run(codex_home: PathBuf) -> io::Result<()> {        let marker = match CompressionRunMarker::try_claim(codex_home.as_path()) {            Ok(Some(marker)) => marker,            Ok(None) => {                metrics::run("skipped_already_running");                debug!(                    "rollout compression worker recently ran or is already running for {}",                    codex_home.display()                );                return Ok(());            }            Err(err) => {                metrics::run("failed");                return Err(err);            }        };        metrics::run("started");        let started_at = Instant::now();        let result = async {            cleanup_stale_temps(codex_home.as_path()).await?;            let mut stats = CompressionStats::default();            for root in [                codex_home.join(ARCHIVED_SESSIONS_SUBDIR),                codex_home.join(SESSIONS_SUBDIR),            ] {                if started_at.elapsed() >= WORKER_MAX_RUNTIME {                    break;                }                compress_rollouts_in_root(root.as_path(), started_at, &mut stats).await?;            }            Ok::<_, io::Error>(stats)        }        .await;        let stats = match result {            Ok(stats) => stats,            Err(err) => {                metrics::run("failed");                metrics::run_duration("failed", started_at.elapsed());                return Err(err);            }        };        info!(            "rollout compression worker finished: scanned={}, compressed={}, skipped={}, failed={}",            stats.scanned, stats.compressed, stats.skipped, stats.failed        );        metrics::run("completed");        metrics::run_duration("completed", started_at.elapsed());        marker.persist();        Ok(())    }    fn create_run_marker_file(path: &Path) -> io::Result<()> {        let mut file = std::fs::OpenOptions::new()            .write(true)            .create_new(true)            .open(path)?;        writeln!(            file,            "pid={} started_at={:?}",            std::process::id(),            SystemTime::now()        )?;        Ok(())    }    async fn compress_rollouts_in_root(        root: &Path,        started_at: Instant,        stats: &mut CompressionStats,    ) -> io::Result<()> {        if !tokio::fs::try_exists(root).await.unwrap_or(false) {            return Ok(());        }        let mut stack = vec![root.to_path_buf()];        let mut jobs = JoinSet::new();        while let Some(dir) = stack.pop() {            if started_at.elapsed() >= WORKER_MAX_RUNTIME {                break;            }            let mut read_dir = match tokio::fs::read_dir(dir.as_path()).await {                Ok(read_dir) => read_dir,                Err(err) => {                    warn!(                        "failed to read rollout compression directory {}: {err}",                        dir.display()                    );                    continue;                }            };            loop {                let entry = match read_dir.next_entry().await {                    Ok(Some(entry)) => entry,                    Ok(None) => break,                    Err(err) => {                        drain_compression_jobs(&mut jobs, stats).await;                        return Err(err);                    }                };                if started_at.elapsed() >= WORKER_MAX_RUNTIME {                    break;                }                let path = entry.path();                let file_type = match entry.file_type().await {                    Ok(file_type) => file_type,                    Err(err) => {                        warn!(                            "failed to read rollout compression file type {}: {err}",                            path.display()                        );                        continue;                    }                };                if file_type.is_dir() {                    stack.push(path);                    continue;                }                if !file_type.is_file() {                    continue;                }                let Some(rollout_file) = RolloutFile::from_path(path) else {                    continue;                };                if rollout_file.is_compressed() {                    continue;                }                let path = rollout_file.into_path();                stats.scanned = stats.scanned.saturating_add(1);                metrics::file("scanned");                while jobs.len() >= MAX_CONCURRENT_COMPRESSION_JOBS {                    collect_next_compression_job(&mut jobs, stats).await;                }                jobs.spawn_blocking(move || {                    let started_at = Instant::now();                    let result = compress_rollout_if_cold_blocking(path.as_path());                    let duration = started_at.elapsed();                    (path, duration, result)                });            }        }        drain_compression_jobs(&mut jobs, stats).await;        Ok(())    }    type CompressionJobResult = (PathBuf, Duration, io::Result<CompressionMeasurement>);    #[derive(Debug, Clone, Copy, PartialEq, Eq)]    enum CompressionOutcome {        Compressed,        SkippedNotCold,        SkippedChanged,        SkippedAlreadyCompressed,    }    impl CompressionOutcome {        fn tag(self) -> &'static str {            match self {                CompressionOutcome::Compressed => "compressed",                CompressionOutcome::SkippedNotCold => "skipped_not_cold",                CompressionOutcome::SkippedChanged => "skipped_changed",                CompressionOutcome::SkippedAlreadyCompressed => "skipped_already_compressed",            }        }    }    struct CompressionMeasurement {        outcome: CompressionOutcome,        source_bytes: Option<u64>,        compressed_bytes: Option<u64>,    }    impl CompressionMeasurement {        fn new(            outcome: CompressionOutcome,            source_bytes: Option<u64>,            compressed_bytes: Option<u64>,        ) -> Self {            Self {                outcome,                source_bytes,                compressed_bytes,            }        }    }    enum ColdFileState {        Cold(FileState),        NotCold(Option<FileState>),    }    async fn drain_compression_jobs(        jobs: &mut JoinSet<CompressionJobResult>,        stats: &mut CompressionStats,    ) {        while !jobs.is_empty() {            collect_next_compression_job(jobs, stats).await;        }    }    async fn collect_next_compression_job(        jobs: &mut JoinSet<CompressionJobResult>,        stats: &mut CompressionStats,    ) {        let Some(result) = jobs.join_next().await else {            return;        };        match result {            Ok((_, duration, Ok(measurement))) => {                let outcome = measurement.outcome;                match outcome {                    CompressionOutcome::Compressed => {                        stats.compressed = stats.compressed.saturating_add(1);                    }                    CompressionOutcome::SkippedNotCold                    | CompressionOutcome::SkippedChanged                    | CompressionOutcome::SkippedAlreadyCompressed => {                        stats.skipped = stats.skipped.saturating_add(1);                    }                }                metrics::file(outcome.tag());                metrics::file_duration(outcome.tag(), duration);                if let Some(source_bytes) = measurement.source_bytes {                    metrics::source_bytes(outcome.tag(), source_bytes);                }                if let Some(compressed_bytes) = measurement.compressed_bytes {                    metrics::compressed_bytes(outcome.tag(), compressed_bytes);                    if let Some(source_bytes) = measurement.source_bytes {                        metrics::compression_ratio(outcome.tag(), source_bytes, compressed_bytes);                    }                }            }            Ok((path, duration, Err(err))) => {                stats.failed = stats.failed.saturating_add(1);                metrics::file("failed");                metrics::file_duration("failed", duration);                warn!("failed to compress rollout {}: {err}", path.display());            }            Err(err) => {                stats.failed = stats.failed.saturating_add(1);                metrics::file("failed");                warn!("rollout compression task failed: {err}");            }        }    }    fn compress_rollout_if_cold_blocking(path: &Path) -> io::Result<CompressionMeasurement> {        let before = match cold_file_state(path)? {            ColdFileState::Cold(state) => state,            ColdFileState::NotCold(state) => {                return Ok(CompressionMeasurement::new(                    CompressionOutcome::SkippedNotCold,                    state.map(|state| state.len),                    /*compressed_bytes*/ None,                ));            }        };        let source_bytes = Some(before.len);        let compressed_path = path::compressed_rollout_path(path);        if compressed_path.exists() {            return Ok(CompressionMeasurement::new(                CompressionOutcome::SkippedAlreadyCompressed,                source_bytes,                /*compressed_bytes*/ None,            ));        }        let temp_dir = compressed_path            .parent()            .filter(|parent| !parent.as_os_str().is_empty())            .unwrap_or_else(|| Path::new("."));        std::fs::create_dir_all(temp_dir)?;        let mut temp_file = tempfile::Builder::new()            .prefix("rollout-compress-")            .suffix(TEMP_SUFFIX)            .tempfile_in(temp_dir)?;        encode_zstd_to_writer(path, temp_file.as_file_mut())?;        temp_file.as_file_mut().flush()?;        verify_zstd(temp_file.path())?;        if !same_file_state(path, &before)? {            return Ok(CompressionMeasurement::new(                CompressionOutcome::SkippedChanged,                source_bytes,                /*compressed_bytes*/ None,            ));        }        set_file_metadata(temp_file.as_file(), before.modified, &before.permissions)?;        temp_file.as_file().sync_all()?;        let compressed_bytes = temp_file.as_file().metadata()?.len();        match temp_file.persist_noclobber(compressed_path.as_path()) {            Ok(_) => {}            Err(err) if err.error.kind() == io::ErrorKind::AlreadyExists => {                return Ok(CompressionMeasurement::new(                    CompressionOutcome::SkippedAlreadyCompressed,                    source_bytes,                    /*compressed_bytes*/ None,                ));            }            Err(err) => return Err(err.error),        }        if !same_file_state(path, &before)? {            let _ = std::fs::remove_file(compressed_path.as_path());            return Ok(CompressionMeasurement::new(                CompressionOutcome::SkippedChanged,                source_bytes,                /*compressed_bytes*/ None,            ));        }        std::fs::remove_file(path)?;        Ok(CompressionMeasurement::new(            CompressionOutcome::Compressed,            source_bytes,            Some(compressed_bytes),        ))    }    struct FileState {        len: u64,        modified: SystemTime,        permissions: Permissions,    }    fn cold_file_state(path: &Path) -> io::Result<ColdFileState> {        let metadata = match std::fs::metadata(path) {            Ok(metadata) => metadata,            Err(err) if err.kind() == io::ErrorKind::NotFound => {                return Ok(ColdFileState::NotCold(None));            }            Err(err) => return Err(err),        };        if !metadata.is_file() {            return Ok(ColdFileState::NotCold(None));        }        let modified = metadata.modified()?;        let state = FileState {            len: metadata.len(),            modified,            permissions: metadata.permissions(),        };        let age = SystemTime::now()            .duration_since(modified)            .unwrap_or(Duration::ZERO);        if age < MIN_ROLLOUT_AGE {            return Ok(ColdFileState::NotCold(Some(state)));        }        Ok(ColdFileState::Cold(state))    }    fn same_file_state(path: &Path, expected: &FileState) -> io::Result<bool> {        match std::fs::metadata(path) {            Ok(metadata) => Ok(metadata.len() == expected.len                && metadata.modified()? == expected.modified                && metadata.permissions() == expected.permissions),            Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(false),            Err(err) => Err(err),        }    }    fn encode_zstd_to_writer(source: &Path, output: impl Write) -> io::Result<()> {        let mut input = File::open(source)?;        let mut encoder = zstd::stream::write::Encoder::new(output, COMPRESSION_LEVEL)?;        io::copy(&mut input, &mut encoder)?;        encoder.finish()?;        Ok(())    }    fn verify_zstd(path: &Path) -> io::Result<()> {        let input = File::open(path)?;        let mut decoder = zstd::stream::read::Decoder::new(input)?;        let mut sink = io::sink();        io::copy(&mut decoder, &mut sink)?;        Ok(())    }    fn set_file_metadata(        file: &File,        modified: SystemTime,        permissions: &Permissions,    ) -> io::Result<()> {        file.set_times(FileTimes::new().set_modified(modified))?;        file.set_permissions(permissions.clone())    }    async fn cleanup_stale_temps(codex_home: &Path) -> io::Result<()> {        for root in [            codex_home.join(SESSIONS_SUBDIR),            codex_home.join(ARCHIVED_SESSIONS_SUBDIR),        ] {            cleanup_stale_temps_in_root(root.as_path()).await?;        }        Ok(())    }    async fn cleanup_stale_temps_in_root(root: &Path) -> io::Result<()> {        if !tokio::fs::try_exists(root).await.unwrap_or(false) {            return Ok(());        }        let mut stack = vec![root.to_path_buf()];        while let Some(dir) = stack.pop() {            let mut read_dir = match tokio::fs::read_dir(dir.as_path()).await {                Ok(read_dir) => read_dir,                Err(err) => {                    warn!(                        "failed to read rollout temp cleanup directory {}: {err}",                        dir.display()                    );                    continue;                }            };            while let Some(entry) = read_dir.next_entry().await? {                let path = entry.path();                let file_type = match entry.file_type().await {                    Ok(file_type) => file_type,                    Err(err) => {                        warn!(                            "failed to read rollout temp cleanup file type {}: {err}",                            path.display()                        );                        continue;                    }                };                if file_type.is_dir() {                    stack.push(path);                    continue;                }                if file_type.is_file()                    && path                        .file_name()                        .and_then(OsStr::to_str)                        .is_some_and(|name| name.ends_with(TEMP_SUFFIX))                {                    let stale = entry                        .metadata()                        .await                        .ok()                        .and_then(|metadata| metadata.modified().ok())                        .and_then(|modified| SystemTime::now().duration_since(modified).ok())                        .is_some_and(|age| age >= TEMP_FILE_STALE_AFTER);                    if !stale {                        continue;                    }                    match tokio::fs::remove_file(path.as_path()).await {                        Ok(()) => metrics::temp_cleanup("removed"),                        Err(err) if err.kind() == io::ErrorKind::NotFound => {}                        Err(err) => {                            metrics::temp_cleanup("failed");                            warn!(                                "failed to remove stale rollout temp {}: {err}",                                path.display()                            );                        }                    }                }            }        }        Ok(())    }}mod metrics {    use std::time::Duration;    const FILE_COMPRESSED_BYTES_HISTOGRAM: &str = "codex.rollout_compression.file.compressed_bytes";    const FILE_COUNTER: &str = "codex.rollout_compression.file";    const FILE_DURATION_HISTOGRAM: &str = "codex.rollout_compression.file.duration_ms";    const FILE_SOURCE_BYTES_HISTOGRAM: &str = "codex.rollout_compression.file.source_bytes";    const FILE_COMPRESSION_RATIO_HISTOGRAM: &str =        "codex.rollout_compression.file.compression_ratio";    const MATERIALIZE_COUNTER: &str = "codex.rollout_compression.materialize";    const RUN_COUNTER: &str = "codex.rollout_compression.run";    const RUN_DURATION_HISTOGRAM: &str = "codex.rollout_compression.run.duration_ms";    const RATIO_BASIS_POINTS: u128 = 10_000;    const TEMP_CLEANUP_COUNTER: &str = "codex.rollout_compression.temp_cleanup";    pub(super) fn file(outcome: &'static str) {        counter(FILE_COUNTER, &[("outcome", outcome)]);    }    pub(super) fn file_duration(outcome: &'static str, duration: Duration) {        duration_histogram(FILE_DURATION_HISTOGRAM, duration, &[("outcome", outcome)]);    }    pub(super) fn source_bytes(outcome: &'static str, bytes: u64) {        histogram(            FILE_SOURCE_BYTES_HISTOGRAM,            saturating_i64(bytes),            &[("outcome", outcome)],        );    }    pub(super) fn compressed_bytes(outcome: &'static str, bytes: u64) {        histogram(            FILE_COMPRESSED_BYTES_HISTOGRAM,            saturating_i64(bytes),            &[("outcome", outcome)],        );    }    pub(super) fn compression_ratio(        outcome: &'static str,        source_bytes: u64,        compressed_bytes: u64,    ) {        if source_bytes == 0 {            return;        }        // Keep the ratio histogram integer-valued while preserving sub-percent precision.        let ratio = (u128::from(compressed_bytes) * RATIO_BASIS_POINTS) / u128::from(source_bytes);        histogram(            FILE_COMPRESSION_RATIO_HISTOGRAM,            saturating_i64(ratio),            &[("outcome", outcome)],        );    }    pub(super) fn materialize(outcome: &'static str) {        counter(MATERIALIZE_COUNTER, &[("outcome", outcome)]);    }    pub(super) fn run(status: &'static str) {        counter(RUN_COUNTER, &[("status", status)]);    }    pub(super) fn run_duration(status: &'static str, duration: Duration) {        duration_histogram(RUN_DURATION_HISTOGRAM, duration, &[("status", status)]);    }    pub(super) fn temp_cleanup(outcome: &'static str) {        counter(TEMP_CLEANUP_COUNTER, &[("outcome", outcome)]);    }    fn counter(name: &str, tags: &[(&str, &str)]) {        let Some(metrics) = codex_otel::global() else {            return;        };        let _ = metrics.counter(name, /*inc*/ 1, tags);    }    fn histogram(name: &str, value: i64, tags: &[(&str, &str)]) {        let Some(metrics) = codex_otel::global() else {            return;        };        let _ = metrics.histogram(name, value, tags);    }    fn duration_histogram(name: &str, duration: Duration, tags: &[(&str, &str)]) {        let Some(metrics) = codex_otel::global() else {            return;        };        let _ = metrics.record_duration(name, duration, tags);    }    fn saturating_i64(value: impl TryInto<i64>) -> i64 {        value.try_into().unwrap_or(i64::MAX)    }}/// Returns the existing rollout path, preferring the plain `.jsonl` file over/// its `.jsonl.zst` compressed sibling.pub async fn existing_rollout_path(path: &Path) -> Option<PathBuf> {    path::existing_rollout_path(path).await}mod path {    use std::ffi::OsStr;    use std::path::Path;    use std::path::PathBuf;    use super::COMPRESSED_SUFFIX;    pub(super) fn compressed_rollout_path(path: &Path) -> PathBuf {        if is_compressed_rollout_path(path) {            return path.to_path_buf();        }        let mut file_name = path            .file_name()            .map(OsStr::to_os_string)            .unwrap_or_else(|| OsStr::new("rollout.jsonl").to_os_string());        file_name.push(COMPRESSED_SUFFIX);        path.with_file_name(file_name)    }    pub(super) fn plain_rollout_path(path: &Path) -> PathBuf {        let Some(file_name) = path.file_name().and_then(OsStr::to_str) else {            return path.to_path_buf();        };        let Some(plain_file_name) = file_name.strip_suffix(COMPRESSED_SUFFIX) else {            return path.to_path_buf();        };        path.with_file_name(plain_file_name)    }    pub(super) fn is_compressed_rollout_path(path: &Path) -> bool {        path.file_name()            .and_then(OsStr::to_str)            .is_some_and(|name| name.ends_with(".jsonl.zst"))    }    pub(super) fn should_skip_compressed_sibling(path: &Path) -> bool {        is_compressed_rollout_path(path) && plain_rollout_path(path).exists()    }    pub(super) async fn existing_rollout_path(path: &Path) -> Option<PathBuf> {        let plain_path = plain_rollout_path(path);        if matches!(tokio::fs::metadata(plain_path.as_path()).await, Ok(metadata) if metadata.is_file())        {            return Some(plain_path);        }        let compressed_path = compressed_rollout_path(plain_path.as_path());        if matches!(tokio::fs::metadata(compressed_path.as_path()).await, Ok(metadata) if metadata.is_file())        {            return Some(compressed_path);        }        None    }}mod file_name {    use super::COMPRESSED_SUFFIX;    pub(super) fn parse_rollout_file_name(name: &str) -> Option<&str> {        let name = name.strip_suffix(COMPRESSED_SUFFIX).unwrap_or(name);        if name.starts_with("rollout-") && name.ends_with(".jsonl") {            Some(name)        } else {            None        }    }}mod reader {    use std::fs::File;    use std::io;    use std::io::BufRead;    use std::io::Read;    use std::path::Path;    use super::RolloutLineReader;    use super::RolloutLineReaderInner;    use super::path;    use tokio::io::AsyncBufReadExt;    pub(super) async fn open_once(path: &Path) -> io::Result<RolloutLineReader> {        let path = path::existing_rollout_path(path)            .await            .unwrap_or_else(|| path.to_path_buf());        if path::is_compressed_rollout_path(path.as_path()) {            let reader = tokio::task::spawn_blocking(move || {                let input = File::open(path.as_path())?;                let decoder = zstd::stream::read::Decoder::new(input)?;                Ok::<_, io::Error>(                    io::BufReader::new(Box::new(decoder) as Box<dyn Read + Send>).lines(),                )            })            .await            .map_err(io::Error::other)??;            return Ok(RolloutLineReader {                inner: RolloutLineReaderInner::Blocking(Some(reader)),            });        }        let file = tokio::fs::File::open(path).await?;        Ok(RolloutLineReader {            inner: RolloutLineReaderInner::Plain(tokio::io::BufReader::new(file).lines()),        })    }}#[cfg(unix)]fn create_file_with_permissions(path: &Path, permissions: &Permissions) -> io::Result<File> {    let file = std::fs::OpenOptions::new()        .write(true)        .create_new(true)        .mode(permissions.mode() & 0o7777)        .open(path)?;    file.set_permissions(permissions.clone())?;    Ok(file)}#[cfg(not(unix))]fn create_file_with_permissions(path: &Path, permissions: &Permissions) -> io::Result<File> {    let file = std::fs::OpenOptions::new()        .write(true)        .create_new(true)        .open(path)?;    file.set_permissions(permissions.clone())?;    Ok(file)}fn temp_path_for(path: &Path, operation: &str) -> PathBuf {    let mut file_name = path        .file_name()        .map(OsStr::to_os_string)        .unwrap_or_else(|| OsStr::new("rollout").to_os_string());    let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);    file_name.push(format!(        ".{operation}.{}.{counter}{TEMP_SUFFIX}",        std::process::id()    ));    path.with_file_name(file_name)}#[cfg(test)]#[path = "compression_tests.rs"]mod tests;