tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1411Shell 141Markdown 85Rust 842918 Definitions

ADR 0012: registrar como PENDIENTE el dilema del árbol de fuentes (caché vs workspace)

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>

c963b24b5ed7095b6cb9c419d48ff4b692638b4e · by sergio · (3 months ago) · parent 062ae142a · browse the tree at this commit

Semantic delta

No definition changed: this change is text, documentation or data.

3 files · +104 −0

diff --git a/crates/hammer-build/src/fetch.rs b/crates/hammer-build/src/fetch.rs
index d1c17bab..44614a45 100644
--- a/crates/hammer-build/src/fetch.rs
+++ b/crates/hammer-build/src/fetch.rs
@@ -114,6 +114,16 @@ fn fetch_tarball(
         verify_sha256(&cached, sha256)?;
     }
 
+    // ⚠ CARRERA CONOCIDA, SIN RESOLVER — ver docs/adr/0012-arbol-de-fuentes-compartido-vs-privado.md
+    // El path es determinista y NO lleva nada del constructor ⇒ dos builds de la misma receta (la
+    // misma dep alcanzada por dos caminos) resuelven al mismo directorio: uno borra mientras el otro
+    // extrae y el árbol queda roto con "Directory not empty" (os error 39) hasta borrarlo a mano.
+    // Mitigado SÓLO entre campana-deuda.sh y farm-worker-loop.sh con un lock de script (commit
+    // 1b436a7); el `xargs -P2` de build-farm.sh sigue expuesto.
+    // NO "arreglar" con un lock alrededor de esta extracción: parece correcto y no lo es, porque un
+    // segundo proceso puede borrar un árbol que un bwrap ya está usando para compilar. Este árbol es
+    // caché y workspace mutable a la vez (lib.rs lo parchea y vendorea in situ); el ADR 0012 plantea
+    // las tres salidas y qué medir para elegir.
     let work_tree = sources_dir.join(format!("{}-{}", recipe.name, &sha256[..16]));
     if work_tree.is_dir() {
         std::fs::remove_dir_all(&work_tree)?;
diff --git a/docs/README.md b/docs/README.md
index 947f9b68..9c758a24 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -42,3 +42,7 @@ Decisiones tomadas, con su contexto y consecuencias. Ver [`adr/`](adr/).
 | 0006 | [Commits fijados, no HEAD vivo](adr/0006-pinned-commits.md) |
 | 0007 | [`arje` como init propio del track posterior](adr/0007-arje-como-init-propio.md) |
 | 0008 | [Bootstrap en 3 stages, `zig` como semilla](adr/0008-bootstrap-stages.md) |
+| 0009 | [Código direccionado por contenido (estilo Unison)](adr/0009-content-addressed-code.md) |
+| 0010 | [Arranque por grafo: el menú de boot como navegación del grafo](adr/0010-arranque-grafo-mirada.md) |
+| 0011 | [Etapa Escritorio: campaña KDE Plasma 6](adr/0011-escritorio-kde-plasma.md) |
+| 0012 | [El árbol de fuentes es caché y workspace a la vez — **PENDIENTE, dilema abierto**](adr/0012-arbol-de-fuentes-compartido-vs-privado.md) |
diff --git a/docs/adr/0012-arbol-de-fuentes-compartido-vs-privado.md b/docs/adr/0012-arbol-de-fuentes-compartido-vs-privado.md
new file mode 100644
index 00000000..8f6274d6
--- /dev/null
+++ b/docs/adr/0012-arbol-de-fuentes-compartido-vs-privado.md
@@ -0,0 +1,90 @@
+# 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.