fix(cron): handle ALTER TABLE race condition in schema migration

Problem: add_column_if_missing() checks PRAGMA table_info for column existence, then issues ALTER TABLE ADD COLUMN if not found. When two concurrent processes both pass the check before either executes the ALTER, the second process fails with a 'duplicate column name' error.

Fix: Catch the 'duplicate column name' SQLite error after the ALTER TABLE and treat it as a benign no-op. Also explicitly drop statement/rows handles before ALTER to release locks.

Ref: #710 (Item 8)
This commit is contained in:
Alex Gorevski 2026-02-17 20:50:08 -08:00 committed by GitHub
parent 63602a262f
commit 5f5cb27690
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -443,13 +443,25 @@ fn add_column_if_missing(conn: &Connection, name: &str, sql_type: &str) -> Resul
return Ok(()); return Ok(());
} }
} }
// Drop the statement/rows before executing ALTER to release any locks
drop(rows);
drop(stmt);
conn.execute( // Tolerate "duplicate column name" errors to handle the race where
// another process adds the column between our PRAGMA check and ALTER.
match conn.execute(
&format!("ALTER TABLE cron_jobs ADD COLUMN {name} {sql_type}"), &format!("ALTER TABLE cron_jobs ADD COLUMN {name} {sql_type}"),
[], [],
) ) {
.with_context(|| format!("Failed to add cron_jobs.{name}"))?; Ok(_) => Ok(()),
Err(rusqlite::Error::SqliteFailure(err, Some(ref msg)))
if msg.contains("duplicate column name") =>
{
tracing::debug!("Column cron_jobs.{name} already exists (concurrent migration): {err}");
Ok(()) Ok(())
}
Err(e) => Err(e).with_context(|| format!("Failed to add cron_jobs.{name}")),
}
} }
fn with_connection<T>(config: &Config, f: impl FnOnce(&Connection) -> Result<T>) -> Result<T> { fn with_connection<T>(config: &Config, f: impl FnOnce(&Connection) -> Result<T>) -> Result<T> {