From d5863ae554c17259cfb84546f71f0caa5753e7b9 Mon Sep 17 00:00:00 2001 From: Sergio Date: Sat, 5 Sep 2026 19:03:48 +0000 Subject: [PATCH] verificar-repro: el apartadero iba a tmpfs, no tomaba el lock, y dejaba el veredicto a medias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tres defectos, encontrados corriéndolo y pagando uno de ellos: perdí el artefacto de `aichat` al interrumpir una corrida. Está reconstruyéndose en el worker. **1. El apartadero estaba en `/tmp`.** Para verificar hay que sacar el artefacto de su sitio y dejar que hammer lo reconstruya; eso iba a un `mktemp -d`. La cabecera prometía «un verificador que destruye lo que verifica es peor que no tenerlo», y esa ubicación rompía la promesa en tres sitios a la vez: `/tmp` es **tmpfs**, así que apartar copiaba el artefacto a RAM en una caja de 7,6 GiB sin swap; al ser otro sistema de ficheros el `mv` no era un rename atómico sino copiar-y-borrar, o sea que una interrupción a mitad lo dejaba en ninguna parte; y lo que sobreviviera moría al reiniciar. Ahora va a `store/.verificar-repro`: el `mv` es instantáneo, no cuesta RAM ni disco, y lo apartado SOBREVIVE a un kill — la corrida siguiente lo repone sola antes de empezar. **2. Llamaba a `hammer build` sin tomar el `flock`.** Es la regla 1 del repo, y saltársela arriesga el árbol compartido de `work/sources` de forma irreversible (ADR 0012). Lo toma el script y no quien lo llama, por lo mismo que `poda-fuentes.sh`: así no hay forma de correrlo mal — y correrlo mal fue exactamente lo que hice. Con `-E 3`, porque sin eso «no conseguí el lock» sale como exit 1, que es lo que este script usa para «hay divergencias»: informar «el corpus no reproduce» cuando lo que pasaba era que había otro build corriendo es peor que no correr. **3. Confundía DERIVA con NO-DETERMINISMO y lo delegaba en la memoria de quien leía.** Que el guardado difiera de una reconstrucción de hoy puede ser que el lab se movió (deriva, sano) o que las mismas entradas dan salidas distintas (no-determinismo, bug). La cabecera decía «corré el script dos veces y mirá la segunda» — con lo que la primera corrida informaba «DIVERGE» sobre cosas sanas y entrenaba a no creerle. Ahora, ante una diferencia, reconstruye una segunda vez y compara las dos reconstrucciones ENTRE SÍ: si coinciden es DERIVA, si no, NO-DETERMINISMO. El rebuild extra sólo se paga cuando ya hubo una diferencia. Y cuando es no-determinismo de verdad, los dos ejemplares se conservan en `store/.divergen/` en vez de borrarse: antes el guardián tiraba la única evidencia justo en el caso que existe para encontrar. Medido de paso, y es la parte buena: `libassuan`, `libksba`, `libffi` y `packaging` —artefactos del 21 de agosto— reproducen BIT A BIT con el lab de hoy. `scdoc` derivaba y ya está al día; `gron`, `age` y `anew` reproducen. La rama de DERIVA se probó ensuciando a propósito una copia guardada: clasifica bien y el store queda con la reconstrucción limpia. --- scripts/verificar-repro.sh | 108 ++++++++++++++++++++++++++++++++----- 1 file changed, 95 insertions(+), 13 deletions(-) diff --git a/scripts/verificar-repro.sh b/scripts/verificar-repro.sh index e6931648..a8698cce 100755 --- a/scripts/verificar-repro.sh +++ b/scripts/verificar-repro.sh @@ -15,9 +15,14 @@ # · si el artefacto guardado es viejo, se construyó con OTRO estado del lab ⇒ una diferencia es # DERIVA («el mundo se movió»), no no-determinismo; # · el no-determinismo es: mismas entradas, MISMO lab, salidas distintas. -# ⇒ Para medir NO-DETERMINISMO hay que correr el script DOS VECES: la primera pone el artefacto al -# día con el lab actual, y es la SEGUNDA la que contesta la pregunta. Medido: anew, gron y age -# divergían en la primera corrida y REPRODUCEN en la segunda. +# ⇒ Esto YA NO lo tiene que hacer quien lee: ante una diferencia el script reconstruye una SEGUNDA +# vez y compara las dos reconstrucciones ENTRE SÍ. Si coinciden, el que estaba fuera de fecha era +# el guardado ⇒ `DERIVA`, que no es un fallo. Si no coinciden ⇒ `NO-DETERMINISMO`, y ahí sí. Antes +# la cabecera decía «corré el script dos veces y mirá la segunda», y eso dejaba el veredicto a +# cargo de que alguien se acordara: la primera corrida informaba «DIVERGE» sobre cosas sanas y +# entrenaba a no creerle. El rebuild extra sólo se paga cuando ya hubo una diferencia. +# (Medido antes de automatizarlo: anew, gron, age y scdoc divergían en la primera y reproducen en +# la segunda.) # # ── EL MÉTODO, Y POR QUÉ ES BARATO ───────────────────────────────────────────────────────────── # Se APARTA el artefacto (no se borra) y se reconstruye. Como las DEPS siguen en el store, el rebuild @@ -27,24 +32,74 @@ # ── LA RED DE SEGURIDAD ──────────────────────────────────────────────────────────────────────── # Si el rebuild FALLA (y fallará en alguna: hay recetas que sólo construyen en ciertas máquinas), el # artefacto original se RESTAURA. Un verificador que destruye lo que verifica es peor que no tenerlo. -# Por eso se aparta a un directorio temporal en vez de borrar, y hay `trap` para restaurar todo lo -# pendiente si se interrumpe. +# Por eso se APARTA en vez de borrar, y hay `trap` para reponer lo pendiente si se interrumpe. +# +# ⚠ El apartadero vive DENTRO del store (`store/.verificar-repro`), no en `/tmp`. Estaba en `/tmp` y +# eso rompía la promesa de arriba en tres sitios a la vez: `/tmp` es tmpfs, así que apartar copiaba +# el artefacto a RAM en una caja de 7,6 GiB sin swap; al ser otro sistema de ficheros el `mv` no era +# un rename atómico sino copiar-y-borrar, así que una interrupción a mitad lo dejaba en ninguna +# parte; y lo que sobreviviera se perdía al reiniciar. Costó un artefacto el 2026-09-05. Dentro del +# store el `mv` es instantáneo y lo apartado SOBREVIVE a un kill: la corrida siguiente lo repone sola +# antes de empezar. Y cuando hay NO-DETERMINISMO de verdad, los dos ejemplares se conservan en +# `store/.divergen/` — antes se borraban, o sea que el guardián tiraba la única evidencia justo en el +# caso que existe para encontrar. # # Uso: scripts/verificar-repro.sh [receta.toml ...] (sin args: muestra por clase) # Env: STORE (def ./store) N (def 30, tamaño de la muestra) set -uo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT" STORE="${STORE:-./store}"; HAMMER="${HAMMER:-./target/release/hammer}"; N="${N:-30}" -GUARDA="$(mktemp -d)"; PENDIENTE="" + +# ── EL LOCK LO TOMA EL SCRIPT, Y ANTES DE TOCAR NADA ─────────────────────────────────────────── +# Este script llama a `hammer build`, y la regla 1 del repo es que todo build va envuelto en el +# `flock` de la granja: dos builds que compartan una dep se pisan el árbol de `work/sources` y lo +# rompen PARA SIEMPRE (ADR 0012). Lo toma el script y no quien lo llama, por lo mismo que en +# `poda-fuentes.sh`: así no hay forma de correrlo mal — y correrlo mal fue exactamente lo que pasó. +# +# `-E 3` importa: sin él, «no conseguí el lock» sale como exit 1, que es lo que este script usa para +# «hay divergencias». Un guardián que informa «el corpus no reproduce» cuando lo que pasó es que +# había otro build corriendo es peor que uno que no corre. +if [ -z "${VERIFICAR_REPRO_CON_LOCK:-}" ]; then + export VERIFICAR_REPRO_CON_LOCK=1 + flock -w 120 -E 3 "$ROOT/work/.farm-build.lock" "$0" "$@" + rc=$? + [ "$rc" -eq 3 ] && echo "── verificar-repro: build en vuelo (lock no tomado en 120s) ⇒ no toco nada" + exit "$rc" +fi + +# ── ⚠ EL APARTADERO VA DENTRO DEL STORE, NO EN /tmp ──────────────────────────────────────────── +# Para verificar hay que sacar el artefacto de su sitio y dejar que hammer lo reconstruya. Ese +# `mv` era a un `mktemp -d`, o sea a `/tmp`, y ahí se juntan tres cosas que costaron un artefacto +# el 2026-09-05: +# +# 1. `/tmp` es **tmpfs** en gioser: mover un artefacto ahí lo copia a RAM, en una máquina de +# 7,6 GiB y swap 0, compitiendo con el build que se está verificando. +# 2. Al ser otro sistema de ficheros, `mv` NO es un rename atómico sino copiar-y-borrar. Si el +# proceso muere a mitad, el artefacto no está ni en un sitio ni en el otro. +# 3. Y lo que quede ahí se pierde al reiniciar. +# +# Dentro del store el `mv` es un rename instantáneo, no cuesta RAM ni disco, y **sobrevive a un +# kill**: lo que quedó apartado sigue en disco y la próxima corrida lo repone sola. El apartadero +# empieza por punto, como `.dmerge`, y los consumidores del store filtran por `^[0-9a-f]{64}-`. +GUARDA="$STORE/.verificar-repro"; mkdir -p "$GUARDA"; PENDIENTE="" restaurar_todo() { for h in $PENDIENTE; do [ -d "$GUARDA/$h" ] && [ ! -d "$STORE/$h" ] && mv "$GUARDA/$h" "$STORE/$h" && echo " ↩ restaurado $h" done - rm -rf "$GUARDA" + rmdir "$GUARDA" 2>/dev/null || true } trap restaurar_todo EXIT INT TERM +# Repone lo que dejó apartado una corrida ANTERIOR que no llegó a su trap. Sin esto el apartadero +# es un agujero silencioso: el artefacto existe, no está donde se lo busca, y el grafo lo llama +# `debt` sin que nadie sepa por qué. +for d in "$GUARDA"/*; do + b=$(basename "$d"); [ -d "$d" ] || continue + case "$b" in '*') continue ;; esac + if [ -d "$STORE/$b" ]; then rm -rf "$d"; else mv "$d" "$STORE/$b"; echo " ↩ repuesto de una corrida anterior: $b"; fi +done + if [ $# -gt 0 ]; then RECETAS="$*" else # Muestra por CLASE de build: si sólo se verifican paquetes C con meson, el resultado no dice nada @@ -53,7 +108,7 @@ else RECETAS=$(ls recipes/*.toml recipes/incoming-wlr/*.toml 2>/dev/null | shuf -n "$N" --random-source=/dev/zero | tr '\n' ' ') fi -ok=0; dif=0; fallo=0; saltada=0; DIVERGEN="" +ok=0; dif=0; fallo=0; saltada=0; deriva=0; DIVERGEN="" for f in $RECETAS; do [ -f "$f" ] || continue n=$(awk -F'"' '/^name[[:space:]]*=/{print $2; exit}' "$f"); [ -n "$n" ] || n=$(basename "$f" .toml) @@ -68,9 +123,35 @@ for f in $RECETAS; do printf " ✓ %-26s REPRODUCE\n" "$n"; ok=$((ok+1)) rm -rf "$GUARDA/$D" else - printf " ✗ %-26s DIVERGE\n" "$n"; dif=$((dif+1)); DIVERGEN="$DIVERGEN $n" - "$HAMMER" why-differs "$GUARDA/$D" "$STORE/$D" 2>&1 | tail -4 | sed 's/^/ /' - rm -rf "$GUARDA/$D" + # ── DERIVA vs NO-DETERMINISMO: SE DECIDE ACÁ, NO EN LA CABEZA DE QUIEN LEE ────────────── + # Que el guardado difiera de una reconstrucción de hoy puede ser dos cosas MUY distintas: + # DERIVA (el artefacto es viejo y el lab se movió) o NO-DETERMINISMO (mismas entradas, mismo + # lab, salidas distintas). La cabecera decía «corré el script dos veces y mirá la segunda», + # y eso deja el veredicto a cargo de que alguien se acuerde — con lo que la primera corrida + # informa «DIVERGE» sobre cosas sanas y entrena a no creerle. + # + # Una segunda reconstrucción lo resuelve sin ambigüedad: si las DOS coinciden entre sí, el + # que estaba fuera de fecha era el guardado ⇒ deriva. Si no coinciden, es no-determinismo de + # verdad. Sólo se paga el rebuild extra cuando ya hubo una diferencia. + mv "$STORE/$D" "$GUARDA/$D.r1"; PENDIENTE="$PENDIENTE $D.r1" + if "$HAMMER" --store "$STORE" build "$f" >/dev/null 2>&1 && [ -d "$STORE/$D" ] \ + && "$HAMMER" why-differs "$GUARDA/$D.r1" "$STORE/$D" >/dev/null 2>&1; then + printf " ~ %-26s DERIVA (el guardado era viejo; dos reconstrucciones coinciden)\n" "$n" + deriva=$((deriva+1)) + rm -rf "$GUARDA/$D" "$GUARDA/$D.r1" + else + printf " ✗ %-26s NO-DETERMINISMO (dos reconstrucciones difieren entre sí)\n" "$n" + dif=$((dif+1)); DIVERGEN="$DIVERGEN $n" + "$HAMMER" why-differs "$GUARDA/$D.r1" "$STORE/$D" 2>&1 | tail -4 | sed 's/^/ /' + # ⚠ NO se borran los ejemplares. Borrarlos destruía la evidencia justo en el único caso que + # este guardián existe para encontrar: decía «no reproduce» y tiraba las copias con las que + # se podía averiguar POR QUÉ. Ocupan disco a propósito. + mkdir -p "$STORE/.divergen"; rm -rf "$STORE/.divergen/$D" "$STORE/.divergen/$D.r1" + mv "$GUARDA/$D" "$STORE/.divergen/$D" 2>/dev/null + mv "$GUARDA/$D.r1" "$STORE/.divergen/$D.r1" 2>/dev/null + printf " ejemplares guardados en %s/.divergen/ para poder mirarlos\n" "$STORE" + fi + PENDIENTE=$(echo "$PENDIENTE" | sed "s| $D.r1||") fi else # El rebuild falló: restaurar y NO contarlo como divergencia (no es lo mismo «no reproduce» que @@ -82,7 +163,8 @@ for f in $RECETAS; do done echo "════════════════════════════════════════════════════════════" -echo " REPRODUCEN: $ok · DIVERGEN: $dif · no construyeron: $fallo · sin artefacto: $saltada" +echo " REPRODUCEN: $ok · DERIVA: $deriva · NO-DETERMINISMO: $dif · no construyeron: $fallo · sin artefacto: $saltada" +[ "$deriva" -gt 0 ] && echo " ~ la deriva NO es un fallo: el artefacto guardado quedó viejo y ya se puso al día" [ "$dif" -gt 0 ] && echo " ⚠ divergen:$DIVERGEN" -[ "$dif" -eq 0 ] && [ "$ok" -gt 0 ] && echo " ✅ PUERTA SUPERADA: 0 divergencias sobre $ok verificadas" +[ "$dif" -eq 0 ] && [ $((ok + deriva)) -gt 0 ] && echo " ✅ PUERTA SUPERADA: 0 no-determinismos sobre $((ok + deriva)) verificadas" exit $(( dif > 0 ? 1 : 0 ))