jakubkulhan / bunny

Performant pure-PHP AMQP (RabbitMQ) sync/async (ReactPHP) library
MIT License
697 stars 99 forks source link

Failing connections cause exceptions thrown outside of Promise #74

Open jraubswitch opened 5 years ago

jraubswitch commented 5 years ago

I'm getting this error when connecting to RabbitMQ PHP Fatal error: Uncaught Bunny\Exception\ClientException: Broken pipe or closed connection. in /.../vendor/bunny/bunny/src/Bunny/AbstractClient.php:289

This happens because when I connect, I'm connecting with specific credentials and RabbitMQ is denying the connection. When this happens the stream is closed so we get an end of file:

https://github.com/jakubkulhan/bunny/blob/d0dec8fbbac4e4edd6cdc892fe14cfa607c386ce/src/Bunny/AbstractClient.php#L288-L290

I have multiple connections opened by different users, so I need to handle this gracefully. This is the extension I've written to handle the problem.

I'm using https://packagist.org/packages/evenement/evenement as well.

Hope this helps.

<?php
declare(strict_types=1);

namespace MyClient;

use Bunny\Async\Client;
use Evenement\EventEmitterInterface;
use Evenement\EventEmitterTrait;
use React\Promise;

class BunnyAsyncClient extends Client implements EventEmitterInterface
{
    use EventEmitterTrait;

    /**
     *
     */
    public function onDataAvailable()
    {
        try {
            parent::onDataAvailable();
        } catch (\Throwable $e) {
            $this->eventLoop->removeReadStream($this->getStream());
            $this->eventLoop->futureTick(function () use ($e) {
                $this->emit('error', [$e, $this]);
            });
        }
    }

    /**
     * @return Promise\PromiseInterface
     */
    public function connect()
    {
        $deferred = new Promise\Deferred();

        $errBack = function (\Throwable $e) use ($deferred, &$errBack) {
            $this->removeListener('error', $errBack);
            $deferred->reject($e);
        };

        $this->on('error', $errBack);

        parent::connect()->then(
            function () use ($deferred) {
                return $deferred->resolve($this);
            },
            function (\Throwable $e) use ($deferred) {
                // needed in case rejected not by the errBack
                $deferred->reject($e);
            }
        )->always(function () use ($errBack) {
            $this->removeListener('error', $errBack);
        });

        return $deferred->promise();
    }
}
hsldymq commented 5 years ago

I noticed when this exception caused,call $client->isConnected() gave you the wrong result which is true while I was expecting false.

I'm wondering if it can be fixed by just write following code in that if condition:

    if (@feof($this->stream)) { 
        $this->eventLoop->removeReadStream($this->getStream());
        $this->state = ClientStateEnum::ERROR;
        throw new ClientException("Broken pipe or closed connection."); 
    } 
fritz-gerneth commented 4 years ago

A similar issue occurs when disconnecting a client that has not opened a channel yet.

======================================================================
   The application has thrown an exception!
======================================================================
 Bunny\Exception\ClientException
 Connection closed by server: CHANNEL_ERROR - expected 'channel.open'
----------------------------------------------------------------------
/var/www/vendor/bunny/bunny/src/Bunny/AbstractClient.php:448
#0 /var/www/vendor/bunny/bunny/src/Bunny/Async/Client.php(306): Bunny\AbstractClient->onFrameReceived(Object(Bunny\Protocol\MethodConnectionCloseFrame))
#1 /var/www/vendor/react/event-loop/src/StreamSelectLoop.php(245): Bunny\Async\Client->onDataAvailable(Resource id #2073)
#2 /var/www/vendor/react/event-loop/src/StreamSelectLoop.php(212): React\EventLoop\StreamSelectLoop->waitForStreamActivity(NULL)
#3 /var/www/Common/src/Adapter/Cli/CliController.php(88): React\EventLoop\StreamSelectLoop->run()