なんで今さら ZF1 やねんという話ですが、Zend_Http_Client_Adapter_Curl
の timeout
は接続のタイムアウトなので、Read/Write も含めたタイムアウトである request_timeout
が未指定だと接続後に相手が突然死すると無限に待ちます。
ネットワーク経路のステートフルなファイアウォールとかが RST なりなんなり返すならそうでもないと思いますけど、そういうのが中間に入っていなくてかつサーバが RST も返すことなく突然死したりあるいは経路上のネットワークがぶつ切りになったりしたときなどです。
curl のオプションとは次のように対応しています。
timeout
->CURLOPT_CONNECTTIMEOUT
request_timeout
->CURLOPT_TIMEOUT
とあるシステムでこれが原因で PHP のプロセスがいつまでも終わらずにいつまでも実行し続けてしまう問題が発生しました。しかたないので TERM シグナルで殺そうとしたところ・・死なない。
実はそのプロセスは簡易なデーモンになっていて TERM シグナルをハンドリングしていたんですね。
PHP の pcntl_signal
で指定するハンドラは本当のシグナルハンドラとして実行されるわけではないので curl の中で止まっているとシグナルを撃っても PHP のシグナルハンドラは呼ばれません。
PHP のシグナル周りは これ とか これ とか が詳しいです。
ただ、I/O 待ちでシグナルを受ければ curl も EINTR とかで中断されてエラーになって PHP まで戻ってきそうな気がしますが・・・
うーん?
https://github.com/curl/curl/blob/curl-7_64_0/lib/select.c#L211-L230
if(error && ERROR_NOT_EINTR(error)) break;
https://github.com/curl/curl/blob/curl-7_64_0/lib/select.c#L57
#define ERROR_NOT_EINTR(error) (Curl_ack_eintr || error != EINTR)
https://github.com/curl/curl/blob/curl-7_64_0/lib/easy.c#L266-L267
if(flags & CURL_GLOBAL_ACK_EINTR) Curl_ack_eintr = 1;
PHP からこのフラグを指定する術は無さそうです。ので、PHP で curl がネットワーク I/O 待ちになったとき、シグナルハンドラを仕込んでいるとそのシグナルでは死ななくなります。
下記のようなコードで確認できます。シグナルが撃たれてもリクエストが完了するまで待たされます。
<?php pcntl_async_signals(true); $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => 'http://httpbin.org/delay/10', ]); pcntl_signal(SIGTERM, function ($signo) { var_dump($signo); }); $mypid = getmypid(); putenv("mypid=$mypid"); system('(sleep 1 && kill -- "$mypid") </dev/null >/dev/null 2>&1 &'); curl_exec($curl);
なお、シグナルハンドラをコメントアウトすればシグナルで死ぬようになります。curl が EINTR をどう扱っているかにせよ PHP からシグナルハンドラを設定していないのならシグナルでプロセスが死ぬからです。
さいごに
curl あるいは curl を使用する HTTP クライアントライブラリを使うときは CURLOPT_CONNECTTIMEOUT
だけでなく CURLOPT_TIMEOUT
も指定しないと意図せず無限に待ち続けてしまうことがあるかもしれないので注意。
ただ、curl を使う PHP の HTTP クライアントライブラリは大抵は普通にタイムアウトを設定すれば CURLOPT_TIMEOUT
が設定されるようです。
CURLOPT_CONNECTTIMEOUT
を指定するのは connect_timeout
とか(Guzzle)、connecttimeout
とか(ZF2)、いかにもそれっぽい名前になっていることが多いようです。