From f3206d752074761637d796de1aa402d5cb394c19 Mon Sep 17 00:00:00 2001 From: sergio Date: Wed, 22 Jul 2026 16:21:27 -0400 Subject: [PATCH] =?UTF-8?q?latido:=20colgar=20el=20latido=20de=20la=20sesi?= =?UTF-8?q?=C3=B3n,=20no=20del=20init=20(+=20lock=20anti-solapamiento)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El latido vivía sólo en el crontab y eso resultó frágil: este laptop arranca a veces con `init=/usr/local/sbin/arje-zero` y a veces con OpenRC (KDE), y no hay systemd en ninguno de los dos. `cronie` es un servicio OpenRC ⇒ en el arranque arje no existe y el latido no late. Peor: no late EN SILENCIO. Se destapó tras un corte sucio del 2026-07-22, en el que además se vio el runlevel `default` quedar a medias (cronie/metalog/acpid caídos, NetworkManager/dbus arriba) ⇒ ni siquiera arrancando OpenRC era garantía. El costo del síntoma es caro y callado: sin latido el hub no siembra, el worker agota la cola y se queda idle quemando € sin moler. `latido.sh` cuelga el latido de la SESIÓN: cualquier terminal, en cualquier arranque, asegura que haya exactamente un latido vivo (`--ensure` desde ~/.zshrc, ~5ms y mudo si ya hay uno). Sin root, sin unidad de servicio, sin mantener lo mismo por duplicado en dos inits. Contra asumido: no late sin sesión abierta — es un laptop, no un server, y el primer ciclo dispara al instante de abrir la terminal (el cron esperaba hasta 30 min al próximo tick). El lock va DENTRO de cosecha-cron.sh, no en el llamador, para que valga venga de donde venga. Eso además tapa un bug latente que ya existía: nada impedía que dos ciclos se solaparan haciendo rsync sobre el mismo store y `git commit`+`push` a la vez. Con el lock, el crontab puede quedarse: bajo KDE dispara cron, bajo arje dispara la sesión, y nunca se pisan. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/farm/cosecha-cron.sh | 15 ++++++ scripts/farm/latido.sh | 94 ++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100755 scripts/farm/latido.sh diff --git a/scripts/farm/cosecha-cron.sh b/scripts/farm/cosecha-cron.sh index 9f6b68a4..dd7a0a0a 100755 --- a/scripts/farm/cosecha-cron.sh +++ b/scripts/farm/cosecha-cron.sh @@ -25,6 +25,21 @@ set -uo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" cd "$ROOT" + +# LOCK (2026-07-22): el latido ya no viene sólo del crontab — `latido.sh` lo lanza desde la sesión +# porque el laptop arranca a veces con arje-zero y a veces con OpenRC, y cronie sólo existe en uno +# de los dos. Con dos disparadores posibles, dos ciclos pueden solaparse: ambos hacen rsync sobre el +# mismo store y, peor, `git commit`+`push` a la vez (index.lock, o un push que pisa al otro). El lock +# va ACÁ y no en el llamador para que valga venga de donde venga: cron, latido o a mano. +# -n = no esperar: si ya hay un ciclo corriendo, este sobra (el próximo tick recoge lo mismo). +LOCKFILE="${LOCKFILE:-$ROOT/work/.cosecha-cron.lock}" +mkdir -p "$(dirname "$LOCKFILE")" +exec 9>"$LOCKFILE" +if ! flock -n 9; then + echo "── $(date -u +%FT%TZ) cosecha-cron: ya hay un ciclo en curso ⇒ salgo (no me solapo)" + exit 0 +fi + SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}" REMOTE="${REMOTE:-/opt/hammer}" FLEET="$ROOT/scripts/farm/.fleet" diff --git a/scripts/farm/latido.sh b/scripts/farm/latido.sh new file mode 100755 index 00000000..848aa3ef --- /dev/null +++ b/scripts/farm/latido.sh @@ -0,0 +1,94 @@ +#!/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 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. + ( cd "$ROOT" && ./scripts/farm/cosecha-cron.sh ) >>"$COSECHA_LOG" 2>&1 || \ + echo "── $(ts) latido: el ciclo salió != 0 (ver cosecha-cron.log) — sigo" >>"$LOG" + sleep "$INTERVALO" +done