diff --git a/scripts/farm/deadman.sh b/scripts/farm/deadman.sh index 0fc99b30..193f24dc 100644 --- a/scripts/farm/deadman.sh +++ b/scripts/farm/deadman.sh @@ -18,6 +18,21 @@ # comprometido puede borrar servers del proyecto. Se acepta porque la alternativa medida fue peor: # 5 días de VPS idle. Mitigación: el worker es efímero y no guarda secretos propios. # +# AUTOMUERTE TOTAL, y por qué NO hay guarda de "cosecha pendiente" (2026-07-17): la primera versión +# abortaba el borrado si quedaba cosecha sin recoger. Eso es exactamente el bug de hworker-4 con +# otra cara: el worker se queda VIVO esperando a que alguien lo rescate. Un switch que se desarma +# solo cuando más falta hace no sirve. +# +# La salida correcta no es "no morir": es que **el store del worker VIVA EN EL VOLUMEN** +# (/mnt/cosecha/store, symlinkeado desde el store local por farm-up). Así "guardar lo generado" no +# es un paso que pueda fallar antes de morir — es la estructura misma. Cada artefacto ya está en el +# volumen persistente en el instante en que se sella. Morir no pierde NADA, y por eso puede ser +# incondicional. El volumen sobrevive al server; el hub cosecha cuando esté vivo (harkaq-vol.sh +# collect) y clausura con `close`. +# +# Antes de morir sólo queda una cosa honesta: sync(1) + umount, para que el ext4 del volumen quede +# consistente. No es "guardar" (ya está guardado), es cerrar la puerta al salir. +# # QUÉ CUENTA COMO TRABAJO: un `hammer build` en vuelo, el loop del worker, o un heartbeat fresco # (`/run/hammer-heartbeat`, que un job largo puede tocar). Si nada de eso aparece durante # IDLE_MAX_TICKS ticks seguidos, se borra. @@ -76,12 +91,21 @@ log "sin trabajo: tick $ticks/$IDLE_MAX_TICKS" self="$(hostname)" log "IDLE $ticks ticks ⇒ borrando $self" -# GUARDA: nunca borrar con cosecha sin recoger. El worker no sabe qué cosechó el hub, pero sí puede -# negarse a morir si alguien dejó una marca explícita. `farm-run`/la campaña la ponen mientras haya -# artefactos que nadie bajó; el hub la quita al cosechar. -if [ -f /var/lib/hammer-no-borrar ]; then - log "ABORTA: /var/lib/hammer-no-borrar presente (cosecha pendiente) ⇒ sigo vivo, avisá al hub" - exit 0 +# CERRAR LA PUERTA AL SALIR. No es "guardar" — lo generado YA está en el volumen (el store vive +# ahí). Es dejar el ext4 consistente para que el `collect` del hub lo monte limpio. Sin guarda de +# "cosecha pendiente" A PROPÓSITO: esa guarda dejaba al worker vivo justo cuando había trabajo que +# salvar, que es el bug original con otra cara. La automuerte es TOTAL. +if mountpoint -q /mnt/cosecha 2>/dev/null; then + art=$(ls -d /mnt/cosecha/store/*/ 2>/dev/null | wc -l) + log "volumen: $art artefactos ya persistidos; sync+umount antes de morir" + sync + # -l (lazy): si algo aún tiene el FS abierto, no queremos bloquear la muerte. Los datos ya + # están en disco por el sync; el volumen se detacha solo al borrarse el server. + umount /mnt/cosecha 2>/dev/null || umount -l /mnt/cosecha 2>/dev/null || \ + log "aviso: umount falló; el sync ya persistió los datos" +else + log "AVISO: /mnt/cosecha NO montado ⇒ este worker no tenía volumen. Lo que haya en su store" + log " LOCAL se pierde al borrarlo. Nació por un camino sin volumen (¿farm-up viejo?)." fi if [ -z "${HCLOUD_TOKEN:-}" ]; then diff --git a/scripts/farm/farm-up.sh b/scripts/farm/farm-up.sh index 5dd716f8..bfa3e5e0 100755 --- a/scripts/farm/farm-up.sh +++ b/scripts/farm/farm-up.sh @@ -25,6 +25,14 @@ LOCATION="${LOCATION:-hel1}" SSHKEY="${SSHKEY:-desarrollo@jlsoltech.com}" SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}" REMOTE="${REMOTE:-/opt/hammer}" +# MISMO volumen que harkaq-vol.sh, a propósito: eran DOS caminos de crear workers y sólo uno tenía +# volumen+automuerte. hworker-4 nació por ÉSTE (farm-up) y por eso quedó 5 días idle con 37G que +# nadie salvó — la protección existía y este camino la esquivaba. Compartir el volumen hace que +# `harkaq-vol.sh collect` y `close` cosechen y clausuren lo de AMBOS caminos. +VOL_NAME="${VOL_NAME:-harkaq-cosecha}" +VOL_SIZE="${VOL_SIZE:-100}" # GB. ~€0.044/GB/mes ⇒ 100G ≈ €4.4/mes MIENTRAS exista. + # El baseline €0 lo da `harkaq-vol.sh close`, que es + # tuyo: el volumen es el único que sobrevive a propósito. ROOT="$(cd "$(dirname "$0")/../.." && pwd)" FLEET="$ROOT/scripts/farm/.fleet" # Workers efímeros RECICLAN IPs de Hetzner ⇒ un known_hosts persistente choca ("HOST KEY CHANGED") @@ -58,6 +66,35 @@ for k in $(seq 1 "$N"); do # el servicio ya arrancó en el boot (baked/enabled); reiniciar fuerza rescan inmediato de la cola $SSH root@"$ip" 'systemctl restart hammer-farm' 2>/dev/null || true + # VOLUMEN PERSISTENTE + STORE DENTRO. Sin esto la automuerte perdería lo construido: hworker-4 + # tenía 438 artefactos (37G) SÓLO en su disco local. El store vive en /mnt/cosecha/store ⇒ cada + # artefacto está en el volumen persistente EN EL INSTANTE en que se sella. "Guardar lo generado" + # deja de ser un paso que puede fallar antes de morir y pasa a ser la estructura. El volumen + # sobrevive al server; se cosecha con harkaq-vol.sh collect y se clausura con close. + echo " montando volumen persistente $VOL_NAME y anclando el store dentro…" + vid=$(hcloud volume list -o noheader -o columns=id,name 2>/dev/null | awk -v v="$VOL_NAME" '$2==v{print $1}') + if [ -z "$vid" ]; then + hcloud volume create --name "$VOL_NAME" --size "$VOL_SIZE" --location "$LOCATION" --format ext4 >/dev/null 2>&1 || true + 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 + dev="/dev/disk/by-id/scsi-0HC_Volume_$vid" + $SSH root@"$ip" "mkdir -p /mnt/cosecha + mount $dev /mnt/cosecha 2>/dev/null || { mkfs.ext4 -q -F $dev && mount $dev /mnt/cosecha; } + mkdir -p /mnt/cosecha/store /mnt/cosecha/verdicts /mnt/cosecha/logs + # el store del worker ES el del volumen: sellar YA es persistir + rm -rf $REMOTE/store 2>/dev/null; ln -sfn /mnt/cosecha/store $REMOTE/store + grep -q /mnt/cosecha /etc/fstab || echo '$dev /mnt/cosecha ext4 defaults,nofail 0 0' >> /etc/fstab" >/dev/null 2>&1 + if $SSH root@"$ip" 'mountpoint -q /mnt/cosecha && test -L '"$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." + fi + else + echo " ⚠ sin volumen ⇒ este worker construiría a disco local y la automuerte perdería todo." + fi + # DEAD-MAN SWITCH: el worker se borra SOLO tras 1h sin trabajo. Va acá, en el nacimiento, porque # el modelo "efímero" NO puede depender de que alguien llame a farm-down: hworker-4 estuvo 5 días # idle (2026-07-17) justo por eso — el agente que lo creó se fue y el server quedó. Un invariante