Queda una carrera abierta que el lock de script NO cierra: `build-farm.sh` corre `xargs -P2` y dos recetas de la misma cola que compartan dep se pisan igual, porque `fetch` nombra el árbol de forma determinista y sin nada del constructor. El fondo no es "falta un lock", es que el árbol cumple DOS papeles que se contradicen bajo concurrencia: caché direccionada por contenido (clave = nombre+sha, re-extraer es caro) y workspace mutable de build (lib.rs lo parchea, aísla el workspace Cargo y vendorea EN EL SITIO, y después el sandbox lo bind-montea). Por eso un lock alrededor de la extracción parece correcto y NO lo es: un segundo proceso puede borrar un árbol que un bwrap ya está compilando. El ADR deja las tres salidas (lock por árbol durante todo el build / árbol privado por build / separar caché inmutable + copia privada) con su contra concreta cada una —incluida que el worker corre ext4, sin reflink, así que la copia de la opción C es real— y los tres números que habría que medir para elegir en vez de opinar. Se ancla en los dos sitios donde alguien va a chocar: el ADR indexado en docs/README.md (de paso se agregan 0009-0011, que faltaban) y un comentario en el propio `fetch_tarball` que dice explícitamente que no se arregle a medias. Va como PENDIENTE y no decidido a propósito: hay otro agente sobre este repo y esto es lo que evita que la carrera se "arregle" de una forma que parece bien y deja el bug. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
4.7 KiB
Markdown
91 lines
4.7 KiB
Markdown
# ADR 0012 — El árbol de fuentes es caché y workspace a la vez: carrera abierta en `fetch`
|
|
|
|
- **Estado:** PENDIENTE — dilema abierto, **sin decidir**. No implementar sin resolverlo (ver §Opciones).
|
|
- **Fecha:** 2026-07-22
|
|
- **Frontera:** `crates/hammer-build/src/fetch.rs` (`fetch_tarball` / `fetch_git`),
|
|
`crates/hammer-build/src/lib.rs:208`. Mitigación parcial ya en árbol: commit `1b436a7`.
|
|
|
|
## Contexto
|
|
|
|
`fetch` nombra el árbol de fuentes de forma **determinista y sin componente por-constructor**:
|
|
|
|
```rust
|
|
let work_tree = sources_dir.join(format!("{}-{}", recipe.name, &sha256[..16]));
|
|
if work_tree.is_dir() { std::fs::remove_dir_all(&work_tree)?; }
|
|
std::fs::create_dir_all(&work_tree)?;
|
|
// tar -x dentro de work_tree
|
|
```
|
|
|
|
Dos procesos que construyan **la misma receta** (típicamente la misma *dep* alcanzada por dos
|
|
caminos) resuelven al MISMO directorio. Uno hace `remove_dir_all` mientras el otro corre `tar -x`:
|
|
el árbol queda a medias y devuelve `Directory not empty` (os error 39) — y **se queda roto hasta que
|
|
alguien lo borra a mano**, porque el siguiente intento vuelve a chocar igual.
|
|
|
|
No es teórico. El 2026-07-22 la campaña de deuda y el worker-loop pidieron `mesa` a la vez (el loop
|
|
lo arrastraba desde la cola KDE al invalidarse `libdrm`) y se llevó puesta media cascada GUI, con un
|
|
error que no nombra la causa.
|
|
|
|
### Lo que YA está mitigado (no rehacer)
|
|
|
|
Un lock compartido de grano grueso entre `campana-deuda.sh` y `farm-worker-loop.sh`
|
|
(`work/.farm-build.lock`, commit `1b436a7`): la campaña lo retiene toda su corrida, el loop por ciclo
|
|
de cola. Verificado: la carrera entre esos dos scripts desapareció.
|
|
|
|
### Lo que NO cubre
|
|
|
|
`build-farm.sh` lanza `xargs -P2`: **dos recetas de la misma cola que compartan dep se siguen
|
|
pisando exactamente igual**. El lock entre scripts no puede cubrir esto sin serializar el `-P2` y
|
|
tirar el paralelismo del worker.
|
|
|
|
## El dilema
|
|
|
|
El árbol cumple **dos papeles que se contradicen bajo concurrencia**:
|
|
|
|
1. **Caché direccionada por contenido** — la clave es `(nombre, sha)`, así que dos builds de la
|
|
misma dep "deberían" compartirlo, y re-extraer es caro.
|
|
2. **Workspace mutable de build** — `lib.rs:208-234` lo MUTA en el sitio: `apply_patches`,
|
|
`ensure_cargo_workspace_isolation`, `vendor_cargo_deps` (que para Go/Cargo puede tardar minutos y
|
|
ocupar decenas de GB), y después el sandbox lo bind-montea para compilar.
|
|
|
|
Por eso **un lock sólo alrededor de la extracción NO alcanza**: aunque serialice el `tar`, un segundo
|
|
proceso puede hacer `remove_dir_all` sobre un árbol que un `bwrap` está usando para compilar. La
|
|
protección tiene que cubrir extracción **y** build, o los papeles tienen que separarse.
|
|
|
|
## Opciones (ninguna elegida)
|
|
|
|
**A. Lock por árbol sostenido durante TODO el build** (extract → patch → vendor → compile → seal).
|
|
Correcto y contenido (un lock en `fetch` + guard hasta el final de `build`). Contra: serializa
|
|
cualquier par de builds que necesiten la misma receta. El grafo es un diamante que converge —el
|
|
propio código lo dice: "TODO KF6/Qt converge en qtbase/kcoreaddons, alcanzable por decenas de
|
|
rutas"— así que puede degenerar en serializar casi todo y matar el `-P2`.
|
|
|
|
**B. Árbol privado por build** (sufijo único por invocación). Elimina la carrera por construcción,
|
|
sin lock ni serialización. Contra: re-extrae y **re-vendorea por build**. El `go mod vendor` ya está
|
|
documentado como el cuello real del worker (decenas de GB sin tope, el watchdog de disco existe por
|
|
eso, `DISK_HIGH=82`). Esta opción empuja justo donde ya duele.
|
|
|
|
**C. Separar los dos papeles**: caché inmutable (`sources-cache/<nombre>-<sha>`, extraída una vez
|
|
bajo lock y **nunca** mutada) + copia privada por build. Es la respuesta principista: cada papel con
|
|
su ciclo de vida. Contra concreto: **el worker corre ext4, que no tiene reflink** (`cp --reflink`
|
|
es btrfs/xfs), y hardlinkear no sirve porque el build muta en el sitio y corrompería la caché ⇒ la
|
|
copia es real, con su coste en disco y tiempo. Reevaluar si el store vivo del SDD 18 (composefs)
|
|
cambia el piso.
|
|
|
|
## Qué haría falta para decidir
|
|
|
|
Medir, no opinar:
|
|
|
|
1. Cuántos pares `(receta, receta)` de una misma cola comparten dep de verdad ⇒ cuánto serializaría
|
|
la opción A en la práctica.
|
|
2. Coste real de re-extraer + re-vendorear las recetas más caras (Go/Cargo grandes) ⇒ cuánto cuesta
|
|
B por build.
|
|
3. Espacio pico de la opción C con el corpus actual, contra el `DISK_HIGH` del watchdog.
|
|
|
|
Sin esos tres números, cualquiera de las tres se elige a ciegas.
|
|
|
|
## Nota de coordinación
|
|
|
|
Hay **otro agente trabajando sobre este repo**. Este ADR existe para que la carrera quede
|
|
registrada y nadie la "arregle" a medias: un lock sólo en la extracción parece correcto y **no lo
|
|
es** (§El dilema). Si vas a tocarlo, resolvé el dilema primero.
|