Estado: ✅ Aceptada Fecha: 2026-05-18 Contexto que la motiva: bloque “locking simple entre procesos” de Fase 2 → cierre del gap de corrupción silenciosa al abrir la misma DB desde dos procesos.

🧭 Contexto

Hasta este bloque, Pager::create / Pager::create_force / Pager::open no negociaban ningún tipo de exclusión con otros procesos. La WAL+CRC de gabysql asume un único escritor por archivo: las páginas dirty del Pager activo se escriben al .db desde memoria sin coordinación con el sistema operativo más allá de los fsync propios.

Escenarios reales que esto rompía:

El motor detectaba la corrupción a posteriori (vía CRC, INTEGRITY CHECK), pero ya con el daño hecho. No había forma de prevenirla.

Restricciones del proyecto:

💡 Decisión

Adquirir un lock advisory exclusivo sobre el archivo .db usando la API estable std::fs::File::try_lock() (estabilizada en Rust 1.89.0, agosto 2025).

Implementación en src/storage.rs:

fn acquire_db_lock(file: &File, path: &Path) -> DbResult<()> {
    match file.try_lock() {
        Ok(()) => Ok(()),
        Err(TryLockError::WouldBlock) => Err(DbError::new(format!(
            "database is locked by another process: {}. \
             Close the other gabysql process or wait for it to release the lock.",
            path.display()
        ))),
        Err(TryLockError::Error(err)) => Err(DbError::new(format!(
            "failed to acquire DB lock on {}: {}",
            path.display(), err
        ))),
    }
}

Puntos de llamada:

El lock se libera en Pager::close con file.unlock() explícito, y también automáticamente cuando el File se dropea (red de seguridad).

🤔 Alternativas evaluadas

  1. Sentinel file (<path>.lock con PID dentro): trivial, sin deps, pero requiere cleanup manual cuando el proceso muere por kill -9. Lock huérfano = DB inutilizable hasta limpieza manual. Descartada: la propiedad mínima esperable de “lock de DB” es que se libere automáticamente al morir el proceso.

  2. fs2 / fd-lock crate: APIs maduras, pero violan ADR-0001.

  3. Lock vía libc::flock (Unix) + LockFileEx (Windows) a mano: hace 6 meses era la opción honesta cross-platform sin deps. Hoy std::fs::File::try_lock ya cubre exactamente este caso con la API canónica. Descartada por obsolescencia.

  4. Lock compartido para lectores + exclusivo para escritores: gabysql no tiene modo read-only todavía, así que la distinción no aporta valor hoy. Cuando exista --read-only, este lock se relaja a try_lock_shared() en ese modo. Diferida.

  5. Lock por rango (header byte 0 solamente): irrelevante con un solo escritor; añade complejidad por nada. Lock sobre archivo completo es lo correcto.

✅ Consecuencias

Positivas:

Negativas / a vigilar:

🔗 Referencias