PostgreSQL: database is not accepting commands to avoid wraparound data loss

ERROR:  database is not accepting commands to avoid wraparound data loss in database "appdb"
HINT:  Stop the postmaster and vacuum that database in single-user mode.

Questo è il freno d'emergenza dell'MVCC di PostgreSQL — e se lo stai leggendo durante l'emergenza: la soluzione è rimuovere qualunque cosa stia bloccando il vacuum e poi lasciare che VACUUM faccia il freeze delle tabelle più vecchie. Nota che il classico HINT sul single-user mode è ampiamente considerato un consiglio obsoleto sulle versioni moderne; un normale VACUUM di solito fa il lavoro meglio e in modo più sicuro.

Cosa significa questo errore

I transaction ID (XID) sono valori a 32 bit assegnati da un contatore che gira in cerchio. Perché la visibilità delle righe resti corretta, nessuna riga viva può essere più vecchia di circa 2 miliardi di transazioni — quindi VACUUM congela continuamente le righe vecchie, marcandole come visibili-per-sempre e recuperando la loro età. Se il freezing resta abbastanza indietro, PostgreSQL prima avvisa (database "appdb" must be vacuumed within N transactions), poi smette del tutto di accettare comandi che modificano dati, un margine di sicurezza prima che il wraparound vero corromperebbe la visibilità.

L'intuizione critica: quasi mai è "il vacuum era troppo lento" da solo — qualcosa stava trattenendo l'orizzonte del freeze. Trovalo prima; un VACUUM in gara contro un orizzonte immobile non ottiene nulla.

Cause comuni

Come diagnosticarlo

-- Quanto è vecchio il database / le tabelle peggiori:
SELECT datname, age(datfrozenxid) FROM pg_database ORDER BY 2 DESC;
SELECT relname, age(relfrozenxid) FROM pg_class
WHERE relkind = 'r' ORDER BY 2 DESC LIMIT 20;

-- I tre soliti colpevoli dell'orizzonte bloccato:
SELECT pid, now() - xact_start AS age, state, query
FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_start LIMIT 5;

SELECT * FROM pg_prepared_xacts;

SELECT slot_name, active, xmin FROM pg_replication_slots;

Come risolverlo

  1. Rimuovi chi blocca l'orizzonte: termina le transazioni antiche, fai COMMIT PREPARED / ROLLBACK PREPARED delle prepared transaction orfane, elimina i replication slot morti.
  2. Fai il vacuum prima dei peggiori offender: esegui VACUUM (VERBOSE) sulle tabelle con l'età relfrozenxid più alta (o sull'intero database se il tempo lo consente). Con chi bloccava rimosso, le età scendono e il database riprende ad accettare scritture una volta tornato sotto la soglia di sicurezza.
  3. Dopo, rendilo strutturale: monitora age(datfrozenxid) con alert ben al di sotto del tetto dei 2 miliardi, tieni autovacuum attivo e dotato di risorse adeguate, e fai alert su prepared transaction e slot inattivi — chi trattiene l'orizzonte in silenzio.
🔍 Contesto essenziale: VACUUM, autovacuum e bloat — il meccanismo di freezing e come autovacuum si pianifica.