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
- Una transazione aperta molto vecchia che blocca l'orizzonte (una sessione
idle in transactionda giorni o settimane). - Prepared transaction orfane — create dal two-phase commit, dimenticate per sempre, invisibili a un'ispezione superficiale.
- Replication slot obsoleti (con feedback) che tengono l'orizzonte per conto di un replica che non esiste più.
- Autovacuum disabilitato o cronicamente affamato su tabelle enormi.
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
- Rimuovi chi blocca l'orizzonte: termina le transazioni antiche, fai
COMMIT PREPARED/ROLLBACK PREPAREDdelle prepared transaction orfane, elimina i replication slot morti. - Fai il vacuum prima dei peggiori offender: esegui
VACUUM (VERBOSE)sulle tabelle con l'etàrelfrozenxidpiù 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. - 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.
