web-dev-qa-db-ja.com

バックグラウンドプロセスでConnectionExceptionではなくRejectionExceptionをスローするGuzzle

複数のキューワーカーで実行されるジョブがあり、Guzzleを使用したHTTPリクエストがいくつか含まれています。ただし、このジョブ内のtry-catchブロックは、これらのジョブをバックグラウンドプロセスで実行しているときに_GuzzleHttp\Exception\RequestException_を取得しないようです。実行中のプロセスは_php artisan queue:work_であり、Laravelキューを監視し、ジョブを取得するキューシステムワーカーです。

代わりに、スローされる例外は、次のメッセージのある_GuzzleHttp\Promise\RejectionException_の1つです。

プロミスは次の理由で拒否されました:cURLエラー28:受信した0バイトで30001ミリ秒後に操作がタイムアウトしました( https://curl.haxx.se/libcurl/c/libcurl-errors.html を参照)

これは実際には偽装された_GuzzleHttp\Exception\ConnectException_です( https://github.com/guzzle/promises/blob/master/src/RejectionException.php#L22 を参照してください)。通常のPHPプロセスでのジョブは、URLへのアクセスによってトリガーされます。メッセージで意図したとおりConnectExceptionを取得します。

cURLエラー28:100ミリ秒後に操作がタイムアウトし、0バイトのうち0バイトが受信されました( https://curl.haxx.se/libcurl/c/libcurl-errors.html を参照)

このタイムアウトをトリガーするサンプルコード:

_try {
    $c = new \GuzzleHttp\Client([
        'timeout' => 0.1
    ]);
    $response = (string) $c->get('https://example.com')->getBody();
} catch(GuzzleHttp\Exception\RequestException $e) {
    // This occasionally gets catched when a ConnectException (child) is thrown,
    // but it doesnt happen with RejectionException because it is not a child
    // of RequestException.
}
_

上記のコードは、ワーカープロセスで実行されるとRejectionExceptionまたはConnectExceptionのいずれかをスローしますが、ブラウザーで手動でテストすると(私がわかる範囲から)、常にConnectExceptionをスローします。

つまり、基本的に私が導き出したのは、このRejectionExceptionConnectExceptionからのメッセージをラップしていることですが、Guzzleの非同期機能は使用していません。私のリクエストは単純に連続して行われます。異なるのは、複数のPHPプロセスがGuzzle HTTP呼び出しを行っているか、ジョブ自体がタイムアウトしていることです(これにより、Laravelの_Illuminate\Queue\MaxAttemptsExceededException_である別の例外が発生するはずです))。しかし、これによりコードがどのように動作するかはわかりません。

ブラウザーのトリガーではなく、CLIから実行すると、php_sapi_name()/_PHP_SAPI_(使用するインターフェイスを決定する)を使用して別のものを実行するコードがGuzzleパッケージ内に見つかりませんでした。

tl; dr

GuzzleがワーカープロセスでRejectionExceptionsをスローするのに、通常のConnectExceptionsでPHPスクリプトがブラウザーからトリガーされるのはなぜですか?

編集1

残念ながら、最小限の再現可能な例を作成することはできません。 Sentryの課題追跡に多くのエラーメッセージが表示されますが、上記とまったく同じです。ソースは_Starting Artisan command: horizon:work_(Laravel Horizo​​nであり、Laravelキューを監視します)として記述されています。私はもう一度確認して、 PHPバージョン間の不一致ですが、Webサイトとワーカープロセスの両方が同じPHP _7.3.14_を実行します。正しい:

_PHP 7.3.14-1+ubuntu18.04.1+deb.sury.org+1 (cli) (built: Jan 23 2020 13:59:16) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.14, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.3.14-1+ubuntu18.04.1+deb.sury.org+1, Copyright (c) 1999-2018, by Zend Technologies
_
  • CURLのバージョンは_cURL 7.58.0_です。
  • Guzzleのバージョンは_guzzlehttp/guzzle 6.5.2_です
  • Laravelのバージョンは_laravel/framework 6.12.0_です

編集2(スタックトレース)

_    GuzzleHttp\Promise\RejectionException: The promise was rejected with reason: cURL error 28: Operation timed out after 30000 milliseconds with 0 bytes received (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)
    #44 /vendor/guzzlehttp/promises/src/functions.php(112): GuzzleHttp\Promise\exception_for
    #43 /vendor/guzzlehttp/promises/src/Promise.php(75): GuzzleHttp\Promise\Promise::wait
    #42 /vendor/guzzlehttp/guzzle/src/Client.php(183): GuzzleHttp\Client::request
    #41 /app/Bumpers/Client.php(333): App\Bumpers\Client::callRequest
    #40 /app/Bumpers/Client.php(291): App\Bumpers\Client::callFunction
    #39 /app/Bumpers/Client.php(232): App\Bumpers\Client::bumpThread
    #38 /app/Models/Bumper.php(206): App\Models\Bumper::post
    #37 /app/Jobs/PostBumper.php(59): App\Jobs\PostBumper::handle
    #36 [internal](0): call_user_func_array
    #35 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(32): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}
    #34 /vendor/laravel/framework/src/Illuminate/Container/Util.php(36): Illuminate\Container\Util::unwrapIfClosure
    #33 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(90): Illuminate\Container\BoundMethod::callBoundMethod
    #32 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(34): Illuminate\Container\BoundMethod::call
    #31 /vendor/laravel/framework/src/Illuminate/Container/Container.php(590): Illuminate\Container\Container::call
    #30 /vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(94): Illuminate\Bus\Dispatcher::Illuminate\Bus\{closure}
    #29 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(130): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
    #28 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(105): Illuminate\Pipeline\Pipeline::then
    #27 /vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(98): Illuminate\Bus\Dispatcher::dispatchNow
    #26 /vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(83): Illuminate\Queue\CallQueuedHandler::Illuminate\Queue\{closure}
    #25 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(130): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
    #24 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(105): Illuminate\Pipeline\Pipeline::then
    #23 /vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(85): Illuminate\Queue\CallQueuedHandler::dispatchThroughMiddleware
    #22 /vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(59): Illuminate\Queue\CallQueuedHandler::call
    #21 /vendor/laravel/framework/src/Illuminate/Queue/Jobs/Job.php(88): Illuminate\Queue\Jobs\Job::fire
    #20 /vendor/laravel/framework/src/Illuminate/Queue/Worker.php(354): Illuminate\Queue\Worker::process
    #19 /vendor/laravel/framework/src/Illuminate/Queue/Worker.php(300): Illuminate\Queue\Worker::runJob
    #18 /vendor/laravel/framework/src/Illuminate/Queue/Worker.php(134): Illuminate\Queue\Worker::daemon
    #17 /vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(112): Illuminate\Queue\Console\WorkCommand::runWorker
    #16 /vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(96): Illuminate\Queue\Console\WorkCommand::handle
    #15 /vendor/laravel/horizon/src/Console/WorkCommand.php(46): Laravel\Horizon\Console\WorkCommand::handle
    #14 [internal](0): call_user_func_array
    #13 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(32): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}
    #12 /vendor/laravel/framework/src/Illuminate/Container/Util.php(36): Illuminate\Container\Util::unwrapIfClosure
    #11 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(90): Illuminate\Container\BoundMethod::callBoundMethod
    #10 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(34): Illuminate\Container\BoundMethod::call
    #9 /vendor/laravel/framework/src/Illuminate/Container/Container.php(590): Illuminate\Container\Container::call
    #8 /vendor/laravel/framework/src/Illuminate/Console/Command.php(201): Illuminate\Console\Command::execute
    #7 /vendor/symfony/console/Command/Command.php(255): Symfony\Component\Console\Command\Command::run
    #6 /vendor/laravel/framework/src/Illuminate/Console/Command.php(188): Illuminate\Console\Command::run
    #5 /vendor/symfony/console/Application.php(1012): Symfony\Component\Console\Application::doRunCommand
    #4 /vendor/symfony/console/Application.php(272): Symfony\Component\Console\Application::doRun
    #3 /vendor/symfony/console/Application.php(148): Symfony\Component\Console\Application::run
    #2 /vendor/laravel/framework/src/Illuminate/Console/Application.php(93): Illuminate\Console\Application::run
    #1 /vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(131): Illuminate\Foundation\Console\Kernel::handle
    #0 /artisan(37): null
_

Client::callRequest()関数には、私が$client->request($request['method'], $request['url'], $request['options']);を呼び出すGuzzleクライアントが含まれているだけです(つまり、requestAsync()を使用していません)。この問題を引き起こすのは、ジョブの並列実行と関係があると思います。

編集3(ソリューションが見つかりました)

HTTPリクエストを発行する次のテストケースを検討してください(通常の200応答を返す必要があります)。

_        try {
            $c = new \GuzzleHttp\Client([
                'base_uri' => 'https://example.com'
            ]);
            $handler = $c->getConfig('handler');
            $handler->Push(\GuzzleHttp\Middleware::mapResponse(function(ResponseInterface $response) {
                // Create a fake connection exception:
                $e = new \GuzzleHttp\Exception\ConnectException('abc', new \GuzzleHttp\Psr7\Request('GET', 'https://example.com/2'));

                // These 2 lines both cascade as `ConnectException`:
                throw $e;
                return \GuzzleHttp\Promise\rejection_for($e);

                // This line cascades as a `RejectionException`:                
                return \GuzzleHttp\Promise\rejection_for($e->getMessage());
            }));
            $c->get('');
        } catch(\Exception $e) {
            var_dump($e);
        }
_

今私が最初にしたことは、メッセージ文字列に基づいて独自のRejectionExceptionを作成するrejection_for($e->getMessage())を呼び出すことでした。 rejection_for($e)を呼び出すことが、ここでの正しい解決策でした。答える必要があるのは、この_rejection_for_関数が単純な_throw $e_と同じかどうかだけです。

9
Flame

私の答えへのスターターとしてコメントセクション内の著者との議論:

質問:

カスタムguzzleミドルウェアが用意されていますか(ヒント:HandlerStack)?

著者の回答:

はい、さまざまです。しかし、ミドルウェアは基本的にリクエスト/レスポンス修飾子であり、そこで作成したguzzleリクエストでさえ同期的に行われます。


これによると、これが私の論文です:

ミドルウェアの内部でタイムアウトが発生し、それがガズルを呼び出します。それでは、再現可能なケースを実装してみましょう。

ここでは、guzzleを呼び出し、サブコールの例外メッセージで拒否の失敗を返すカスタムミドルウェアがあります。内部エラー処理のためにスタックトレース内で非表示になるため、これはかなりトリッキーです。

function custom_middleware(string $baseUri = 'http://127.0.0.1:8099', float $timeout = 0.2)
{
    return function (callable $handler) use ($baseUri, $timeout) {
        return function ($request, array $options) use ($handler, $baseUri, $timeout) {
            try {
                $client = new GuzzleHttp\Client(['base_uri' => $baseUri, 'timeout' => $timeout,]);
                $client->get('/a');
            } catch (Exception $exception) {
                return \GuzzleHttp\Promise\rejection_for($exception->getMessage());
            }
            return $handler($request, $options);
        };
    };
}

これは、使用方法のテスト例です。

$baseUri = 'http://127.0.0.1:8099'; // php -S 127.0.0.1:8099 test.php << includes a simple sleep(10); statement
$timeout = 0.2;

$handler = \GuzzleHttp\HandlerStack::create();
$handler->Push(custom_middleware($baseUri, $timeout));

$client = new Client([
    'handler' => $handler,
    'base_uri' => $baseUri,
]);

try {
    $response = $client->get('/b');
} catch (Exception $exception) {
    var_dump(get_class($exception), $exception->getMessage());
}

これに対してテストを実行するとすぐに、私は受け取ります

$ php test2.php 
string(37) "GuzzleHttp\Promise\RejectionException"
string(174) "The promise was rejected with reason: cURL error 28: Operation timed out after 202 milliseconds with 0 bytes received (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)"

したがって、メインのguzzle呼び出しが失敗したように見えますが、実際には、失敗したのはサブ呼び出しです。

これが特定の問題の特定に役立つかどうかをお知らせください。これをもう少しデバッグするためにミドルウェアを共有していただければ幸いです。

1
Christoph Kluge

これは環境で散発的に発生し、RejectionExceptionをスローして複製するのが難しいため(少なくとも私はできませんでした)、別のcatchブロックをコードに追加できます。以下を参照してください:

try {
    $c = new \GuzzleHttp\Client([
        'timeout' => 0.1
    ]);
    $response = (string) $c->get('https://example.com')->getBody();
} catch (GuzzleHttp\Promise\RejectionException $e) {
    // Log the output of $e->getTraceAsString();
} catch(GuzzleHttp\Exception\RequestException $e) {
    // This occasionally gets catched when a ConnectException (child) is thrown,
    // but it doesnt happen with RejectionException because it is not a child
    // of RequestException.
}

それはあなたと私たちになぜ、いつこれが起こるのかについていくつかのアイデアを与えなければなりません。

0
Vladimir

こんにちは、あなたがあなたの問題を解決したかどうかわかりませんでした。

エラーログとは何かを投稿してください。 PHPとサーバーのエラーログ内の両方で検索します

私はあなたのフィードバックを待っています

0
PauloBoaventura