Problemas com uploads após mudança do datacenter do DO Spaces

Olá a todos,

Depois de pesquisar o fórum com o máximo de minhas capacidades sem encontrar uma solução, estou solicitando suporte para uma situação estranha que surgiu após uma mudança recente do Datacenter da Digital Ocean.

Tínhamos todos os nossos uploads armazenados em um Bucket do Digital Ocean Spaces, no datacenter ams3.
Após dois grandes problemas de hardware e a consequente interrupção do serviço em pouco mais de um mês, no último fim de semana decidimos mover todos os nossos arquivos para o datacenter fra1.

Aqui estão as etapas que segui:

  1. Em preparação para a transferência, fiz o upload de todos os arquivos que tínhamos no ams3 (os 3 diretórios clássicos: originals, optimized e tombstone) para o novo bucket no fra1 usando o s3cmd.
  2. Acessei as configurações do fórum e configurei o novo endpoint para anexos, cdnl e bucket de backup.
  3. Iniciei um rebake completo de posts, esperando que corrigisse tudo de uma vez só.

Infelizmente, não foi isso que aconteceu. A maioria dos anexos foi “portada” corretamente, mas algumas centenas não foram. Não está claro para mim o que aconteceu, mas esses anexos faltantes foram movidos para o diretório tombstone.

Pensei que executar a tarefa rake rake uploads:recover_from_tombstone resolveria isso, mas não. Os arquivos são detectados, mas ao final da tarefa nenhum anexo é recuperado e as imagens ainda não estão visíveis nos posts.

Comecei a investigar um pouco mais e descobri que, ao executar UploadRecovery.new(dry_run: true).recover (encontrado pesquisando no meta) no console do Rails, eu recebia informações valiosas, como a URL do post, bem como a URL curta ou longa da imagem problemática.

Para as URLs retornadas no formato curto, escrevi um trecho de código em Python para “traduzir” o nome do arquivo de upload curto para o formato longo, para que eu pudesse verificar a presença do arquivo no bucket.
Fiz isso e posso confirmar que todos os arquivos faltantes estão lá, tanto no novo bucket quanto no antigo. Parte dos uploads faltantes encontrei no diretório tombstone, como esperado, mas alguns outros estranhamente ainda estão no diretório original. Os arquivos não estão corrompidos. Se eu acessá-los via URL, eles abrem corretamente em ambos os datacenters, e se eu fizer o dump deles localmente no meu Linux, consigo abri-los sem erros.

De alguma forma, o processo de recuperação de upload falha ao detectá-los e corrigir qualquer coisa que esteja desconfigurada no banco de dados. :man_shrugging:

Minhas perguntas são:

  • Existe alguma maneira de entender por que, mesmo que os arquivos de upload estejam em tombstone (ou em original), a tarefa rake falha ao recuperá-los?
  • Qual seria o conjunto correto de etapas para garantir que, em caso de mudança de bucket ou mesmo transição da DO para outro ambiente compatível com AWS, todos os anexos sejam movidos e preparados corretamente para a troca? De forma mais geral, o que se deve fazer, passo a passo, nesses casos? Claramente, um rebake simples não é suficiente. :confused:
  • O que a tarefa posts:invalidate_broken_images faz? Quero dizer, o que significa invalidar?

Obrigado antecipadamente. Estou lutando com isso há uma semana e realmente preciso resolver isso, senão vou enlouquecer :smiley: :stuck_out_tongue:
Observação: a sugestão de recarregar manualmente os 800+ anexos não é considerada uma resposta válida. Deve haver uma razão algorítmica… :laughing:

2 curtidas

Acho que você esqueceu de executar um DbHelper.remap('oldbucketurl', 'newbucketurl') entre as etapas 2 e 3.

4 curtidas

Oi @falco, obrigado pela sua resposta.
Sim, inicialmente eu esqueci.
Executei isso depois que encontrei, pesquisando aqui no meta. :confused: e isso ajudou a recuperar alguns dos arquivos.
Aliás, fiz uma rebake completa depois de executá-lo.

O que mais eu poderia tentar?

1 curtida

Então, talvez eu tenha uma ideia do que está acontecendo aqui.

Não havia pensado em mencionar um fato relacionado à saída da tarefa rake rake uploads:recover_from_tombstone, que pode apontar para alguma pista interessante.

Parece que a tarefa está realmente encontrando os arquivos de upload no tombstone, mas lança um aviso sobre algo (o nome completo do arquivo de upload) estar incorreto. Algo assim:

Aviso /t/i-miei-modellini-volanti/28272/212 tinha um 487b613752a0c338646fecc942512e5de9afeb3f incorreto, deveria ser c87c4f08d1a9aac3f43d19722cfd5a94f2544272 armazenando no campo personalizado 'rake uploads:fix_relative_upload_links' pode corrigir isso

Executando um comando find na minha cópia local dos diretórios de uploads, descobri que tenho um arquivo chamado 487b613752a0c338646fecc942512e5de9afeb3f.jpeg.

O link curto pertencente a este upload específico é upload://alcIv6jVlmjiEOEBh8fNDJyRms7.jpeg, e aplicando o algoritmo base62 que calcula o nome completo do arquivo correspondente a ele, descobre-se que o valor é 487b613752a0c338646fecc942512e5de9afeb3f, exatamente o nome do arquivo com o qual a tarefa rake recover_from_tombstone me avisa que está errado. :thinking:

Por que a ferramenta está afirmando que está errado e deveria ser c87c4f08d1a9aac3f43d19722cfd5a94f2544272 em vez disso?

Apenas para garantir, executei a tarefa rake uploads:fix_relative_upload_links várias vezes e, em seguida, executei novamente rake uploads:recover_from_tombstone, mas nada parece ter mudado.

Edição:
Ao pesquisar por 487b613752a0c338646fecc942512e5de9afeb3f em um backup do banco de dados que fiz antes de alterar o bucket, posso ver que o registro na tabela de uploads pertencente a esta imagem mostrava exatamente este nome de arquivo hexadecimal, então ainda mais não consigo entender por que a tarefa rake reclama disso.

image

Esta é um dos mal-entendidos mais antigos no Meta.
Você não precisa fazer uma nova renderização após um remapeamento bem direcionado.

2 curtidas

Pode ser que você esteja certo, mas o fato é que é difícil saber exatamente o que fazer e o que não fazer nesses casos sem um tutorial ou guia dos desenvolvedores.
Sempre se tem a sensação de que deveria ter feito algo mais ou em uma ordem diferente, como se estivesse descobrindo uma receita que funciona, extraindo-a de dezenas de posts escritos ao longo dos últimos 3 ou 4 anos. :stuck_out_tongue:
O rebaking parece ser a panaceia para muitas coisas e inofensivo para os posts existentes.

É uma maneira complicada de dizer que, dado quantas vezes as pessoas tropeçaram em problemas com o gerenciamento de uploads e coisas assim, um bom guia oficial da equipe seria uma referência importante. :wink:

1 curtida

Desculpe, preciso reemergir este tópico.

Na semana passada, passei algum tempo lendo o código das tarefas rake de uploads, tentando entender o que acontece nos bastidores das tarefas recover_from_tombstone e recover.
É algo difícil devido ao encapsulamento das classes, então diria que, em grande parte, falhei.

No entanto, o que entendi (por favor, @Falco, corrija-me se eu estiver errado) é que o nome do arquivo no disco de um upload é criado combinando seu sha1 e sua extensão original. Em seguida, ele é armazenado no disco/AWS em um diretório cujo caminho depende da primeira e, às vezes, da segunda letra do nome, dentro de 1X, 2X, 3X… (como esses são determinados, não entendo).
Por fim, o sha1 e o nome do arquivo são armazenados, entre outras coisas, nos registros da tabela uploads no PostgreSQL.

Voltando ao que aconteceu durante nossa mudança de datacenter do Digital Ocean, isso é o que ocorreu, segundo meu melhor entendimento:

  1. Copiamos todos os arquivos de ams3 para fra1.
  2. Não realizamos o DbHelper.remap('oldbucketurl', 'newbucketurl') conforme sugerido por @falco, mas não ficou claro para nós que deveríamos fazê-lo neste caso.
  3. Iniciamos um rebake global. Nesta etapa, milhares de imagens “quebraram” e muitas foram movidas para o tombstone. Não está completamente claro para mim o motivo.
  4. Percebi que algo estava errado, interrompi o rebake em andamento e descobri o comando remap pesquisando aqui no meta. Lançamos a tarefa DbHelper.remap('oldbucketurl', 'newbucketurl').
  5. Para recuperar as imagens que foram movidas para o tombstone na etapa 3, executamos um rake uploads:recover_from_tombstone, que recuperou algumas, mas deixou centenas de outras sem recuperação, e exibiu erros sobre o sha1 dos arquivos, como Warning /t/eclisse-parziale-di-sole-04-01-2011/14456/50 had an incorrect 3f5a1c136b97aebac4a188432c8e3ab7487f3bca should be ec88ee9eea18f3b8424bfef796345c68582911b5 storing in custom field 'rake uploads:fix_relative_upload_links' can fix this, como se o arquivo tivesse sido alterado de alguma forma e, portanto, o sha1 agora é diferente. A recuperação desses arquivos falha.

Nunca alteramos os arquivos ao movê-los entre os dois datacenters. Usando s3cmd, estávamos literalmente fazendo o dump deles localmente do bucket antigo e imediatamente reenviando-os para o novo.

Por que o sha1 calculado pelo Discourse seria diferente?

Seria possível forçar a tarefa recover a ignorar a discrepância de sha1 e simplesmente adaptar a importação no banco de dados ao que está lá ou renomear os arquivos existentes com o novo sha1 durante a recuperação?

Estou ignorando algo óbvio? Obrigado a todos pela ajuda.

Então, apenas para dar um fechamento a este tópico que possa ser útil para alguém, é assim que resolvemos a situação.

Essencialmente, como era impossível recuperar os anexos perdidos através das várias tarefas rake de recuperação de uploads, criei um script em Ruby (peço desculpas antecipadas, definitivamente NÃO sou desenvolvedor Ruby ou Rails, então aposto que o código é ineficiente e feio, mas isso é irrelevante :P) que:

  1. Encontra todos os posts contendo a string upload://
  2. Extrai o shortlink de cada upload e o transforma em seu hash sha1 de formato longo
  3. Consulta a tabela Uploads
  4. Se um anexo com o hash sha1 for encontrado em Uploads, esse upload é ignorado; caso contrário, a URL desse upload é verificada no antigo bucket/spaces do Digital Ocean.
  5. Se o link for encontrado no antigo bucket/spaces, o shortlink é substituído pela URL para o mesmo upload no bucket antigo.
  6. Se houver modificação, dispara um rebake do post original, permitindo que o Discourse faça o trabalho pesado de baixar novamente localmente o upload “perdido” e recriar tudo o que é necessário no banco de dados.

Para evitar bloqueio por blacklist e reduzir a carga no servidor, um intervalo de 20 segundos é introduzido sempre que um rebake é solicitado.


def remoteFileExist(url, retries=3)
    puts "Requesting #{url} ..."
    uri = URI(url)
    response = nil
    res = Net::HTTP.get_response(uri)
    puts res['content-type']
    if res.code[0,1] == "2" and res['content-type'].include? 'image'
        return true
    else
        return false
    end
    rescue Net::ReadTimeout => e
        puts "TRY #{retries}/n ERROR: timed out while trying to connect #{e}"
        if retries <= 1
            raise
        end
        remoteFileExist(url, retries - 1)
    end
end


####################################################################


posts=Post.where("raw like '%upload://%' " ).order('topic_id ASC, post_number DESC');
idx = 0;
posts.each do |p|
    idx = idx + 1;
    puts ""

    matches = p.raw.scan(/(!\[(.)*\]\(upload:\/\/([a-zA-Z0-9]+)\.(jpeg|jpg|png|gif|pdf|mp3|mp4|mov)\))/)

    new_raw = p.raw

    matches.each do |m|  
        short_url = m[0];
        short_sha = m[2];
        ext = m[3];
        long_sha = Base62.decode(short_sha).to_s(16).rjust(40,"0")

        upload = Upload.where('sha1 = ?', long_sha)

        puts "#{short_url} -> #{long_sha}\n"

        if upload.all.count == 0
            puts "#{long_sha} not found in DB. Recovering from ams3...\n"

            subdir1 = long_sha[0]
            subdir2 = long_sha[1]

            new_url1 = "https://discourse-data.ams3.digitaloceanspaces.com/original/3X/#{subdir1}/#{subdir2}/#{long_sha}.#{ext}"
            test1 = remoteFileExist(new_url1)
            if test1
                new_raw = new_raw.gsub(short_url, "\n#{new_url1}")
            else
                new_url2 = "https://discourse-data.ams3.digitaloceanspaces.com/original/2X/#{subdir1}/#{long_sha}.#{ext}"
                if remoteFileExist(new_url2)
                    new_raw = new_raw.gsub(short_url, "\n#{new_url2}")
                end
            end
            puts ""
            sleep 5
        end
    end

    if p.raw != new_raw
        puts "OLD\n"
        puts p.raw
        puts "-----------"
        puts "NEW\n"
        puts new_raw
        puts "-----------"
        puts "UPDATING!"
        # goahead = gets
        p.raw = new_raw
        p.cooked = ''
        p.save
        p.rebake!(invalidate_broken_images: true);
        puts "*******************************************"
        sleep 30
    else
        puts "SKIP!"
        puts "*******************************************"
        sleep 1
    end
end

1 curtida