Importação falhando com `could not create unique index`

TL;DR: Cometi um erro em uma atualização e estamos buscando ajuda


Com o Home Assistant, usamos o Discourse para impulsionar nossa comunidade. Operamos com o método discourse_docker em uma instância EC2 na AWS.

Sendo um projeto de código aberto, a manutenção do fórum acabou ficando de lado e acabamos com uma versão antiga, atualizada pela última vez no início de 2019.

Para piorar, em uma atualização anterior, travamos o Postgres na versão 9.5 porque não tínhamos espaço em disco suficiente para atualizar para o Postgres 10. Nunca resolvemos esse problema.

Também fizemos uma vez uma alteração no modelo do Cloudflare e a commitamos no repositório, o que impediu que a branch discourse_docker se atualizasse para a versão mais recente.

Ontem decidimos prosseguir com a atualização…

Quando o banco de dados estava sendo migrado, encontramos um problema: foi usada uma sintaxe incompatível com a versão 9.5:

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

Percebemos rapidamente o problema de ter travado na versão 9.5. Então decidimos migrar para o Postgres 10. Isso não funcionou e recebemos o erro:

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.

Tínhamos 47 GB disponíveis, então isso era estranho. Percebemos então que o discourse_docker estava desatualizado e o atualizamos para a versão mais recente. Surpresa: o Postgres 12 acabara de chegar.

Após executar o rebuild novamente, desta vez recebemos este erro:

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.

Isso exige um pouco mais de espaço, mas tudo bem. Vamos apenas aumentar nosso espaço em disco para 300 GB e executar novamente.

Desta vez, o pg_upgrade falhou durante a migração:

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

Ao examinar o arquivo pg_upgrade_dump_16384.log, vimos o seguinte erro:

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

Ah, merda.

Foi então que decidimos dar alguns passos para trás. Poderíamos apenas colocar o fórum no ar novamente e colocá-lo em modo somente leitura enquanto resolvemos esse problema de backup? Conseguimos fazer isso corrigindo alguns problemas de permissão tanto para o postgres quanto para o redis, e o fórum voltou ao ar na versão antiga. Nem tudo funciona; por exemplo, ao acessar admin → usuário → grupos, recebemos este erro:

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'

Mas o resto parece estar funcionando.

Neste ponto, decidimos que, como tivemos que usar chown para voltar a uma instância funcional, deveríamos simplesmente iniciar uma nova instância e importar nosso backup.

Então iniciamos uma nova instância EC2, seguimos as instruções de início do discourse_docker e iniciamos nossa importação. Em seguida, encontramos um problema estranho: não foi possível criar um índice porque os dados não correspondiam aos requisitos de unicidade do índice:

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'

Mas quando entramos no console do Rails da nossa instância em execução, não era um duplicado:

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

É aí que estamos agora. E estamos meio perdidos.

  • Temos uma instância em execução com um banco de dados ruim em comparação ao código Ruby, que não consegue migrar para versões mais recentes do Postgres
  • Temos um backup que não pode ser importado em uma nova instância

Exploramos a possibilidade de migrar para um Discourse hospedado pago, mas como temos 3 milhões de visualizações de página e um milhão de postagens, o preço corporativo é um compromisso muito grande para nós.

Então precisamos encontrar uma saída. Preferencialmente, poderíamos importar nosso backup, mas migrar nossa instância antiga também funcionaria.

Alguém tem alguma ideia? Não nos importamos em pagar alguém para nos ajudar também.

Acredito que o caminho mais fácil para você é obter um backup funcional e importá-lo para uma nova instância, como você tentou anteriormente.

Vamos tentar corrigir os dados duplicados:

# ssh na máquina
cd /var/discourse
./launcher enter app
su postgres
psql
\connect discourse
SELECT * FROM incoming_domains WHERE name LIKE '%homeassistant.home%';

# isso deve imprimir várias linhas
# use instruções SQL DELETE para corrigir
# e saia com \q

Pode tentar o acima e pedir mais ajuda se ficar travado?

Precisaria limpar também o IncomingLink e o IncoingReferrer, já que o referrer aponta para o IncomingDomain e o IncomingLink aponta para o IncomingReferrer?

Estou executando a consulta agora e vou tentar importar outro backup. Consultar via postgres realmente me dá resultados diferentes de usar o Rails. Mas acho que isso pode ser por causa de um escopo padrão?

Tentamos importar outro backup, mas falhou devido a outro índice corrompido. Reindexamos todos os índices únicos na instância original e agora estamos enfrentando este problema com alguns usuários.

Mantemos você informado.

Tudo certo, conseguimos sair do outro lado e estamos de volta online. Obrigado pelas dicas @Falco.

Para ajudar outras pessoas com seus problemas, aqui está um resumo do que fizemos.

Tivemos alguns índices corrompidos que causaram falha na importação. Conseguimos corrigir manualmente excluindo os duplicados. Também tínhamos 8 usuários com username_lower duplicado (muitos “mike” e “marco”). Renomeamos esses usuários atualizando tanto username quanto username_lower. Pelos dados dos usuários, notamos que a primeira corrupção ocorreu em dezembro de 2019.

Em vez de seguir o ciclo “fazer backup” → “restaurar backup” → “falhar por duplicidade” → “corrigir”, decidimos reindexar todos os índices. Encontramos todos os índices com restrições de unicidade usando a seguinte consulta:

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 -- << apenas índices únicos
  and tnsp.nspname = 'public'

Assim que todos os índices funcionaram, conseguimos fazer um backup e importá-lo corretamente na nova instância. As migrações foram executadas conforme o esperado, trocamos as instâncias e tudo voltou a funcionar :+1: Um brinde à resiliência do Discourse :beers:

Obrigado novamente @Falco.

Tenham um bom fim de semana :slight_smile:

Uma última dica para quem está depurando problemas de corrupção de dados. Inicialmente, quando nossa importação falhou devido a dados duplicados, entrei no console do Rails e pesquisei pelos dados que causaram a falha na criação do índice.

No entanto, ao consultar usando os campos indexados, o Postgres usou o índice corrompido para gerar os resultados! Então, minha consulta inicial mostrou 1 resultado, e mais tarde, ao excluir essa entrada, mostrou 0 resultados.

Consultas que fazem varredura completa na tabela venceram :slight_smile: