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
104 lines
5.0 KiB
Bash
Executable File
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
|