feat: implementazione V1 - app.py, Dockerfile, docker-compose, .env.example, requirements
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user