缺少交换时不会调用ConfirmListener.handleNack

时间:2015-09-01 15:32:20

标签: java rabbitmq amqp

在我的应用程序中,我需要确定消息是否已成功发布到AMQP交换中或发生了一些错误。似乎Publisher Confirms是为解决这个问题而发明的,所以我开始尝试它们。

对于我的Java应用程序,我使用com.rabbitmq:amqp-client:jar:3.5.4,当交换(我尝试发布的地方)丢失时,我选择了一个非常简单的场景。我希望在这种情况下调用ConfirmListener.handleNack

这是我的Java代码:

package wheleph.rabbitmq_tutorial.confirmed_publishes;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class ConfirmedPublisher {
    private static final Logger logger = LoggerFactory.getLogger(ConfirmedPublisher.class);

    private final static String EXCHANGE_NAME = "confirmed.publishes";

    public static void main(String[] args) throws IOException, InterruptedException, TimeoutException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");

        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        channel.confirmSelect();
        channel.addConfirmListener(new ConfirmListener() {
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                logger.debug(String.format("Received ack for %d (multiple %b)", deliveryTag, multiple));
            }

            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                logger.debug(String.format("Received nack for %d (multiple %b)", deliveryTag, multiple));
            }
        });

        for (int i = 0; i < 100; i++) {
            String message = "Hello world" + channel.getNextPublishSeqNo();
            channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
            logger.info(" [x] Sent '" + message + "'");
            Thread.sleep(2000);
        }

        channel.close();
        connection.close();
    }
}

然而事实并非如此。日志显示没有执行回调:

17:49:34,988 [main] ConfirmedPublisher -  [x] Sent 'Hello world1'
Exception in thread "main" com.rabbitmq.client.AlreadyClosedException: channel is already closed due to channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'confirmed.publishes' in vhost '/', class-id=60, method-id=40)
    at com.rabbitmq.client.impl.AMQChannel.ensureIsOpen(AMQChannel.java:195)
    at com.rabbitmq.client.impl.AMQChannel.transmit(AMQChannel.java:309)
    at com.rabbitmq.client.impl.ChannelN.basicPublish(ChannelN.java:657)
    at com.rabbitmq.client.impl.ChannelN.basicPublish(ChannelN.java:640)
    at com.rabbitmq.client.impl.ChannelN.basicPublish(ChannelN.java:631)
    at wheleph.rabbitmq_tutorial.confirmed_publishes.ConfirmedPublisher.main(ConfirmedPublisher.java:38)

有趣的是,当我尝试使用NodeJS amqp-coffee(0.1.24)时,pubilsher确认按预期工作。

这是我的NodeJS代码:

var AMQP = require('amqp-coffee');

var connection = new AMQP({host: 'localhost'});
connection.setMaxListeners(0);

console.log('Connection started')

connection.publish('node.confirm.publish', '', 'some message', {deliveryMode: 2, confirm: true}, function(err) {
     if (err && err.error && err.error.replyCode === 404) {
         console.log('Got 404 error')
     } else if (err) {
         console.log('Got some error')
     } else {
         console.log('Message successfully published')
     }
  })

这是输出,指示使用适当的参数调用回调:

Connection started
Got 404 error

我是否错误地使用com.rabbitmq:amqp-client或该库中存在某些不一致的内容?

1 个答案:

答案 0 :(得分:1)

事实证明我的假设不正确,在这种情况下不应调用ConfirmListener.handleNack

以下是针对amqp-coffee库所观察到的问题中描述的方案的AMQP消息的相关部分:

ch#1 -> {#method<channel.open>(out-of-band=), null, ""}
ch#1 <- {#method<channel.open-ok>(channel-id=), null, ""}
ch#1 -> {#method<confirm.select>(nowait=false), null, ""}
ch#1 <- {#method<confirm.select-ok>(), null, ""}
ch#1 -> {#method<basic.publish>(ticket=0, exchange=node.confirm.publish, routing-key=, mandatory=false, immediate=false), #contentHeader<basic>(content-type=string/utf8, content-encoding=null, headers=null, delivery-mode=2, priority=null, correlation-id=null, reply-to=null, expiration=null, message-id=null, timestamp=null, type=null, user-id=null, app-id=null, cluster-id=null), "some message"}
ch#1 <- {#method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'node.confirm.publish' in vhost '/', class-id=60, method-id=40), null, ""}
ch#2 -> {#method<channel.open>(out-of-band=), null, ""}
ch#2 <- {#method<channel.open-ok>(channel-id=), null, ""}
ch#2 -> {#method<confirm.select>(nowait=false), null, ""}
ch#2 <- {#method<confirm.select-ok>(), null, ""}

你可以看到:

  1. 发布失败后,代理使用包含原因的channel.close关闭频道。
  2. basic.nack未发送。
  3. 库会自动打开另一个频道以进行后续操作。
  4. 可以使用ShutdownListener

    在Java中实现此行为
    package wheleph.rabbitmq_tutorial.confirmed_publishes;
    
    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    import com.rabbitmq.client.ShutdownListener;
    import com.rabbitmq.client.ShutdownSignalException;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    public class ConfirmedPublisher {
        private static final Logger logger = LoggerFactory.getLogger(ConfirmedPublisher.class);
        private final static String EXCHANGE_NAME = "confirmed.publishes";
    
        // Beware that proper synchronization of channel is needed because current approach may lead to race conditions
        private volatile static Channel channel;
    
        public static void main(String[] args) throws IOException, InterruptedException, TimeoutException {
            ConnectionFactory connectionFactory = new ConnectionFactory();
            connectionFactory.setHost("localhost");
            connectionFactory.setPort(5672);
    
            final Connection connection = connectionFactory.newConnection();
    
            for (int i = 0; i < 100; i++) {
                if (channel == null) {
                    createChannel(connection);
                }
                String message = "Hello world" + i;
                channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
                logger.info(" [x] Sent '" + message + "'");
                Thread.sleep(2000);
            }
    
            channel.close();
            connection.close();
        }
    
        private static void createChannel(final Connection connection) throws IOException {
            channel = connection.createChannel();
            channel.confirmSelect(); // This in fact is not necessary
            channel.addShutdownListener(new ShutdownListener() {
                public void shutdownCompleted(ShutdownSignalException cause) {
                    // Beware that proper synchronization is needed here
                    logger.debug("Handling channel shutdown...", cause);
                    if (cause.isInitiatedByApplication()) {
                        logger.debug("Shutdown is initiated by application. Ignoring it.");
                    } else {
                        logger.error("Shutdown is NOT initiated by application. Resetting the channel.");
                        /* We cannot re-initialize channel here directly because ShutdownListener callbacks run in the connection's thread,
                           so the call to createChannel causes a deadlock since it blocks waiting for a response (whilst the connection's thread
                           is stuck executing the listener). */
                        channel = null;
                    }
                }
            });
        }
    }
    

    有几点需要注意:

    1. 在这种情况下,发布者确认不是必需的,因为我们不使用ConfirmListener或该方法特有的任何其他功能。但是,如果我们想要跟踪哪些消息已成功发送,哪些消息未成功发送,则发布者确认将非常有用。
    2. 如果我们启动ConfirmedPublisher并在一段时间后创建缺少的交换,则将成功发布以下所有消息。但是,之前失败的所有消息都将丢失。
    3. 需要进行额外同步。