190 lines
5.4 KiB
Python
190 lines
5.4 KiB
Python
"""
|
|
supabase-pinger
|
|
---------------
|
|
Servizio long-running che esegue periodicamente una query di keep-alive
|
|
verso un database Supabase/PostgreSQL per evitarne la sospensione automatica
|
|
nel free tier.
|
|
|
|
Configurazione: file .env (vedi .env.example)
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import signal
|
|
import sys
|
|
import time
|
|
|
|
import psycopg2
|
|
from dotenv import load_dotenv
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Logging
|
|
# ---------------------------------------------------------------------------
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s %(levelname)-8s %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
stream=sys.stdout,
|
|
)
|
|
log = logging.getLogger("supabase-pinger")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shutdown ordinato
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_shutdown = False
|
|
|
|
|
|
def _handle_signal(signum, _frame):
|
|
global _shutdown
|
|
log.info("Segnale di arresto ricevuto (%s). Chiusura in corso...", signum)
|
|
_shutdown = True
|
|
|
|
|
|
signal.signal(signal.SIGTERM, _handle_signal)
|
|
signal.signal(signal.SIGINT, _handle_signal)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configurazione
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_REQUIRED_VARS = [
|
|
"SUPABASE_DB_HOST",
|
|
"SUPABASE_DB_PORT",
|
|
"SUPABASE_DB_NAME",
|
|
"SUPABASE_DB_USER",
|
|
"SUPABASE_DB_PASSWORD",
|
|
]
|
|
|
|
|
|
def load_config() -> dict:
|
|
"""Carica e valida la configurazione dal file .env o dall'ambiente."""
|
|
load_dotenv()
|
|
|
|
missing = [v for v in _REQUIRED_VARS if not os.environ.get(v)]
|
|
if missing:
|
|
log.error(
|
|
"Variabili di ambiente obbligatorie mancanti: %s", ", ".join(missing)
|
|
)
|
|
sys.exit(1)
|
|
|
|
try:
|
|
port = int(os.environ["SUPABASE_DB_PORT"])
|
|
except ValueError:
|
|
log.error("SUPABASE_DB_PORT deve essere un numero intero.")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
interval = int(os.environ.get("PING_INTERVAL_MINUTES", "4320"))
|
|
if interval <= 0:
|
|
raise ValueError
|
|
except ValueError:
|
|
log.error("PING_INTERVAL_MINUTES deve essere un numero intero positivo.")
|
|
sys.exit(1)
|
|
|
|
return {
|
|
"host": os.environ["SUPABASE_DB_HOST"],
|
|
"port": port,
|
|
"dbname": os.environ["SUPABASE_DB_NAME"],
|
|
"user": os.environ["SUPABASE_DB_USER"],
|
|
"password": os.environ["SUPABASE_DB_PASSWORD"],
|
|
"query": os.environ.get("PING_QUERY", "SELECT 1;"),
|
|
"interval_minutes": interval,
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Keep-alive
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def ping(cfg: dict) -> bool:
|
|
"""
|
|
Apre una connessione, esegue la query di keep-alive e chiude tutto.
|
|
Restituisce True in caso di successo, False in caso di errore.
|
|
Non espone mai credenziali nei log.
|
|
"""
|
|
conn = None
|
|
try:
|
|
log.info(
|
|
"Connessione a %s:%d/%s...",
|
|
cfg["host"],
|
|
cfg["port"],
|
|
cfg["dbname"],
|
|
)
|
|
conn = psycopg2.connect(
|
|
host=cfg["host"],
|
|
port=cfg["port"],
|
|
dbname=cfg["dbname"],
|
|
user=cfg["user"],
|
|
password=cfg["password"],
|
|
connect_timeout=10,
|
|
)
|
|
with conn.cursor() as cur:
|
|
log.info("Esecuzione query di keep-alive: %s", cfg["query"])
|
|
cur.execute(cfg["query"])
|
|
log.info("Keep-alive eseguito con successo.")
|
|
return True
|
|
except psycopg2.OperationalError as exc:
|
|
log.error("Errore di connessione al database: %s", exc)
|
|
return False
|
|
except psycopg2.Error as exc:
|
|
log.error("Errore durante l'esecuzione della query: %s", exc)
|
|
return False
|
|
finally:
|
|
if conn is not None:
|
|
try:
|
|
conn.close()
|
|
except Exception:
|
|
pass
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Loop principale
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def run(cfg: dict) -> None:
|
|
"""Loop principale del servizio."""
|
|
interval_seconds = cfg["interval_minutes"] * 60
|
|
|
|
log.info("=" * 60)
|
|
log.info("supabase-pinger avviato")
|
|
log.info("Host : %s:%d/%s", cfg["host"], cfg["port"], cfg["dbname"])
|
|
log.info("Intervallo : %d minuti", cfg["interval_minutes"])
|
|
log.info("Query : %s", cfg["query"])
|
|
log.info("=" * 60)
|
|
|
|
while not _shutdown:
|
|
success = ping(cfg)
|
|
|
|
if not success:
|
|
log.warning(
|
|
"Keep-alive fallito. Nuovo tentativo al prossimo ciclo."
|
|
)
|
|
|
|
if _shutdown:
|
|
break
|
|
|
|
next_run = time.strftime(
|
|
"%Y-%m-%d %H:%M:%S",
|
|
time.localtime(time.time() + interval_seconds),
|
|
)
|
|
log.info("Prossima esecuzione: %s. In attesa...", next_run)
|
|
|
|
# Sleep a blocchi per rispondere rapidamente a SIGTERM/SIGINT
|
|
elapsed = 0
|
|
while elapsed < interval_seconds and not _shutdown:
|
|
time.sleep(min(10, interval_seconds - elapsed))
|
|
elapsed += 10
|
|
|
|
log.info("supabase-pinger arrestato.")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entrypoint
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if __name__ == "__main__":
|
|
config = load_config()
|
|
run(config)
|