大量在后台进程中抛出RejectionException而不是ConnectionException


9

我有在多个队列工作器上运行的作业,其中包含一些使用Guzzle的HTTP请求。但是,GuzzleHttp\Exception\RequestException当我在后台进程中运行这些作业时,该作业中的try-catch块似乎没有出现。正在运行的进程是php artisan queue:workLaravel队列系统工作程序,它监视队列并提取作业。

相反,抛出的异常是以下GuzzleHttp\Promise\RejectionException消息之一:

该承诺因以下原因而被拒绝: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),因为如果我在通过访问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.
}

上面的代码在工作进程中运行时抛出a RejectionExceptionConnectExceptionwhen,但是ConnectException在浏览器中手动测试时始终抛出a (据我所知)。

因此,基本上,我得出的结论是,这RejectionException是包装的消息ConnectException,但是我没有使用Guzzle的异步功能。我的要求只是简单地按顺序完成。唯一不同的是,多个PHP进程可能正在进行Guzzle HTTP调用,或者作业本身正在超时(这将导致Laravel产生另一个异常Illuminate\Queue\MaxAttemptsExceededException),但是我看不到这如何导致代码的行为不同。

我无法在Guzzle软件包中找到任何代码,这些代码使用php_sapi_name()/ PHP_SAPI(确定使用的接口)从CLI运行而不是浏览器触发器时执行不同的操作。

tl; dr

为什么Guzzle RejectionException在我的工作进程中抛出我,而ConnectException在通过浏览器触发的常规PHP脚本中抛出我?

编辑1

可悲的是,我无法创建一个最小的可复制示例。我的Sentry问题跟踪器中看到许多错误消息,上面显示了确切的异常。来源声明为Starting Artisan command: horizon:work(这是Laravel Horizo​​n,它负责监督Laravel队列)。我再次检查了PHP版本之间是否存在差异,但是网站和工作进程都运行相同的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
  • 食尸鬼的版本是 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()函数仅包含一个我调用的Guzzle客户端$client->request($request['method'], $request['url'], $request['options']);(因此我未使用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);
        }

现在,我最初所做的是调用rejection_for($e->getMessage()),该调用RejectionException根据消息字符串创建自己的消息。rejection_for($e)在这里打电话是正确的解决方案。唯一需要回答的就是此rejection_for功能是否与simple相同throw $e


您使用哪个Guzzle版本?
弗拉基米尔

1
您为laravel使用哪个队列驱动程序?一个实例/每个实例上有多少工人并行运行?您是否已安装了定制的中间件中间件(提示:)HandlerStack
Christoph Kluge

您可以提供Sentry的堆栈跟踪吗?
弗拉基米尔·

@Vladimir ive添加了堆栈跟踪。我认为这不会对您有太大帮助。在Guzzle(通常是PHP)中实现诺言的方式很难理解。
Flame

1
@Flame可以共享执行子项请求的中间件吗?我想问题就在那里。同时,我将在论文中添加一个可重复的答案。
Christoph Kluge

Answers:


3

您好,我想知道您是错误4xx还是错误5xx

但是即使如此,我还是会为发现与您的问题类似的解决方案提供一些替代方案

替代1

我想解决这个问题,与新的生产服务器相比,与预期的开发和测试环境相比,它返回​​了400个意外响应。只需安装apt install php7.0-curl即可修复它。

这是一个全新的Ubuntu 16.04 LTS安装,通过ppa:ondrej / php安装了php,在调试过程中,我注意到标头是不同的。两者都发送包含被查封数据的多部分表单,但是没有php7.0-curl时,它发送的是Connection:close标头,而不是Expect:100-Continue; 两个请求都具有Transfer-Encoding:分块。

  替代2

也许你应该试试这个

try {
$client = new Client();
$guzzleResult = $client->put($url, [
    'body' => $postString
]);
} catch (\GuzzleHttp\Exception\RequestException $e) {
$guzzleResult = $e->getResponse();
}

var_export($guzzleResult->getStatusCode());
var_export($guzzleResult->getBody());

如果响应代码不为200,则需要咬紧牙关

替代3

就我而言,这是因为我在请求的$ options ['json']中传递了一个空数组,即使传递Content-Type:application / json请求标头,我也无法使用Postman或cURL在服务器上复制500。

无论如何,从请求的options数组中删除json键解决了问题。

我花了大约30分钟的时间试图找出问题所在,因为这种行为非常不一致。对于我正在执行的所有其他请求,传递$ options ['json'] = []不会引起任何问题。可能是服务器问题,我无法控制服务器。

发送有关获得的详细信息的反馈


好...有一个更快,更准确的答案。我主动将问题发布到GitHub的项目页面上。希望您不要介意 github.com/guzzle/guzzle/issues/2599
PauloBoaventura

1
一个ConnectException没有相关的响应,因此据我所知没有400或500错误。看来您实际上应该在捉住BadResponseException(或ClientException(4xx)/ ServerException(5xx)都是它们的孩子)
Flame


2

Guzzle对同步和异步请求都使用Promises。唯一的区别是,当您使用同步请求(根据您的情况)时,可以通过调用wait() method立即实现。注意这一部分:

调用wait已被拒绝的Promise将引发异常。如果拒绝原因是原因的一个实例,\Exception则抛出该错误。否则,将GuzzleHttp\Promise\RejectionException 引发a并可以通过调用getReason 异常的方法来获取原因。

因此,它抛出的RequestException是它的一个实例,\Exception并且总是在4xx和5xx HTTP错误上发生,除非通过选项禁用了抛出异常。如您所见,RejectionException如果原因不是实例\Exception的原因(例如,如果原因是字符串),它也可能抛出,这似乎发生在您的情况下。奇怪的是,您得到的RejectException不是RequestExceptionGuzzle抛出ConnectException的连接超时错误。无论如何,如果您RejectException在Sentry中遍历堆栈跟踪并找到reject()在Promise上调用该方法的位置,则可能会找到原因。


1

首先在评论部分与作者讨论我的答案:

题:

您是否有自定义的耗材中间件(提示: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)"

因此,看来您的主打电话失败了,但实际上是次呼叫失败了。

让我知道这是否有助于您确定特定的问题。如果您可以共享中间件以便进一步调试,我也将不胜感激。


看来您是对的!我在那个中间件中打电话rejection_for($e->getMessage())而不是在rejection_for($e)某个地方。我一直在寻找默认中间件的原始来源(如此处:github.com/guzzle/guzzle/blob/master/src/Middleware.php#L106),但无法完全知道为什么有而rejection_for($e)不是throw $e。根据我的测试用例,它似乎以相同的方式级联。有关简化的测试用例,请参见原始文章。
Flame

1
@Flame很高兴可以为您提供帮助:)根据您的第二个问题:如果两者之间有区别。好吧,这完全取决于用例。在您的特定情况下,它没有任何区别(使用的异常类除外),因为您只有一个调用。如果您考虑一次切换到多个异步调用,则应考虑使用promise避免在其他请求仍在运行时代码中断。如果您需要更多信息以使我的答案被接受,请让我知道:)
Christoph Kluge

0

您好,我不明白您是否最终解决了问题。

好吧,我希望您发布什么是错误日志。在PHP中和服务器的错误日志中搜索

我等待您的反馈


1
上面已经发布了一个异常,除了来自后台进程的异常外,没有其他要发布的异常,并且抛出异常的行也是如此$client->request('GET', ...)(只是一个普通的guzzle客户)。
火焰

0

由于这种情况偶尔会在您的环境中发生,并且很难重复抛出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.
}

它必须给您和我们一些有关为什么以及何时发生这种情况的想法。


可悲的是事实并非如此。我在Sentry中获得了stacktrace,因为没有捕获它,最终它到达了Laravel Exception处理程序(并被发送到Sentry)。堆栈跟踪仅将我指向Guzzle库的深处,但我无法弄清楚为什么它承诺了。
火焰

请参阅我的另一个答案,以了解为什么要兑现承诺:stackoverflow.com/a/60498078/1568963
弗拉基米尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.