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 1532Shell 196Rust 108Markdown 993686 Definitions

⎇ 74b1a0580cb18b3e6249ba9110ace800d1e04a44 docs/adr/0012-arbol-de-fuentes-compartido-vs-privado.md Markdown · 90 lines · 4.7 KB History raw

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

1# ADR 0012 — El árbol de fuentes es caché y workspace a la vez: carrera abierta en `fetch`
2
3- **Estado:** PENDIENTE — dilema abierto, **sin decidir**. No implementar sin resolverlo (ver §Opciones).
4- **Fecha:** 2026-07-22
5- **Frontera:** `crates/hammer-build/src/fetch.rs` (`fetch_tarball` / `fetch_git`),
6  `crates/hammer-build/src/lib.rs:208`. Mitigación parcial ya en árbol: commit `1b436a7`.
7
8## Contexto
9
10`fetch` nombra el árbol de fuentes de forma **determinista y sin componente por-constructor**:
11
12```rust
13let work_tree = sources_dir.join(format!("{}-{}", recipe.name, &sha256[..16]));
14if work_tree.is_dir() { std::fs::remove_dir_all(&work_tree)?; }
15std::fs::create_dir_all(&work_tree)?;
16// tar -x dentro de work_tree
17```
18
19Dos procesos que construyan **la misma receta** (típicamente la misma *dep* alcanzada por dos
20caminos) resuelven al MISMO directorio. Uno hace `remove_dir_all` mientras el otro corre `tar -x`:
21el árbol queda a medias y devuelve `Directory not empty` (os error 39) — y **se queda roto hasta que
22alguien lo borra a mano**, porque el siguiente intento vuelve a chocar igual.
23
24No es teórico. El 2026-07-22 la campaña de deuda y el worker-loop pidieron `mesa` a la vez (el loop
25lo arrastraba desde la cola KDE al invalidarse `libdrm`) y se llevó puesta media cascada GUI, con un
26error que no nombra la causa.
27
28### Lo que YA está mitigado (no rehacer)
29
30Un lock compartido de grano grueso entre `campana-deuda.sh` y `farm-worker-loop.sh`
31(`work/.farm-build.lock`, commit `1b436a7`): la campaña lo retiene toda su corrida, el loop por ciclo
32de cola. Verificado: la carrera entre esos dos scripts desapareció.
33
34### Lo que NO cubre
35
36`build-farm.sh` lanza `xargs -P2`: **dos recetas de la misma cola que compartan dep se siguen
37pisando exactamente igual**. El lock entre scripts no puede cubrir esto sin serializar el `-P2` y
38tirar el paralelismo del worker.
39
40## El dilema
41
42El árbol cumple **dos papeles que se contradicen bajo concurrencia**:
43
441. **Caché direccionada por contenido** — la clave es `(nombre, sha)`, así que dos builds de la
45   misma dep "deberían" compartirlo, y re-extraer es caro.
462. **Workspace mutable de build** — `lib.rs:208-234` lo MUTA en el sitio: `apply_patches`,
47   `ensure_cargo_workspace_isolation`, `vendor_cargo_deps` (que para Go/Cargo puede tardar minutos y
48   ocupar decenas de GB), y después el sandbox lo bind-montea para compilar.
49
50Por eso **un lock sólo alrededor de la extracción NO alcanza**: aunque serialice el `tar`, un segundo
51proceso puede hacer `remove_dir_all` sobre un árbol que un `bwrap` está usando para compilar. La
52protección tiene que cubrir extracción **y** build, o los papeles tienen que separarse.
53
54## Opciones (ninguna elegida)
55
56**A. Lock por árbol sostenido durante TODO el build** (extract → patch → vendor → compile → seal).
57Correcto y contenido (un lock en `fetch` + guard hasta el final de `build`). Contra: serializa
58cualquier par de builds que necesiten la misma receta. El grafo es un diamante que converge —el
59propio código lo dice: "TODO KF6/Qt converge en qtbase/kcoreaddons, alcanzable por decenas de
60rutas"— así que puede degenerar en serializar casi todo y matar el `-P2`.
61
62**B. Árbol privado por build** (sufijo único por invocación). Elimina la carrera por construcción,
63sin lock ni serialización. Contra: re-extrae y **re-vendorea por build**. El `go mod vendor` ya está
64documentado como el cuello real del worker (decenas de GB sin tope, el watchdog de disco existe por
65eso, `DISK_HIGH=82`). Esta opción empuja justo donde ya duele.
66
67**C. Separar los dos papeles**: caché inmutable (`sources-cache/<nombre>-<sha>`, extraída una vez
68bajo lock y **nunca** mutada) + copia privada por build. Es la respuesta principista: cada papel con
69su ciclo de vida. Contra concreto: **el worker corre ext4, que no tiene reflink** (`cp --reflink`
70es btrfs/xfs), y hardlinkear no sirve porque el build muta en el sitio y corrompería la caché ⇒ la
71copia es real, con su coste en disco y tiempo. Reevaluar si el store vivo del SDD 18 (composefs)
72cambia el piso.
73
74## Qué haría falta para decidir
75
76Medir, no opinar:
77
781. Cuántos pares `(receta, receta)` de una misma cola comparten dep de verdad ⇒ cuánto serializaría
79   la opción A en la práctica.
802. Coste real de re-extraer + re-vendorear las recetas más caras (Go/Cargo grandes) ⇒ cuánto cuesta
81   B por build.
823. Espacio pico de la opción C con el corpus actual, contra el `DISK_HIGH` del watchdog.
83
84Sin esos tres números, cualquiera de las tres se elige a ciegas.
85
86## Nota de coordinación
87
88Hay **otro agente trabajando sobre este repo**. Este ADR existe para que la carrera quede
89registrada y nadie la "arregle" a medias: un lock sólo en la extracción parece correcto y **no lo
90es** (§El dilema). Si vas a tocarlo, resolvé el dilema primero.

0012-arbol-de-fuentes-compartido-vs-privado.md

ADR 0012 — El árbol de fuentes es caché y workspace a la vez: carrera abierta en fetch

Contexto

fetch nombra el árbol de fuentes de forma determinista y sin componente por-constructor:

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.