ARTEMIS-2560 Duplicate amqp messages over cluster
When AMQPMessages are redistributed from one node to another, the internal property of message is not cleaned up and this causes a message to be routed to a same queue more than once, causing duplicated messages.
This commit is contained in:
parent
1fe910f8ed
commit
044319da05
|
@ -20,6 +20,7 @@ import java.io.InputStream;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import org.apache.activemq.artemis.core.message.impl.CoreMessageObjectPools;
|
import org.apache.activemq.artemis.core.message.impl.CoreMessageObjectPools;
|
||||||
|
@ -77,6 +78,11 @@ public interface Message {
|
||||||
// The value is somewhat higher on 64 bit architectures, probably due to different alignment
|
// The value is somewhat higher on 64 bit architectures, probably due to different alignment
|
||||||
int memoryOffset = 352;
|
int memoryOffset = 352;
|
||||||
|
|
||||||
|
// We use properties to establish routing context on clustering.
|
||||||
|
// However if the client resends the message after receiving, it needs to be removed, so we mark these internal
|
||||||
|
Predicate<SimpleString> INTERNAL_PROPERTY_NAMES_PREDICATE =
|
||||||
|
name -> (name.startsWith(Message.HDR_ROUTE_TO_IDS) && !name.equals(Message.HDR_ROUTE_TO_IDS)) ||
|
||||||
|
(name.startsWith(Message.HDR_ROUTE_TO_ACK_IDS) && !name.equals(Message.HDR_ROUTE_TO_ACK_IDS));
|
||||||
|
|
||||||
SimpleString HDR_ROUTE_TO_IDS = new SimpleString("_AMQ_ROUTE_TO");
|
SimpleString HDR_ROUTE_TO_IDS = new SimpleString("_AMQ_ROUTE_TO");
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,6 @@ package org.apache.activemq.artemis.core.message.impl;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Predicate;
|
|
||||||
import java.util.zip.DataFormatException;
|
import java.util.zip.DataFormatException;
|
||||||
import java.util.zip.Inflater;
|
import java.util.zip.Inflater;
|
||||||
|
|
||||||
|
@ -52,11 +51,6 @@ import org.jboss.logging.Logger;
|
||||||
* consumers */
|
* consumers */
|
||||||
public class CoreMessage extends RefCountMessage implements ICoreMessage {
|
public class CoreMessage extends RefCountMessage implements ICoreMessage {
|
||||||
|
|
||||||
// We use properties to establish routing context on clustering.
|
|
||||||
// However if the client resends the message after receiving, it needs to be removed, so we mark these internal
|
|
||||||
private static final Predicate<SimpleString> INTERNAL_PROPERTY_NAMES_PREDICATE =
|
|
||||||
name -> (name.startsWith(Message.HDR_ROUTE_TO_IDS) && !name.equals(Message.HDR_ROUTE_TO_IDS)) ||
|
|
||||||
(name.startsWith(Message.HDR_ROUTE_TO_ACK_IDS) && !name.equals(Message.HDR_ROUTE_TO_ACK_IDS));
|
|
||||||
public static final int BUFFER_HEADER_SPACE = PacketImpl.PACKET_HEADERS_SIZE;
|
public static final int BUFFER_HEADER_SPACE = PacketImpl.PACKET_HEADERS_SIZE;
|
||||||
|
|
||||||
private volatile int memoryEstimate = -1;
|
private volatile int memoryEstimate = -1;
|
||||||
|
|
|
@ -849,7 +849,7 @@ public class AMQPMessage extends RefCountMessage {
|
||||||
|
|
||||||
public TypedProperties createExtraProperties() {
|
public TypedProperties createExtraProperties() {
|
||||||
if (extraProperties == null) {
|
if (extraProperties == null) {
|
||||||
extraProperties = new TypedProperties();
|
extraProperties = new TypedProperties(INTERNAL_PROPERTY_NAMES_PREDICATE);
|
||||||
}
|
}
|
||||||
return extraProperties;
|
return extraProperties;
|
||||||
}
|
}
|
||||||
|
@ -878,6 +878,13 @@ public class AMQPMessage extends RefCountMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearInternalProperties() {
|
||||||
|
if (extraProperties != null) {
|
||||||
|
extraProperties.clearInternalProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] removeExtraBytesProperty(SimpleString key) throws ActiveMQPropertyConversionException {
|
public byte[] removeExtraBytesProperty(SimpleString key) throws ActiveMQPropertyConversionException {
|
||||||
if (extraProperties == null) {
|
if (extraProperties == null) {
|
||||||
|
|
|
@ -75,7 +75,7 @@ public class AMQPMessagePersisterV2 extends AMQPMessagePersister {
|
||||||
int size = buffer.readInt();
|
int size = buffer.readInt();
|
||||||
|
|
||||||
if (size != 0) {
|
if (size != 0) {
|
||||||
TypedProperties properties = new TypedProperties();
|
TypedProperties properties = new TypedProperties(Message.INTERNAL_PROPERTY_NAMES_PREDICATE);
|
||||||
properties.decode(buffer.byteBuf());
|
properties.decode(buffer.byteBuf());
|
||||||
message.setExtraProperties(properties);
|
message.setExtraProperties(properties);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.activemq.artemis.tests.integration.cluster.distribution;
|
||||||
|
|
||||||
|
import org.apache.activemq.artemis.api.core.RoutingType;
|
||||||
|
import org.apache.activemq.artemis.core.server.cluster.impl.MessageLoadBalancingType;
|
||||||
|
import org.apache.activemq.artemis.core.settings.impl.AddressSettings;
|
||||||
|
import org.apache.activemq.artemis.protocol.amqp.broker.ProtonProtocolManagerFactory;
|
||||||
|
import org.apache.qpid.jms.JmsConnectionFactory;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import javax.jms.Connection;
|
||||||
|
import javax.jms.Message;
|
||||||
|
import javax.jms.MessageConsumer;
|
||||||
|
import javax.jms.MessageProducer;
|
||||||
|
import javax.jms.Session;
|
||||||
|
import javax.jms.TextMessage;
|
||||||
|
|
||||||
|
public class AMQPMessageRedistributionTest extends ClusterTestBase {
|
||||||
|
|
||||||
|
final String queue = "exampleQueue";
|
||||||
|
final String broker0 = "amqp://localhost:61616";
|
||||||
|
final String broker1 = "amqp://localhost:61617";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
super.setUp();
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void start() throws Exception {
|
||||||
|
setupServers();
|
||||||
|
|
||||||
|
setRedistributionDelay(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isNetty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMessageRedistributionWithoutDupAMQP() throws Exception {
|
||||||
|
setupCluster(MessageLoadBalancingType.ON_DEMAND);
|
||||||
|
|
||||||
|
startServers(0, 1);
|
||||||
|
|
||||||
|
setupSessionFactory(0, isNetty());
|
||||||
|
setupSessionFactory(1, isNetty());
|
||||||
|
|
||||||
|
createQueue(0, queue, queue, null, true, null, null, RoutingType.ANYCAST);
|
||||||
|
createQueue(1, queue, queue, null, true, null, null, RoutingType.ANYCAST);
|
||||||
|
|
||||||
|
waitForBindings(0, queue, 1, 0, true);
|
||||||
|
waitForBindings(1, queue, 1, 0, true);
|
||||||
|
|
||||||
|
waitForBindings(0, queue, 1, 0, false);
|
||||||
|
waitForBindings(1, queue, 1, 0, false);
|
||||||
|
|
||||||
|
final int NUMBER_OF_MESSAGES = 20;
|
||||||
|
|
||||||
|
JmsConnectionFactory factory = new JmsConnectionFactory(broker0);
|
||||||
|
Connection connection = factory.createConnection();
|
||||||
|
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
|
||||||
|
MessageProducer producer = session.createProducer(session.createQueue(queue));
|
||||||
|
|
||||||
|
for (int i = 0; i < NUMBER_OF_MESSAGES; i++) {
|
||||||
|
producer.send(session.createTextMessage("hello " + i));
|
||||||
|
}
|
||||||
|
connection.close();
|
||||||
|
|
||||||
|
receiveOnBothNodes(NUMBER_OF_MESSAGES);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void receiveOnBothNodes(int NUMBER_OF_MESSAGES) throws Exception {
|
||||||
|
receiveBroker(broker1, 1);
|
||||||
|
receiveBroker(broker0, 1);
|
||||||
|
receiveBrokerAll(broker1, NUMBER_OF_MESSAGES - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void receiveBrokerAll(String brokerurl, int num) throws Exception {
|
||||||
|
JmsConnectionFactory factory = new JmsConnectionFactory(brokerurl);
|
||||||
|
Connection connection = factory.createConnection();
|
||||||
|
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
|
||||||
|
MessageConsumer consumer = session.createConsumer(session.createQueue(queue));
|
||||||
|
connection.start();
|
||||||
|
|
||||||
|
for (int i = 0; i < num; i++) {
|
||||||
|
TextMessage msg = (TextMessage) consumer.receive(5000);
|
||||||
|
assertNotNull(msg);
|
||||||
|
}
|
||||||
|
Message msg = consumer.receive(2000);
|
||||||
|
assertNull(msg);
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void receiveBroker(String brokerurl, int num) throws Exception {
|
||||||
|
JmsConnectionFactory factory = new JmsConnectionFactory(brokerurl);
|
||||||
|
Connection connection = factory.createConnection();
|
||||||
|
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
|
||||||
|
MessageConsumer consumer = session.createConsumer(session.createQueue(queue));
|
||||||
|
connection.start();
|
||||||
|
|
||||||
|
for (int i = 0; i < num; i++) {
|
||||||
|
Message msg = consumer.receive(5000);
|
||||||
|
assertNotNull(msg);
|
||||||
|
}
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setupCluster(final MessageLoadBalancingType messageLoadBalancingType) throws Exception {
|
||||||
|
setupClusterConnection("cluster0", queue, messageLoadBalancingType, 1, isNetty(), 0, 1);
|
||||||
|
|
||||||
|
setupClusterConnection("cluster1", queue, messageLoadBalancingType, 1, isNetty(), 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setRedistributionDelay(final long delay) {
|
||||||
|
AddressSettings as = new AddressSettings().setRedistributionDelay(delay);
|
||||||
|
|
||||||
|
getServer(0).getAddressSettingsRepository().addMatch("*", as);
|
||||||
|
getServer(1).getAddressSettingsRepository().addMatch("*", as);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setupServers() throws Exception {
|
||||||
|
setupServer(0, isFileStorage(), isNetty());
|
||||||
|
setupServer(1, isFileStorage(), isNetty());
|
||||||
|
|
||||||
|
servers[0].addProtocolManagerFactory(new ProtonProtocolManagerFactory());
|
||||||
|
servers[1].addProtocolManagerFactory(new ProtonProtocolManagerFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void stopServers() throws Exception {
|
||||||
|
closeAllConsumers();
|
||||||
|
|
||||||
|
closeAllSessionFactories();
|
||||||
|
|
||||||
|
closeAllServerLocatorsFactories();
|
||||||
|
|
||||||
|
stopServers(0, 1);
|
||||||
|
|
||||||
|
clearServer(0, 1);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue