I'm trying to connect to RabbitMQ from a PHP consumer using PhpAmqpLib
. The connection occasionally silently closes. Mean time to failure is around a couple weeks; I have tried to replicate by blocking ports, shutting down the PHP consumer, and force-closing the connection from the RabbitMQ Management UI - but none of those replicates the error:
The error appears to only happen if there are no messages on the queue for an extended period. I assume the problem is that there's no heartbeat, and this seems to be confirmed by the Management UI:
Great, so I just need to find out how to turn on heartbeats. According to the RabbitMQ docs, it is sufficient to set a non-zero heartbeat with either the client or the server.
Both have non-zero heartbeats.
Here's the relevant lines from rabbitmq-diagnostics
on the server:
# rabbitmq-diagnostics environment | grep -i heartbeat
{heartbeat_interval,100},
{heartbeat,60},
Here's an excerpt from the class where I'm creating the connection, adapted from (undocumented) code I inherited:
use PhpAmqpLib\Connection\AMQPConnectionConfig;
use PhpAmqpLib\Connection\AMQPSSLConnection;
use PhpAmqpLib\Connection\AMQPStreamConnection;
...
private AMQPStreamConnection $connection;
...
$heartbeat = 20;
$conconf = new AMQPConnectionConfig();
$conconf->setHeartbeat($heartbeat);
$conconf->setKeepalive(true);
# See
# https://php-amqplib.github.io/php-amqplib/classes/PhpAmqpLib-Connection-AMQPStreamConnection.html
$this->connection = new AMQPStreamConnection(
$this->host,
$this->port,
$this->user,
$this->password,
'/', # $vhost
# $insist - insist on connecting to a specified server, default
# value:
false,
AMQPConnectionConfig::AUTH_AMQPPLAIN, # $login_method, default value
null, # $ login_response, default value
'en_US', # $locale, default value
3.0, # $connection_timeout, default value
40.0, # $read_write_timeout, supposed to be at least 2*heartbeat?
null, # $context, default value
true, # $ keepalive
null, # $io, default value
# $heartbeat, heartbeat interval; default is 0 which RabbitMQ
# recommends against. See
# https://www.rabbitmq.com/docs/heartbeats
$heartbeat,
0.0, # $channel_rpc_timeout,
null, # $ssl_protocol ?
$conconf # $config
);
It works to consume messages until the connection drops, so it seems like I'm close...
There were several problems, and I had to fix them all before I got confirmation of a heartbeat in the management UI:
AMQPStreamConnection
constructor. Possibly that was a different version of PhpAmqpLib
? But using a long list of positional arguments made it hard to notice that. When I found this constructor source code and compared the call in detail, it gave me a couple more things to try.PhpAmqpLib
in this case) decides not to follow the AMQP/RabbitMQ protocol on heartbeat negotiation, the server won't force it to do so.PhpAmqpLib
community was aware heartbeats weren't fully supported, and they made a change in version 2.12.2 to fix that - but heartbeats were still an optional feature. "PHP can't do heartbeats as configured" is why the client was overriding the server (see #2) in a way counter to the RabbitMQ docs. The source code and constructors from that pull request was useful in helping me figure out how to configure my connection.Putting it all together, here's the corresponding excerpt of the code after making the necessary changes:
use Illuminate\Support\Facades\Log;
use PhpAmqpLib\Connection\AMQPConnectionConfig;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Connection\Heartbeat\PCNTLHeartbeatSender;
...
private AMQPStreamConnection $connection;
# For info on PCNTLHeartbeatSender, see
# https://github.com/php-amqplib/php-amqplib/pull/815
private PCNTLHeartbeatSender $sender; # Asynchronous AMQP heartbeat-sender
...
$heartbeat = 20;
# See
# https://php-amqplib.github.io/php-amqplib/classes/PhpAmqpLib-Connection-AMQPStreamConnection.html
# https://github.com/php-amqplib/php-amqplib/blob/2.12.2/PhpAmqpLib/Connection/AMQPStreamConnection.php
$this->connection = new AMQPStreamConnection(
$this->host,
$this->port,
$this->user,
$this->password,
'/', # $vhost
# $insist - insist on connecting to a specified server, default
# value:
false,
AMQPConnectionConfig::AUTH_AMQPPLAIN, # $login_method, default value
null, # $ login_response, default value
'en_US', # $locale, default value
3.0, # $connection_timeout, default value
40.0, # $read_write_timeout, supposed to be at least 2*heartbeat?
null, # $context, default value
true, # $ keepalive
# $heartbeat, heartbeat interval; default is 0 which RabbitMQ
# recommends against. See
# https://www.rabbitmq.com/docs/heartbeats
$heartbeat,
0.0, # $channel_rpc_timeout,
null, # $ssl_protocol ?
);
$this->sender = new PCNTLHeartbeatSender($this->connection);
$this->sender->register();
Log::debug('Heartbeat values:', [
"input"=>$heartbeat,
"connection"=>$this->connection->getHeartbeat(),
]);
Now both logs and the RabbitMQ Management UI confirm I've got the intended 20s heartbeat in place. Because of the issues with replicating the silently-closed-connection bug described above, it'll be some time before I can confirm this is completely fixed, but I'm hopeful.