ブログ

読んで思い出す。忘れるために書く

件数の多い処理をグループ分けして開始時間を遅らせる(ActiveJob)

「趣味プロジェクトで実装を書いたよ」というお話

github.com

説明しないこと

状況

レコード全件(0 ~ 20,000 程度)が外部URL を持っていて、その中には HTTP status code 404 を返すものが混じっている(古くなったりして削除されている)

これを丁寧に全件舐めて、見つけ次第 削除したい

ただ、一度に全件のチェックをするとなると...

  • チェック処理を実行するサーバの、当該処理実行中に他の処理が重くなる
  • 実際にURL にアクセスしないとわからないので、当該ドメインに負荷を掛ける
    • 最悪、アクセスがブロックされる...?!

...などが考えられるので、もう少し、お行儀の良い方法で処理を実装したい

アイディア

  • 全レコードに対して適当な数...30件ごとでグループ分け
  • グループに対して、順々に「現時刻より30秒後, 60秒後, 90秒後...」とグループごとに実行時間を指定する

実装

# Search 404 response URL and dispatch deletion
class Dispatch404UrlDeletionJob < ApplicationJob
  queue_as :default

  DELAY_SECONDS = 30
  PROCESS_GROUP_CHUNK = 30

  def perform
    # split records into group by chunk size
    process_group = ReadLater.all.map(&:id).each_slice(0.step(ReadLater.count, PROCESS_GROUP_CHUNK).size).lazy
    # => [[2, 4, 8, ...], [128, 256, 512, ...], ...]

    delay_group = Array.new(process_group.size, nil).inject([]) { |ret, _| ret << ret.last.to_i + DELAY_SECONDS }.lazy
    # => [30, 60, 90, 120, ...]

    delay_group.zip(process_group).each do |delay, ids|
    # => [
    #   [30, [128, 256, 512, ...]],
    #   [60, [2, 4, 8, ...]],
    #   ...,
    # ]
      ids.each { |id| Delete404UrlJob.set(wait: delay.seconds).perform_later(id: id) }
    end
  end
end
  • 0.step(ReadLater.count, PROCESS_GROUP_CHUNK).size
    • ReadLater レコード数に対して PROCESS_GROUP_CHUNK で分割したときのサイズ
  • ReadLater.all.map(&:id).each_slice(...)
    • ReadLater のすべてのレコードを ID だけの配列にしつつ、適当な数に分割...グループ分け
  • Array.new(process_group.size, nil).inject(.... }
    • Array.new の最初の引数に数値を渡すと、その数だけ 2番目の引数で埋めた要素を持つ配列を生成する
    • inject で注入された配列の最後の要素を参照・計算しつつ その配列に要素を追加していく
  • delay_group.zip(process_group).each do...end
    • zip することで配列を合成して、 each do...end の中で使える変数名を |delay, ids| と宣言し易しくしている
  • ids.each { |id| Delete404UrlJob.set(wait: delay.seconds).perform_later(id: id) }
    • iddelay を利用して、実際の処理をする Delete404UrlJob ジョブに開始時間と実行に必要な値を渡している

(行数の割に結構な数の処理を詰め込んでしまった)

Heroku Scheduler

今回は Heroku 環境で定期実行させたい、ので適当な Rake タスクを新規に定義する

bundle  exec rails generate task dispatch_404_url_deletion
desc "Split all ReadLater records to some chunks and dispatch find 404 URL and delete"
task dispatch_404_url_deletion: :environment do
  DispatchUnshortenUrlJob.perform_now
end

ドキュメント に沿って、スケジュールを追加する(Rake タスクなのでたとえば bundle exec rails dispatch_404_url_deletion とする)

Links