| name | batch-cron-jobs |
| description | Patrón para fragmentar cron jobs de larga duración en unidades pequeñas con progreso JSON |
Batch Cron Jobs — Patrón de procesamiento fragmentado
Cuándo aplicar
Un cron job falla por timeout (horas corriendo, 9009, OOMKill) porque ejecuta un script de larga duración que procesa TODO de golpe.
Señales:
- Cron con
schedule: "once in 30m" ejecutando un script de scrapers/batch downloads
- Error "el script lleva ~7 horas corriendo", timeout, OOMKill
- Script que itera sobre una lista grande (países, años, ciudades, archivos)
Patrón: Wrapper grupo por ejecución
Paso 1: Crear wrapper en shell
#!/usr/bin/env bash
set -euo pipefail
DATA_DIR="/ruta/al/proyecto/Data"
SCRIPT_PRINCIPAL="/ruta/al/script.py"
PROGRESS_FILE="$DATA_DIR/progress.json"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
GRUPOS=()
for d in "$DATA_DIR"/*/; do
nombre=$(basename "$d")
[[ "$nombre" == *progress* ]] && continue
GRUPOS+=("$nombre")
done
CURRENT_INDEX=0
COMPLETADO=False
if [ -f "$PROGRESS_FILE" ]; then
CURRENT_INDEX=$(python3 -c "import json; d=json.load(open('$PROGRESS_FILE')); print(d.get('actual', 0))" 2>/dev/null || echo 0)
COMPLETADO=$(python3 -c "import json; d=json.load(open('$PROGRESS_FILE')); print(str(d.get('completado', False)).lower())" 2>/dev/null || echo "false")
fi
if [ "$COMPLETADO" = "true" ]; then
echo -e "${GREEN}✅ Completado. ${#GRUPOS[@]} grupos procesados.${NC}"
exit 0
fi
if [ "$CURRENT_INDEX" -ge "${#GRUPOS[@]}" ]; then
CURRENT_INDEX=0
fi
GRUPO_ACTUAL="${GRUPOS[$CURRENT_INDEX]}"
SIGUIENTE=$((CURRENT_INDEX + 1))
echo -e "${GREEN}▶️ Próximo grupo: $GRUPO_ACTUAL (${SIGUIENTE}/${#GRUPOS[@]})${NC}"
START=$(date +%s)
set +e
OUTPUT=$(python3 "$SCRIPT_PRINCIPAL" --filtro "$GRUPO_ACTUAL" 2>&1)
EC=$?
set -e
ELAPSED=$(( $(date +%s) - START ))
echo "$OUTPUT"
cat > "$PROGRESS_FILE" <<EOF
{
"grupos": [$(printf '"%s",' "${GRUPOS[@]}" | sed 's/,$//')],
"actual": $SIGUIENTE,
"completado": $([ "$SIGUIENTE" -ge "${#GRUPOS[@]}" ] && echo "true" || echo "false"),
"ultima_ejecucion": "$(date -u '+%Y-%m-%d %H:%M UTC')",
"grupo_procesado": "$GRUPO_ACTUAL",
"status": "$([ $EC -eq 0 ] && echo SUCCESS || echo ERROR)",
"duracion_seg": $ELAPSED
}
EOF
if [ $SIGUIENTE -lt "${#GRUPOS[@]}" ]; then
echo -e "🔄 Siguiente: ${GRUPOS[$SIGUIENTE]}"
else
echo -e "${GREEN}🎉 ¡Todos completados!${NC}"
fi
Paso 2: Cron configuration
job_id: batch-procesamiento
no_agent: true ← CRUCIAL: sin LLM, ejecuta directo
script: wrapper-por-grupo.sh
schedule: every 1h ← cada hora procesa 1 grupo
repeat: N ← número de grupos + margen (ej: 20 grupos → 25)
deliver: local ← guarda resultado en archivos
enabled_toolsets: [] ← sin toolsets, solo filesystem
Paso 3: Script principal con soporte de filtro
El script original necesita un flag de filtro (ej: --pais, --año, --grupo). Si no lo tiene, añadirlo. Ejemplo:
if "--pais" in sys.argv and i + 1 < len(sys.argv):
SOLO_PAIS = sys.argv[i + 1]
Estructura de archivos de progreso
{
"grupos": ["AT", "BE", "BG", "CH", "CZ", "DE"],
"actual": 3,
"completado": false,
"ultima_ejecucion": "2026-07-08 14:00 UTC",
"grupo_procesado": "BG",
"status": "SUCCESS",
"duracion_seg": 247
}
Pitfalls
- Sin
no_agent: true → el cron pasa por LLM cada ejecución → lento, caro, innecesario
- Repeat mayor que grupos → no pasa nada, al llegar a "completado: true" el wrapper sale 0
- Script sin soporte de filtro → añadir el parámetro de filtro al script original
- Progreso en
completed=true → el wrapper debe detectar esto y salir sin hacer nada
- Bash arrays con nombres con espacios → usar
IFS= read -r en loops
- 🔥 Script path resolution → el cron solo encuentra scripts en
~/.hermes/scripts/. Si el script está en /hermes-home/scripts/, crear symlink: ln -sf /hermes-home/scripts/foo.sh ~/.hermes/scripts/foo.sh
- 🔥 JSON booleans en bash →
True/False (Python) generan JSON inválido. Usar true/false (minúsculas). Ejemplo: echo "true" NO echo "True".
- 🔥
pipefail + find en directorio inexistente → Con set -euo pipefail, find en directorio que no existe da exit code 1. 2>/dev/null suprime el error pero NO el código de salida. Con pipefail se propaga por | wc -l y mata el script. Fix: find "$DIR" -name "*.pdf" 2>/dev/null | wc -l || true. Clásico en batch downloads donde el primer ítem aún no tiene carpeta.
Comparación: cron antiguo vs cron con wrapper
| Aspecto | Cron antiguo | Cron con wrapper |
|---|
| Ejecución | 1 → todo el lote | N → 1 grupo por ejecución |
| Duración | 7+ horas, timeout | 5-30 minutos |
| Reinicio tras crash | Pierde todo, empieza de 0 | Reanuda desde progreso |
| Coste LLM | Sí (si no es no_agent) | No (no_agent: true) |
| Visibilidad | Un fallo, no sabes dónde | Logs por grupo |