LWP::UserAgent リクエスト メソッドでの真のタイムアウト
質問
信頼できないサーバーへのリクエストを実装しようとしています。このリクエストはあると便利ですが、Perl スクリプトが正常に完了するために 100% 必要なわけではありません。問題は、サーバーが時々デッドロックし (その理由を解明しようとしています)、リクエストが決して成功しないことです。サーバーはライブであると考えるため、ソケット接続を開いたままにし、LWP::UserAgent のタイムアウト値は何の役にも立ちません。リクエストに絶対タイムアウトを強制する最善の方法は何ですか?
参考までに、これは DNS の問題ではありません。このデッドロックは、Postgres データベースに同時にアクセスする大量の更新に関係しています。テスト目的で、基本的にサーバー応答ハンドラーに while(1) {} 行を追加しました。
現在、コードは次のようになります。
my $ua = LWP::UserAgent->new;
ua->timeout(5); $ua->cookie_jar({});
my $req = HTTP::Request->new(POST => "http://$host:$port/auth/login");
$req->content_type('application/x-www-form-urlencoded');
$req->content("login[user]=$username&login[password]=$password");
# This line never returns
$res = $ua->request($req);
シグナルを使用してタイムアウトをトリガーしようとしましたが、うまくいかないようです。
eval {
local $SIG{ALRM} = sub { die "alarm\n" };
alarm(1);
$res = $ua->request($req);
alarm(0);
};
# This never runs
print "here\n";
私が使用する最終的な答えはオフラインで誰かによって提案されたものですが、ここで言及しておきます。何らかの理由で、SigAction は機能しますが、$SIG(ALRM) は機能しません。理由はまだわかりませんが、これは動作することがテストされています。以下に 2 つの動作バージョンを示します。
# Takes a LWP::UserAgent, and a HTTP::Request, returns a HTTP::Request
sub ua_request_with_timeout {
my $ua = $_[0];
my $req = $_[1];
# Get whatever timeout is set for LWP and use that to
# enforce a maximum timeout per request in case of server
# deadlock. (This has happened.)
use Sys::SigAction qw( timeout_call );
our $res = undef;
if( timeout_call( 5, sub {$res = $ua->request($req);}) ) {
return HTTP::Response->new( 408 ); #408 is the HTTP timeout
} else {
return $res;
}
}
sub ua_request_with_timeout2 {
print "ua_request_with_timeout\n";
my $ua = $_[0];
my $req = $_[1];
# Get whatever timeout is set for LWP and use that to
# enforce a maximum timeout per request in case of server
# deadlock. (This has happened.)
my $timeout_for_client = $ua->timeout() - 2;
our $socket_has_timedout = 0;
use POSIX;
sigaction SIGALRM, new POSIX::SigAction(
sub {
$socket_has_timedout = 1;
die "alarm timeout";
}
) or die "Error setting SIGALRM handler: $!\n";
my $res = undef;
eval {
alarm ($timeout_for_client);
$res = $ua->request($req);
alarm(0);
};
if ( $socket_has_timedout ) {
return HTTP::Response->new( 408 ); #408 is the HTTP timeout
} else {
return $res;
}
}
解決
試してみてもいいかもしれません LWPx::ParanoidAgent, 、LWP::UserAgent のサブクラスで、リモート Web サーバーとの対話方法についてより慎重です。
とりわけ、グローバル タイムアウトを指定できます。LiveJournal プロジェクトの一環として Brad Fitzpatrick によって開発されました。
他のヒント
次のように独自のタイムアウトを作成できます。
use LWP::UserAgent;
use IO::Pipe;
my $agent = new LWP::UserAgent;
my $finished = 0;
my $timeout = 5;
$SIG{CHLD} = sub { wait, $finished = 1 };
my $pipe = new IO::Pipe;
my $pid = fork;
if($pid == 0) {
$pipe->writer;
my $response = $agent->get("http://stackoverflow.com/");
$pipe->print($response->content);
exit;
}
$pipe->reader;
sleep($timeout);
if($finished) {
print "Finished!\n";
my $content = join('', $pipe->getlines);
}
else {
kill(9, $pid);
print "Timed out.\n";
}
私の理解では、タイムアウトプロパティはDNSタイムアウトを考慮していません。DNS ルックアップを個別に作成し、それが機能する場合は、ユーザー エージェントに正しいタイムアウト値を設定してサーバーにリクエストを送信することも可能です。
これはサーバーの DNS の問題ですか、それとも他の何かですか?
編集:IO::Socket に問題がある可能性もあります。IO::Socket モジュールを更新して、それが役立つかどうかを確認してください。LWP::UserAgent のタイムアウトの動作を妨げるバグがあったと確信しています。
アレックス
元の回答の 1 つを以下に一般化すると、アラーム信号ハンドラーが前のハンドラーに復元され、評価クロックの呼び出しで非アラーム例外がスローされ、アラームをキャンセルしたい場合に備えて、2 番目の呼び出しが Alarm(0) に追加されます。さらに $@ 検査と処理を追加できます。
sub ua_request_with_timeout {
my $ua = $_[0];
my $request = $_[1];
# Get whatever timeout is set for LWP and use that to
# enforce a maximum timeout per request in case of server
# deadlock. (This has happened.)`enter code here`
my $timeout_for_client_sec = $ua->timeout();
our $res_has_timedout = 0;
use POSIX ':signal_h';
my $newaction = POSIX::SigAction->new(
sub { $res_has_timedout = 1; die "web request timeout"; },# the handler code ref
POSIX::SigSet->new(SIGALRM),
# not using (perl 5.8.2 and later) 'safe' switch or sa_flags
);
my $oldaction = POSIX::SigAction->new();
if(!sigaction(SIGALRM, $newaction, $oldaction)) {
log('warn',"Error setting SIGALRM handler: $!");
return $ua->request($request);
}
my $response = undef;
eval {
alarm ($timeout_for_client_sec);
$response = $ua->request($request);
alarm(0);
};
alarm(0);# cancel alarm (if eval failed because of non alarm cause)
if(!sigaction(SIGALRM, $oldaction )) {
log('warn', "Error resetting SIGALRM handler: $!");
};
if ( $res_has_timedout ) {
log('warn', "Timeout($timeout_for_client_sec sec) while waiting for a response from cred central");
return HTTP::Response->new(408); #408 is the HTTP timeout
} else {
return $response;
}
}