""" 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)