codex_state/runtime/
recovery.rs1use std::borrow::Cow;
9use std::path::Path;
10use std::path::PathBuf;
11
12const BACKUP_DIR_NAME: &str = "db-backups";
13
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct RuntimeDbBackup {
16 pub original_path: PathBuf,
18 pub backup_path: PathBuf,
20}
21
22#[derive(Debug)]
23pub(crate) struct RuntimeDbInitError {
24 label: &'static str,
25 operation: &'static str,
26 path: PathBuf,
27 source: anyhow::Error,
28}
29
30impl RuntimeDbInitError {
31 pub(crate) fn new(
32 label: &'static str,
33 operation: &'static str,
34 path: &Path,
35 source: anyhow::Error,
36 ) -> Self {
37 Self {
38 label,
39 operation,
40 path: path.to_path_buf(),
41 source,
42 }
43 }
44
45 fn path(&self) -> &Path {
46 self.path.as_path()
47 }
48}
49
50impl std::fmt::Display for RuntimeDbInitError {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 write!(
53 f,
54 "failed to {} {} at {}: {}",
55 self.operation,
56 self.label,
57 self.path.display(),
58 self.source
59 )
60 }
61}
62
63impl std::error::Error for RuntimeDbInitError {
64 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
65 Some(self.source.as_ref())
66 }
67}
68
69pub async fn backup_runtime_db_for_fresh_start(
72 db_path: &Path,
73) -> std::io::Result<Vec<RuntimeDbBackup>> {
74 let sqlite_home = db_path.parent().ok_or_else(|| {
75 std::io::Error::other(format!(
76 "database path does not have a parent directory: {}",
77 db_path.display()
78 ))
79 })?;
80 match tokio::fs::metadata(sqlite_home).await {
81 Ok(metadata) if metadata.is_dir() => backup_runtime_db_files(db_path).await,
82 Ok(_) => backup_blocking_sqlite_home(sqlite_home).await,
83 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
84 tokio::fs::create_dir_all(sqlite_home).await?;
85 Err(std::io::Error::other(format!(
86 "no Codex runtime database files were found to back up for {}",
87 db_path.display()
88 )))
89 }
90 Err(err) => Err(err),
91 }
92}
93
94pub fn runtime_db_path_for_corruption_error(err: &anyhow::Error) -> Option<PathBuf> {
95 if !is_sqlite_corruption_error(err) {
96 return None;
97 }
98 err.chain()
99 .find_map(|source| source.downcast_ref::<RuntimeDbInitError>())
100 .map(|err| err.path().to_path_buf())
101}
102
103pub fn is_sqlite_corruption_error(err: &anyhow::Error) -> bool {
104 err.chain().any(sqlite_error_source_is_corruption)
105}
106
107fn sqlite_error_source_is_corruption(source: &(dyn std::error::Error + 'static)) -> bool {
108 let Some(err) = source.downcast_ref::<sqlx::Error>() else {
109 return false;
110 };
111 let sqlx::Error::Database(database_error) = err else {
112 return false;
113 };
114 sqlite_error_detail_is_corruption(database_error.message())
115 || database_error
116 .code()
117 .is_some_and(sqlite_database_code_is_corruption)
118}
119
120fn sqlite_database_code_is_corruption(code: Cow<'_, str>) -> bool {
121 matches!(
122 code.as_ref().to_ascii_lowercase().as_str(),
123 "11" | "26" | "sqlite_corrupt" | "sqlite_notadb"
124 )
125}
126
127pub fn sqlite_error_detail_is_corruption(detail: &str) -> bool {
128 let detail = detail.to_ascii_lowercase();
129 detail.contains("database disk image is malformed")
130 || detail.contains("database schema is malformed")
131 || detail.contains("database is corrupt")
132 || detail.contains("file is not a database")
133 || detail.contains("sqlite_corrupt")
134 || detail.contains("sqlite_notadb")
135 || detail.contains("(code: 11)")
136 || detail.contains("(code: 26)")
137}
138
139pub fn sqlite_error_detail_is_lock(detail: &str) -> bool {
140 let detail = detail.to_ascii_lowercase();
141 detail.contains("database is locked") || detail.contains("database is busy")
142}
143
144async fn backup_runtime_db_files(db_path: &Path) -> std::io::Result<Vec<RuntimeDbBackup>> {
145 let sqlite_home = db_path.parent().ok_or_else(|| {
146 std::io::Error::other(format!(
147 "database path does not have a parent directory: {}",
148 db_path.display()
149 ))
150 })?;
151 backup_sqlite_paths(sqlite_home, sqlite_paths(db_path)).await
152}
153
154async fn backup_sqlite_paths(
155 sqlite_home: &Path,
156 paths: impl IntoIterator<Item = PathBuf>,
157) -> std::io::Result<Vec<RuntimeDbBackup>> {
158 let backup_dir = create_unique_backup_dir(sqlite_home.join(BACKUP_DIR_NAME).as_path()).await?;
159 let mut backups = Vec::new();
160
161 for path in paths {
162 if tokio::fs::try_exists(path.as_path()).await? {
163 let backup_path = backup_dir.join(file_name(path.as_path())?);
164 tokio::fs::rename(path.as_path(), backup_path.as_path()).await?;
165 backups.push(RuntimeDbBackup {
166 original_path: path,
167 backup_path,
168 });
169 }
170 }
171
172 if backups.is_empty() {
173 let _ = tokio::fs::remove_dir(backup_dir).await;
174 return Err(std::io::Error::other(
175 "no Codex runtime database files were found to back up",
176 ));
177 }
178
179 Ok(backups)
180}
181
182async fn backup_blocking_sqlite_home(sqlite_home: &Path) -> std::io::Result<Vec<RuntimeDbBackup>> {
183 let parent = sqlite_home.parent().ok_or_else(|| {
184 std::io::Error::other(format!(
185 "cannot create a backup folder for {}",
186 sqlite_home.display()
187 ))
188 })?;
189 let mut backup_dir_name = file_name(sqlite_home)?.to_os_string();
190 backup_dir_name.push(format!(".{BACKUP_DIR_NAME}"));
191 let backup_parent = parent.join(backup_dir_name);
192 let backup_dir = create_unique_backup_dir(backup_parent.as_path()).await?;
193 let backup_path = backup_dir.join(file_name(sqlite_home)?);
194 tokio::fs::rename(sqlite_home, backup_path.as_path()).await?;
195 tokio::fs::create_dir_all(sqlite_home).await?;
196 Ok(vec![RuntimeDbBackup {
197 original_path: sqlite_home.to_path_buf(),
198 backup_path,
199 }])
200}
201
202fn sqlite_paths(db_path: &Path) -> Vec<PathBuf> {
203 let mut wal_path = db_path.as_os_str().to_os_string();
204 wal_path.push("-wal");
205 let mut shm_path = db_path.as_os_str().to_os_string();
206 shm_path.push("-shm");
207 vec![
208 db_path.to_path_buf(),
209 PathBuf::from(wal_path),
210 PathBuf::from(shm_path),
211 ]
212}
213
214async fn create_unique_backup_dir(backup_parent: &Path) -> std::io::Result<PathBuf> {
215 tokio::fs::create_dir_all(backup_parent).await?;
216 let timestamp = std::time::SystemTime::now()
217 .duration_since(std::time::UNIX_EPOCH)
218 .map_or(0, |duration| duration.as_secs());
219 let mut sequence = 0_u32;
220 loop {
221 let backup_dir = backup_parent.join(format!("sqlite-{timestamp}-{sequence}"));
222 match tokio::fs::create_dir(backup_dir.as_path()).await {
223 Ok(()) => return Ok(backup_dir),
224 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
225 sequence += 1;
226 }
227 Err(err) => return Err(err),
228 }
229 }
230}
231
232fn file_name(path: &Path) -> std::io::Result<&std::ffi::OsStr> {
233 path.file_name().ok_or_else(|| {
234 std::io::Error::other(format!(
235 "cannot create a backup name for {}",
236 path.display()
237 ))
238 })
239}
240
241#[cfg(test)]
242#[path = "recovery_tests.rs"]
243mod tests;