Files
takana/scripts/farm/latido.sh
T
SergioandClaude Opus 5 5247821455 granja: el lock ya no se lo puede quedar un proceso fugado
El lock lo sostiene la descripción de fichero abierta y los hijos la heredan: con 'exec 9>' +
'flock 9' cualquier descendiente que sobreviva al script retiene el lock para siempre, y el
siguiente que lo pida espera sin que nada falle. Medido hoy: dos firefox colgados de una caza
sobrevivieron al kill de su bwrap y dejaron a la granja hora y media sin poder compilar.

Dos formas de defecto ⇒ dos arreglos, y no son intercambiables:

  · cosecha-cron, campana-deuda, harvest-harkaq — el lock cubre el script ENTERO, así que se
    re-ejecutan bajo 'flock -o' (cierra el fd antes de ejecutar). Ir poniendo '9>&-' comando a
    comando ahí es jugar a los topos. '-E 77' separa 'estaba ocupado' de 'el trabajo falló',
    que con el 9> no se distinguían.

  · farm-worker-loop, latido — el lock vive en un subshell / es a propósito el mecanismo de
    vida, así que basta '9>&-' en los hijos. En latido NO se puede usar -o: ahí el fd retenido
    ES como --status sabe que el latido vive sin pidfile. Su modo de fallo era el peor de
    todos: un nieto fugado dejaba el latido MUERTO PERO APARENTANDO ESTAR VIVO.

Medido antes de elegir, no deducido del manual:
  exec 9> + flock 9      → el nieto RETIENE  (control negativo: reproduce el fallo)
  exec {L}> (fd auto)    → el nieto RETIENE  (bash NO lo marca close-on-exec)
  flock -o / 9>&- hijo   → lock LIBRE

Y probados los cinco sobre el fichero real, no sobre una maqueta: copias truncadas justo tras
el bloque del lock, invocadas con RUTA RELATIVA DESDE OTRO DIRECTORIO (que es lo que rompe un
$0 sin resolver — de hecho la primera versión de harvest-harkaq calculaba YO después del cd y
habría fallado ahí). Los tres entran, ninguno deja el lock tomado por el nieto, y la exclusión
mutua sigue funcionando con su mensaje y su exit 0 — que es lo que un arreglo de locks puede
romper en silencio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
2026-09-08 16:34:55 +00:00

104 lines
5.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# latido.sh — el latido de la granja SIN depender del init.
#
# POR QUÉ EXISTE (2026-07-22): el latido vivía en el crontab, y eso resultó frágil por dos motivos
# que se destaparon juntos tras un corte sucio:
# 1. Este laptop arranca a veces con `init=/usr/local/sbin/arje-zero` y a veces con OpenRC (KDE).
# No hay systemd en NINGUNO de los dos. `cronie` es un servicio OpenRC ⇒ en el arranque arje
# simplemente no está, y el latido no late. Peor: no late EN SILENCIO, nadie se entera.
# 2. Aun arrancando OpenRC, se vio el runlevel `default` quedar a medias (cronie/metalog/acpid
# caídos, NetworkManager/dbus arriba) ⇒ ni siquiera ahí es garantía.
# El síntoma final es caro: sin latido el hub no siembra, el worker agota la cola y se queda idle
# quemando € sin moler (viola [[vps-nunca-idle]]).
#
# LA IDEA: colgar el latido de la SESIÓN en vez del init. Cualquier terminal que abras, en cualquier
# arranque, se asegura de que haya exactamente un latido vivo. Sin root, sin unidad de servicio, sin
# nada que mantener por duplicado en dos inits.
#
# CONTRA (asumido a propósito): no late con la máquina encendida pero sin sesión tuya abierta. Es un
# laptop, no un server: cuando no hay sesión tampoco hay nadie esperando la cosecha. Y el primer
# ciclo dispara AL INSTANTE de abrir la terminal, así que un arranque nunca arrastra el hueco (el
# cron, en cambio, esperaba hasta 30 min al próximo tick).
#
# SINGLETON: el lock lo lleva `cosecha-cron.sh` (así vale también si el cron de OpenRC dispara a la
# vez); acá hay un segundo lock, el del BUCLE, para no acumular un demonio por terminal abierta.
#
# Uso:
# latido.sh --ensure # idempotente y barato: si no hay latido, lo lanza. Esto va en ~/.zshrc
# latido.sh --loop # el bucle en sí (lo lanza --ensure; no hace falta llamarlo a mano)
# latido.sh --status # ¿late? desde cuándo, y cuándo fue el último ciclo
# latido.sh --stop # parar el latido
# Env: INTERVALO (def 1800s = los mismos 30 min que tenía el crontab)
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
INTERVALO="${INTERVALO:-1800}"
LOCK="$ROOT/work/.latido.lock"
LOG="$ROOT/work/latido.log"
COSECHA_LOG="$ROOT/work/cosecha-cron.log"
mkdir -p "$ROOT/work"
ts() { date -u +%FT%TZ; }
case "${1:---ensure}" in
--ensure)
# Barato a propósito: esto corre en CADA terminal que abrís. Un flock que falla es ~1ms y no
# imprime nada — abrir una terminal no puede volverse lento ni ruidoso por el latido.
exec 9>"$LOCK"
if flock -n 9; then
flock -u 9
# setsid + nohup: que sobreviva al cierre de ESTA terminal. Si no, el latido se muere con la
# ventana que lo parió y volvemos al problema de origen.
setsid nohup "$0" --loop </dev/null >/dev/null 2>&1 &
disown 2>/dev/null || true
fi
exit 0
;;
--status)
if exec 9>"$LOCK" && flock -n 9; then
flock -u 9
echo "latido: PARADO"
exit 1
else
echo "latido: VIVO"
[ -f "$LOG" ] && tail -3 "$LOG"
exit 0
fi
;;
--stop)
pkill -f "latido.sh --loop" && echo "latido: parado" || echo "latido: no había ninguno vivo"
exit 0
;;
--loop) ;;
*) echo "uso: latido.sh --ensure|--loop|--status|--stop" >&2; exit 2 ;;
esac
# --- el bucle ---
# El lock se mantiene TOMADO mientras el bucle vive: es lo que hace que --ensure sepa que ya hay uno
# y que --status pueda responder sin pidfiles (un pidfile miente si el proceso murió; el lock no).
exec 9>"$LOCK"
flock -n 9 || exit 0 # otro latido ganó la carrera (dos terminales abiertas a la vez)
echo "── $(ts) latido arranca (pid $$, intervalo ${INTERVALO}s)" >>"$LOG"
trap 'echo "── $(ts) latido termina (pid $$)" >>"$LOG"' EXIT
while :; do
# `|| true` + `set -uo pipefail` sin `-e`: un ciclo que falla (worker caído, red, gitea) NO puede
# matar el latido. Se loguea y se reintenta al próximo tick; ésa es toda la robustez que hace falta.
# `9>&-` cierra el fd DEL LOCK DEL LATIDO en el hijo. Sin esto, un proceso fugado de la cosecha
# lo retiene para siempre y el latido queda MUERTO PERO APARENTANDO ESTAR VIVO: `--status` lee
# el lock, lo ve tomado y dice VIVO, y `--ensure` se niega a levantar otro. Es el peor modo de
# fallo posible acá — la máquina deja de latir y todo indica que late. Medido 2026-09-08 con
# dos `firefox` colgados que retuvieron el lock de build hora y media (regla 1 de CLAUDE.md).
# ⚠ Y acá NO va `flock -o`, al revés que en cosecha-cron/campana-deuda/harvest: en este script
# el fd retenido ES el mecanismo de vida a propósito (así `--status` responde sin pidfile, que
# mentiría si el proceso muriera). Lo que sobra es que lo hereden los HIJOS, no que lo tenga el
# bucle. Por eso el arreglo es distinto aunque el defecto sea el mismo.
( cd "$ROOT" && ./scripts/farm/cosecha-cron.sh ) 9>&- >>"$COSECHA_LOG" 2>&1 || \
echo "── $(ts) latido: el ciclo salió != 0 (ver cosecha-cron.log) — sigo" >>"$LOG"
sleep "$INTERVALO"
done