5 playbooks nuevos en docs/playbooks/ del portal: - SQLite en modo DELETE causando SQLITE_BUSY, por que WAL lo resuelve. - Anchors YAML no soportados por el parser de Gitea Actions, incluido el diagnostico equivocado inicial (indentacion) que se corrigio despues -- documentado con honestidad como leccion de proceso. - Docker-in-Docker vs sibling containers, y por que Semgrep corre nativo en un venv en vez de como container aparte. - El SNI/realm HTTP del tunel rompiendo especificamente a Cosign (docker push funcionaba, cosign sign no), conectado con el incidente ya documentado del 504 -- misma capa de ingress, dos fallas distintas. - Contencion de recursos entre Gitea y su runner: causa raiz real confirmada en vivo durante esta misma sesion (ambos contenedores reiniciados a mitad de un build, sin limites de CPU/memoria reales), con fix propuesto (ver rama fix/gitea-runner-resource-limits en scripts/) pendiente de aplicacion manual. Se actualiza el playbook del 504 para linkear hacia el nuevo hallazgo de contencion, cerrando el loop que habia quedado abierto ahi. cosign.md: nueva seccion "Limitaciones conocidas y mejoras futuras" con los dos TODO reales -- firma por tag en vez de digest (con el warning real de Cosign visto en los logs) y transparency log de Rekor deshabilitado, explicado en simple. index.md: tabla resumen de los 3 pipelines (imagen + adaptacion de Semgrep por stack), diagrama generalizado para reflejar que es el mismo patron en los 3 workflows, y "que falta" actualizado. Validado con mkdocs build --strict (0 errores, 0 warnings) antes de commitear.
104 lines
4.5 KiB
Markdown
104 lines
4.5 KiB
Markdown
# Incidente: SQLite en modo DELETE causando contención en Gitea (2026-08)
|
|
|
|
| | |
|
|
|---|---|
|
|
| **Ventana del incidente** | 2026-08-15, coincidiendo con el arranque del runner de Gitea Actions y el polling frecuente de tareas |
|
|
| **Servicio afectado** | `gitea` (base de datos SQLite propia, `gitea.db`) |
|
|
| **Impacto** | Errores intermitentes `database is locked (SQLITE_BUSY)` al escribir desde distintos componentes de Gitea (runner, web, cron) al mismo tiempo |
|
|
|
|
## Contexto: por qué Gitea usa SQLite acá
|
|
|
|
Este lab corre Gitea con `DB_TYPE = sqlite3` (no Postgres/MySQL) — una
|
|
sola base de datos, un solo archivo (`gitea.db`), sin servidor de base
|
|
de datos aparte. Es la opción correcta para un lab de un solo nodo,
|
|
pero SQLite tiene una particularidad importante: por defecto abre el
|
|
archivo en **modo DELETE** (el modo "clásico" de journaling).
|
|
|
|
## Qué es el modo DELETE y por qué molesta acá
|
|
|
|
En modo DELETE, cada transacción de escritura:
|
|
|
|
1. Crea un archivo de journal temporal (`gitea.db-journal`).
|
|
2. Toma un **lock exclusivo sobre el archivo completo** de la base de
|
|
datos mientras dura la escritura.
|
|
3. Borra el journal al terminar.
|
|
|
|
Ese lock exclusivo es el problema: **mientras una escritura está en
|
|
curso, nada más puede leer ni escribir** — ni siquiera otro proceso
|
|
que solo quiere leer una fila que no tiene nada que ver con la que se
|
|
está escribiendo.
|
|
|
|
!!! info "Por qué esto pega justo en Gitea Actions"
|
|
El runner de Gitea Actions hace *polling* constante contra la API
|
|
de Gitea (cada pocos segundos, según `fetch_interval` en su
|
|
`config.yaml`) para preguntar "¿hay una tarea nueva?", y además
|
|
escribe actualizaciones de estado de cada step casi en tiempo real
|
|
mientras un job corre. Sumale los cron jobs internos de Gitea y el
|
|
tráfico normal de la web UI. Con la base en modo DELETE, todo eso
|
|
compite por el mismo lock exclusivo — cuantas más cosas escriben
|
|
seguido, más chance de pisarse.
|
|
|
|
## Síntoma
|
|
|
|
`SQLITE_BUSY` intermitente en los logs de Gitea, típicamente en
|
|
operaciones de corta duración (actualizar el estado de un step,
|
|
registrar un heartbeat del runner) que no deberían tener motivo para
|
|
fallar por contención:
|
|
|
|
```text
|
|
[E] can't update runner status: database is locked (5) (SQLITE_BUSY)
|
|
```
|
|
|
|
## Causa raíz
|
|
|
|
Modo de journaling `DELETE` (el default de SQLite si no se configura
|
|
nada distinto) combinado con el patrón de acceso de Gitea Actions:
|
|
muchas escrituras cortas y frecuentes desde procesos distintos
|
|
(runner, servidor web, cron), cada una compitiendo por un lock
|
|
exclusivo de archivo completo.
|
|
|
|
## Fix aplicado
|
|
|
|
Se cambió el modo de journaling a **WAL** (Write-Ahead Logging) en
|
|
`app.ini`:
|
|
|
|
```ini
|
|
[database]
|
|
; journal_mode por defecto (DELETE) toma un lock exclusivo del archivo
|
|
; completo en cada escritura. WAL permite lectores concurrentes mientras
|
|
; hay una escritura en curso -- fix para "database is locked" bajo el
|
|
; polling frecuente de Actions + cron jobs (diagnosticado 2026-08-15).
|
|
SQLITE_JOURNAL_MODE = WAL
|
|
```
|
|
|
|
### Por qué WAL resuelve esto (y qué NO resuelve)
|
|
|
|
En modo WAL, las escrituras no se aplican directo al archivo principal
|
|
de la base: se van agregando a un archivo aparte (`gitea.db-wal`), y
|
|
los lectores siguen leyendo del estado consistente más reciente sin
|
|
bloquearse. La regla cambia de *"una escritura bloquea todo"* a
|
|
*"una escritura bloquea solo a otra escritura"* — los lectores dejan
|
|
de competir por el lock.
|
|
|
|
!!! warning "WAL reduce la contención, no la elimina"
|
|
WAL sigue permitiendo **una sola escritura a la vez** — dos
|
|
escrituras concurrentes todavía pueden chocar y devolver
|
|
`SQLITE_BUSY` si el proceso no reintenta. En una corrida de este
|
|
mismo playbook se vio, después de aplicado el fix, una única
|
|
ocurrencia aislada de `database is locked` coincidiendo con un
|
|
reinicio del contenedor de `gitea` (ver
|
|
[contención de recursos de Gitea/runner](incidente-contencion-recursos-gitea-runner-2026-08.md)) —
|
|
consistente con una escritura en curso justo en el momento del
|
|
reinicio, no con que WAL no esté funcionando. Frecuencia bajó de
|
|
"molesta con cierta regularidad" a "un caso aislado en varias
|
|
horas de uso intensivo".
|
|
|
|
## Validación
|
|
|
|
El cambio se aplicó directo en el `app.ini` montado como volumen del
|
|
contenedor de `gitea` (no en un `docker exec` puntual) — persiste
|
|
across reinicios del contenedor, confirmado leyendo el archivo montado
|
|
después de un reinicio real del contenedor (ver
|
|
[contención de recursos](incidente-contencion-recursos-gitea-runner-2026-08.md)),
|
|
no solo aplicado en caliente y perdido al reciclar el contenedor.
|