Webアプリで時間の掛かる処理をおこなう

(2007.11.28)

Webアプリケーションで時間の掛かる処理をしたいことがあります。例えば、ファイルのインポートを夜間バッチで行うのではなく、ユーザの指示で開始する場合など。

CGIでもそれ以外でも、単純にWebサーバから起動されて延々と時間を掛けていると、クライアント(Webブラウザ)とWebサーバがタイムアウトしてしまい、レスポンスを正しく返せません。

まずWebブラウザが「500 Internal Server Error」になります。CGIのプロセスはその後もしばらく動いていますが、ずっと終了しないと、強制的に終了させられます。

Apacheでは、タイムアウトの設定は、httpd.conf の Timeout でおこないます。CGIのプロセスが時間内に終了しないと、SIGTERMとSIGKILLを送って強制的に終了させるようです。

例えば、少しずつ出力 (とfflush) して、Webサーバに中断されないようにする方法もあります。

今回は毛色をかえ、プロセスをforkして、

  1. 親プロセスはすぐに戻る。処理が長引く旨を表示、JavaScriptを送り込む。
  2. 子プロセスで実際の処理をおこなう。
  3. クライアントからは JavaScript で定期的に状況確認する
方法を考えます。

UNIXのプロセス

実際に作ってみる前に、UNIXでのプロセスの取り扱いについて確認しておきましょう。

次のサイトを押さえておけば十分です;

プロセスはプロセスグループに属し、さらにプロセスグループはセッションに属します。

デーモンの作り方

上記サイトに詳しい解説がありますが、デーモンは、制御端末を持たず、独立したセッションで動くバックグラウンドプロセスです。

  1. fork() し、親プロセス(Webサーバに起動されたプロセス)はすぐに終了します。次のsetsid() の呼び出しを成功させるために必要です。
  2. 子プロセスで setsid() を呼び出します。新しいセッションが作られ、そこのセッションリーダ兼プロセスグループリーダになります。
  3. セッションリーダは後から制御端末を得ることができるため、もう一度 fork() し、親プロセスはすぐに終了します。
  4. chdir("/") します。プロセスが握っているディレクトリはunmountできないので、邪魔にならないようにします。
  5. umask(0) するか、しないか。
  6. stdin, stdout, stderr を close() します。

やってみよう

(2007.12.12 更新)

実際に試してみると、Apacheではわざわざデーモンにならなくてもよく、単純にfork() すれば十分でした。mongrel_rails でも同じです。

Ruby on Railsアプリケーションで説明します。

まずコントローラクラスでインポートスクリプトを起動するスクリプトを呼び出します。そして、ログを表示する画面にリダイレクトします。

Ruby
[POPUP]
  1. class ImportController < ApplicationController
  2. def do_import
  3. `/起動スクリプトへのパス/run_import.rb`
  4. redirect_to :action => 'import_log'
  5. end
  6. end

起動スクリプトでは、標準出力と標準エラーをファイルへ繋いだうえで、実際のインポートスクリプトを起動します。

Ruby
[POPUP]
  1. #!/usr/bin/ruby
  2. SCRIPT_DIR = File.dirname __FILE__
  3. Process.fork {
  4. outfname = SCRIPT_DIR + '/import_out.log'
  5. errfname = SCRIPT_DIR + '/import_err.log'
  6. File.unlink(outfname) if FileTest.exist?(outfname)
  7. STDOUT.reopen outfname
  8. File.unlink(errfname) if FileTest.exist?(errfname)
  9. STDERR.reopen errfname
  10. exec SCRIPT_DIR + '/import.rb'
  11. }

インポートスクリプトでは、必要に応じて、状況を標準出力へ出力します。また、適宜バッファをフラッシュします。

ログを表示するページでは、例えば、次のようなJavaScriptを書いて、適時リロードするようにします。

JavaScript
[POPUP]
  1. <script type="text/javascript">
  2. function reload_page() {
  3. window.location.reload();
  4. }
  5. setTimeout(reload_page, 5000); // ミリ秒単位
  6. </script>