如何使用Java RabbitMQ客户端实现自动恢复?

如何使用Java RabbitMQ客户端实现自动恢复?,java,rabbitmq,Java,Rabbitmq,我使用Java RabbitMQ客户机从多线程代码对远程系统进行RPC风格的调用,尽管我尽了最大努力,但仍然无法使该功能正常工作。在生产过程中,每样东西平均每天掉一次。当然,我没有找到一种在测试设置中复制它的方法 因此,我的问题是:假设Java RabbitMQ客户机的自动恢复功能确实如广告中所宣传的那样工作,我一定是误解了一些基本的东西。谁能告诉我我做错了什么 这里也有一些类似的ish问题,但他们通常会在后面添加一个回答,比如“从3.3.0版开始,您可以使用自动恢复,这是Java客户端的一个新

我使用Java RabbitMQ客户机从多线程代码对远程系统进行RPC风格的调用,尽管我尽了最大努力,但仍然无法使该功能正常工作。在生产过程中,每样东西平均每天掉一次。当然,我没有找到一种在测试设置中复制它的方法

因此,我的问题是:假设Java RabbitMQ客户机的自动恢复功能确实如广告中所宣传的那样工作,我一定是误解了一些基本的东西。谁能告诉我我做错了什么

这里也有一些类似的ish问题,但他们通常会在后面添加一个回答,比如“从3.3.0版开始,您可以使用自动恢复,这是Java客户端的一个新功能。”我只使用了3.5.0版之后的版本,因此我希望我的问题能够被接受,因为我的意图是,这并不是一个复制品

是的,我知道-但是如果我的问题的唯一答案是“您应该使用Lyra”,这对我来说意味着我关于“假设Java RabbitMQ客户端的自动恢复功能确实如广告所示工作”的陈述可能需要重新评估

文档没有提供太多的信息,这可能是问题的一部分。因此,我将我的生产代码简化为一个最小值(ish…但我不知道如何才能真正删减更多代码,并且仍然期望得到一个有用的答案-但我对长度表示歉意)案例,它应该清楚地显示我在做什么。唯一缺少的是创建启用TLS的连接工厂的方法和RPC另一端的C#服务。希望有人能给我一些建议

在生产中,这段代码充满了日志记录,还注册了连接和通道恢复侦听器,仅用于日志记录。在两个月的日志记录中,由于RabbitMQ相关代码中的故障,系统几乎每天都必须重新启动,日志中没有一次显示正在调用的恢复侦听器或
handleRecoverOk
。我看到的是对
handleCancel
的调用,随后线程上的所有后续操作都因超时或
ConsumerCancelledException
而失败

如果有不同,生产将在Solaris 11上进行。我曾试图通过使用TCPView关闭套接字,在Windows PC上的测试用例中按需导致失败,但这会导致调用
handleShutdownSignal
,然后调用
alreadyclesedexception
,这是一种完全不同的失败模式

在我的代码中,
消费者
以不推荐的
排队消费者
为模型,但试图支持恢复。我可以看出,将有毒对象放入队列可能会消除检测到故障时正在进行的调用的任何可能性(因为
nextDelivery
可能会被调用,并在有机会恢复之前导致异常),但是我希望handleRecoverOk能在某个时候再次清除毒素,留下一个功能齐全的线程。这似乎从未发生过

import com.rabbitmq.client.*;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.utility.Utility;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.*;
import javax.net.ssl.SSLException;

public class RabbitMQCall {
    private final String requestQueueName;
    private final int maxRetries = 5;
    private final Connection connection;
    private volatile boolean closed = false;
    private final ThreadLocal<PerThreadDetails> perThreadDetails = new ThreadLocal<>();
    private final static Charset CHARSET = Charset.forName("UTF-8");

    // Method to run minimal, reduced-to-one-class example code.
    public static void main(String[] arg) throws Exception {
        ConnectionFactory factory = setupTLSEnabledConnectionFactory();
        factory.setAutomaticRecoveryEnabled(true);
        factory.setTopologyRecoveryEnabled(true);
        RabbitMQCall rmqcall = new RabbitMQCall(factory, "call");
        String data = "{\"---METHOD---\":\"ECHO\",\"USERNAME\":\"Test\"}";
        while (true) {
            rmqcall.call(data, Duration.ofSeconds(20));
            System.out.println("OK");
            TimeUnit.SECONDS.sleep(1);
        }
    }

    /**
     * Constructs a new instance of {@code RabbitMQCall} configured with the supplied information.
     * Automatic connection and topology recovery must be enabled on the {@code ConnectionFactory}
     * as this class makes no attempt to reconnect in case of errors.
     * @param connectionFactory the {@link com.rabbitmq.client.ConnectionFactory} specifying all the
     * details for the connection to the RabbitMQ server
     * @param requestQueueName the request queue name
     * @throws IllegalArgumentException if the {@code ConnectionFactory} has invalid settings
     * @throws IOException in case of error setting up the RabbitMQ connection
     * @throws TimeoutException in case of timeout setting up the RabbitMQ connection
     */
    public RabbitMQCall(ConnectionFactory connectionFactory, String requestQueueName)
            throws IOException, TimeoutException {
        this.requestQueueName = requestQueueName;
        connection = connectionFactory.newConnection();
    }

    /**
     * Calls a remote method (passing the supplied data) using RabbitMQ with the specified timeout.
     * Up to the configured number of retries of the complete operation will be attempted before an
     * {@link Exception} is thrown.
     * @param data a textual representation of the data for the call
     * @param timeout the timeout to use when awaiting a response from the remote process, or
     * {@code null} to wait forever
     * @return a textual representation of the result or error
     * @throws NullPointerException if either argument is null
     * @throws IllegalArgumentException if {@code timeout} is negative
     * @throws Exception if there was a failure invoking the remote call (rather than in the
     * operation of the remote call itself)
     * @throws InterruptedException if any thread has interrupted the current thread. The
     * <i>interrupted status</i> of the current thread is cleared when this exception is thrown.
     */
    public String call(String data, Duration timeout) throws Exception, InterruptedException {
        if (closed) {
            throw (new Exception("RabbitMQCall.call() cannot be called on a closed instance"));
        }
        if (timeout != null) {
            if (timeout.isNegative() || timeout.isZero()) {
                throw (new IllegalArgumentException("The timeout must be positive or null"));
            }
        }

        // See if this thread already has a channel set up, and create one if not.
        PerThreadDetails details = perThreadDetails.get();
        if (details == null) {
            details = new PerThreadDetails();
            try {
                details.channel = connection.createChannel();
                if (!(details.channel instanceof Recoverable)) {
                    throw (new AssertionError("Channel doesn't implement Recoverable"));
                }
                details.replyQueueName = details.channel.queueDeclare().getQueue();
                details.consumer = new Consumer(details.channel);
                details.channel.basicConsume(details.replyQueueName, true, details.consumer);
            } catch (IOException e) {
                throw (new Exception("Failed to set up RabbitMQ Channel: " + e, e));
            }
            perThreadDetails.set(details);
        }

        // Wrap the whole thing in a retry loop to handle timeouts.
        RETRY: for (int attempt = 0; attempt <= maxRetries; ++attempt) {
            String corrId = UUID.randomUUID().toString();
            BasicProperties props = new BasicProperties.Builder()
                    .correlationId(corrId)
                    .replyTo(details.replyQueueName)
                    .build();

            // Encode the data with the appropriate character encoding into a byte array.
            ByteBuffer bb = CHARSET.encode(data);
            byte[] bytes = new byte[bb.remaining()];
            bb.get(bytes);

            try {
                details.channel.basicPublish("", requestQueueName, props, bytes);
            } catch (IOException e) {
                throw (new Exception("Error publishing to RabbitMQ Channel: " + e, e));
            }

            // Loop receiving messages until we get the one we're waiting for.
            while (true) {
                Consumer.Delivery delivery = null;
                if (timeout != null) {
                    delivery = details.consumer.nextDelivery(timeout);
                } else {
                    delivery = details.consumer.nextDelivery();
                }

                if (delivery == null) {
                    break; // Break out of inner loop for the next iteration of the retry loop.
                }

                String response = new String(delivery.getBody(), CHARSET);

                // If response matches our request then we have what we're waiting for, so return.
                if (delivery.getProperties().getCorrelationId().equals(corrId)) {
                    return response;
                }
            }
        }

        throw (new Exception("Timeout waiting for response from remote system via RabbitMQ"));
    }

    public void close() {
        if (!closed) {
            closed = true;
            if (connection != null) {
                connection.abort();
            }
        }
    }

    private static class Consumer extends DefaultConsumer {
        private static final Delivery CANCELLED = new Delivery(null, null, null);
        private static final Delivery SHUTDOWN = new Delivery(null, null, null);
        private final BlockingQueue<Delivery> queue = new LinkedBlockingQueue<>();
        private volatile ShutdownSignalException shutdownException;

        Consumer(Channel channel) {
            super(channel);
        }

        Delivery nextDelivery() throws InterruptedException, ShutdownSignalException,
                ConsumerCancelledException {
            Delivery d = queue.take();
            return processDelivery(d);
        }

        Delivery nextDelivery(Duration timeout) throws InterruptedException,
                ShutdownSignalException, ConsumerCancelledException {
            Delivery d = queue.poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
            return processDelivery(d);
        }

        private Delivery processDelivery(Delivery d) {
            if (d == SHUTDOWN) {
                queue.add(SHUTDOWN);
                throw (Utility.fixStackTrace(shutdownException));
            }
            if (d == CANCELLED) {
                throw (new ConsumerCancelledException());
            }
            return d;
        }

        @Override public void handleConsumeOk(String consumerTag) {
            super.handleConsumeOk(consumerTag);
        }

        @Override public void handleCancel(String consumerTag) {
            queue.add(CANCELLED);
        }

        @Override public void handleCancelOk(String consumerTag) {
            queue.add(CANCELLED);
        }

        @Override public void handleDelivery(String consumerTag, Envelope envelope,
                AMQP.BasicProperties properties, byte[] body) {
            if (shutdownException != null) {
                throw (Utility.fixStackTrace(shutdownException));
            }
            queue.add(new Delivery(envelope, properties, body));
        }

        @Override public void handleRecoverOk(String consumerTag) {
            super.handleConsumeOk(consumerTag); // Set the new tag in the only way we can.
            while (queue.contains(CANCELLED)) { // Remove our poison message(s).
                queue.remove(CANCELLED);
            }
        }

        @Override public void handleShutdownSignal(String consumerTag, ShutdownSignalException sig) {
            shutdownException = sig;
            queue.add(SHUTDOWN);
        }

        private static class Delivery {
            private final Envelope envelope;
            private final AMQP.BasicProperties properties;
            private final byte[] body;

            Delivery(Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                this.envelope = envelope;
                this.properties = properties;
                this.body = body;
            }

            public byte[] getBody() {
                return body;
            }

            public Envelope getEnvelope() {
                return envelope;
            }

            public AMQP.BasicProperties getProperties() {
                return properties;
            }
        }
    }

    private static class PerThreadDetails {
        public Channel channel;
        public String replyQueueName;
        public Consumer consumer;
    }
}
导入com.rabbitmq.client.*;
导入com.rabbitmq.client.AMQP.BasicProperties;
导入com.rabbitmq.utility.utility;
导入java.io.IOException;
导入java.nio.ByteBuffer;
导入java.nio.charset.charset;
导入java.time.Duration;
导入java.util.UUID;
导入java.util.concurrent.*;
导入javax.net.ssl.SSLException;
公共类RabbitMQCall{
私有最终字符串requestQueueName;
私有最终整数maxRetries=5;
专用终端连接;
私有易失性布尔闭合=假;
private final ThreadLocal perThreadDetails=new ThreadLocal();
私有最终静态字符集Charset=Charset.forName(“UTF-8”);
//方法来运行最小的、简化为一个类的示例代码。
公共静态void main(字符串[]arg)引发异常{
ConnectionFactory工厂=setupTLSEnabledConnectionFactory();
factory.setAutomaticRecoveryEnabled(真);
factory.setTopologyRecoveryEnabled(真);
RabbitMQCall rmqcall=新RabbitMQCall(工厂,“调用”);
字符串数据=“{\”---方法--\:\“ECHO\”,\“USERNAME\”:\“Test\”}”;
while(true){
rmqcall.call(数据,持续时间秒(20));
System.out.println(“OK”);
时间单位。秒。睡眠(1);
}
}
/**
*构造用提供的信息配置的{@code RabbitMQCall}的新实例。
*必须在{@code ConnectionFactory}上启用自动连接和拓扑恢复
*因为此类在发生错误时不会尝试重新连接。
*@param connectionFactory指定所有
*连接到RabbitMQ服务器的详细信息
*@param requestQueueName请求队列名称
*如果{@code ConnectionFactory}的设置无效,@将引发IllegalArgumentException
*@在设置RabbitMQ连接时出错时引发IOException
*@在设置RabbitMQ连接超时时引发TimeoutException
*/
公共RabbitMQCall(ConnectionFactory ConnectionFactory,String requestQueueName)
抛出IOException、TimeoutException{
this.requestQueueName=requestQueueName;
connection=connectionFactory.newConnection();
}
/**
*使用具有指定超时的RabbitMQ调用远程方法(传递提供的数据)。
*在重试之前,将尝试完成操作的重试次数不超过配置的次数
*已引发{@link Exception}。
*@param data调用数据的文本表示形式
*@param timeout等待远程进程响应时使用的超时,或
*{@code null}永远等待
*
factory.setConnectionTimeout(30000);
 factory.setAutomaticRecoveryEnabled(true);
 factory.setTopologyRecoveryEnabled(true);
 factory.setNetworkRecoveryInterval(10000);
 factory.setExceptionHandler(new DefaultExceptionHandler());
 factory.setRequestedHeartbeat(360);`