Gu ném ném RejectionException thay vì ConnectionException trong quá trình nền


9

Tôi có các công việc chạy trên nhiều nhân viên xếp hàng, có chứa một số yêu cầu HTTP bằng cách sử dụng Guheads. Tuy nhiên, khối thử bắt trong công việc này dường như không nhận được GuzzleHttp\Exception\RequestExceptionkhi tôi đang chạy các công việc này trong quá trình nền. Quá trình đang chạy là một php artisan queue:worknhân viên hệ thống hàng đợi của Laravel theo dõi hàng đợi và chọn công việc.

Thay vào đó, ngoại lệ được ném là một trong GuzzleHttp\Promise\RejectionExceptionnhững thông báo:

Lời hứa đã bị từ chối với lý do: lỗi cURL 28: Đã hết thời gian hoạt động sau 30001 mili giây với 0 byte nhận được (xem https://curl.haxx.se/libcurl/c/libcurl-errors.html )

Đây thực sự là một sự ngụy trang GuzzleHttp\Exception\ConnectException(xem https://github.com/guzz/promises/blob/master/src/RejectionException.php#L22 ), bởi vì nếu tôi chạy một công việc tương tự trong một quy trình PHP thông thường được kích hoạt bằng cách truy cập một URL, tôi nhận được ConnectExceptionnhư dự định với thông báo:

Lỗi cURL 28: Đã hết thời gian hoạt động sau 100 mili giây với 0 trong số 0 byte nhận được (xem https://curl.haxx.se/libcurl/c/libcurl-errors.html )

Mã mẫu sẽ kích hoạt thời gian chờ này:

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.
}

Đoạn mã trên ném một RejectionExceptionhoặc ConnectExceptionkhi được chạy trong quy trình worker, nhưng luôn luôn là ConnectExceptionkhi được kiểm tra thủ công thông qua trình duyệt (từ những gì tôi có thể nói).

Vì vậy, về cơ bản những gì tôi rút ra, là điều này RejectionExceptionbao bọc thông điệp từ ConnectException, tuy nhiên tôi không sử dụng các tính năng không đồng bộ của Guheads. Yêu cầu của tôi chỉ đơn giản là được thực hiện trong loạt. Điều duy nhất khác biệt là nhiều quy trình PHP có thể thực hiện các cuộc gọi HTTP Guheads hoặc bản thân các công việc đã hết thời gian (điều này sẽ dẫn đến một ngoại lệ khác là của LaravelIlluminate\Queue\MaxAttemptsExceededException ), nhưng tôi không thấy cách này khiến mã hoạt động khác đi.

Tôi không thể tìm thấy bất kỳ mã nào bên trong các gói Guheads đang sử dụng php_sapi_name()/PHP_SAPI (xác định giao diện đã sử dụng) để thực thi các nội dung khác nhau khi chạy từ CLI thay vì kích hoạt trình duyệt.

tl; dr

Tại sao Guheads ném tôi RejectionExceptionvào quy trình công nhân của tôi, nhưngConnectException trên các tập lệnh PHP thông thường được kích hoạt thông qua trình duyệt?

Chỉnh sửa 1

Đáng buồn là tôi không thể tạo ra một ví dụ tái sản xuất tối thiểu. Tôi thấy nhiều thông báo lỗi trong trình theo dõi vấn đề Sentry của tôi, với ngoại lệ chính xác được hiển thị ở trên. Nguồn được nêu là Starting Artisan command: horizon:work(đó là Laravel Horizon, nó giám sát các hàng đợi của Laravel). Tôi đã kiểm tra lại để xem liệu có sự khác biệt giữa các phiên bản PHP hay không, nhưng cả quy trình trang web và công nhân đều chạy cùng một PHP 7.3.14đúng:

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
  • Phiên bản cURL là cURL 7.58.0 .
  • Phiên bản hướng dẫn là guzzlehttp/guzzle 6.5.2
  • Phiên bản của Laravel là laravel/framework 6.12.0

Chỉnh sửa 2 (theo dõi ngăn xếp)

    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

Các Client::callRequest()chức năng đơn giản chứa một Client tật ham ăn mà tôi gọi $client->request($request['method'], $request['url'], $request['options']);(để im không sử dụngrequestAsync() ). Tôi nghĩ rằng nó có liên quan đến việc chạy các công việc song song gây ra vấn đề này.

Chỉnh sửa 3 (tìm thấy giải pháp)

Hãy xem xét các testcase sau đây thực hiện một yêu cầu HTTP (sẽ trả về 200 phản hồi thông thường):

        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);
        }

Bây giờ, những gì tôi đã làm ban đầu là gọi rejection_for($e->getMessage())nó tạo ra RejectionExceptiondựa trên chuỗi tin nhắn. Gọi rejection_for($e)là giải pháp chính xác ở đây. Điều duy nhất còn lại để trả lời là nếu rejection_forchức năng này giống như một đơn giản throw $e.


Bạn sử dụng phiên bản Guheads nào?
Vladimir

1
Những trình điều khiển hàng đợi nào bạn sử dụng cho laravel? Có bao nhiêu công nhân đang chạy song song trên cá thể / mỗi cá thể? Bạn có cài đặt phần mềm trung gian tùy chỉnh tại chỗ không (gợi ý HandlerStack:)?
Christoph Kluge

Bạn có thể cung cấp một dấu vết ngăn xếp từ Sentry?
Vladimir

@Vladimir ive đã thêm dấu vết ngăn xếp. Tôi không nghĩ rằng nó sẽ giúp bạn nhiều. Cách hứa hẹn được thực hiện trong Guheads (và PHP nói chung) rất khó đọc.
Ngọn lửa ngày

1
@Flame bạn có thể chia sẻ phần mềm trung gian thực hiện yêu cầu phụ đề không? Tôi đoán vấn đề sẽ ở đó. Trong khi đó tôi sẽ thêm một câu trả lời có thể lặp lại với luận án của mình.
Christoph Kluge

Câu trả lời:


3

Xin chào, tôi muốn biết nếu bạn gặp lỗi 4xx hoặc lỗi 5xx

Nhưng ngay cả như vậy tôi sẽ đặt một số giải pháp thay thế cho các giải pháp được tìm thấy giống với vấn đề của bạn

thay thế 1

Tôi muốn khắc phục điều này, tôi đã gặp vấn đề này với một máy chủ sản xuất mới trả về 400 phản hồi không mong đợi so với môi trường phát triển và thử nghiệm hoạt động như mong đợi; chỉ cần cài đặt apt cài đặt php7.0-curl đã sửa nó.

Đó là bản cài đặt Ubuntu 16.04 LTS hoàn toàn mới với php được cài đặt qua ppa: ondrej / php, trong quá trình gỡ lỗi tôi nhận thấy rằng các tiêu đề khác nhau. Cả hai đều đang gửi một biểu mẫu nhiều phần với dữ liệu được xử lý, tuy nhiên không có php7.0-curl, nó đang gửi một kết nối: đóng tiêu đề chứ không phải là Expect: 100-Contin; cả hai yêu cầu trong đó có Mã hóa chuyển: chunked.

  thay thế 2

Có lẽ bạn nên thử cái này

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());

Yêu cầu cần xử lý nếu mã phản hồi không 200

thay thế 3

Trong trường hợp của tôi, đó là vì tôi đã chuyển một mảng trống trong tùy chọn $ của yêu cầu ['json'] Tôi không thể sao chép 500 trên máy chủ bằng Postman hoặc cURL ngay cả khi chuyển tiêu đề yêu cầu Content-Type: application / json.

Dù sao, loại bỏ khóa json khỏi mảng tùy chọn của yêu cầu đã giải quyết vấn đề.

Tôi đã dành 30 phút để cố gắng tìm ra những gì sai vì hành vi này rất không phù hợp. Đối với tất cả các yêu cầu khác mà tôi đang thực hiện, việc chuyển các tùy chọn $ ['json'] = [] không gây ra bất kỳ vấn đề nào. Đó có thể là sự cố máy chủ tho, tôi không kiểm soát máy chủ.

gửi phản hồi về chi tiết thu được


tốt ... Để có câu trả lời nhanh hơn và chính xác hơn. Tôi đã chủ động đăng câu hỏi trên Trang dự án trên GitHub. Tôi hy vọng bạn không
bận

1
a ConnectExceptionkhông có phản hồi liên quan, do đó, không có lỗi 400 hoặc 500 theo như tôi biết. Có vẻ như bạn thực sự nên bắt BadResponseException(hoặc ClientException(4xx) / ServerException(5xx) cả hai đều là con của nó)
Flame


2

Hướng dẫn sử dụng Lời hứa cho cả yêu cầu đồng bộ và không đồng bộ. Sự khác biệt duy nhất là khi bạn sử dụng yêu cầu đồng bộ (trường hợp của bạn) - nó được thực hiện ngay bằng cách gọi một wait() phương thức . Lưu ý phần này:

Gọi waitmột lời hứa đã bị từ chối sẽ ném một ngoại lệ. Nếu lý do từ chối là một ví dụ của \Exceptionlý do được ném. Mặt khác, a GuzzleHttp\Promise\RejectionException được ném và lý do có thể thu được bằng cách gọi getReason phương thức ngoại lệ.

Vì vậy, nó đưa ra RequestExceptionmột ví dụ \Exceptionvà nó luôn xảy ra với các lỗi HTTP 4xx và 5xx, trừ khi việc ném ngoại lệ bị vô hiệu hóa thông qua các tùy chọn. Như bạn thấy, nó cũng có thể ném RejectionExceptionnếu lý do không phải là một ví \Exceptiondụ, ví dụ nếu lý do là một chuỗi dường như xảy ra trong trường hợp của bạn. Điều kỳ lạ là bạn nhận được RejectExceptionchứ không phải là RequestExceptionkhi Gu ném ConnectExceptionvào lỗi hết thời gian kết nối. Dù sao, bạn có thể tìm thấy một lý do nếu bạn đi qua RejectExceptiondấu vết ngăn xếp của mình trong Sentry và tìm nơi reject()phương thức được gọi trên Promise.


1

Thảo luận với tác giả bên trong phần bình luận như là một khởi đầu cho câu trả lời của tôi:

Câu hỏi:

Bạn có cài đặt phần mềm trung gian tùy chỉnh tại chỗ không (gợi ý: HandlerStack)?

Trả lời của tác giả:

Có khác nhau. Nhưng phần mềm trung gian về cơ bản là một công cụ sửa đổi yêu cầu / phản hồi, ngay cả các yêu cầu ách tắc tôi thực hiện cũng được thực hiện đồng bộ.


Theo đây là luận án của tôi:

Bạn có một khoảng thời gian chờ bên trong một trong những phần mềm trung gian của bạn, điều này gọi là ách tắc. Vì vậy, hãy cố gắng thực hiện một trường hợp tái sản xuất.

Ở đây chúng tôi có một phần mềm trung gian tùy chỉnh gọi gu gu và trả về lỗi từ chối với thông báo ngoại lệ của cuộc gọi phụ. Điều này khá khó khăn, do xử lý lỗi bên trong, nó trở nên vô hình bên trong dấu vết ngăn xếp.

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);
        };
    };
}

Đây là một ví dụ thử nghiệm làm thế nào bạn có thể sử dụng nó:

$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());
}

Ngay khi tôi thực hiện một bài kiểm tra chống lại điều này, tôi sẽ nhận được

$ 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)"

Vì vậy, có vẻ như cuộc gọi ách chính của bạn không thành công nhưng thực tế đó là cuộc gọi phụ thất bại.

Hãy cho tôi biết nếu điều này giúp bạn xác định vấn đề cụ thể của bạn. Tôi cũng sẽ đánh giá cao nếu bạn có thể chia sẻ phần mềm trung gian của mình để gỡ lỗi này thêm một chút nữa.


Có vẻ như bạn đúng! Tôi đã gọi một rejection_for($e->getMessage())thay vì rejection_for($e)một nơi nào đó trong phần mềm trung gian đó. Tôi đã xem xét nguồn ban đầu cho phần mềm trung gian mặc định (như ở đây: github.com/gu Muff / gu Muff / blob / master / src / MMestware.php # L106 ), nhưng không thể biết tại sao lại có rejection_for($e)thay vì throw $e. Nó dường như xếp tầng theo cách tương tự theo thử nghiệm của tôi. Xem bài viết gốc cho một testcase đơn giản hóa.
Ngọn lửa

1
@Flame rất vui vì tôi có thể giúp bạn :) Theo câu hỏi thứ hai của bạn: Nếu có sự khác biệt giữa chúng. Vâng, nó thực sự tùy thuộc vào trường hợp sử dụng. Trong kịch bản cụ thể của bạn, nó sẽ không tạo ra bất kỳ sự khác biệt nào (ngoại trừ lớp ngoại lệ được sử dụng) vì bạn chỉ có các cuộc gọi duy nhất. Nếu bạn cân nhắc chuyển sang nhiều cuộc gọi và async cùng một lúc thì bạn nên cân nhắc sử dụng lời hứa để tránh bị gián đoạn mã trong khi các yêu cầu khác vẫn đang chạy. Trong trường hợp bạn cần thêm thông tin để chấp nhận câu trả lời của tôi, vui lòng cho tôi biết :)
Christoph Kluge

0

Xin chào, tôi không hiểu liệu cuối cùng bạn có giải quyết được vấn đề của mình hay không.

Vâng, tôi muốn bạn đăng nhật ký lỗi là gì. Tìm kiếm cả trong PHP và trong nhật ký lỗi máy chủ của bạn

Tôi đang chờ phản hồi của bạn


1
Ngoại lệ đã được đăng ở trên, không có gì để đăng hơn là nó đến từ một quá trình nền và dòng ném nó là $client->request('GET', ...)(chỉ là một ứng dụng khách thông thường).
Ngọn lửa

0

Vì điều này xảy ra lẻ tẻ trên môi trường của bạn và thật khó để sao chép việc ném RejectionException (ít nhất là tôi không thể), bạn có thể thêm một catchkhối khác vào mã của mình không, xem bên dưới:

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.
}

Nó phải cung cấp cho bạn và chúng tôi một số ý tưởng về lý do tại sao và khi điều này xảy ra.


thật đáng buồn. Tôi đã nhận được stacktrace trong Sentry bởi vì không bắt được nó, cuối cùng nó cũng đến được trình xử lý ngoại lệ của Laravel (và được gửi đến Sentry). Dấu vết ngăn xếp chỉ chỉ tôi sâu bên trong thư viện Guheads nhưng tôi không thể hiểu tại sao nó lại giả định một lời hứa.
Ngọn lửa

Xem câu trả lời khác của tôi về lý do tại sao nó giả sử một lời hứa: stackoverflow.com/a/60498078/1568963
Vladimir
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.