Files
supabase-pinger/app.py
T

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)