Mi viaje en un trabajo masivo de reactivar publicaciones

Estoy continuando esta conversación desde ‘Reconstruir HTML para todo el tema’, ya que mis experimentos están tomando un rumbo bastante diferente y pensé que podría ser valioso compartir mis reflexiones y resultados a medida que avanzo.

Mi situación es la siguiente: estamos a punto de lanzar un nuevo foro migrado con más de 4 millones de publicaciones. Estas requerirán una recocción (rebake) cuando cambiemos al dominio final, y las publicaciones necesitan ser procesadas para asegurar que las imágenes se incrusten correctamente, etc.

Mis preocupaciones son:

  • La recocción no es un proceso rápido. He ajustado nuestro servidor de 16 GB/6 núcleos, pero no logro ir más rápido que 2-3 publicaciones/segundo, lo que significa que toda la recocción tomará bien más de 20 días.
  • La recocción comienza con las publicaciones más antiguas; preferiría empezar con las más recientes para ofrecer a nuestra comunidad la mejor experiencia posible (asumiendo que las publicaciones más nuevas recibirán más tráfico).
  • No hay forma de ‘reanudar’ el proceso donde se dejó, y tengo razones para sospechar que necesitaré reconstruir al menos una vez durante los próximos 20 días.
  • Los trabajos de recocción van a la cola predeterminada de Sidekiq y me preocupa que esto genere enormes retrasos para los trabajos de procesamiento habituales.

Hasta ahora, he hecho lo siguiente: después de investigar en el código y recibir asistencia del personal aquí, he modificado lib/tasks/posts.rake para:

  • Trabajar en orden cronológico inverso, comenzando con las publicaciones más recientes.
  • Ignorar los mensajes privados: quiero priorizar los temas públicos primero.
  • Mostrar el ID actual de la publicación/tema para poder agregar fácilmente a la cláusula where de mi consulta y reanudar el procesamiento en otro número de publicación.

Aquí está mi código:

def rebake_posts(opts = {})
  puts "NEW Recociendo el markdown de la publicación para '#{RailsMultisite::ConnectionManagement.current_db}'"

  disable_edit_notifications = SiteSetting.disable_edit_notifications
  SiteSetting.disable_edit_notifications = true

  total = Post.count
  rebaked = 0

    ordered_post_ids = Post.joins(:topic)
      .select('posts.id')
      .where('topics.archetype' => Archetype.default)
      .order("posts.id DESC")
      .pluck(:id)

    ordered_post_ids.in_groups_of(1000).each do |post_ids|
    posts = Post.order(created_at: :desc).where(id:post_ids)
    posts.each do |post|
      rebake_post(post, opts)
      print_status(rebaked += 1, total)
      puts " > recociendo la publicación id #{post.id} para el tema id #{post.topic_id}"
    end
  end

  SiteSetting.disable_edit_notifications = disable_edit_notifications

  puts "", "#{rebaked} publicaciones realizadas!", "-" * 50
end

A continuación: estoy averiguando cómo crear estos trabajos en la cola de baja prioridad. Cualquier pista sería muy bienvenida :slight_smile:

11 Me gusta

Now I’ve started my first large test, I noticed that the jobs processing has made several huge ‘steps’ in speed. I suspect this may have to do with a large number of my attached images having been moved to the tombstone - this is another ongoing project.

1 me gusta

This sounds like an improvement. Perhaps submit a PR.

And it may make sense to do something such that you don’t have to rebske and un-tombstone.

The recover_from_tombstone script is a bit problematic - I’ve discovered several issues with it. I’ll report on those later.

3 Me gusta

Yes this is very dumb, however it appears Rails / ActiveRecord has no concept of descending ID order when iterating through records, apparently.

Yes I learned that too :slight_smile: With the help of your team I figured out how to work around it though. I’m not sure this is a smart or even fast way of doing it, but it works for me.

1 me gusta

Next issue: our new site will already go live while the posts:rebake job is running. Will having a large number of jobs in the default queue slow down regular site processes, and should I try to have posts:rebake start its jobs in the low priority queue instead? Or is this automatically handled?

So far, it seems that the queue that a job will be created in is a property of the job’s class, I’m not sure I could influence this in some way from within the posts.rake script?

If not, I’ll throttle the creation of new jobs to make sure the queue isn’t filling up.

I think there’s also a ‘version’ column on the posts table that you can null out to cause gradual rebaking, too. I think it does 100 posts every time the job triggers.

4 Me gusta

Does that version rebake task go in newest posts first order @sam?

Yes it does, changed that a while back:

Limit is still 100 @riking but can be configured per:

3 Me gusta

So rather than running rake posts:rebake, one should instead do Posts.all.update_all('baked_version: null') and all posts will be rebaked in batches according to rebake_old_posts_count?

3 Me gusta

We should normalize the rake task to go in descending ID order as well @techapj. Unless this is super hard, many hours of work, or something?

1 me gusta

Agree, but it is a bit tricky cause we would need to carry a big list of ids in memory. I wonder if we should amend it so the rake task is resumable?

Have rake posts:rebake reset version and just work through old posts using calls to rebake_old

And add rake posts:rebake:resume that simply resumes an interrupted rebake.

Downside here is that posts:rebake would unconditionally cause posts to rebake at some point in time even if the task is interrupted, but this may not matter.

3 Me gusta

Is carrying a list of integer IDs in memory really that expensive?

1 me gusta

we can probably live with it to be honest … that retains the tasks working exactly as they do today (in reverse order). Though something in me wants these tasks to be resumable cause if you are working through 20 million posts this can take many hours and if it breaks half way through it can be very frustrating to start from scratch.

5 Me gusta

Maybe V1 can be the simple version with a comment

// TODO: make this resumable because carrying around 20 million ids in memory is not a great idea long term

6 Me gusta

Done via:

4 Me gusta

I’ve used a script that was resumable at the topic level by using the custom fields. Here’s one that skips private messages (since my import had a LOT of them and they weren’t a priority):

Topic.includes(:_custom_fields).where(archetype: Archetype.default).find_each do |t|
  unless t.custom_fields["import_rebake"].present?
    t.posts.select(:id).find_each do |post|
      Jobs.enqueue(:process_post, {post_id: post.id, bypass_bump: true, cook: true})
    end
    t.custom_fields["import_rebake"] = Time.zone.now
    t.save
  end
end

(This filled up Sidekiq’s default queue, so it’s not useful if you want to launch your site before the rebakes are completed.)

After they’re all done, all the TopicCustomField records with name “import_rebake” can be deleted.

6 Me gusta

Yes, and @bartv would be able to get his “rebuild for just one topic” by doing:

Posts.where(topic_id: 1234).update_all('baked_version = NULL')
4 Me gusta

What’s the frequency of these new batches, and how can you monitor the progress?

2 Me gusta