From 5cfebf008769dfe2e3bcd8f29bcf946d5f835654 Mon Sep 17 00:00:00 2001 From: Sergio Date: Sun, 23 Aug 2026 17:13:09 +0000 Subject: [PATCH] granja: el aviso de "sin volumen" ahora CORTA en vez de sembrar igual MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La noche del 2026-08-22 la campana corrio sharded en dos workers. Un volumen de Hetzner se adjunta a UN server, y este script crea N workers en un bucle apuntando todos al MISMO VOL_NAME: hworker-1 tomo harkaq-cosecha y para hworker-2 no quedaba nada que adjuntar. Construyo el shard 1/2 entero en su disco raiz y el dead-man se lo llevo a las 03:27Z. 21 G, 397 artefactos sellados; 584 de sus 675 no existen en ningun otro sitio (work/perdidos-hworker2.txt). Lo caro es que la deteccion YA ESTABA y era correcta: echo " ⚠ el store NO quedó en el volumen ⇒ lo que construya se PIERDE..." Ese aviso se imprimio, con esas palabras, y el script siguio adelante: armo el dead-man y sembro la cola igual. Un aviso que no detiene el pipeline no es un guardian cuando no hay nadie leyendo el log. Es la regla 3 del CLAUDE.md con otra cara: el ausente (el volumen) llego hasta el final diciendo que todo fue bien. Tres cambios, todos sobre el mismo fallo: - `hcloud volume attach` dejaba de tragarse el error. Estaba escondido dos veces: `>/dev/null 2>&1` el mensaje y `|| true` el codigo de salida. - Se pregunta ANTES quien tiene el volumen tomado, y el motivo lo nombra. - Los dos ⚠ finales pasan a ser `sin_volumen`, que NO siembra: borra el server recien nacido (vacio, para que no quede idle facturando como hworker-4) y sale 1. Mismo blindaje que el dead-man y farm-down: solo borra con label role=hammer-worker, verificado que gioser no matchea. Escape explicito para el caso deliberado: SIN_VOLUMEN_OK=1. Verificado en negativo, que es como se comprueba lo que un guardian IMPIDE: la llamada corta con exit 1 sin alcanzar la linea siguiente, y con un server sin label no borra nada. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Xd5DComN9Nu47TBpYMBHBn --- scripts/farm/farm-up.sh | 51 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/scripts/farm/farm-up.sh b/scripts/farm/farm-up.sh index 6b336b2a..6409af45 100755 --- a/scripts/farm/farm-up.sh +++ b/scripts/farm/farm-up.sh @@ -59,6 +59,41 @@ SSH="ssh -i $SSH_KEY -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no cd "$ROOT" touch "$FLEET" +# ── GUARDIÁN DEL VOLUMEN (2026-08-23) ───────────────────────────────────────────────────────── +# El chequeo de más abajo YA detectaba este fallo — y sólo imprimía un ⚠ mientras el script seguía +# adelante: armaba el dead-man y sembraba la cola igual. La noche del 2026-08-22 eso costó 21 G y +# 397 artefactos sellados. hworker-2 nació sin volumen (lo tenía hworker-1: un volumen de Hetzner se +# adjunta a UN server), construyó el shard 1/2 entero en su disco raíz y el dead-man se lo llevó a +# las 03:27Z. El aviso salió impreso, con estas mismas palabras, y no lo leyó nadie: el hub estaba +# esperando una respuesta humana que llegó después del borrado. +# +# La lección no es "avisar mejor": es que un aviso que no detiene el pipeline no es un guardián +# cuando nadie mira. Sembrar trabajo en un disco efímero es peor que no crear el worker, porque +# gasta horas de CPU y las tira. Ahora CORTA. +sin_volumen() { + local name="$1" motivo="$2" + echo " ✗ $name: $motivo" >&2 + echo " Sin volumen, lo que selle vive en disco EFÍMERO y el dead-man se lo lleva entero." >&2 + echo " NO lo siembro. Salidas:" >&2 + echo " · un volumen POR worker: VOL_NAME=harkaq-cosecha-$name scripts/farm/farm-up.sh 1" >&2 + echo " · cosechar y clausurar el actual: scripts/farm/harkaq-vol.sh collect (luego close)" >&2 + echo " · worker deliberadamente desechable: SIN_VOLUMEN_OK=1 scripts/farm/farm-up.sh …" >&2 + if [ "${SIN_VOLUMEN_OK:-0}" = "1" ]; then + echo " SIN_VOLUMEN_OK=1 ⇒ sigo igual: este worker construye a disco efímero." >&2 + return 0 + fi + # El server acaba de nacer y todavía no tiene nada dentro. Dejarlo vivo sería un idle facturando + # (es el caso hworker-4, 5 días), así que se borra — con el MISMO blindaje que el dead-man y + # farm-down: sólo si lleva el label role=hammer-worker. gioser no lo lleva ⇒ jamás cae por acá. + if hcloud server describe "$name" -o format='{{.Labels}}' 2>/dev/null | grep -q hammer-worker; then + if hcloud server delete "$name" >/dev/null 2>&1; then + echo " $name borrado (recién creado, vacío): sin factura colgando." >&2 + sed -i "/^$name /d" "$FLEET" 2>/dev/null || true + fi + fi + exit 1 +} + # índice de arranque: el mayor hworker-N vivo + 1 (para escalar sin colisión de nombres) base=$(sed -n 's/^hworker-\([0-9]*\) .*/\1/p' "$FLEET" | sort -n | tail -1) base="${base:-0}" @@ -120,7 +155,17 @@ for k in $(seq 1 "$N"); do vid=$(hcloud volume list -o noheader -o columns=id,name 2>/dev/null | awk -v v="$VOL_NAME" '$2==v{print $1}') fi if [ -n "$vid" ]; then - hcloud volume attach --server "$name" "$VOL_NAME" >/dev/null 2>&1 || true + # UN VOLUMEN SE ADJUNTA A UN SERVER, y este script crea N workers en un bucle apuntando todos al + # MISMO VOL_NAME ⇒ del segundo en adelante el attach no tiene nada que adjuntar. Antes ese fallo + # se perdía dos veces: `>/dev/null 2>&1` escondía el mensaje y `|| true` el código de salida. + # Se pregunta ANTES, y el motivo se dice con el nombre del server que lo tiene tomado. + dueno=$(hcloud volume list -o noheader -o columns=name,server 2>/dev/null | awk -v v="$VOL_NAME" '$1==v{print $2}') + if [ -n "$dueno" ] && [ "$dueno" != "-" ] && [ "$dueno" != "$name" ]; then + sin_volumen "$name" "el volumen $VOL_NAME ya está adjunto a $dueno (un volumen = un server)" + fi + if ! err=$(hcloud volume attach --server "$name" "$VOL_NAME" 2>&1); then + sin_volumen "$name" "no pude adjuntar $VOL_NAME: ${err:-error desconocido}" + fi dev="/dev/disk/by-id/scsi-0HC_Volume_$vid" $SSH root@"$ip" "mkdir -p /mnt/cosecha # ⚠ El mkfs.ext4 -F de rescate SÓLO si el dispositivo no tiene filesystem. La versión previa @@ -175,10 +220,10 @@ for k in $(seq 1 "$N"); do if $SSH root@"$ip" 'mountpoint -q /mnt/cosecha && mountpoint -q '"$REMOTE"'/store' 2>/dev/null; then echo " ✓ store anclado al volumen ($( $SSH root@"$ip" 'ls -d /mnt/cosecha/store/*/ 2>/dev/null | wc -l') artefactos ya presentes)" else - echo " ⚠ el store NO quedó en el volumen ⇒ lo que construya se PIERDE al auto-borrarse. Revisar." + sin_volumen "$name" "el store NO quedó anclado al volumen (mountpoint dice que no)" fi else - echo " ⚠ sin volumen ⇒ este worker construiría a disco local y la automuerte perdería todo." + sin_volumen "$name" "no existe el volumen $VOL_NAME y no pude crearlo" fi # DEAD-MAN SWITCH: el worker se borra SOLO tras 1h sin trabajo. Va acá, en el nacimiento, porque