RabbitMQ RPC:锁定@ PHP的独占队列

时间:2012-06-19 15:12:19

标签: locking rabbitmq task-queue

我正在尝试使用类似于此示例的RabbitMQ在PHP上构建RPC服务:http://www.rabbitmq.com/tutorials/tutorial-six-java.html 我正在使用此PECL扩展:http://pecl.php.net/package/amqp(版本1.0.3)

问题是当我向其添加标记AMQP_EXCLUSIVE时,我的回调队列(在客户端脚本中声明)对于服务器已锁定

这是我的服务器

// connect to server
$cnn = new AMQPConnection('...');
$cnn->connect();
$channel = new AMQPChannel($cnn);
// create exchange
$exchangeName = 'k-exchange';
$exchange = new AMQPExchange($channel);
$exchange->setName($exchangeName);
$exchange->setType(AMQP_EX_TYPE_DIRECT);
$exchange->declare();

// declare queue to consume messages from
$queue = new \AMQPQueue($channel);
$queue->setName('tempQueue');
$queue->declare();

// start consuming messages
$queue->consume(function($envelope, $queue)
    use ($channel, $exchange) {

    // create callback queue
    $callbackQueue = new \AMQPQueue($channel);
    $callbackQueue->setName($envelope->getReplyTo());
    $callbackQueue->setFlags(AMQP_EXCLUSIVE); // set EXCLUSIVE flag

    /* WARNING: Following code line causes error. See rabbit logs below:
     *  connection <0.1224.10>, channel 1 - error:
     *  {amqp_error,resource_locked,
     *  "cannot obtain exclusive access to locked queue 'amq.gen-Q6J...' in vhost '/'",
     *  'queue.bind'}
     */
    $callbackQueue->bind($exchange->getName(), 'rpc_reply');

    // trying to publish response back to client's callback queue
    $exchange->publish(
        json_encode(array('processed by remote service!')),
        'rpc_reply',
        AMQP_MANDATORY & AMQP_IMMEDIATE
    );

    $queue->ack($envelope->getDeliveryTag());
});

这是我的Client.php

// connect to server
$cnn = new AMQPConnection('...');
$cnn->connect();
$channel = new AMQPChannel($cnn);
// create exchange
$exchangeName = 'k-exchange';
$exchange = new AMQPExchange($channel);
$exchange->setName($exchangeName);
$exchange->setType(AMQP_EX_TYPE_DIRECT);
$exchange->declare();

// create a queue which we send messages to server via
$queue = new \AMQPQueue($channel);
$queue->setName('tempQueue');
$queue->declare();

// binding exchange to queue
$queue->bind($exchangeName, 'temp_action');

// create correlation_id
$correlationId = sha1(time() . rand(0, 1000000));

// create anonymous callback queue to get server response response via
$callbackQueue = new \AMQPQueue($channel);
$callbackQueue->setFlags(AMQP_EXCLUSIVE); // set EXCLUSIVE flag
$callbackQueue->declare();

// publishing message to exchange (passing it to server)
$exchange->publish(
    json_encode(array('process me!')),
    'temp_action',
    AMQP_MANDATORY,
    array(
        'reply_to' => $callbackQueue->getName(), // pass callback queue name
        'correlation_id' => $correlationId
    )
);

// going to wait for remote service complete tasks. tick once a second
$attempts = 0;
while ($attempts < 5)
{
    echo 'Attempt ' . $attempts . PHP_EOL;
    $envelope = $callbackQueue->get();
    if ($envelope) {
        echo 'Got response! ';
        print_r($envelope->getBody());
        echo PHP_EOL;
        exit;
    }

    sleep(1);
    $attempts++;
}

所以最后我只看到RabbitMQ日志中的错误:

connection <0.1224.10>, channel 1 - error:
{amqp_error,resource_locked,
    "cannot obtain exclusive access to locked queue 'amq.gen-Q6J...' in vhost '/'", 
    'queue.bind'}

问题: 在Server.php中创建callbackQueue对象的正确方法是什么? 看来我的Server.php与Client.php连接到RabbitMQ服务器有所不同。我该怎么办? 我应该如何在Server.php端“共享”相同的(到Client.php)连接。

更新 这里有一些RabbitMQ日志

我的Server.php连接(Id为:&lt; 0.22322.27&gt;)

=INFO REPORT==== 20-Jun-2012::13:30:22 ===
    accepting AMQP connection <0.22322.27> (127.0.0.1:58457 -> 127.0.0.1:5672)

我的Client.php连接(Id为:&lt; 0.22465.27&gt;)

=INFO REPORT==== 20-Jun-2012::13:30:38 ===
    accepting AMQP connection <0.22465.27> (127.0.0.1:58458 -> 127.0.0.1:5672)

现在我看到Server.php导致错误:

=ERROR REPORT==== 20-Jun-2012::13:30:38 ===
    connection <0.22322.27>, channel 1 - error:
{amqp_error,resource_locked,
"cannot obtain exclusive access to locked queue 'amq.gen-g6Q...' in vhost '/'",
'queue.bind'}

我的假设 我怀疑,因为Client.php和Server.php没有共享相同ID的连接,所以他们都不可能使用Client.php中声明的独占队列

3 个答案:

答案 0 :(得分:4)

您的实施存在一些问题:

  1. 交换声明
  2. 手动设置与之对应的回复队列 使用临时队列
  3. 在两个方向使用AMQP_EXCLUSIVE
  4. 交换声明

    您无需声明交换(AMQPExchange)即可发布消息。在此RPC示例中,您需要将其用作广播消息的方式(例如,临时队列或临时交换)。所有通信都将直接在队列中进行,理论上会绕过交换。

    $exchange = new AMQPExchange($channel);
    $exchange->publish(...);
    

    QUEUE&amp;回复:

    当您使用AMQPQueue :: setName()以及AMQPQueue :: declare()时,您将绑定到具有用户定义名称的队列。如果声明队列没有名称,则称为临时队列。当您需要从特定路由键接收广播消息时,这非常有用。因此,RabbitMQ / AMQP生成一个随机临时名称。由于队列名称是针对给定实例专门使用信息的,因此它本身就会在连接关闭时被丢弃。

    当RPC客户端想要发布消息(AMQPExchange :: publish())时,它必须将回复指定为发布参数之一。通过这种方式,RPC服务器可以在收到请求时获取随机生成的名称。它使用reply-to name作为服务器将回复给定客户端的QUEUE的名称。除临时队列名称外,实例还必须发送correlationId以确保它收到的回复消息对请求实例是唯一的。

    RabbitMQ Tutorial Six Diagram

    <强> 客户端

    $exchange = new AMQPExchange($channel);
    
    $rpcServerQueueName = 'rpc_queue';
    
    $client_queue = new AMQPQueue($this->channel);
    $client_queue->setFlags(AMQP_EXCLUSIVE);
    $client_queue->declareQueue();   
    $callbackQueueName = $client_queue->getName(); //e.g. amq.gen-JzTY20BRgKO-HjmUJj0wLg
    
    //Set Publish Attributes
    $corrId = uniqid();
    $attributes = array(
        'correlation_id' => $corrId,
        'reply_to'       => $this->callbackQueueName
    );  
    
    $exchange->publish(
        json_encode(['request message']),
        $rpcServerQueueName,
        AMQP_NOPARAM,
        $attributes
    );  
    
    //listen for response
    $callback = function(AMQPEnvelope $message, AMQPQueue $q) {
        if($message->getCorrelationId() == $this->corrId) {
            $this->response = $message->getBody();
            $q->nack($message->getDeliveryTag());
            return false; //return false to signal to consume that you're done. other wise it continues to block
        }   
    };  
    
    $client_queue->consume($callback);
    

    <强> 服务器

    $exchange = new AMQPExchange($channel);
    
    $rpcServerQueueName = 'rpc_queue';
    
    
    $srvr_queue = new AMQPQueue($channel);
    $srvr_queue->setName($rpcServerQueueName); //intentionally declares the rpc_server queue name
    $srvr_queue->declareQueue();
    ...
    $srvr_queue->consume(function(AMQPEnvelope $message, AMQPQueue $q) use (&$exchange) {
    
        //publish with the exchange instance to the reply to queue
        $exchange->publish(
            json_encode(['response message']),  //reponse message
            $message->getReplyTo(),             //get the reply to queue from the message
            AMQP_NOPARAM,                       //disable all other params
            $message->getCorrelationId()        //obtain and respond with correlation id
        );
    
        //acknowledge receipt of the message
        $q->ack($message->getDeliveryTag());
    });
    

    AMQP_EXCLUSIVE

    在这种情况下,EXCLUSIVE仅用于每个实例的Rpc客户端的临时队列,以便它可以发布消息。换句话说,客户端创建一个一次性临时队列,以便自己从RPC服务器专门接收答案。这可以确保没有其他通道线程可以在该队列上发布。它仅为客户端及其响应者锁定。请务必注意,AQMP_EXCLUSIVE不会阻止RPC服务器响应客户端的回复队列。 AMQP_EXCLUSIVE属于尝试发布到同一队列资源的两个独立线程(通道实例)。发生这种情况时,队列基本上被锁定以用于后续连接。交换声明会出现相同的行为。

    @Denis:在这种情况下,您的实现是正确的

    错误 - 不要在服务器中重新声明队列。这是客户的工作

    $callbackQueue = new \AMQPQueue($channel);
    $callbackQueue->setName($envelope->getReplyTo());
    $callbackQueue->setFlags(AMQP_EXCLUSIVE); // set EXCLUSIVE flag 
    ...
    $callbackQueue->bind($exchange->getName(), 'rpc_reply');
    

    您正在尝试绑定到名为tempQueue的队列。但是你已经在client.php中创建了一个名为tempQueue的队列。根据首先启动的服务,另一个将抛出错误。所以你可以删除所有这些并保留最后一部分:

    // trying to publish response back to client's callback queue
    $exchange->publish(
        json_encode(array('processed by remote service!')),
        'rpc_reply', //<--BAD Should be: $envelope->getReplyTo()
        AMQP_MANDATORY & AMQP_IMMEDIATE
    );
    

    然后通过替换:

    修改上述内容
    'rpc_reply'
    
     with
    
     $envelope->getReplyTo()
    

    不要在客户端声明队列名称

    // create a queue which we send messages to server via
    $queue = new \AMQPQueue($channel);
    //$queue->setName('tempQueue'); //remove this line
    //add exclusivity
    $queue->setFlags(AMQP_EXCLUSIVE);
    $queue->declare();
    
    //no need for binding... we're communicating on the queue directly
    //there is no one listening to 'temp_action' so this implementation will send your message into limbo
    //$queue->bind($exchangeName, 'temp_action'); //remove this line
    

答案 1 :(得分:0)

在您的服务器上,您还应将队列声明为独占。请记住,RabbitMQ队列应该具有相同的标志。例如,如果您声明设置为“持久”的队列,则另一端也应该将队列声明为“持久”。因此,在您的服务器上放置一个标志$callbackQueue->setFlags(AMQP_EXCLUSIVE);,就像您的客户端一样。

答案 2 :(得分:0)

我在RabbitMQ官方邮件列表中回答了这个问题的答案

虽然这里没有使用相同的库,但您可以使用移植到PHP的官方教程

https://github.com/rabbitmq/rabbitmq-tutorials/tree/master/php

代码中的问题是您使用不同的选项声明队列。

正如一个回复所说,如果您将队列A声明为持久,那么该队列的每个其他声明都必须是持久的。独家旗帜也一样。

此外,您无需重新声明队列以向其发布消息。作为RPC服务器,您假设已经存在'reply_to'属性中发送的地址。我认为RpcClient有责任确保等待回复的队列已经存在。

附录:

队列中的排他性意味着唯一声明队列的通道可以访问它。