PyForge è in vendita solo qui, sul sito ufficiale py-forge.com — diffida delle imitazioni.

Tutti gli articoli
automationcrondevopsmicrosaas

Eliminare i Fallimenti Silenziosi dei Cron con un Watchdog Heartbeat Self-Hosted

PyForge Team 6 min di lettura2026-10-16

I job cron falliscono in silenzio. Un exit code 0 su uno script wrapper non garantisce che la pipeline ETL abbia realmente elaborato i dati: spesso significa che un sottoprocesso è terminato in modo anomalo, è andato in timeout su una socket non gestita o è stato terminato dal kernel OOM. Affidarsi alle e-mail locali di sistema o al syslog per task critici è un rischio operativo imprevedibile.

Il Pattern 'Dead Man's Switch'

Invece di spendere $80/mese per servizi SaaS di heartbeat monitoring, è possibile garantire visibilità totale sui task tramite un pattern watchdog invertito. Il job notifica l'avvio, invia ping periodici durante l'esecuzione e segnala lo stato finale. Se il server watchdog salta un intervallo previsto, scattano immediatamente gli alert verso Slack o Discord.

Implementazione del Wrapper di Esecuzione

Di seguito è riportato un wrapper Python leggero che racchiude qualsiasi comando CLI, raccoglie metriche di runtime e gestisce l'invio dell'heartbeat prima di terminare con lo stesso exit code del processo gestito.

python
import sys
import time
import subprocess
import urllib.request
import json

def pulse(endpoint: str, payload: dict):
    try:
        req = urllib.request.Request(
            endpoint,
            data=json.dumps(payload).encode('utf-8'),
            headers={'Content-Type': 'application/json'},
            method='POST'
        )
        urllib.request.urlopen(req, timeout=5)
    except Exception as e:
        sys.stderr.write(f"[CronPulse Warning] Impossibile inviare il ping al watchdog: {e}\n")

def run_task(watchdog_url: str, job_id: str, command: list):
    pulse(f"{watchdog_url}/api/v1/ping/{job_id}/start", {"status": "running"})
    start_time = time.time()
    
    result = subprocess.run(command, capture_output=True, text=True)
    duration = time.time() - start_time
    
    payload = {
        "exit_code": result.returncode,
        "duration_seconds": round(duration, 2),
        "stdout": result.stdout[-500:],
        "stderr": result.stderr[-500:]
    }
    
    status_endpoint = "complete" if result.returncode == 0 else "fail"
    pulse(f"{watchdog_url}/api/v1/ping/{job_id}/{status_endpoint}", payload)
    sys.exit(result.returncode)

if __name__ == "__main__":
    # Uso: python wrapper.py http://watchdog:8080 daily-backup -- tar -czf backup.tar.gz /data
    run_task(sys.argv[1], sys.argv[2], sys.argv[4:])

Questo wrapper utilizza unicamente la libreria standard di Python senza dipendenze esterne, rendendolo perfetto per container leggeri, ambienti systemd minimali o server legacy.

Integrazione Systemd e Deployment

bash
# /etc/systemd/system/cronpulse-watchdog.service
[Unit]
Description=CronPulse Heartbeat Watchdog Daemon
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/cronpulse-server --config=/etc/cronpulse/config.yaml
Restart=always
RestartSec=5
User=cronpulse

[Install]
WantedBy=multi-user.target