Skip to main content

interstice_core/persistence/
log_rotation.rs

1//! Log rotation for managing large transaction logs.
2//!
3//! Provides automatic rotation of transaction logs when they exceed
4//! a configurable size, keeping older logs for archival.
5
6use std::fs;
7use std::io;
8use std::path::{Path, PathBuf};
9
10use crate::error::IntersticeError;
11
12/// Configuration for log rotation behavior
13#[derive(Debug, Clone)]
14pub struct RotationConfig {
15    /// Maximum size of a single log file in bytes (default: 100MB)
16    pub max_log_size: u64,
17    /// Maximum number of rotated logs to keep (default: 10)
18    pub max_rotated_logs: usize,
19}
20
21impl Default for RotationConfig {
22    fn default() -> Self {
23        Self {
24            max_log_size: 100 * 1024 * 1024, // 100MB
25            max_rotated_logs: 10,
26        }
27    }
28}
29
30/// Manages log rotation for transaction logs
31pub struct LogRotator {
32    config: RotationConfig,
33}
34
35impl LogRotator {
36    /// Create a new log rotator with custom configuration
37    pub fn new(config: RotationConfig) -> Self {
38        Self { config }
39    }
40
41    /// Check if a log file needs rotation based on size
42    pub fn should_rotate(&self, log_path: &Path) -> Result<bool, IntersticeError> {
43        match fs::metadata(log_path) {
44            Ok(metadata) => Ok(metadata.len() > self.config.max_log_size),
45            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
46            Err(e) => Err(IntersticeError::Internal(format!(
47                "Shouold rotate log error: {}",
48                e
49            ))),
50        }
51    }
52
53    /// Rotate a log file by renaming it and starting a new one
54    ///
55    /// Example:
56    /// - `txn.log` becomes `txn.log.0`
57    /// - `txn.log.0` becomes `txn.log.1`
58    /// - etc., up to `max_rotated_logs`
59    pub fn rotate(&self, log_path: &Path) -> Result<(), IntersticeError> {
60        if !log_path.exists() {
61            return Ok(()); // Nothing to rotate
62        }
63
64        let dir = log_path
65            .parent()
66            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid log path"))
67            .map_err(|err| {
68                IntersticeError::Internal(format!(
69                    "Couldn't get transaction log file path: {}",
70                    err
71                ))
72            })?;
73
74        let base_name_str = log_path
75            .file_name()
76            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "No filename"))
77            .map_err(|err| {
78                IntersticeError::Internal(format!(
79                    "Couldn't retreive base name transaction log file: {}",
80                    err
81                ))
82            })?
83            .to_string_lossy();
84
85        // Find the max existing numbered log
86        let mut max_num: i32 = -1;
87        for entry in fs::read_dir(dir).map_err(|err| {
88            IntersticeError::Internal(format!(
89                "Couldn't retreive the number of transaction log files: {}",
90                err
91            ))
92        })? {
93            let entry = entry.map_err(|err| {
94                IntersticeError::Internal(format!(
95                    "Couldn't get transaction log file rotation: {}",
96                    err
97                ))
98            })?;
99            let path = entry.path();
100            if let Some(name) = path.file_name() {
101                let name_str = name.to_string_lossy();
102                if let Some(num_str) = name_str.strip_prefix(&format!("{}.", base_name_str)) {
103                    if let Ok(num) = num_str.parse::<i32>() {
104                        max_num = max_num.max(num);
105                    }
106                }
107            }
108        }
109
110        // Rotate backwards from the highest number
111        // When max_num is -1 (no numbered logs exist), start with i=0 (the base log)
112        for i in (0..=max_num.max(-1)).rev() {
113            let new_num = i + 1;
114
115            // Skip if exceeds retention limit
116            if new_num as usize >= self.config.max_rotated_logs {
117                let path_to_remove = if i == -1 {
118                    log_path.to_path_buf()
119                } else {
120                    dir.join(format!("{}.{}", base_name_str, i))
121                };
122                if path_to_remove.exists() {
123                    fs::remove_file(&path_to_remove).ok();
124                }
125                continue;
126            }
127
128            let old_path = if i == -1 {
129                log_path.to_path_buf()
130            } else {
131                dir.join(format!("{}.{}", base_name_str, i))
132            };
133
134            let new_path = dir.join(format!("{}.{}", base_name_str, new_num));
135
136            if old_path.exists() {
137                fs::rename(&old_path, &new_path).map_err(|err| {
138                    IntersticeError::Internal(format!(
139                        "Couldn't sync transaction log file to disk: {}",
140                        err
141                    ))
142                })?;
143            }
144        }
145
146        Ok(())
147    }
148
149    /// List all rotated logs for a given path
150    pub fn list_rotated_logs(&self, log_path: &Path) -> io::Result<Vec<PathBuf>> {
151        let dir = log_path
152            .parent()
153            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid log path"))?;
154
155        let base_name = log_path
156            .file_name()
157            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "No filename"))?
158            .to_string_lossy();
159
160        let mut logs = Vec::new();
161
162        for entry in fs::read_dir(dir)? {
163            let entry = entry?;
164            let path = entry.path();
165            if let Some(name) = path.file_name() {
166                let name_str = name.to_string_lossy();
167                if name_str.starts_with(&format!("{}.", base_name)) {
168                    logs.push(path);
169                }
170            }
171        }
172
173        logs.sort();
174        Ok(logs)
175    }
176}