// Copyright (c) 2007-Present Pivotal Software, Inc. All rights reserved. // // This software, the RabbitMQ Java client library, is triple-licensed under the // Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2 // ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see // LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL, // please see LICENSE-APACHE2. // // This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, // either express or implied. See the LICENSE file for specific language governing // rights and limitations of this software. // // If you have any questions regarding licensing, please contact us at // info@rabbitmq.com. package com.rabbitmq.client.test.server; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import org.junit.Test; import com.rabbitmq.client.AMQP; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; import com.rabbitmq.client.test.BrokerTestCase; class RejectingConsumer extends DefaultConsumer { private CountDownLatch latch; private Map<String, Object> headers; private AtomicLong counter; public RejectingConsumer(Channel channel, CountDownLatch latch) { super(channel); this.latch = latch; this.counter = new AtomicLong(latch.getCount()); } @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { if(this.latch.getCount() > 0) { this.getChannel().basicReject(envelope.getDeliveryTag(), false); } else { if(this.getChannel().isOpen()) { this.getChannel().basicAck(envelope.getDeliveryTag(), false); } } if(this.counter.decrementAndGet() == 0) { // get headers only when the message has been redelivered // the expected number of times. // it looks like the message can be redelivered because // of the reject when handleDelivery isn't done yet or // before the latch releases the main thread. There's then // an additional delivery and the checks (transiently) fail. this.headers = properties.getHeaders(); } latch.countDown(); } public Map<String, Object> getHeaders() { return headers; } } public class XDeathHeaderGrowth extends BrokerTestCase { @SuppressWarnings("unchecked") @Test public void boundedXDeathHeaderGrowth() throws IOException, InterruptedException { final String x1 = "issues.rabbitmq-server-78.fanout1"; declareTransientFanoutExchange(x1); final String x2 = "issues.rabbitmq-server-78.fanout2"; declareTransientFanoutExchange(x2); final String x3 = "issues.rabbitmq-server-78.fanout3"; declareTransientFanoutExchange(x3); final String q1 = "issues.rabbitmq-server-78.queue1"; declareTransientQueue(q1, argumentsForDeadLetteringTo(x1)); final String q2 = "issues.rabbitmq-server-78.queue2"; declareTransientQueue(q2, argumentsForDeadLetteringTo(x2)); this.channel.queueBind(q2, x1, ""); final String q3 = "issues.rabbitmq-server-78.queue3"; declareTransientQueue(q3, argumentsForDeadLetteringTo(x3)); this.channel.queueBind(q3, x2, ""); final String qz = "issues.rabbitmq-server-78.destination"; declareTransientQueue(qz, argumentsForDeadLetteringWithoutTtlTo(x3)); this.channel.queueBind(qz, x3, ""); CountDownLatch latch = new CountDownLatch(10); RejectingConsumer cons = new RejectingConsumer(this.channel, latch); this.channel.basicConsume(qz, cons); this.channel.basicPublish("", q1, null, "msg".getBytes()); assertTrue(latch.await(5, TimeUnit.SECONDS)); List<Map<String, Object>> events = (List<Map<String, Object>>)cons.getHeaders().get("x-death"); assertEquals(4, events.size()); List<String> qs = new ArrayList<String>(); for (Map<String, Object> evt : events) { qs.add(evt.get("queue").toString()); } Collections.sort(qs); assertEquals(Arrays.asList(qz, q1, q2, q3), qs); List<Long> cs = new ArrayList<Long>(); for (Map<String, Object> evt : events) { cs.add((Long)evt.get("count")); } Collections.sort(cs); assertEquals(Arrays.asList(1L, 1L, 1L, 9L), cs); cleanUpExchanges(x1, x2, x3); cleanUpQueues(q1, q2, q3, qz); } private void cleanUpExchanges(String... xs) throws IOException { for(String x : xs) { this.channel.exchangeDelete(x); } } private void cleanUpQueues(String... qs) throws IOException { for(String q : qs) { this.channel.queueDelete(q); } } @SuppressWarnings("unchecked") @Test public void handlingOfXDeathHeadersFromEarlierVersions() throws IOException, InterruptedException { final String x1 = "issues.rabbitmq-server-152.fanout1"; declareTransientFanoutExchange(x1); final String x2 = "issues.rabbitmq-server-152.fanout2"; declareTransientFanoutExchange(x2); final String q1 = "issues.rabbitmq-server-152.queue1"; declareTransientQueue(q1, argumentsForDeadLetteringTo(x1)); final String q2 = "issues.rabbitmq-server-152.queue2"; declareTransientQueue(q2, argumentsForDeadLetteringTo(x2)); this.channel.queueBind(q2, x1, ""); final String qz = "issues.rabbitmq-server-152.destination"; declareTransientQueue(qz, argumentsForDeadLetteringWithoutTtlTo(x2)); this.channel.queueBind(qz, x2, ""); CountDownLatch latch = new CountDownLatch(10); RejectingConsumer cons = new RejectingConsumer(this.channel, latch); this.channel.basicConsume(qz, cons); final AMQP.BasicProperties.Builder bldr = new AMQP.BasicProperties.Builder(); AMQP.BasicProperties props = bldr.headers( propsWithLegacyXDeathsInHeaders("issues.rabbitmq-server-152.queue97", "issues.rabbitmq-server-152.queue97", "issues.rabbitmq-server-152.queue97", "issues.rabbitmq-server-152.queue98", "issues.rabbitmq-server-152.queue99")).build(); this.channel.basicPublish("", q1, props, "msg".getBytes()); assertTrue(latch.await(5, TimeUnit.SECONDS)); List<Map<String, Object>> events = (List<Map<String, Object>>)cons.getHeaders().get("x-death"); assertEquals(6, events.size()); List<String> qs = new ArrayList<String>(); for (Map<String, Object> evt : events) { qs.add(evt.get("queue").toString()); } Collections.sort(qs); assertEquals(Arrays.asList(qz, q1, q2, "issues.rabbitmq-server-152.queue97", "issues.rabbitmq-server-152.queue98", "issues.rabbitmq-server-152.queue99"), qs); List<Long> cs = new ArrayList<Long>(); for (Map<String, Object> evt : events) { cs.add((Long)evt.get("count")); } Collections.sort(cs); assertEquals(Arrays.asList(1L, 1L, 4L, 4L, 9L, 12L), cs); cleanUpExchanges(x1, x2); cleanUpQueues(q1, q2, qz, "issues.rabbitmq-server-152.queue97", "issues.rabbitmq-server-152.queue98", "issues.rabbitmq-server-152.queue99"); } private Map<String, Object> propsWithLegacyXDeathsInHeaders(String... qs) { Map<String, Object> m = new HashMap<String, Object>(); List<Map<String, Object>> xDeaths = new ArrayList<Map<String, Object>>(); for(String q : qs) { xDeaths.add(newXDeath(q)); xDeaths.add(newXDeath(q)); xDeaths.add(newXDeath(q)); xDeaths.add(newXDeath(q)); } m.put("x-death", xDeaths); return m; } private Map<String, Object> newXDeath(String q) { Map<String, Object> m = new HashMap<String, Object>(); m.put("reason", "expired"); m.put("queue", q); m.put("exchange", "issues.rabbitmq-server-152.fanout0"); m.put("routing-keys", Arrays.asList("routing-key-1", "routing-key-2")); m.put("random", UUID.randomUUID().toString()); return m; } private Map<String, Object> argumentsForDeadLetteringTo(String dlx) { return argumentsForDeadLetteringTo(dlx, 1); } private Map<String, Object> argumentsForDeadLetteringWithoutTtlTo(String dlx) { return argumentsForDeadLetteringTo(dlx, -1); } private Map<String, Object> argumentsForDeadLetteringTo(String dlx, int ttl) { Map<String, Object> m = new HashMap<String, Object>(); m.put("x-dead-letter-exchange", dlx); m.put("x-dead-letter-routing-key", "some-routing-key"); if(ttl > 0) { m.put("x-message-ttl", ttl); } return m; } }