GH-70 Allow the client to discover new brokers
Fixes https://github.com/spring-projects/spring-integration-kafka/issues/70 * Refactor the message listener container to finish tasks on broker connection failure and spawn new ones whenever a new broker is discovered; * Add an explicit disconnect operation on the ConnectionFactory to release the connection on failure; * Other improvements and test refactorings; Optimize the fetch logic Add test for recognizing new brokers Formatting Formatting and refactoring Test improvements, polishing
This commit is contained in:
committed by
Artem Bilan
parent
4ff25fd894
commit
d6a3b343aa
@@ -35,6 +35,12 @@ public interface ConnectionFactory {
|
||||
*/
|
||||
Connection connect(BrokerAddress brokerAddress);
|
||||
|
||||
/**
|
||||
* Close the connection to the broker
|
||||
* @param brokerAddress brokerAddress
|
||||
*/
|
||||
void disconnect(BrokerAddress brokerAddress);
|
||||
|
||||
/**
|
||||
* Retrieve the leaders for a set of partitions.
|
||||
* @param partitions whose leaders are queried
|
||||
|
||||
@@ -24,6 +24,13 @@ import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import com.gs.collections.api.block.function.Function;
|
||||
import com.gs.collections.api.block.function.Function2;
|
||||
import com.gs.collections.api.tuple.Pair;
|
||||
import com.gs.collections.impl.list.mutable.FastList;
|
||||
import com.gs.collections.impl.tuple.Tuples;
|
||||
import com.gs.collections.impl.utility.LazyIterate;
|
||||
import com.gs.collections.impl.utility.MapIterate;
|
||||
import kafka.api.FetchRequestBuilder;
|
||||
import kafka.api.PartitionOffsetRequestInfo;
|
||||
import kafka.cluster.Broker;
|
||||
@@ -45,18 +52,10 @@ import kafka.javaapi.TopicMetadataResponse;
|
||||
import kafka.javaapi.consumer.SimpleConsumer;
|
||||
import kafka.javaapi.message.ByteBufferMessageSet;
|
||||
import kafka.message.MessageAndOffset;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.gs.collections.api.block.function.Function;
|
||||
import com.gs.collections.api.block.function.Function2;
|
||||
import com.gs.collections.api.tuple.Pair;
|
||||
import com.gs.collections.impl.list.mutable.FastList;
|
||||
import com.gs.collections.impl.tuple.Tuples;
|
||||
import com.gs.collections.impl.utility.LazyIterate;
|
||||
import com.gs.collections.impl.utility.MapIterate;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A connection to a Kafka broker.
|
||||
@@ -123,8 +122,8 @@ public class DefaultConnection implements Connection {
|
||||
ResultBuilder<KafkaMessageBatch> resultBuilder = new ResultBuilder<KafkaMessageBatch>();
|
||||
for (final FetchRequest request : requests) {
|
||||
Partition partition = request.getPartition();
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Reading from " + partition + "@" + request.getOffset());
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Reading from " + partition + "@" + request.getOffset());
|
||||
}
|
||||
short errorCode = fetchResponse.errorCode(partition.getTopic(), partition.getId());
|
||||
if (ErrorMapping.NoError() == errorCode) {
|
||||
|
||||
@@ -24,13 +24,6 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.gs.collections.api.block.function.Function;
|
||||
import com.gs.collections.api.block.predicate.Predicate;
|
||||
import com.gs.collections.api.partition.PartitionIterable;
|
||||
@@ -38,13 +31,18 @@ import com.gs.collections.impl.block.factory.Functions;
|
||||
import com.gs.collections.impl.map.mutable.UnifiedMap;
|
||||
import com.gs.collections.impl.utility.Iterate;
|
||||
import com.gs.collections.impl.utility.ListIterate;
|
||||
|
||||
import kafka.client.ClientUtils$;
|
||||
import kafka.common.ErrorMapping;
|
||||
import kafka.javaapi.TopicMetadata;
|
||||
import kafka.javaapi.TopicMetadataResponse;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import scala.collection.JavaConversions;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ConnectionFactory}
|
||||
*
|
||||
@@ -60,8 +58,8 @@ public class DefaultConnectionFactory implements InitializingBean, ConnectionFac
|
||||
|
||||
private final Configuration configuration;
|
||||
|
||||
private final AtomicReference<MetadataCache> metadataCacheHolder =
|
||||
new AtomicReference<MetadataCache>(new MetadataCache(Collections.<TopicMetadata>emptySet()));
|
||||
private final AtomicReference<MetadataCache> metadataCacheHolder = new AtomicReference<MetadataCache>(
|
||||
new MetadataCache(Collections.<TopicMetadata>emptySet()));
|
||||
|
||||
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
|
||||
@@ -169,20 +167,14 @@ public class DefaultConnectionFactory implements InitializingBean, ConnectionFac
|
||||
public void refreshMetadata(Collection<String> topics) {
|
||||
try {
|
||||
this.lock.writeLock().lock();
|
||||
for (Connection connection : this.kafkaBrokersCache) {
|
||||
connection.close();
|
||||
}
|
||||
String brokerAddressesAsString =
|
||||
ListIterate.collect(this.configuration.getBrokerAddresses(), Functions.getToString())
|
||||
.makeString(",");
|
||||
TopicMetadataResponse topicMetadataResponse =
|
||||
new TopicMetadataResponse(
|
||||
ClientUtils$.MODULE$.fetchTopicMetadata(
|
||||
JavaConversions.asScalaSet(new HashSet<String>(topics)),
|
||||
ClientUtils$.MODULE$.parseBrokerList(brokerAddressesAsString),
|
||||
this.configuration.getClientId(), this.configuration.getFetchMetadataTimeout(), 0));
|
||||
PartitionIterable<TopicMetadata> selectWithoutErrors = Iterate.partition(topicMetadataResponse.topicsMetadata(),
|
||||
errorlessTopicMetadataPredicate);
|
||||
String brokerAddressesAsString = ListIterate
|
||||
.collect(this.configuration.getBrokerAddresses(), Functions.getToString()).makeString(",");
|
||||
TopicMetadataResponse topicMetadataResponse = new TopicMetadataResponse(ClientUtils$.MODULE$
|
||||
.fetchTopicMetadata(JavaConversions.asScalaSet(new HashSet<String>(topics)),
|
||||
ClientUtils$.MODULE$.parseBrokerList(brokerAddressesAsString),
|
||||
this.configuration.getClientId(), this.configuration.getFetchMetadataTimeout(), 0));
|
||||
PartitionIterable<TopicMetadata> selectWithoutErrors = Iterate
|
||||
.partition(topicMetadataResponse.topicsMetadata(), errorlessTopicMetadataPredicate);
|
||||
this.metadataCacheHolder.set(this.metadataCacheHolder.get().merge(selectWithoutErrors.getSelected()));
|
||||
if (log.isInfoEnabled()) {
|
||||
for (TopicMetadata topicMetadata : selectWithoutErrors.getRejected()) {
|
||||
@@ -196,6 +188,20 @@ public class DefaultConnectionFactory implements InitializingBean, ConnectionFac
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(BrokerAddress brokerAddress) {
|
||||
try {
|
||||
this.lock.writeLock().lock();
|
||||
Connection connection = this.kafkaBrokersCache.get(brokerAddress);
|
||||
if (connection != null) {
|
||||
connection.close();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ConnectionFactory#getPartitions(String)
|
||||
*/
|
||||
|
||||
@@ -21,16 +21,16 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.integration.kafka.core.KafkaMessage;
|
||||
import org.springframework.integration.kafka.core.Partition;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.gs.collections.api.block.procedure.Procedure;
|
||||
import com.gs.collections.api.block.procedure.Procedure2;
|
||||
import com.gs.collections.api.map.MutableMap;
|
||||
import com.gs.collections.impl.factory.Maps;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.integration.kafka.core.KafkaMessage;
|
||||
import org.springframework.integration.kafka.core.Partition;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Dispatches {@link KafkaMessage}s to a {@link MessageListener}. Messages may be
|
||||
@@ -71,14 +71,12 @@ class ConcurrentMessageListenerDispatcher {
|
||||
private boolean autoCommitOnError;
|
||||
|
||||
public ConcurrentMessageListenerDispatcher(Object delegateListener, ErrorHandler errorHandler,
|
||||
Collection<Partition> partitions, OffsetManager offsetManager,
|
||||
int consumers, int queueSize, Executor taskExecutor,
|
||||
boolean autoCommitOnError) {
|
||||
Assert.isTrue
|
||||
(delegateListener instanceof MessageListener
|
||||
|| delegateListener instanceof AcknowledgingMessageListener,
|
||||
"Either a " + MessageListener.class.getName() + " or a "
|
||||
+ AcknowledgingMessageListener.class.getName() + " must be provided");
|
||||
Collection<Partition> partitions, OffsetManager offsetManager, int consumers, int queueSize,
|
||||
Executor taskExecutor, boolean autoCommitOnError) {
|
||||
Assert.isTrue(
|
||||
delegateListener instanceof MessageListener || delegateListener instanceof AcknowledgingMessageListener,
|
||||
"Either a " + MessageListener.class.getName() + " or a " + AcknowledgingMessageListener.class.getName()
|
||||
+ " must be provided");
|
||||
Assert.notEmpty(partitions, "A set of partitions must be provided");
|
||||
Assert.isTrue(consumers <= partitions.size(),
|
||||
"The number of consumers must be smaller or equal to the number of partitions");
|
||||
@@ -120,17 +118,16 @@ class ConcurrentMessageListenerDispatcher {
|
||||
|
||||
private void initializeAndStartDispatching() {
|
||||
// allocate delegate instances index them
|
||||
List<QueueingMessageListenerInvoker> delegateList = new ArrayList<QueueingMessageListenerInvoker>(consumers);
|
||||
List<QueueingMessageListenerInvoker> delegateList = new ArrayList<>(consumers);
|
||||
for (int i = 0; i < consumers; i++) {
|
||||
QueueingMessageListenerInvoker queueingMessageListenerInvoker =
|
||||
new QueueingMessageListenerInvoker(queueSize, offsetManager, delegateListener, errorHandler,
|
||||
taskExecutor, autoCommitOnError);
|
||||
QueueingMessageListenerInvoker queueingMessageListenerInvoker = new QueueingMessageListenerInvoker(
|
||||
queueSize, offsetManager, delegateListener, errorHandler, taskExecutor, autoCommitOnError);
|
||||
delegateList.add(queueingMessageListenerInvoker);
|
||||
}
|
||||
|
||||
// evenly distribute partitions across delegates
|
||||
delegates = Maps.mutable.of();
|
||||
int i = 0;
|
||||
delegates = Maps.mutable.of();
|
||||
for (Partition partition : partitions) {
|
||||
delegates.put(partition, delegateList.get((i++) % consumers));
|
||||
}
|
||||
|
||||
@@ -16,25 +16,39 @@
|
||||
|
||||
package org.springframework.integration.kafka.listener;
|
||||
|
||||
import static com.gs.collections.impl.utility.ArrayIterate.flatCollect;
|
||||
import static com.gs.collections.impl.utility.Iterate.partition;
|
||||
import static com.gs.collections.impl.utility.MapIterate.forEachKeyValue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import com.gs.collections.api.RichIterable;
|
||||
import com.gs.collections.api.block.function.Function;
|
||||
import com.gs.collections.api.block.predicate.Predicate;
|
||||
import com.gs.collections.api.list.ImmutableList;
|
||||
import com.gs.collections.api.list.MutableList;
|
||||
import com.gs.collections.api.multimap.list.ImmutableListMultimap;
|
||||
import com.gs.collections.api.multimap.set.MutableSetMultimap;
|
||||
import com.gs.collections.api.partition.PartitionIterable;
|
||||
import com.gs.collections.api.set.MutableSet;
|
||||
import com.gs.collections.api.tuple.Pair;
|
||||
import com.gs.collections.impl.block.factory.Functions;
|
||||
import com.gs.collections.impl.block.function.checked.CheckedFunction;
|
||||
import com.gs.collections.impl.factory.Lists;
|
||||
import com.gs.collections.impl.factory.Sets;
|
||||
import com.gs.collections.impl.list.mutable.FastList;
|
||||
import com.gs.collections.impl.map.mutable.UnifiedMap;
|
||||
import com.gs.collections.impl.utility.ArrayIterate;
|
||||
import com.gs.collections.impl.utility.Iterate;
|
||||
import kafka.common.ErrorMapping;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.integration.kafka.core.BrokerAddress;
|
||||
import org.springframework.integration.kafka.core.ConnectionFactory;
|
||||
import org.springframework.integration.kafka.core.ConsumerException;
|
||||
@@ -47,26 +61,6 @@ import org.springframework.integration.kafka.core.Partition;
|
||||
import org.springframework.integration.kafka.core.Result;
|
||||
import org.springframework.scheduling.SchedulingAwareRunnable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import com.gs.collections.api.RichIterable;
|
||||
import com.gs.collections.api.block.function.Function;
|
||||
import com.gs.collections.api.block.predicate.Predicate;
|
||||
import com.gs.collections.api.block.procedure.Procedure;
|
||||
import com.gs.collections.api.block.procedure.Procedure2;
|
||||
import com.gs.collections.api.collection.MutableCollection;
|
||||
import com.gs.collections.api.list.ImmutableList;
|
||||
import com.gs.collections.api.list.MutableList;
|
||||
import com.gs.collections.api.multimap.MutableMultimap;
|
||||
import com.gs.collections.api.partition.PartitionIterable;
|
||||
import com.gs.collections.impl.block.factory.Functions;
|
||||
import com.gs.collections.impl.block.function.checked.CheckedFunction;
|
||||
import com.gs.collections.impl.factory.Lists;
|
||||
import com.gs.collections.impl.factory.Multimaps;
|
||||
import com.gs.collections.impl.list.mutable.FastList;
|
||||
import com.gs.collections.impl.utility.Iterate;
|
||||
|
||||
import kafka.common.ErrorMapping;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
@@ -79,16 +73,12 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
|
||||
private static final Log log = LogFactory.getLog(KafkaMessageListenerContainer.class);
|
||||
|
||||
public static final Function<Map.Entry<Partition, ?>, Partition> keyFunction = Functions.getKeyFunction();
|
||||
|
||||
private final GetOffsetForPartitionFunction getOffset = new GetOffsetForPartitionFunction();
|
||||
|
||||
private final PartitionToLeaderFunction getLeader = new PartitionToLeaderFunction();
|
||||
|
||||
private final Function<Partition, Partition> passThru = Functions.getPassThru();
|
||||
|
||||
private final LaunchFetchTaskProcedure launchFetchTask = new LaunchFetchTaskProcedure();
|
||||
|
||||
private final Object lifecycleMonitor = new Object();
|
||||
|
||||
private final KafkaTemplate kafkaTemplate;
|
||||
@@ -101,7 +91,7 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
|
||||
private Executor fetchTaskExecutor;
|
||||
|
||||
private Executor adminTaskExecutor = Executors.newSingleThreadExecutor();
|
||||
private Executor adminTaskExecutor;
|
||||
|
||||
private Executor dispatcherTaskExecutor;
|
||||
|
||||
@@ -125,7 +115,7 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
|
||||
private ConcurrentMessageListenerDispatcher messageDispatcher;
|
||||
|
||||
private final MutableMultimap<BrokerAddress, Partition> partitionsByBrokerMap = Multimaps.mutable.set.with();
|
||||
private final ConcurrentMap<BrokerAddress, FetchTask> fetchTasksByBroker = new ConcurrentHashMap<>();
|
||||
|
||||
private boolean autoCommitOnError = false;
|
||||
|
||||
@@ -159,11 +149,10 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
}
|
||||
|
||||
public void setMessageListener(Object messageListener) {
|
||||
Assert.isTrue
|
||||
(messageListener instanceof MessageListener
|
||||
|| messageListener instanceof AcknowledgingMessageListener,
|
||||
"Either a " + MessageListener.class.getName() + " or a "
|
||||
+ AcknowledgingMessageListener.class.getName() + " must be provided");
|
||||
Assert.isTrue(
|
||||
messageListener instanceof MessageListener || messageListener instanceof AcknowledgingMessageListener,
|
||||
"Either a " + MessageListener.class.getName() + " or a " + AcknowledgingMessageListener.class.getName()
|
||||
+ " must be provided");
|
||||
this.messageListener = messageListener;
|
||||
}
|
||||
|
||||
@@ -180,8 +169,8 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum number of concurrent {@link MessageListener}s running. Messages from within the same
|
||||
* partition will be processed sequentially.
|
||||
* The maximum number of concurrent {@link MessageListener}s running. Messages from
|
||||
* within the same partition will be processed sequentially.
|
||||
* @param concurrency the concurrency maximum number
|
||||
*/
|
||||
public void setConcurrency(int concurrency) {
|
||||
@@ -189,7 +178,8 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
}
|
||||
|
||||
/**
|
||||
* The timeout for waiting for each concurrent {@link MessageListener} to finish on stopping.
|
||||
* The timeout for waiting for each concurrent {@link MessageListener} to finish on
|
||||
* stopping.
|
||||
* @param stopTimeout timeout in milliseconds
|
||||
* @since 1.1
|
||||
*/
|
||||
@@ -206,21 +196,20 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
}
|
||||
|
||||
/**
|
||||
* The task executor for fetch operations
|
||||
* The task executor for fetch operations.
|
||||
* @param fetchTaskExecutor the Executor for fetch operations
|
||||
*/
|
||||
public void setFetchTaskExecutor(Executor fetchTaskExecutor) {
|
||||
this.fetchTaskExecutor = fetchTaskExecutor;
|
||||
}
|
||||
|
||||
|
||||
public Executor getAdminTaskExecutor() {
|
||||
return adminTaskExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* The task executor for leader and offset updates
|
||||
* @param adminTaskExecutor the task executor for leader and offset updates.
|
||||
* The task executor for leader, offset, and partition reassignment updates.
|
||||
* @param adminTaskExecutor the task executor for leader, offset and partition reassignment updates
|
||||
*/
|
||||
public void setAdminTaskExecutor(Executor adminTaskExecutor) {
|
||||
this.adminTaskExecutor = adminTaskExecutor;
|
||||
@@ -246,9 +235,9 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum number of messages that are buffered by each concurrent {@link MessageListener} runner.
|
||||
* Increasing the value may increase throughput, but also increases the memory consumption.
|
||||
* Must be a positive number and a power of 2.
|
||||
* The maximum number of messages that are buffered by each concurrent
|
||||
* {@link MessageListener} runner. Increasing the value may increase throughput, but
|
||||
* also increases the memory consumption. Must be a positive number and a power of 2.
|
||||
* @param queueSize the queue size
|
||||
*/
|
||||
public void setQueueSize(int queueSize) {
|
||||
@@ -268,7 +257,6 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
* component will try to continue processing incoming messages. In the latter case, it is possible that
|
||||
* a successful message will commit an offset after a series of failures, so the component should rely on
|
||||
* the `errorHandler` to capture failures.
|
||||
*
|
||||
* @param autoCommitOnError false if offsets should be committed only for successful messages
|
||||
* @since 1.3
|
||||
*/
|
||||
@@ -326,12 +314,20 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
Arrays.asList(partitions), offsetManager, concurrency, queueSize, dispatcherTaskExecutor,
|
||||
autoCommitOnError);
|
||||
this.messageDispatcher.start();
|
||||
partitionsByBrokerMap.clear();
|
||||
partitionsByBrokerMap.putAll(partitionsAsList.groupBy(getLeader));
|
||||
fetchTasksByBroker.clear();
|
||||
ImmutableListMultimap<BrokerAddress, Partition> partitionsByLeader = partitionsAsList
|
||||
.groupBy(getLeader);
|
||||
if (fetchTaskExecutor == null) {
|
||||
fetchTaskExecutor = Executors.newFixedThreadPool(partitionsByBrokerMap.keysView().size());
|
||||
fetchTaskExecutor = new SimpleAsyncTaskExecutor("kafka-fetch-");
|
||||
}
|
||||
if (adminTaskExecutor == null) {
|
||||
adminTaskExecutor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
for (Pair<BrokerAddress, RichIterable<Partition>> entry : partitionsByLeader.keyMultiValuePairsView()) {
|
||||
FetchTask fetchTask = new FetchTask(entry.getOne(), entry.getTwo());
|
||||
fetchTaskExecutor.execute(fetchTask);
|
||||
fetchTasksByBroker.put(entry.getOne(), fetchTask);
|
||||
}
|
||||
partitionsByBrokerMap.forEachKey(launchFetchTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,7 +348,8 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
}
|
||||
|
||||
private static Partition[] getPartitionsForTopics(final ConnectionFactory connectionFactory, String[] topics) {
|
||||
MutableList<Partition> partitionList = flatCollect(topics, new GetPartitionsForTopic(connectionFactory));
|
||||
MutableList<Partition> partitionList =
|
||||
ArrayIterate.flatCollect(topics, new GetPartitionsForTopic(connectionFactory));
|
||||
return partitionList.toArray(new Partition[partitionList.size()]);
|
||||
}
|
||||
|
||||
@@ -363,8 +360,21 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
|
||||
private final BrokerAddress brokerAddress;
|
||||
|
||||
public FetchTask(BrokerAddress brokerAddress) {
|
||||
private final MutableSet<Partition> listenedPartitions = Sets.mutable.<Partition>of().asSynchronized();
|
||||
|
||||
private volatile boolean active;
|
||||
|
||||
private final PartitionToFetchRequestFunction partitionToFetchRequestFunction =
|
||||
new PartitionToFetchRequestFunction();
|
||||
|
||||
private final IsLeaderErrorPredicate isLeaderPredicate = new IsLeaderErrorPredicate();
|
||||
|
||||
private final IsOffsetOutOfRangePredicate offsetOutOfRangePredicate = new IsOffsetOutOfRangePredicate();
|
||||
|
||||
public FetchTask(BrokerAddress brokerAddress, RichIterable<Partition> initialPartitions) {
|
||||
this.brokerAddress = brokerAddress;
|
||||
this.active = true;
|
||||
this.listenedPartitions.addAll(initialPartitions.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -372,117 +382,104 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean addListenedPartitionsIfActive(Iterable<Partition> partitions) {
|
||||
synchronized (listenedPartitions) {
|
||||
if (active) {
|
||||
listenedPartitions.addAllIterable(partitions);
|
||||
}
|
||||
return active;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
boolean wasInterrupted = false;
|
||||
while (isRunning()) {
|
||||
MutableCollection<Partition> fetchPartitions;
|
||||
synchronized (partitionsByBrokerMap) {
|
||||
// retrieve the partitions for the current polling cycle
|
||||
fetchPartitions = partitionsByBrokerMap.get(brokerAddress);
|
||||
// do not proceed until there is something to read from
|
||||
while (isRunning() && CollectionUtils.isEmpty(fetchPartitions)) {
|
||||
try {
|
||||
while (active && isRunning()) {
|
||||
synchronized (listenedPartitions) {
|
||||
try {
|
||||
// we only got here because there were no partitions to read from,
|
||||
// so block until there is a change this prevents FetchTasks
|
||||
// from busy waiting while leaders or offsets are being refreshed
|
||||
// TODO: ideally we should use separate monitors for each task
|
||||
partitionsByBrokerMap.wait();
|
||||
// see if the changes affect us
|
||||
fetchPartitions = partitionsByBrokerMap.get(brokerAddress);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
wasInterrupted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// we've just exited a potentially blocking operation. Is the component still running?
|
||||
if (isRunning()) {
|
||||
Set<Partition> partitionsWithRemainingData;
|
||||
boolean hasErrors;
|
||||
do {
|
||||
partitionsWithRemainingData = new HashSet<Partition>();
|
||||
hasErrors = false;
|
||||
try {
|
||||
MutableCollection<FetchRequest> fetchRequests =
|
||||
fetchPartitions.collect(new PartitionToFetchRequestFunction());
|
||||
Result<KafkaMessageBatch> result = kafkaTemplate.receive(fetchRequests);
|
||||
// process successful messages first
|
||||
Iterable<KafkaMessageBatch> batches = result.getResults().values();
|
||||
for (KafkaMessageBatch batch : batches) {
|
||||
if (!batch.getMessages().isEmpty()) {
|
||||
long highestFetchedOffset = 0;
|
||||
for (KafkaMessage kafkaMessage : batch.getMessages()) {
|
||||
// fetch operations may return entire blocks of compressed messages,
|
||||
// which may have lower offsets than the ones requested
|
||||
// thus a batch may contain messages that have been processed already
|
||||
if (kafkaMessage.getMetadata().getOffset() >= fetchOffsets.get(batch.getPartition())) {
|
||||
messageDispatcher.dispatch(kafkaMessage);
|
||||
}
|
||||
highestFetchedOffset =
|
||||
Math.max(highestFetchedOffset, kafkaMessage.getMetadata().getNextOffset());
|
||||
}
|
||||
fetchOffsets.replace(batch.getPartition(), highestFetchedOffset);
|
||||
// if there are still messages on server, we can go on and retrieve more
|
||||
if (highestFetchedOffset < batch.getHighWatermark()) {
|
||||
partitionsWithRemainingData.add(batch.getPartition());
|
||||
}
|
||||
if (!listenedPartitions.isEmpty()) {
|
||||
Result<KafkaMessageBatch> result = fetchAvailableData();
|
||||
handleSuccessful(result);
|
||||
if (result.getErrors().size() > 0) {
|
||||
handleErrors(result);
|
||||
}
|
||||
}
|
||||
// handle errors
|
||||
if (result.getErrors().size() > 0) {
|
||||
hasErrors = true;
|
||||
|
||||
// find partitions with leader errors and
|
||||
PartitionIterable<Map.Entry<Partition, Short>> partitionByLeaderErrors =
|
||||
partition(result.getErrors().entrySet(), new IsLeaderErrorPredicate());
|
||||
RichIterable<Partition> partitionsWithLeaderErrors =
|
||||
partitionByLeaderErrors.getSelected().collect(keyFunction);
|
||||
resetLeaders(partitionsWithLeaderErrors);
|
||||
|
||||
PartitionIterable<Map.Entry<Partition, Short>> partitionsWithOffsetsOutOfRange =
|
||||
partitionByLeaderErrors.getRejected()
|
||||
.partition(new IsOffsetOutOfRangePredicate());
|
||||
resetOffsets(partitionsWithOffsetsOutOfRange.getSelected()
|
||||
.collect(keyFunction)
|
||||
.toSet());
|
||||
// it's not a leader issue
|
||||
stopFetchingFromPartitions(partitionsWithOffsetsOutOfRange.getRejected()
|
||||
.collect(keyFunction));
|
||||
else {
|
||||
active = false;
|
||||
}
|
||||
}
|
||||
catch (ConsumerException e) {
|
||||
resetLeaders(fetchPartitions.toImmutable());
|
||||
active = false;
|
||||
// the connection is broken, terminate the task
|
||||
kafkaTemplate.getConnectionFactory().disconnect(brokerAddress);
|
||||
resetLeaders(listenedPartitions.toImmutable());
|
||||
}
|
||||
} while (!hasErrors && isRunning() && !partitionsWithRemainingData.isEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wasInterrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
finally {
|
||||
active = false;
|
||||
synchronized (fetchTasksByBroker) {
|
||||
if (fetchTasksByBroker.get(brokerAddress) == this) {
|
||||
fetchTasksByBroker.remove(brokerAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Result<KafkaMessageBatch> fetchAvailableData() {
|
||||
return kafkaTemplate.receive(listenedPartitions.collect(partitionToFetchRequestFunction));
|
||||
}
|
||||
|
||||
private void handleSuccessful(Result<KafkaMessageBatch> result) {
|
||||
Iterable<KafkaMessageBatch> batches = result.getResults().values();
|
||||
for (KafkaMessageBatch batch : batches) {
|
||||
if (!batch.getMessages().isEmpty()) {
|
||||
long highestFetchedOffset = 0;
|
||||
for (KafkaMessage kafkaMessage : batch.getMessages()) {
|
||||
// fetch operations may return entire blocks of compressed messages,
|
||||
// which may have lower offsets than the ones requested
|
||||
// thus a batch may contain messages that have been processed already
|
||||
if (kafkaMessage.getMetadata().getOffset() >= fetchOffsets.get(batch.getPartition())) {
|
||||
messageDispatcher.dispatch(kafkaMessage);
|
||||
}
|
||||
highestFetchedOffset = Math.max(highestFetchedOffset, kafkaMessage.getMetadata().getNextOffset());
|
||||
}
|
||||
fetchOffsets.replace(batch.getPartition(), highestFetchedOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleErrors(Result<KafkaMessageBatch> result) {
|
||||
Map<Partition, Short> errors = result.getErrors();
|
||||
PartitionIterable<Map.Entry<Partition, Short>> splitByLeaderError =
|
||||
Iterate.partition(errors.entrySet(), isLeaderPredicate);
|
||||
RichIterable<Partition> partitionsWithLeaderErrors = splitByLeaderError.getSelected()
|
||||
.collect(Functions.<Partition>getKeyFunction());
|
||||
resetLeaders(partitionsWithLeaderErrors);
|
||||
PartitionIterable<Map.Entry<Partition, Short>> splitByOffsetError =
|
||||
splitByLeaderError.getRejected().partition(offsetOutOfRangePredicate);
|
||||
RichIterable<Partition> partitionsWithWrongOffsets =
|
||||
splitByOffsetError.getSelected().collect(Functions.<Partition>getKeyFunction());
|
||||
resetOffsets(partitionsWithWrongOffsets.toSet());
|
||||
// it's not a leader issue, remove everything else
|
||||
RichIterable<Partition> remainingPartitionsWithErrors
|
||||
= splitByOffsetError.getRejected().collect(Functions.<Partition>getKeyFunction());
|
||||
listenedPartitions.removeAllIterable(remainingPartitionsWithErrors);
|
||||
}
|
||||
|
||||
private void resetLeaders(final Iterable<Partition> partitionsToReset) {
|
||||
stopFetchingFromPartitions(partitionsToReset);
|
||||
listenedPartitions.removeAllIterable(partitionsToReset);
|
||||
adminTaskExecutor.execute(new UpdateLeadersTask(partitionsToReset));
|
||||
}
|
||||
|
||||
|
||||
private void resetOffsets(final Collection<Partition> partitionsToResetOffsets) {
|
||||
stopFetchingFromPartitions(partitionsToResetOffsets);
|
||||
listenedPartitions.removeAllIterable(partitionsToResetOffsets);
|
||||
adminTaskExecutor.execute(new UpdateOffsetsTask(partitionsToResetOffsets));
|
||||
}
|
||||
|
||||
private void stopFetchingFromPartitions(Iterable<Partition> partitions) {
|
||||
synchronized (partitionsByBrokerMap) {
|
||||
for (Partition partition : partitions) {
|
||||
partitionsByBrokerMap.remove(brokerAddress, partition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateLeadersTask implements SchedulingAwareRunnable {
|
||||
|
||||
private final Iterable<Partition> partitionsToReset;
|
||||
|
||||
public UpdateLeadersTask(Iterable<Partition> partitionsToReset) {
|
||||
@@ -503,10 +500,23 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
FastList<Partition> partitionsAsList = FastList.newList(partitionsToReset);
|
||||
FastList<String> topics = partitionsAsList.collect(new PartitionToTopicFunction()).distinct();
|
||||
kafkaTemplate.getConnectionFactory().refreshMetadata(topics);
|
||||
Map<Partition, BrokerAddress> leaders = kafkaTemplate.getConnectionFactory().getLeaders(partitionsToReset);
|
||||
synchronized (partitionsByBrokerMap) {
|
||||
forEachKeyValue(leaders, new AddPartitionToBrokerProcedure());
|
||||
partitionsByBrokerMap.notifyAll();
|
||||
|
||||
MutableSetMultimap<BrokerAddress, Partition> partitionsByBroker = UnifiedMap
|
||||
.newMap(kafkaTemplate.getConnectionFactory().getLeaders(partitionsToReset)).flip();
|
||||
for (Pair<BrokerAddress, RichIterable<Partition>> pair : partitionsByBroker
|
||||
.keyMultiValuePairsView()) {
|
||||
synchronized (fetchTasksByBroker) {
|
||||
boolean addedSuccessfully = false;
|
||||
FetchTask fetchTask = fetchTasksByBroker.get(pair.getOne());
|
||||
if (fetchTask != null) {
|
||||
addedSuccessfully = fetchTask.addListenedPartitionsIfActive(pair.getTwo());
|
||||
}
|
||||
if (!addedSuccessfully) {
|
||||
fetchTask = new FetchTask(pair.getOne(), pair.getTwo());
|
||||
fetchTaskExecutor.execute(fetchTask);
|
||||
fetchTasksByBroker.put(pair.getOne(), fetchTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchCompleted = true;
|
||||
}
|
||||
@@ -517,7 +527,8 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
}
|
||||
catch (InterruptedException e1) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.error("Interrupted after refresh leaders failure for: " + Iterate.makeString(partitionsToReset,","));
|
||||
log.error("Interrupted after refresh leaders failure for: " + Iterate
|
||||
.makeString(partitionsToReset, ","));
|
||||
fetchCompleted = true;
|
||||
}
|
||||
}
|
||||
@@ -541,12 +552,17 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
for (Partition partition : partitionsToResetOffsets) {
|
||||
fetchOffsets.replace(partition, offsetManager.getOffset(partition));
|
||||
}
|
||||
synchronized (partitionsByBrokerMap) {
|
||||
for (Partition partitionsToResetOffset : partitionsToResetOffsets) {
|
||||
partitionsByBrokerMap.put(brokerAddress, partitionsToResetOffset);
|
||||
synchronized (fetchTasksByBroker) {
|
||||
boolean addedSuccessfully = false;
|
||||
FetchTask fetchTask = fetchTasksByBroker.get(brokerAddress);
|
||||
if (fetchTask != null) {
|
||||
addedSuccessfully = fetchTask.addListenedPartitionsIfActive(partitionsToResetOffsets);
|
||||
}
|
||||
if (!addedSuccessfully) {
|
||||
fetchTask = new FetchTask(brokerAddress, Sets.immutable.ofAll(partitionsToResetOffsets));
|
||||
fetchTaskExecutor.execute(fetchTask);
|
||||
fetchTasksByBroker.put(brokerAddress, fetchTask);
|
||||
}
|
||||
// notify any waiting task that the partition allocation has changed
|
||||
partitionsByBrokerMap.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,8 +573,8 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
|
||||
@Override
|
||||
public boolean accept(Map.Entry<Partition, Short> each) {
|
||||
return each.getValue() == ErrorMapping.NotLeaderForPartitionCode()
|
||||
|| each.getValue() == ErrorMapping.UnknownTopicOrPartitionCode();
|
||||
return each.getValue() == ErrorMapping.NotLeaderForPartitionCode() || each.getValue() == ErrorMapping
|
||||
.UnknownTopicOrPartitionCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -600,16 +616,6 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private class LaunchFetchTaskProcedure implements Procedure<BrokerAddress> {
|
||||
|
||||
@Override
|
||||
public void value(BrokerAddress brokerAddress) {
|
||||
fetchTaskExecutor.execute(new FetchTask(brokerAddress));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private class PartitionToFetchRequestFunction implements Function<Partition, FetchRequest> {
|
||||
|
||||
@@ -646,14 +652,4 @@ public class KafkaMessageListenerContainer implements SmartLifecycle {
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private class AddPartitionToBrokerProcedure implements Procedure2<Partition, BrokerAddress> {
|
||||
|
||||
@Override
|
||||
public void value(Partition partition, BrokerAddress newBrokerAddress) {
|
||||
partitionsByBrokerMap.put(newBrokerAddress, partition);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.gs.collections.api.RichIterable;
|
||||
import com.gs.collections.api.block.function.Function2;
|
||||
@@ -38,11 +38,13 @@ import kafka.utils.TestUtils;
|
||||
import org.I0Itec.zkclient.ZkClient;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.kafka.clients.producer.Callback;
|
||||
import org.apache.kafka.clients.producer.KafkaProducer;
|
||||
import org.apache.kafka.clients.producer.Producer;
|
||||
import org.apache.kafka.clients.producer.ProducerRecord;
|
||||
import org.apache.kafka.clients.producer.RecordMetadata;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import scala.collection.JavaConversions;
|
||||
import scala.collection.Map;
|
||||
import scala.collection.immutable.List$;
|
||||
@@ -66,6 +68,8 @@ public abstract class AbstractBrokerTests {
|
||||
|
||||
public static final String TEST_TOPIC = "test-topic";
|
||||
|
||||
private static final long SEND_TIMEOUT = 10000;
|
||||
|
||||
public abstract KafkaRule getKafkaRule();
|
||||
|
||||
@After
|
||||
@@ -136,6 +140,16 @@ public abstract class AbstractBrokerTests {
|
||||
return new Sender<>(producer);
|
||||
}
|
||||
|
||||
public Sender<String, String> createMessageSender(String compression, int brokerIndex) {
|
||||
Properties producerConfig = new Properties();
|
||||
producerConfig.setProperty("bootstrap.servers", getKafkaRule().getBrokerAddresses()[brokerIndex].toString());
|
||||
producerConfig.setProperty("compression.type", compression);
|
||||
KafkaProducer<String, String> producer = new KafkaProducer<>(producerConfig,
|
||||
new EncoderAdaptingSerializer<>(new StringEncoder()),
|
||||
new EncoderAdaptingSerializer<>(new StringEncoder()));
|
||||
return new Sender<>(producer);
|
||||
}
|
||||
|
||||
public ConnectionFactory getKafkaBrokerConnectionFactory() throws Exception {
|
||||
DefaultConnectionFactory connectionFactory = new DefaultConnectionFactory(getKafkaConfiguration());
|
||||
connectionFactory.afterPropertiesSet();
|
||||
@@ -175,21 +189,30 @@ public abstract class AbstractBrokerTests {
|
||||
}
|
||||
|
||||
public void send(Collection<ProducerRecord<K,V>> records) {
|
||||
Future<RecordMetadata> lastFuture = null;
|
||||
final CountDownLatch sendLatch = new CountDownLatch(records.size());
|
||||
final ArrayList<Exception> exceptions = new ArrayList<>();
|
||||
for (ProducerRecord<K, V> record : records) {
|
||||
lastFuture = producer.send(record);
|
||||
producer.send(record, new Callback() {
|
||||
@Override
|
||||
public void onCompletion(RecordMetadata metadata, Exception exception) {
|
||||
sendLatch.countDown();
|
||||
if (exception != null) {
|
||||
exceptions.add(exception);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// only block if there is at least one message to be sent
|
||||
if (lastFuture != null) {
|
||||
try {
|
||||
// block until the last message has been sent, so we make this deterministic
|
||||
lastFuture.get();
|
||||
} catch (InterruptedException e) {
|
||||
// not being able to confirm that all messages have been sent, fail the test
|
||||
throw new RuntimeException(e);
|
||||
} catch (ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
try {
|
||||
sendLatch.await(SEND_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (exceptions.size() > 0) {
|
||||
for (Exception exception : exceptions) {
|
||||
log.error("Error while sending messages:", exception);
|
||||
}
|
||||
Assert.fail("Cannot send messages");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.integration.kafka.listener;
|
||||
|
||||
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
|
||||
import static org.hamcrest.core.IsEqual.equalTo;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.springframework.integration.kafka.util.MessageUtils.decodeKey;
|
||||
import static org.springframework.integration.kafka.util.MessageUtils.decodePayload;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.gs.collections.api.multimap.list.MutableListMultimap;
|
||||
import com.gs.collections.impl.multimap.list.SynchronizedPutFastListMultimap;
|
||||
import kafka.serializer.StringDecoder;
|
||||
import kafka.utils.VerifiableProperties;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.integration.kafka.core.BrokerAddressListConfiguration;
|
||||
import org.springframework.integration.kafka.core.ConnectionFactory;
|
||||
import org.springframework.integration.kafka.core.DefaultConnectionFactory;
|
||||
import org.springframework.integration.kafka.core.KafkaMessage;
|
||||
import org.springframework.integration.kafka.core.Partition;
|
||||
import org.springframework.integration.kafka.rule.KafkaEmbedded;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
|
||||
public class NewBrokerRecoveryTests extends AbstractMessageListenerContainerTests {
|
||||
|
||||
@Rule
|
||||
public KafkaEmbedded kafkaEmbeddedBrokerRule = new KafkaEmbedded(2);
|
||||
|
||||
@Override
|
||||
public KafkaEmbedded getKafkaRule() {
|
||||
return kafkaEmbeddedBrokerRule;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompleteShutdown() throws Exception {
|
||||
int partitionCount = 1;
|
||||
|
||||
createTopic(TEST_TOPIC, partitionCount, 2, 2);
|
||||
|
||||
// stop one Kafka instance - the partition is now underreplicated and only one
|
||||
kafkaEmbeddedBrokerRule.bounce(0);
|
||||
|
||||
// connect to the live broker only
|
||||
ConnectionFactory connectionFactory =
|
||||
new DefaultConnectionFactory(new BrokerAddressListConfiguration(kafkaEmbeddedBrokerRule.getBrokerAddress(1)));
|
||||
ArrayList<Partition> readPartitions = new ArrayList<>();
|
||||
readPartitions.add(new Partition(TEST_TOPIC, 0));
|
||||
final KafkaMessageListenerContainer kafkaMessageListenerContainer =
|
||||
new KafkaMessageListenerContainer(connectionFactory,
|
||||
readPartitions.toArray(new Partition[readPartitions.size()]));
|
||||
kafkaMessageListenerContainer.setMaxFetch(100);
|
||||
kafkaMessageListenerContainer.setConcurrency(1);
|
||||
|
||||
final int expectedMessageCount = 200;
|
||||
createMessageSender("none", 1).send(createMessagesInRange(0, 49, TEST_TOPIC, partitionCount));
|
||||
|
||||
final MutableListMultimap<Integer, KeyedMessageWithOffset> receivedData =
|
||||
new SynchronizedPutFastListMultimap<Integer, KeyedMessageWithOffset>();
|
||||
final CountDownLatch latch = new CountDownLatch(expectedMessageCount);
|
||||
kafkaMessageListenerContainer.setMessageListener(new MessageListener() {
|
||||
|
||||
@Override
|
||||
public void onMessage(KafkaMessage message) {
|
||||
StringDecoder decoder = new StringDecoder(new VerifiableProperties());
|
||||
receivedData.put(message.getMetadata().getPartition().getId(),
|
||||
new KeyedMessageWithOffset(decodeKey(message, decoder), decodePayload(message, decoder),
|
||||
message.getMetadata().getOffset(), Thread.currentThread().getName(),
|
||||
message.getMetadata().getPartition().getId()));
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
kafkaMessageListenerContainer.start();
|
||||
|
||||
// now start sending messages again
|
||||
createMessageSender("none", 1).send(createMessagesInRange(50, 99, TEST_TOPIC, partitionCount));
|
||||
|
||||
// start the other Kafka instance
|
||||
kafkaEmbeddedBrokerRule.restart(0);
|
||||
// sleep to let the brokers sync up
|
||||
kafkaEmbeddedBrokerRule.waitUntilSynced(TEST_TOPIC, 0);
|
||||
// bounce the other server
|
||||
kafkaEmbeddedBrokerRule.bounce(1);
|
||||
|
||||
// now start sending messages again
|
||||
createMessageSender("none", 0).send(createMessagesInRange(100, 149, TEST_TOPIC, partitionCount));
|
||||
|
||||
// stop the other Kafka instance
|
||||
kafkaEmbeddedBrokerRule.restart(1);
|
||||
// sleep to let the brokers sync up
|
||||
kafkaEmbeddedBrokerRule.waitUntilSynced(TEST_TOPIC, 1);
|
||||
|
||||
// bounce the other server
|
||||
kafkaEmbeddedBrokerRule.bounce(0);
|
||||
|
||||
// now start sending messages again
|
||||
createMessageSender("none", 1).send(createMessagesInRange(149, 199, TEST_TOPIC, partitionCount));
|
||||
|
||||
|
||||
latch.await(50, TimeUnit.SECONDS);
|
||||
kafkaMessageListenerContainer.stop();
|
||||
|
||||
assertThat(receivedData.valuesView().toList(), hasSize(expectedMessageCount));
|
||||
assertThat(latch.getCount(), equalTo(0L));
|
||||
|
||||
validateMessageReceipt(receivedData, 1, partitionCount, expectedMessageCount, 1);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -100,6 +100,7 @@ public class SingleBrokerRecoveryTests extends AbstractMessageListenerContainerT
|
||||
|
||||
// restart Kafka
|
||||
kafkaEmbeddedBrokerRule.restart(0);
|
||||
kafkaEmbeddedBrokerRule.waitUntilSynced(TEST_TOPIC,0);
|
||||
|
||||
// now start sending messages again
|
||||
createMessageSender("none").send(createMessages(90, TEST_TOPIC,partitionCount));
|
||||
@@ -109,7 +110,6 @@ public class SingleBrokerRecoveryTests extends AbstractMessageListenerContainerT
|
||||
|
||||
assertThat(receivedData.valuesView().toList(), hasSize(expectedMessageCount));
|
||||
assertThat(latch.getCount(), equalTo(0L));
|
||||
System.out.println("All messages received ... checking ");
|
||||
|
||||
validateMessageReceipt(receivedData, 1, partitionCount, expectedMessageCount, 1);
|
||||
|
||||
|
||||
@@ -17,14 +17,20 @@
|
||||
|
||||
package org.springframework.integration.kafka.rule;
|
||||
|
||||
import static scala.collection.JavaConversions.asScalaBuffer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.gs.collections.api.block.function.Function;
|
||||
import com.gs.collections.impl.list.mutable.FastList;
|
||||
import com.gs.collections.impl.utility.ListIterate;
|
||||
import kafka.admin.AdminUtils$;
|
||||
import kafka.api.PartitionMetadata;
|
||||
import kafka.api.TopicMetadata;
|
||||
import kafka.cluster.Broker;
|
||||
import kafka.server.KafkaConfig;
|
||||
import kafka.server.KafkaServer;
|
||||
import kafka.server.NotRunning;
|
||||
@@ -34,10 +40,14 @@ import kafka.utils.TestZKUtils;
|
||||
import kafka.utils.Utils;
|
||||
import kafka.utils.ZKStringSerializer$;
|
||||
import kafka.zk.EmbeddedZookeeper;
|
||||
|
||||
import org.I0Itec.zkclient.ZkClient;
|
||||
import org.I0Itec.zkclient.exception.ZkInterruptedException;
|
||||
import org.apache.kafka.common.protocol.Errors;
|
||||
import org.junit.rules.ExternalResource;
|
||||
import scala.collection.JavaConversions;
|
||||
import scala.collection.Map;
|
||||
import scala.collection.Set;
|
||||
|
||||
import org.springframework.integration.kafka.core.BrokerAddress;
|
||||
import org.springframework.retry.RetryCallback;
|
||||
import org.springframework.retry.RetryContext;
|
||||
@@ -45,12 +55,6 @@ import org.springframework.retry.backoff.ExponentialBackOffPolicy;
|
||||
import org.springframework.retry.policy.SimpleRetryPolicy;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
|
||||
import scala.collection.JavaConversions;
|
||||
|
||||
import com.gs.collections.api.block.function.Function;
|
||||
import com.gs.collections.impl.list.mutable.FastList;
|
||||
import com.gs.collections.impl.utility.ListIterate;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
* @author Artem Bilan
|
||||
@@ -58,6 +62,8 @@ import com.gs.collections.impl.utility.ListIterate;
|
||||
@SuppressWarnings("serial")
|
||||
public class KafkaEmbedded extends ExternalResource implements KafkaRule {
|
||||
|
||||
public static final long METADATA_PROPAGATION_TIMEOUT = 10000L;
|
||||
|
||||
private int count;
|
||||
|
||||
private boolean controlledShutdown;
|
||||
@@ -154,6 +160,11 @@ public class KafkaEmbedded extends ExternalResource implements KafkaRule {
|
||||
return zookeeper.connectString();
|
||||
}
|
||||
|
||||
public BrokerAddress getBrokerAddress(int i) {
|
||||
KafkaServer kafkaServer = this.kafkaServers.get(i);
|
||||
return new BrokerAddress(kafkaServer.config().hostName(),kafkaServer.config().port());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrokerAddress[] getBrokerAddresses() {
|
||||
return ListIterate.collect(this.kafkaServers,
|
||||
@@ -184,8 +195,36 @@ public class KafkaEmbedded extends ExternalResource implements KafkaRule {
|
||||
public void bounce(int index, boolean waitForPropagation) {
|
||||
kafkaServers.get(index).shutdown();
|
||||
if (waitForPropagation) {
|
||||
TestUtils.waitUntilMetadataIsPropagated(asScalaBuffer(kafkaServers), "test-topic", 0, 5000L);
|
||||
long initialTime = System.currentTimeMillis();
|
||||
boolean canExit = false;
|
||||
do {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
canExit = true;
|
||||
Map<String, Properties> topicProperties = AdminUtils$.MODULE$.fetchAllTopicConfigs(getZkClient());
|
||||
Set<TopicMetadata> topicMetadatas =
|
||||
AdminUtils$.MODULE$.fetchTopicMetadataFromZk(topicProperties.keySet(), getZkClient());
|
||||
for (TopicMetadata topicMetadata : JavaConversions.asJavaCollection(topicMetadatas)) {
|
||||
if (Errors.forCode(topicMetadata.errorCode()).exception() == null) {
|
||||
for (PartitionMetadata partitionMetadata :
|
||||
JavaConversions.asJavaCollection(topicMetadata.partitionsMetadata())) {
|
||||
Collection<Broker> inSyncReplicas = JavaConversions.asJavaCollection(partitionMetadata.isr());
|
||||
for (Broker broker : inSyncReplicas) {
|
||||
if (broker.id() == index) {
|
||||
canExit = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!canExit && (System.currentTimeMillis() - initialTime < METADATA_PROPAGATION_TIMEOUT));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void bounce(int index) {
|
||||
@@ -212,13 +251,44 @@ public class KafkaEmbedded extends ExternalResource implements KafkaRule {
|
||||
retryTemplate.execute(new RetryCallback<Void, Exception>() {
|
||||
@Override
|
||||
public Void doWithRetry(RetryContext context) throws Exception {
|
||||
System.out.println("Retrying restart");
|
||||
kafkaServers.get(index).startup();
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void waitUntilSynced(String topic, int brokerId) {
|
||||
long initialTime = System.currentTimeMillis();
|
||||
boolean canExit = false;
|
||||
do {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
canExit = true;
|
||||
TopicMetadata topicMetadata = AdminUtils$.MODULE$.fetchTopicMetadataFromZk(topic, getZkClient());
|
||||
if (Errors.forCode(topicMetadata.errorCode()).exception() == null) {
|
||||
for (PartitionMetadata partitionMetadata :
|
||||
JavaConversions.asJavaCollection(topicMetadata.partitionsMetadata())) {
|
||||
Collection<Broker> isr = JavaConversions.asJavaCollection(partitionMetadata.isr());
|
||||
boolean containsIndex = false;
|
||||
for (Broker broker : isr) {
|
||||
if (broker.id() == brokerId) {
|
||||
containsIndex = true;
|
||||
}
|
||||
}
|
||||
if (!containsIndex) {
|
||||
canExit = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!canExit && (System.currentTimeMillis() - initialTime < METADATA_PROPAGATION_TIMEOUT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBrokersAsString() {
|
||||
return FastList.newList(Arrays.asList(getBrokerAddresses()))
|
||||
|
||||
@@ -7,3 +7,4 @@ log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss.SSS} %-5p [%t][%c] %m
|
||||
log4j.category.org.springframework.integration=WARN
|
||||
log4j.category.org.springframework.integration.kafka=INFO
|
||||
log4j.category.org.apache.kafka.common.network.Selector=ERROR
|
||||
log4j.category.kafka.server.ReplicaFetcherThread=ERROR
|
||||
|
||||
Reference in New Issue
Block a user