// 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.functional;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import org.junit.Test;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.GetResponse;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.QueueingConsumer.Delivery;
import com.rabbitmq.client.test.BrokerTestCase;
public class QosTests extends BrokerTestCase
{
public void setUp()
throws IOException, TimeoutException {
openConnection();
openChannel();
}
public void tearDown()
throws IOException
{
closeChannel();
closeConnection();
}
public void fill(int n)
throws IOException
{
for (int i = 0; i < n; i++) {
channel.basicPublish("amq.fanout", "", null,
Integer.toString(i).getBytes());
}
}
/**
* receive n messages - check that we receive no fewer and cannot
* receive more
**/
public static List<Delivery> drain(QueueingConsumer c, int n)
throws IOException
{
List<Delivery> res = new LinkedList<Delivery>();
try {
long start = System.currentTimeMillis();
for (int i = 0; i < n; i++) {
Delivery d = c.nextDelivery(1000);
assertNotNull(d);
res.add(d);
}
long finish = System.currentTimeMillis();
Thread.sleep( (n == 0 ? 0 : (finish - start) / n) + 10 );
assertNull(c.nextDelivery(0));
} catch (InterruptedException ie) {
fail("interrupted");
}
return res;
}
@Test public void messageLimitPrefetchSizeFails()
throws IOException
{
try {
channel.basicQos(1000, 0, false);
fail("basic.qos{pretfetch_size=NonZero} should not be supported");
} catch (IOException ioe) {
checkShutdownSignal(AMQP.NOT_IMPLEMENTED, ioe);
}
}
@Test public void messageLimitUnlimited()
throws IOException
{
QueueingConsumer c = new QueueingConsumer(channel);
configure(c, 0, 1, 2);
drain(c, 2);
}
@Test public void noAckNoAlterLimit()
throws IOException
{
QueueingConsumer c = new QueueingConsumer(channel);
declareBindConsume(channel, c, true);
channel.basicQos(1, true);
fill(2);
drain(c, 2);
}
@Test public void noAckObeysLimit()
throws IOException
{
channel.basicQos(1, true);
QueueingConsumer c1 = new QueueingConsumer(channel);
declareBindConsume(channel, c1, false);
fill(1);
QueueingConsumer c2 = new QueueingConsumer(channel);
declareBindConsume(channel, c2, true);
fill(1);
try {
Delivery d = c2.nextDelivery(1000);
assertNull(d);
} catch (InterruptedException ie) {
fail("interrupted");
}
List<Delivery> d = drain(c1, 1);
ack(d, false); // must ack before the next one appears
d = drain(c1, 1);
ack(d, false);
drain(c2, 1);
}
@Test public void permutations()
throws IOException
{
closeChannel();
for (int limit : Arrays.asList(1, 2)) {
for (boolean multiAck : Arrays.asList(false, true)) {
for (boolean txMode : Arrays.asList(true, false)) {
for (int queueCount : Arrays.asList(1, 2)) {
openChannel();
runLimitTests(limit, multiAck, txMode, queueCount);
closeChannel();
}
}
}
}
}
@Test public void fairness()
throws IOException
{
QueueingConsumer c = new QueueingConsumer(channel);
final int queueCount = 3;
final int messageCount = 100;
List<String> queues = configure(c, 1, queueCount, messageCount);
for (int i = 0; i < messageCount - 1; i++) {
List<Delivery> d = drain(c, 1);
ack(d, false);
}
//Perfect fairness would result in every queue having
//messageCount * (1 - 1 / queueCount) messages left. Perfect
//unfairness would result in one queue having no messages
//left and the other queues having messageCount messages left.
//
//We simply check that every queue has had *some* message(s)
//consumed from it. That ensures that no queue is "left
//behind" - a notion of fairness somewhat short of perfect but
//probably good enough.
for (String q : queues) {
AMQP.Queue.DeclareOk ok = channel.queueDeclarePassive(q);
assertTrue(ok.getMessageCount() < messageCount);
}
}
@Test public void singleChannelAndQueueFairness()
throws IOException
{
//check that when we have multiple consumers on the same
//channel & queue, and a prefetch limit set, that all
//consumers get a fair share of the messages.
channel.basicQos(1, true);
String q = channel.queueDeclare().getQueue();
channel.queueBind(q, "amq.fanout", "");
final Map<String, Integer> counts =
Collections.synchronizedMap(new HashMap<String, Integer>());
QueueingConsumer c = new QueueingConsumer(channel) {
@Override public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException {
counts.put(consumerTag, counts.get(consumerTag) + 1);
super.handleDelivery(consumerTag, envelope,
properties, body);
}
};
channel.basicConsume(q, false, "c1", c);
channel.basicConsume(q, false, "c2", c);
int count = 10;
counts.put("c1", 0);
counts.put("c2", 0);
fill(count);
try {
for (int i = 0; i < count; i++) {
Delivery d = c.nextDelivery();
channel.basicAck(d.getEnvelope().getDeliveryTag(), false);
}
} catch (InterruptedException ie) {
fail("interrupted");
}
//we only check that the server isn't grossly unfair; perfect
//fairness is too much to ask for (even though RabbitMQ atm
//does actually provide it in this case)
assertTrue(counts.get("c1").intValue() > 0);
assertTrue(counts.get("c2").intValue() > 0);
}
@Test public void consumerLifecycle()
throws IOException
{
channel.basicQos(1, true);
QueueingConsumer c = new QueueingConsumer(channel);
String queue = "qosTest";
channel.queueDeclare(queue, false, false, false, null);
channel.queueBind(queue, "amq.fanout", "");
fill(3);
String tag;
for (int i = 0; i < 2; i++) {
tag = channel.basicConsume(queue, false, c);
List<Delivery> d = drain(c, 1);
channel.basicCancel(tag);
drain(c, 0);
ack(d, true);
drain(c, 0);
}
channel.queueDelete(queue);
}
@Test public void setLimitAfterConsume()
throws IOException
{
QueueingConsumer c = new QueueingConsumer(channel);
declareBindConsume(c);
channel.basicQos(1, true);
fill(3);
//We actually only guarantee that the limit takes effect
//*eventually*, so this can in fact fail. It's pretty unlikely
//though.
List<Delivery> d = drain(c, 1);
ack(d, true);
drain(c, 1);
}
@Test public void limitIncrease()
throws IOException
{
QueueingConsumer c = new QueueingConsumer(channel);
configure(c, 1, 3);
channel.basicQos(2, true);
drain(c, 1);
}
@Test public void limitDecrease()
throws IOException
{
QueueingConsumer c = new QueueingConsumer(channel);
List<Delivery> d = configure(c, 2, 4);
channel.basicQos(1, true);
drain(c, 0);
ack(d, true);
drain(c, 1);
}
@Test public void limitedToUnlimited()
throws IOException
{
QueueingConsumer c = new QueueingConsumer(channel);
configure(c, 1, 3);
channel.basicQos(0, true);
drain(c, 2);
}
@Test public void limitingMultipleChannels()
throws IOException
{
Channel ch1 = connection.createChannel();
Channel ch2 = connection.createChannel();
QueueingConsumer c1 = new QueueingConsumer(ch1);
QueueingConsumer c2 = new QueueingConsumer(ch2);
String q1 = declareBindConsume(ch1, c1, false);
String q2 = declareBindConsume(ch2, c2, false);
ch1.basicConsume(q2, false, c1);
ch2.basicConsume(q1, false, c2);
ch1.basicQos(1, true);
ch2.basicQos(1, true);
fill(5);
List<Delivery> d1 = drain(c1, 1);
List<Delivery> d2 = drain(c2, 1);
ackDelivery(ch1, d1.remove(0), true);
ackDelivery(ch2, d2.remove(0), true);
drain(c1, 1);
drain(c2, 1);
ch1.abort();
ch2.abort();
}
@Test public void limitInheritsUnackedCount()
throws IOException
{
QueueingConsumer c = new QueueingConsumer(channel);
declareBindConsume(c);
fill(1);
drain(c, 1);
channel.basicQos(2, true);
fill(2);
drain(c, 1);
}
@Test public void recoverReducesLimit() throws Exception {
channel.basicQos(2, true);
QueueingConsumer c = new QueueingConsumer(channel);
declareBindConsume(c);
fill(3);
drain(c, 2);
channel.basicRecover(true);
drain(c, 2);
}
protected void runLimitTests(int limit,
boolean multiAck,
boolean txMode,
int queueCount)
throws IOException
{
QueueingConsumer c = new QueueingConsumer(channel);
// We attempt to drain 'limit' messages twice, do one
// basic.get per queue, and need one message to spare
//-> 2*limit + 1*queueCount + 1
List<String> queues = configure(c, limit, queueCount,
2*limit + 1*queueCount + 1);
if (txMode) {
channel.txSelect();
}
//is limit enforced?
List<Delivery> d = drain(c, limit);
//is basic.get not limited?
List<Long> tags = new ArrayList<Long>();
for (String q : queues) {
GetResponse r = channel.basicGet(q, false);
assertNotNull(r);
tags.add(r.getEnvelope().getDeliveryTag());
}
//are acks handled correctly?
//and does the basic.get above have no effect on limiting?
Delivery last = ack(d, multiAck);
if (txMode) {
drain(c, 0);
channel.txRollback();
drain(c, 0);
ackDelivery(last, true);
channel.txCommit();
}
drain(c, limit);
//do acks for basic.gets have no effect on limiting?
for (long t : tags) {
channel.basicAck(t, false);
}
if (txMode) {
channel.txCommit();
}
drain(c, 0);
}
protected Delivery ack(List<Delivery> d, boolean multiAck)
throws IOException
{
Delivery last = null;
for (Delivery tmp : d) {
if (!multiAck) ackDelivery(tmp, false);
last = tmp;
}
if (multiAck) ackDelivery(last, true);
return last;
}
protected List<String> configure(QueueingConsumer c,
int limit,
int queueCount,
int messages)
throws IOException
{
channel.basicQos(limit, true);
//declare/bind/consume-from queues
List <String> queues = new ArrayList<String>();
for (int i = 0; i < queueCount; i++) {
queues.add(declareBindConsume(c));
}
//publish
fill(messages);
return queues;
}
protected List<Delivery> configure(QueueingConsumer c,
int limit,
int messages)
throws IOException
{
channel.basicQos(limit, true);
declareBindConsume(c);
fill(messages);
return drain(c, limit);
}
protected String declareBindConsume(QueueingConsumer c)
throws IOException
{
return declareBindConsume(channel, c, false);
}
protected String declareBindConsume(Channel ch,
QueueingConsumer c,
boolean noAck)
throws IOException
{
String queue = declareBind(ch);
ch.basicConsume(queue, noAck, c);
return queue;
}
protected String declareBind(Channel ch) throws IOException {
AMQP.Queue.DeclareOk ok = ch.queueDeclare();
String queue = ok.getQueue();
ch.queueBind(queue, "amq.fanout", "");
return queue;
}
protected void ackDelivery(Delivery d, boolean multiple)
throws IOException
{
ackDelivery(channel, d, multiple);
}
protected void ackDelivery(Channel ch, Delivery d, boolean multiple)
throws IOException
{
ch.basicAck(d.getEnvelope().getDeliveryTag(), multiple);
}
}