(2007.11.28)
Webアプリケーションで時間の掛かる処理をしたいことがあります。例えば、ファイルのインポートを夜間バッチで行うのではなく、ユーザの指示で開始する場合など。
CGIでもそれ以外でも、単純にWebサーバから起動されて延々と時間を掛けていると、クライアント(Webブラウザ)とWebサーバがタイムアウトしてしまい、レスポンスを正しく返せません。
まずWebブラウザが「500 Internal Server Error」になります。CGIのプロセスはその後もしばらく動いていますが、ずっと終了しないと、強制的に終了させられます。
Apacheでは、タイムアウトの設定は、httpd.conf の Timeout でおこないます。CGIのプロセスが時間内に終了しないと、SIGTERMとSIGKILLを送って強制的に終了させるようです。
例えば、少しずつ出力 (とfflush) して、Webサーバに中断されないようにする方法もあります。
今回は毛色をかえ、プロセスをforkして、
- 親プロセスはすぐに戻る。処理が長引く旨を表示、JavaScriptを送り込む。
- 子プロセスで実際の処理をおこなう。
- クライアントからは JavaScript で定期的に状況確認する
方法を考えます。
UNIXのプロセス
実際に作ってみる前に、UNIXでのプロセスの取り扱いについて確認しておきましょう。
次のサイトを押さえておけば十分です;
プロセスはプロセスグループに属し、さらにプロセスグループはセッションに属します。
デーモンの作り方
上記サイトに詳しい解説がありますが、デーモンは、制御端末を持たず、独立したセッションで動くバックグラウンドプロセスです。
- fork() し、親プロセス(Webサーバに起動されたプロセス)はすぐに終了します。次のsetsid() の呼び出しを成功させるために必要です。
- 子プロセスで setsid() を呼び出します。新しいセッションが作られ、そこのセッションリーダ兼プロセスグループリーダになります。
- セッションリーダは後から制御端末を得ることができるため、もう一度 fork() し、親プロセスはすぐに終了します。
- chdir("/") します。プロセスが握っているディレクトリはunmountできないので、邪魔にならないようにします。
- umask(0) するか、しないか。
- stdin, stdout, stderr を close() します。
やってみよう
(2007.12.12 更新)
実際に試してみると、Apacheではわざわざデーモンにならなくてもよく、単純にfork() すれば十分でした。mongrel_rails でも同じです。
Ruby on Railsアプリケーションで説明します。
まずコントローラクラスでインポートスクリプトを起動するスクリプトを呼び出します。そして、ログを表示する画面にリダイレクトします。
Ruby
- class ImportController < ApplicationController
- def do_import
- `/起動スクリプトへのパス/run_import.rb`
- redirect_to :action => 'import_log'
- end
- end
起動スクリプトでは、標準出力と標準エラーをファイルへ繋いだうえで、実際のインポートスクリプトを起動します。
Ruby
-
-
- SCRIPT_DIR = File.dirname __FILE__
- Process.fork {
- outfname = SCRIPT_DIR + '/import_out.log'
- errfname = SCRIPT_DIR + '/import_err.log'
-
- File.unlink(outfname) if FileTest.exist?(outfname)
- STDOUT.reopen outfname
- File.unlink(errfname) if FileTest.exist?(errfname)
- STDERR.reopen errfname
- exec SCRIPT_DIR + '/import.rb'
- }
インポートスクリプトでは、必要に応じて、状況を標準出力へ出力します。また、適宜バッファをフラッシュします。
ログを表示するページでは、例えば、次のようなJavaScriptを書いて、適時リロードするようにします。
JavaScript
- <script type="text/javascript">
- function reload_page() {
- window.location.reload();
- }
- setTimeout(reload_page, 5000);
- </script>