Rails Web要求から本当に長いタスクを実行する方法


最近、我々の管理は、大量に請求書を輸出する方法を必要としました.マネージャがWebフォームでバッチの最初と最後の請求書を選択した後、非同期プロセスは、請求書のPDFファイルを生成し、ZIPファイルにそれらをパックし、エクスポートをダウンロードするためのリンクをマネージャーにメールを送信開始する必要があります.現在、PDFを生成するのは遅いです.請求書を数百または数千を含む大きなバッチについては、このプロセスは簡単に10または15分またはそれ以上を取ることができます.
では、どのようにしてRailsリクエストからそのような長期的なプロセスを引き起こすのでしょうか?気になる最初のオプションは、キューバックエンドのいくつかによって実行されるバックグラウンドジョブですSidekiq , Resque or DelayedJob , 多分支配されるActiveJob . これが確かに働く間、これらの解決策の問題は通常、彼らがサーバーで利用できる限られた数の労働者を持っているということです、そして、我々はそれほど長い間他の重要なバックグラウンドタスクを潜在的にブロックしたくありませんでした.
代わりに我々はRails要求から新しい、別のプロセスを実行することでした.走るようなものRake task しかし、ウェブ要求によって引き起こされます.実際には、バルクのエクスポートはすでにrakeタスクとして実装されていたので、我々は実際に私たちの管理のWebインターフェイスからこのタスクをアクセスできるようにすることでした.

プロセスをフォークする


Unixライクなシステムで新しいプロセスを生成する標準的な方法はfork それ.Railsコントローラではfork Rakeタスクは次のようになります.
class BulkInvoiceExportsController < ApplicationController
  def create
    child = fork do
      exec("bin/rails export_invoices FROM=20220001 TO=20220100 \\
            >> /tmp/bulk_invoices_export.log 2>&1")
    end
    Process.detach(child)
  end
end
コードについていくつかのことに注意しましょうthis StackOverflow answer :
  • The Process#fork メソッドは現在のプロセス(現在のスレッド)を2つのコピーに分割し、新しい子プロセスがブロック内のコードを実行します.
  • 子プロセスは、それから Process#exec .
  • 最終的な子プロセスは、親プロセスからすべての重要な設定を継承します.これが我々が単に走ることができる理由ですbin/rails 最初に正しいRubyをセットアップする必要がない(例えばRubyバージョンマネージャを使用しても)rvm , rbenv or chruby ) を指定しない.
  • ブロック内のコードはシェルのリダイレクトを使用しているので、子プロセスは直接実行されませんが、通常は/bin/sh ). リダイレクションは、我々がRakeタスクで何が起こっているかをデバッグして、モニターするのを許します.
  • デフォルトでは、オペレーティングシステムは親プロセスが子プロセス終了ステータスに興味を持っていることを予想します.我々はそうではありません-我々はRakeタスクを実行して、それについて忘れてください、タスクはそれ自身で最終的な電子メールを送ることのような他のすべてを取り扱います.そういうわけで我々は電話します Process#detach 私たちは子供のプロセスを気にしないと蓄積を防ぐために知っているOSを聞かせてzombie processes .
  • プロセスの「産卵」


    もし私たちのコードをより移植性(Windows上で使用可能)にしたいなら、私たちは Process#spawn の代わりにfork , ASsuggested Rubyドキュメントで.The spawn また、子プロセス環境、ファイル記述子、制限、または作業ディレクトリを微調整できます.
    Rakeタスクをスケジューリングするほぼ同等の方法spawn このように書くことができます.
    class BulkInvoiceExportsController < ApplicationController
      def create
        child = spawn("bin/rails export_invoices FROM=20220001 TO=20220100",
                      %i[out err] => %w[/tmp/bulk_invoices_export.log a])
        Process.detach(child)
      end
    end
    

    セキュリティ警告


    コントローラからのそのような長期的なプロセスを引き起こすことは安全ではないことを心に留めておいてください.前の例では、create コントローラの動作は、CPUとメモリリソースのおそらくかなりの部分を消費し、おそらくあなたのデータベースサーバーへのより多くの接続を開く、1つの外部レールのプロセスを産卵につながる.これは非常に脆弱なセットアップですDoS attacks .
    テクニックはおそらく、彼らが何をしているかを知っている限られた数の人々にアクセス可能な内部管理領域のような非常に制御された環境だけでOKです、そして、機能が控え目に使用されるとき.このRAKEタスクを公開したい場合(例えば「データ取出し」機能のように)、上記のような実際の待ち行列システム、あるいはシステムレベルでの待ち行列デーモンに確実に頼ります. atd これは、サーバーの負荷に基づいてタスクを保持することができます).
    とにかく、我々のユースケースのために、コントローラからRAKEタスクを直接フォークすることは最も実用的な方法でした.
    あなたがこのように将来のポストを逃したくないならば、ここで、または、私に続いてください.乾杯!