Files
takana/scripts/farm/farm-sync.sh
T
sergioandClaude Opus 5 9388beba49 store en el volumen: «está sellado» dejó de ser «está en este disco»
El store de trabajo se mudó al volumen de la granja y el laptop quedó con
219 artefactos: los de fuente privada (que la granja no puede rehacer) más
lo que todavía no está respaldado. 979 artefactos verificados en el box se
borraron de acá, y con ellos el caché .dmerge de 92 G que sostenía sus
bloques: 128 G → 8,2 G, 115 G libres.

Eso rompía tres cosas que leían el disco local como si fuera la verdad:

- build-state.py habría reportado `never` sobre ~950 sellados y el latido
  lo habría COMMITEADO. Un grafo recién escrito miente con más autoridad
  que uno viejo. Ahora resuelve la presencia contra la unión del store y
  los manifiestos, y expone `sealed_remoto` para que «el store se mudó»
  no se lea nunca como «el corpus creció».
- farm-sync.sh armaba el manifiesto con `ls ./store`, así que le habría
  dicho al worker «el hub tiene 220» y el worker habría rehecho ~950. Es
  el bucle de churn que el propio fichero documenta, al revés. Ahora es la
  unión, con un guardián que aborta si el manifiesto encoge.
- cosecha-cron.sh bajaba el store entero cada 30 min: habría deshecho la
  mudanza sola, como la poda de 24 G que se deshacía en 2026-08-07. Ahora
  baja sólo la lista de nombres.

Los cuatro grafos regenerados dan idéntico a antes del recorte
(766/11/2), que es la prueba de que no se perdió nada.

Queda abierto: los artefactos nuevos viven SÓLO en el volumen hasta que
alguien corra una pasada de respaldo. El volumen tiene borrado protegido,
pero es una copia.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:22:36 -04:00

100 lines
5.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# farm-sync.sh <user@host> — orquesta el worker VPS desde el HUB (laptop). Una sola pasada:
# 1. sube código + recipes/incoming/ al worker (rsync, el worker no toca gitea)
# 2. baja el store/ sellado que el worker construyó
# 3. promueve+firma lo cosechado (cache-hit instantáneo en los artefactos bajados) + commit/push
#
# El acceso es UNO solo: laptop->VPS por SSH (la clave de firma y gitea nunca salen del laptop).
# Idempotente y seguro de re-correr. Pensado para correr cada vez que el laptop está online.
#
# Uso: scripts/farm/farm-sync.sh root@1.2.3.4
# scripts/farm/farm-sync.sh root@1.2.3.4 --no-promote # sólo sincroniza, no firma
# Env: SSH_KEY (def ~/.ssh/github5), REMOTE (def /opt/hammer)
set -euo pipefail
VPS="${1:?uso: farm-sync.sh user@host [--no-promote]}"
SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}"
REMOTE="${REMOTE:-/opt/hammer}"
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"
SSH="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new"
# El MANIFIESTO de lo ya sellado en el hub, para que el worker no rehaga trabajo hecho. Es la otra
# mitad del arreglo del sync unidireccional (ver la nota en `scripts/build-farm.sh`): el store no
# viaja hub→worker porque son gigabytes, pero la lista de nombres son kilobytes y evita que el worker
# reintente y falle cada ciclo lo que el hub ya tiene. Se regenera acá, en cada sync, para que no se
# quede vieja sola.
#
# ── ⚠ EL MANIFIESTO NO ES `ls ./store` ─────────────────────────────────────────────────────────
# Lo fue hasta el 2026-08-09, y desde que el store se mudó al VOLUMEN eso pasó a ser una mentira
# cara: el disco local se quedó con 220 artefactos (los de fuente privada + lo aún no respaldado),
# así que `ls ./store` le habría dicho al worker «el hub tiene 220» y el worker habría reconstruido
# ~950 artefactos que ya existen. Es exactamente el bucle que este fichero documenta más abajo, pero
# al revés y multiplicado: no devolver basura, sino rehacer lo hecho.
#
# La pregunta que el worker hace es «¿esto ya está sellado?», no «¿esto está en el disco del hub?».
# Se responde con la UNIÓN de lo que hay en el disco y lo que registran los manifiestos — el mismo
# criterio que usa `build-state.py`, y por la misma razón: la presencia de los bytes y el hecho de
# estar construido dejaron de ser lo mismo el día que el store dejó de vivir acá.
mkdir -p work
{
ls ./store 2>/dev/null | grep -E '^[0-9a-f]{64}-' || true
# `respaldo-sellados.txt` (lo que hay en el Storage Box) y el propio manifiesto del ciclo anterior,
# que arrastra lo que el worker selló y todavía no llegó a ningún otro registro.
cat work/respaldo-sellados.txt work/farm-sellados.txt 2>/dev/null || true
} | grep -E '^[0-9a-f]{64}-' | sort -u > work/farm-sellados.txt.new
# Guardián: el manifiesto sólo puede CRECER salvo poda explícita. Si encoge, algo se perdió y es más
# barato abortar el sync que mandar al worker a rehacer un corpus.
VIEJO=$(wc -l < work/farm-sellados.txt 2>/dev/null || echo 0)
NUEVO=$(wc -l < work/farm-sellados.txt.new)
if [ "$NUEVO" -lt "$VIEJO" ]; then
echo "⚠ el manifiesto ENCOGIÓ ($VIEJO$NUEVO) — no sincronizo; revisá antes de que el worker rehaga trabajo" >&2
rm -f work/farm-sellados.txt.new; exit 1
fi
mv work/farm-sellados.txt.new work/farm-sellados.txt
echo "==> 0. manifiesto de sellados: $NUEVO artefactos (disco local: $(ls ./store 2>/dev/null | grep -cE '^[0-9a-f]{64}-'))"
echo "==> 1. subiendo código + cola al worker ($VPS:$REMOTE)"
rsync -az --delete -e "$SSH" \
--include '/work/' --include '/work/farm-sellados.txt' \
--exclude /work --exclude /store --exclude '/store-*' --exclude /target \
--exclude /dist --exclude /.dev-fs --exclude /.git --exclude /.scratch \
--exclude '*.png' --exclude '/content*' \
./ "$VPS:$REMOTE/"
echo "==> 2. bajando artefactos sellados del worker"
# ── ⚠ NO BAJAR LO QUE YA PODAMOS: era un BUCLE DE CHURN DE 24 G ────────────────────────────────
# Medido el 2026-08-07: `store-gc.sh` borró 362 artefactos superados (24 G) por la mañana y por la
# tarde volvió a borrar **los mismos 362** — solapamiento del 100%. No era casualidad: este rsync
# bajaba el store del worker ENTERO, y el worker nunca se poda, así que conservaba los superados y
# nos los devolvía en cada cosecha. Podar → cosechar → vuelven → podar. El disco pagaba 24 G por
# vuelta y la métrica de espacio libre mentía sobre el progreso.
#
# El registro `work/store-gc-superados.txt` es la unión de todos los manifiestos de poda: nombres
# `<hash>-<paquete>` de artefactos que YA decidimos que sobran. Como el store es CAS —los nombres
# son inmutables y un artefacto superado no vuelve a ser vigente salvo que una receta retroceda—
# excluirlos es seguro; y si una receta retrocediera, su artefacto se reconstruye, que es barato.
LEDGER="work/store-gc-superados.txt"
if [ -s "$LEDGER" ]; then
echo " (excluyendo $(wc -l < "$LEDGER") artefactos ya podados — ver la nota del bucle de churn)"
rsync -az -e "$SSH" --exclude-from="$LEDGER" "$VPS:$REMOTE/store/" ./store/
else
rsync -az -e "$SSH" "$VPS:$REMOTE/store/" ./store/
fi
if [ "${2:-}" = "--no-promote" ]; then
echo "✓ store sincronizado (sin promote)"; exit 0
fi
echo "==> 3. promote + firma (cache-hit en lo que el worker selló)"
scripts/build-farm.sh
echo "==> 4. commit + push de la cosecha"
git add recipes/ tandas/ 2>/dev/null || true
if ! git diff --cached --quiet; then
git commit -q -m "Etapa G: cosecha del worker VPS — promote + firma"
git push -q origin main && echo "✓ pusheado"
else
echo "(nada nuevo que commitear)"
fi