Package com.rabbitmq.client.test.functional

Source Code of com.rabbitmq.client.test.functional.DeadLetterExchange$WithResponse

package com.rabbitmq.client.test.functional;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.GetResponse;
import com.rabbitmq.client.MessageProperties;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.QueueingConsumer.Delivery;
import com.rabbitmq.client.test.BrokerTestCase;

import java.io.IOException;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class DeadLetterExchange extends BrokerTestCase {
    public static final String DLX = "dead.letter.exchange";
    public static final String DLX_ARG = "x-dead-letter-exchange";
    public static final String DLX_RK_ARG = "x-dead-letter-routing-key";
    public static final String TEST_QUEUE_NAME = "test.queue.dead.letter";
    public static final String DLQ = "queue.dlq";
    public static final String DLQ2 = "queue.dlq2";
    public static final int MSG_COUNT = 10;
    public static final int MSG_COUNT_MANY = 1000;
    public static final int TTL = 1000;

    @Override
    protected void createResources() throws IOException {
        channel.exchangeDeclare(DLX, "direct");
        channel.queueDeclare(DLQ, false, true, false, null);
    }

    @Override
    protected void releaseResources() throws IOException {
        channel.exchangeDelete(DLX);
    }

    public void testDeclareQueueWithExistingDeadLetterExchange()
        throws IOException
    {
        declareQueue(DLX);
    }

    public void testDeclareQueueWithNonExistingDeadLetterExchange()
        throws IOException
    {
        declareQueue("some.random.exchange.name");
    }

    public void testDeclareQueueWithInvalidDeadLetterExchangeArg()
        throws IOException
    {
        try {
            declareQueue(133);
            fail("x-dead-letter-exchange must be a valid exchange name");
        } catch (IOException ex) {
            checkShutdownSignal(AMQP.PRECONDITION_FAILED, ex);
        }
    }

    public void testDeclareQueueWithInvalidDeadLetterRoutingKeyArg()
        throws IOException
    {
        try {
            declareQueue("foo", "amq.direct", 144, null);
            fail("x-dead-letter-routing-key must be a string");
        } catch (IOException ex) {
            checkShutdownSignal(AMQP.PRECONDITION_FAILED, ex);
        }
    }

    public void testDeclareQueueWithRoutingKeyButNoDeadLetterExchange()
        throws IOException
    {
        try {
            Map<String, Object> args = new HashMap<String, Object>();
            args.put(DLX_RK_ARG, "foo");

            channel.queueDeclare("bar", false, true, false, args);
            fail("dlx must be defined if dl-rk is set");
        } catch (IOException ex) {
            checkShutdownSignal(AMQP.PRECONDITION_FAILED, ex);
        }

    }

    public void testDeadLetterQueueTTLExpiredMessages() throws Exception {
        ttlTest(TTL);
    }

    public void testDeadLetterQueueZeroTTLExpiredMessages() throws Exception {
        ttlTest(0);
    }

    public void testDeadLetterQueueTTLPromptExpiry() throws Exception {
        Map<String, Object> args = new HashMap<String, Object>();
        args.put("x-message-ttl", TTL);
        declareQueue(TEST_QUEUE_NAME, DLX, null, args);
        channel.queueBind(TEST_QUEUE_NAME, "amq.direct", "test");
        channel.queueBind(DLQ, DLX, "test");

        //measure round-trip latency
        QueueingConsumer c = new QueueingConsumer(channel);
        String cTag = channel.basicConsume(TEST_QUEUE_NAME, true, c);
        long start = System.currentTimeMillis();
        publish(null, "test");
        Delivery d = c.nextDelivery(TTL);
        long stop = System.currentTimeMillis();
        assertNotNull(d);
        channel.basicCancel(cTag);
        long latency = stop-start;

        channel.basicConsume(DLQ, true, c);

        // publish messages at regular intervals until currentTime +
        // 3/4th of TTL
        int count = 0;
        start = System.currentTimeMillis();
        stop = start + TTL * 3 / 4;
        long now = start;
        while (now < stop) {
            publish(null, Long.toString(now));
            count++;
            Thread.sleep(TTL / 100);
            now = System.currentTimeMillis();
        }

        checkPromptArrival(c, count, latency);

        start = System.currentTimeMillis();
        // publish message - which kicks off the queue's ttl timer -
        // and immediately fetch it in noack mode
        publishAt(start);
        basicGet(TEST_QUEUE_NAME);
        // publish a 2nd message and immediately fetch it in ack mode
        publishAt(start + TTL * 1 / 2);
        GetResponse r = channel.basicGet(TEST_QUEUE_NAME, false);
        // publish a 3rd message
        publishAt(start + TTL * 3 / 4);
        // reject 2nd message after the initial timer has fired but
        // before the message is due to expire
        waitUntil(start + TTL * 5 / 4);
        channel.basicReject(r.getEnvelope().getDeliveryTag(), true);

        checkPromptArrival(c, 2, latency);
    }

    public void testDeadLetterDeletedDLX() throws Exception {
        declareQueue(TEST_QUEUE_NAME, DLX, null, null, 1);
        channel.queueBind(TEST_QUEUE_NAME, "amq.direct", "test");
        channel.queueBind(DLQ, DLX, "test");

        channel.exchangeDelete(DLX);
        publishN(MSG_COUNT);
        sleep(100);

        consumeN(DLQ, 0, WithResponse.NULL);

        channel.exchangeDeclare(DLX, "direct");
        channel.queueBind(DLQ, DLX, "test");

        publishN(MSG_COUNT);
        sleep(100);

        consumeN(DLQ, MSG_COUNT, WithResponse.NULL);
    }

    public void testDeadLetterPerMessageTTLRemoved() throws Exception {
        declareQueue(TEST_QUEUE_NAME, DLX, null, null, 1);
        channel.queueBind(TEST_QUEUE_NAME, "amq.direct", "test");
        channel.queueBind(DLQ, DLX, "test");

        final BasicProperties props = MessageProperties.BASIC.builder().expiration("100").build();
        publish(props, "test message");

        // The message's expiration property should have been removed, thus
        // after 100ms of hitting the queue, the message should get routed to
        // the DLQ *AND* should remain there, not getting removed after a subsequent
        // wait time > 100ms
        sleep(500);
        consumeN(DLQ, 1, new WithResponse() {
                @SuppressWarnings("unchecked")
                public void process(GetResponse getResponse) {
                    assertNull(getResponse.getProps().getExpiration());
                    Map<String, Object> headers = getResponse.getProps().getHeaders();
                    assertNotNull(headers);
                    ArrayList<Object> death = (ArrayList<Object>)headers.get("x-death");
                    assertNotNull(death);
                    assertDeathReason(death, 0, TEST_QUEUE_NAME, "expired");
                    final Map<String, Object> deathHeader =
                        (Map<String, Object>)death.get(0);
                    assertEquals("100", deathHeader.get("original-expiration").toString());
                }
            });
    }

    public void testDeadLetterOnReject() throws Exception {
        rejectionTest(false);
    }

    public void testDeadLetterOnNack() throws Exception {
        rejectionTest(true);
    }

    public void testDeadLetterNoDeadLetterQueue() throws IOException {
        channel.queueDelete(DLQ);

        declareQueue(TEST_QUEUE_NAME, DLX, null, null, 1);
        channel.queueBind(TEST_QUEUE_NAME, "amq.direct", "test");

        publishN(MSG_COUNT);
    }

    public void testDeadLetterMultipleDeadLetterQueues()
        throws IOException
    {
        declareQueue(TEST_QUEUE_NAME, DLX, null, null, 1);

        channel.queueDeclare(DLQ2, false, true, false, null);

        channel.queueBind(TEST_QUEUE_NAME, "amq.direct", "test");
        channel.queueBind(DLQ, DLX, "test");
        channel.queueBind(DLQ2, DLX, "test");

        publishN(MSG_COUNT);
    }

    public void testDeadLetterTwice() throws Exception {
        declareQueue(TEST_QUEUE_NAME, DLX, null, null, 1);

        channel.queueDelete(DLQ);
        declareQueue(DLQ, DLX, null, null, 1);

        channel.queueDeclare(DLQ2, false, true, false, null);

        channel.queueBind(TEST_QUEUE_NAME, "amq.direct", "test");
        channel.queueBind(DLQ, DLX, "test");
        channel.queueBind(DLQ2, DLX, "test");

        publishN(MSG_COUNT);

        sleep(100);

        // There should now be two copies of each message on DLQ2: one
        // with one set of death headers, and another with two sets.
        consumeN(DLQ2, MSG_COUNT*2, new WithResponse() {
                @SuppressWarnings("unchecked")
                public void process(GetResponse getResponse) {
                    Map<String, Object> headers = getResponse.getProps().getHeaders();
                    assertNotNull(headers);
                    ArrayList<Object> death = (ArrayList<Object>)headers.get("x-death");
                    assertNotNull(death);
                    if (death.size() == 1) {
                        assertDeathReason(death, 0, TEST_QUEUE_NAME, "expired");
                    } else if (death.size() == 2) {
                        assertDeathReason(death, 0, DLQ, "expired");
                        assertDeathReason(death, 1, TEST_QUEUE_NAME, "expired");
                    } else {
                        fail("message was dead-lettered more times than expected");
                    }
                }
            });
    }

    public void testDeadLetterSelf() throws Exception {
        declareQueue(TEST_QUEUE_NAME, "amq.direct", "test", null, 1);
        channel.queueBind(TEST_QUEUE_NAME, "amq.direct", "test");

        publishN(MSG_COUNT);
        // This test hangs if the queue doesn't process ALL the
        // messages before being deleted, so make sure the next
        // sleep is long enough.
        sleep(200);

        // The messages will NOT be dead-lettered to self.
        consumeN(TEST_QUEUE_NAME, 0, WithResponse.NULL);
    }

    public void testDeadLetterCycle() throws Exception {
        // testDeadLetterTwice and testDeadLetterSelf both test that we drop
        // messages in pure-expiry cycles. So we just need to test that
        // non-pure-expiry cycles do not drop messages.

        declareQueue("queue1", "", "queue2", null, 1);
        declareQueue("queue2", "", "queue1", null, 0);

        channel.basicPublish("", "queue1", MessageProperties.BASIC, "".getBytes());
        final CountDownLatch latch = new CountDownLatch(10);
        channel.basicConsume("queue2", false,
            new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope,
                                           AMQP.BasicProperties properties, byte[] body) throws IOException {
                    channel.basicReject(envelope.getDeliveryTag(), false);
                    latch.countDown();
                }
            });
        assertTrue(latch.await(10, TimeUnit.SECONDS));
    }

    public void testDeadLetterNewRK() throws Exception {
        declareQueue(TEST_QUEUE_NAME, DLX, "test-other", null, 1);

        channel.queueDeclare(DLQ2, false, true, false, null);

        channel.queueBind(TEST_QUEUE_NAME, "amq.direct", "test");
        channel.queueBind(DLQ, DLX, "test");
        channel.queueBind(DLQ2, DLX, "test-other");

        Map<String, Object> headers = new HashMap<String, Object>();
        headers.put("CC", Arrays.asList(new String[]{"foo"}));
        headers.put("BCC", Arrays.asList(new String[]{"bar"}));

        publishN(MSG_COUNT, (new AMQP.BasicProperties.Builder())
                               .headers(headers)
                               .build());

        sleep(100);

        consumeN(DLQ, 0, WithResponse.NULL);
        consumeN(DLQ2, MSG_COUNT, new WithResponse() {
                @SuppressWarnings("unchecked")
                public void process(GetResponse getResponse) {
                    Map<String, Object> headers = getResponse.getProps().getHeaders();
                    assertNotNull(headers);
                    assertNull(headers.get("CC"));
                    assertNull(headers.get("BCC"));

                    ArrayList<Object> death = (ArrayList<Object>)headers.get("x-death");
                    assertNotNull(death);
                    assertEquals(1, death.size());
                    assertDeathReason(death, 0, TEST_QUEUE_NAME,
                                      "expired", "amq.direct",
                                      Arrays.asList(new String[]{"test", "foo"}));
                }
            });
    }

    public void rejectionTest(final boolean useNack) throws Exception {
        deadLetterTest(new Callable<Void>() {
                public Void call() throws Exception {
                    for (int x = 0; x < MSG_COUNT; x++) {
                        GetResponse getResponse =
                            channel.basicGet(TEST_QUEUE_NAME, false);
                        long tag = getResponse.getEnvelope().getDeliveryTag();
                        if (useNack) {
                            channel.basicNack(tag, false, false);
                        } else {
                            channel.basicReject(tag, false);
                        }
                    }
                    return null;
                }
            }, null, "rejected");
    }

    private void deadLetterTest(final Runnable deathTrigger,
                                Map<String, Object> queueDeclareArgs,
                                String reason)
        throws Exception
    {
        deadLetterTest(new Callable<Object>() {
                public Object call() throws Exception {
                    deathTrigger.run();
                    return null;
                }
            }, queueDeclareArgs, reason);
    }

    private void deadLetterTest(Callable<?> deathTrigger,
                                Map<String, Object> queueDeclareArgs,
                                final String reason)
        throws Exception
    {
        declareQueue(TEST_QUEUE_NAME, DLX, null, queueDeclareArgs);

        channel.queueBind(TEST_QUEUE_NAME, "amq.direct", "test");
        channel.queueBind(DLQ, DLX, "test");

        publishN(MSG_COUNT);

        deathTrigger.call();

        consume(channel, reason);
    }

    public static void consume(final Channel channel, final String reason) throws IOException {
        consumeN(channel, DLQ, MSG_COUNT, new WithResponse() {
            @SuppressWarnings("unchecked")
            public void process(GetResponse getResponse) {
                Map<String, Object> headers = getResponse.getProps().getHeaders();
                assertNotNull(headers);
                ArrayList<Object> death = (ArrayList<Object>) headers.get("x-death");
                assertNotNull(death);
                assertEquals(1, death.size());
                assertDeathReason(death, 0, TEST_QUEUE_NAME, reason,
                        "amq.direct",
                        Arrays.asList(new String[]{"test"}));
            }
        });
    }

    private void ttlTest(final long ttl) throws Exception {
        Map<String, Object> args = new HashMap<String, Object>();
        args.put("x-message-ttl", ttl);
        deadLetterTest(new Runnable() {
                public void run() { sleep(ttl + 1500); }
            }, args, "expired");
    }

    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException ex) {
            // whoosh
        }
    }

    /* check that each message arrives within epsilon of the
       publication time + TTL + latency */
    private void checkPromptArrival(QueueingConsumer c,
                                    int count, long latency) throws Exception {
        long epsilon = TTL / 50;
        for (int i = 0; i < count; i++) {
            Delivery d = c.nextDelivery(TTL + TTL + latency + epsilon);
            assertNotNull("message #" + i + " did not expire", d);
            long now = System.currentTimeMillis();
            long publishTime = Long.valueOf(new String(d.getBody()));
            long targetTime = publishTime + TTL + latency;
            assertTrue("expiry outside bounds (+/- " + epsilon + "): " +
                       (now - targetTime),
                       (now >= targetTime - epsilon) &&
                       (now <= targetTime + epsilon));
        }
    }

    private void declareQueue(Object deadLetterExchange) throws IOException {
        declareQueue(TEST_QUEUE_NAME, deadLetterExchange, null, null);
    }

    private void declareQueue(String queue, Object deadLetterExchange,
                              Object deadLetterRoutingKey,
                              Map<String, Object> args) throws IOException {
        declareQueue(queue, deadLetterExchange, deadLetterRoutingKey, args, 0);
    }

    private void declareQueue(String queue, Object deadLetterExchange,
                              Object deadLetterRoutingKey,
                              Map<String, Object> args, int ttl)
        throws IOException
    {
        if (args == null) {
            args = new HashMap<String, Object>();
        }

        if (ttl > 0){
            args.put("x-message-ttl", ttl);
        }

        args.put(DLX_ARG, deadLetterExchange);
        if (deadLetterRoutingKey != null) {
            args.put(DLX_RK_ARG, deadLetterRoutingKey);
        }
        channel.queueDeclare(queue, false, true, false, args);
    }

    private void publishN(int n) throws IOException {
        publishN(n, null);
    }

    private void publishN(int n, AMQP.BasicProperties props)
        throws IOException
    {
        for(int x = 0; x < n; x++) { publish(props, "test message"); }
    }

    private void publish(AMQP.BasicProperties props, String body)
        throws IOException
    {
        channel.basicPublish("amq.direct", "test", props, body.getBytes());
    }

    private void publishAt(long when) throws Exception {
        waitUntil(when);
        publish(null, Long.toString(System.currentTimeMillis()));
    }

    private void waitUntil(long when) throws Exception {
        long delay = when - System.currentTimeMillis();
        Thread.sleep(delay > 0 ? delay : 0);
    }

    private void consumeN(String queue, int n, WithResponse withResponse)
        throws IOException
    {
        consumeN(channel, queue, n, withResponse);
    }

    private static void consumeN(Channel channel, String queue, int n, WithResponse withResponse)
        throws IOException
    {
        for(int x = 0; x < n; x++) {
            GetResponse getResponse =
                channel.basicGet(queue, true);
            assertNotNull("Messages not dead-lettered (" + (n-x) + " left)",
                          getResponse);
            assertEquals("test message", new String(getResponse.getBody()));
            withResponse.process(getResponse);
        }
        GetResponse getResponse = channel.basicGet(queue, true);
        assertNull("expected empty queue", getResponse);
    }

    @SuppressWarnings("unchecked")
    private static void assertDeathReason(List<Object> death, int num,
                                   String queue, String reason,
                                   String exchange, List<String> routingKeys)
    {
        Map<String, Object> deathHeader =
            (Map<String, Object>)death.get(num);
        assertEquals(exchange, deathHeader.get("exchange").toString());

        List<String> deathRKs = new ArrayList<String>();
        for (Object rk : (ArrayList<?>)deathHeader.get("routing-keys")) {
            deathRKs.add(rk.toString());
        }
        Collections.sort(deathRKs);
        Collections.sort(routingKeys);
        assertEquals(routingKeys, deathRKs);

        assertDeathReason(death, num, queue, reason);
    }

    @SuppressWarnings("unchecked")
    private static void assertDeathReason(List<Object> death, int num,
                                   String queue, String reason) {
        Map<String, Object> deathHeader =
            (Map<String, Object>)death.get(num);
        assertEquals(queue, deathHeader.get("queue").toString());
        assertEquals(reason, deathHeader.get("reason").toString());
    }

    private static interface WithResponse {
        static final WithResponse NULL = new WithResponse() {
                public void process(GetResponse getResponse) {
                }
            };

        public void process(GetResponse response);
    }
}
TOP

Related Classes of com.rabbitmq.client.test.functional.DeadLetterExchange$WithResponse

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.