Importazione fallita con `could not create unique index`

TL;DR: Abbiamo sbagliato un aggiornamento e cerchiamo aiuto


Con Home Assistant utilizziamo Discourse per gestire la nostra community. Lo gestiamo con il metodo discourse_docker su un’istanza EC2 di AWS.

Essendo un progetto open source, la manutenzione del forum è stata trascurata e ci siamo ritrovati con una versione obsoleta, aggiornata per l’ultima volta all’inizio del 2019.

Per peggiorare le cose, in un precedente aggiornamento avevamo bloccato Postgres alla versione 9.5 perché non disponevamo dello spazio su disco necessario per passare a Postgres 10. Non abbiamo mai risolto quel problema.

Inoltre, avevamo apportato una modifica al template di Cloudflare e l’avevamo committata nel repository; questo ha impedito al ramo docker_discourse di aggiornarsi all’ultima versione.

Ieri abbiamo deciso di procedere con l’aggiornamento…

Durante la migrazione del database, abbiamo riscontrato un problema: veniva utilizzata una sintassi non compatibile con la versione 9.5:

== 20200429095034 AddTopicThumbnailInformation: migrating =====================
-- execute("ALTER TABLE posts\nADD COLUMN IF NOT EXISTS image_upload_id bigint\n")

Abbiamo capito rapidamente il problema legato al blocco su 9.5, quindi abbiamo deciso di migrare a Postgres 10. Non ha funzionato e abbiamo ricevuto questo errore:

I, [2020-06-12T00:30:55.448351 #1]  INFO -- : Upgrading PostgreSQL from version 9.5 to 10
WARNING: Upgrading PostgresSQL would require an addtional 89M of disk space
Please free up some space, or expand your disk, before continuing.

Avevamo 47 GB disponibili, quindi era strano. Ci siamo poi resi conto che discourse_docker era obsoleto, quindi l’abbiamo aggiornato all’ultima versione. Sorpresa: Postgres 12 era appena stato rilasciato.

Dopo aver eseguito di nuovo rebuild, questa volta abbiamo ottenuto questo errore:

I, [2020-06-12T00:41:17.378129 #1]  INFO -- : Upgrading PostgreSQL from version 9.5 to 12
WARNING: Upgrading PostgresSQL would require an addtional 92G of disk space
Please free up some space, or expand your disk, before continuing.

È un po’ più spazio, ma va bene. Aumentiamo semplicemente lo spazio su disco a 300 GB e riproviamo.

Questa volta pg_upgrade si è interrotto durante la migrazione:

Restoring database schemas in the new cluster
  template1
  discourse

*failure* Consult the last few lines of "pg_upgrade_dump_16384.log" for the probable cause of the failure. Failure, exiting

Quando abbiamo esaminato il file pg_upgrade_dump_16384.log, abbiamo visto il seguente errore:

pg_restore: creating VIEW "postgres_exporter.pg_stat_activity"
pg_restore: [archiver (db)] Error while PROCESSING TOC:
pg_restore: [archiver (db)] Error from TOC entry 721; 1259 678554 VIEW pg_stat_activity postgres
pg_restore: [archiver (db)] could not execute query: ERROR:  column pg_stat_activity.waiting does not exist
LINE 27:     "pg_stat_activity"."waiting",
             ^
    Command was:
-- For binary upgrade, must preserve pg_type oid
SELECT pg_catalog.binary_upgrade_set_next_pg_type_oid('678556'::pg_catalog.oid);


-- For binary upgrade, must preserve pg_type array oid
SELECT pg_catalog.binary_upgrade_set_next_array_pg_type_oid('678555'::pg_catalog.oid);


-- For binary upgrade, must preserve pg_class oids
SELECT pg_catalog.binary_upgrade_set_next_heap_pg_class_oid('678554'::pg_catalog.oid);

CREATE VIEW "postgres_exporter"."pg_stat_activity" AS
 SELECT "pg_stat_activity"."datid",
    "pg_stat_activity"."datname",
    "pg_stat_activity"."pid",
    "pg_stat_activity"."usesysid",
    "pg_stat_activity"."usename",
    "pg_stat_activity"."application_name",
    "pg_stat_activity"."client_addr",
    "pg_stat_activity"."client_hostname",
    "pg_stat_activity"."client_port",
    "pg_stat_activity"."backend_start",
    "pg_stat_activity"."xact_start",
    "pg_stat_activity"."query_start",
    "pg_stat_activity"."state_change",
    "pg_stat_activity"."waiting",
    "pg_stat_activity"."state",
    "pg_stat_activity"."backend_xid",
    "pg_stat_activity"."backend_xmin",
    "pg_stat_activity"."query"
   FROM "pg_stat_activity";

Oh no.

A questo punto abbiamo deciso di fare un passo indietro. Potremmo semplicemente riportare i forum online e metterli in sola lettura mentre risolviamo la questione del backup? Siamo riusciti a farlo correggendo alcuni problemi di permessi sia per postgres che per redis, e i forum sono tornati online sulla versione vecchia. Non tutto funziona, ad esempio andare su admin → user → groups ci restituisce questo errore:

NoMethodError (undefined method `automatic_membership_retroactive' for #<Group:0x00007fcaca3045e8>)
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/activemodel-6.0.1/lib/active_model/attribute_methods.rb:431:in `method_missing'

Ma il resto sembra funzionare.

A questo punto abbiamo deciso che, dato che dovevamo correggere i permessi per tornare a un’istanza funzionante, era meglio avviare una nuova istanza e importare il nostro backup.

Quindi abbiamo avviato una nuova istanza EC2, seguito le istruzioni di avvio per discourse_docker e iniziato l’importazione. Poi ci siamo imbattuti in un problema strano: non è stato possibile creare un indice perché i dati non corrispondevano ai requisiti di unicità dell’indice:

ERROR:  could not create unique index "index_incoming_domains_on_name_and_https_and_port"
DETAIL:  Key (name, https, port)=(homeassistant.home, f, 8123) is duplicated.
EXCEPTION: psql failed: DETAIL:  Key (name, https, port)=(homeassistant.home, f, 8123) is duplicated.
/var/www/discourse/lib/backup_restore/database_restorer.rb:95:in `restore_dump'

Ma quando siamo entrati nella console rails della nostra istanza in esecuzione, non era un duplicato:

[7] pry(main)> IncomingDomain.where(name: "homeassistant.home")
=> [#<IncomingDomain:0x000055e5cabc3760 id: 8648, name: "homeassistant.home", https: false, port: 8123>]

Quindi è qui che ci troviamo ora. E siamo un po’ persi.

  • Abbiamo un’istanza funzionante con un DB errato rispetto al codice Ruby, che non riesce a migrare su versioni più recenti di Postgres
  • Abbiamo un backup che non può essere importato in una nuova istanza

Abbiamo esplorato la possibilità di passare a un Discourse ospitato a pagamento, ma dato che abbiamo 3 milioni di visualizzazioni di pagina e un milione di post, i prezzi enterprise sono un impegno troppo grande per noi.

Quindi dobbiamo trovare una soluzione. Preferibilmente potremmo importare il nostro backup, ma anche migrare la nostra vecchia istanza andrebbe bene.

Qualcuno ha idee? Non abbiamo problemi a pagare qualcuno per aiutarci.

Credo che la strada più semplice per te sia ottenere un backup funzionante e importarlo in una nuova istanza, come hai già provato in passato.

Proviamo a correggere i dati duplicati:

# accedi alla macchina via ssh
cd /var/discourse
./launcher enter app
su postgres
psql
\connect discourse
SELECT * FROM incoming_domains WHERE name LIKE '%homeassistant.home%';

# questo dovrebbe stampare più righe
# usa le istruzioni SQL DELETE per correggere il problema
# ed esci con \q

Puoi provare quanto sopra e chiedere ulteriore aiuto se ti blocchi?

Dovrei pulire anche IncomingLink e IncoingReferrer, dato che referrer punta a IncomingDomain e IncomingLink punta a IncomingReferrer?

Sto eseguendo la query ora e proverò a importare un altro backup. Esecutando la query direttamente tramite postgres ottengo risultati diversi rispetto all’uso di Rails. Ma immagino che ciò possa essere dovuto a uno scope di default?

Abbiamo tentato di importare un altro backup, ma è fallito a causa di un altro indice corrotto. Abbiamo reindicizzato tutti gli indici univoci sull’istanza originale e ora stiamo riscontrando questo problema con alcuni utenti.

Ti terremo aggiornato.

Bene, ce l’abbiamo fatta e siamo di nuovo online. Grazie per i suggerimenti @Falco.

Per aiutare altre persone con i loro problemi, ecco un riepilogo di ciò che abbiamo fatto.

Avevamo diversi indici corrotti che causavano il fallimento dell’importazione. Siamo riusciti a risolvere il problema eliminando manualmente i duplicati. Avevamo anche 8 utenti con un username_lower duplicato (troppi ‘mike’ e ‘marco’). Abbiamo rinominato questi utenti aggiornando sia username che username_lower. Dai dati degli utenti abbiamo notato che la prima corruzione è avvenuta nel dicembre 2019.

Invece di seguire il ciclo “crea backup” → “ripristina backup” → “errore su duplicato” → “correggi”, abbiamo deciso di reindicizzare tutti gli indici. Abbiamo trovato tutti gli indici con vincoli di unicità con la seguente query:

select idx.relname as index_name, 
       insp.nspname as index_schema,
       tbl.relname as table_name,
       tnsp.nspname as table_schema
from pg_index pgi
  join pg_class idx on idx.oid = pgi.indexrelid
  join pg_namespace insp on insp.oid = idx.relnamespace
  join pg_class tbl on tbl.oid = pgi.indrelid
  join pg_namespace tnsp on tnsp.oid = tbl.relnamespace
where pgi.indisunique --<< solo indici unici
  and tnsp.nspname = 'public'

Una volta che tutti gli indici funzionavano, siamo riusciti a creare un backup e importarlo correttamente nella nuova istanza. Le migrazioni sono state eseguite come previsto, abbiamo scambiato le istanze e siamo tornati operativi :+1: Un brindisi alla resilienza di Discourse :beers:

Grazie ancora @Falco.

Buon fine settimana :slight_smile:

Un ultimo consiglio per chi sta debuggando problemi di corruzione dei dati. Inizialmente, quando la nostra importazione è fallita a causa di dati duplicati, sono entrato nella console di Rails e ho cercato i dati che avevano causato il fallimento della creazione dell’indice.

Tuttavia, eseguendo query sui campi indicizzati, Postgres ha utilizzato l’indice danneggiato per generare i risultati! Quindi la mia query iniziale mostrava 1 risultato, e successivamente, dopo aver eliminato quella voce, ne mostrava 0.

Le query che eseguono la scansione completa della tabella vincono :slight_smile: