diff --git a/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java b/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java index a328aec5d70..0fcb22a0446 100644 --- a/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java +++ b/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java @@ -39,6 +39,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; + import org.apache.commons.lang.Validate; import org.apache.kafka.clients.admin.Admin; import org.apache.kafka.clients.consumer.Consumer; @@ -154,7 +155,7 @@ public void open(Map conf, TopologyContext context, SpoutOutputC tupleListener.open(conf, context); this.kafkaOffsetMetricManager - = new KafkaOffsetMetricManager<>(() -> Collections.unmodifiableMap(offsetManagers), () -> admin, context); + = new KafkaOffsetMetricManager<>(() -> Collections.unmodifiableMap(offsetManagers), () -> admin, context); LOG.info("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig); } @@ -183,7 +184,7 @@ public void onPartitionsRevoked(Collection partitions) { previousAssignment = partitions; LOG.info("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]", - kafkaSpoutConfig.getConsumerGroupId(), consumer, partitions); + kafkaSpoutConfig.getConsumerGroupId(), consumer, partitions); if (isAtLeastOnceProcessing()) { commitOffsetsForAckedTuples(); @@ -193,7 +194,7 @@ public void onPartitionsRevoked(Collection partitions) { @Override public void onPartitionsAssigned(Collection partitions) { LOG.info("Partitions reassignment. [task-ID={}, consumer-group={}, consumer={}, topic-partitions={}]", - context.getThisTaskId(), kafkaSpoutConfig.getConsumerGroupId(), consumer, partitions); + context.getThisTaskId(), kafkaSpoutConfig.getConsumerGroupId(), consumer, partitions); initialize(partitions); tupleListener.onPartitionsReassigned(partitions); @@ -221,7 +222,7 @@ private void initialize(Collection partitions) { final OffsetAndMetadata committedOffset = consumer.committed(newTp); final long fetchOffset = doSeek(newTp, committedOffset); LOG.debug("Set consumer position to [{}] for topic-partition [{}] with [{}] and committed offset [{}]", - fetchOffset, newTp, firstPollOffsetStrategy, committedOffset); + fetchOffset, newTp, firstPollOffsetStrategy, committedOffset); if (isAtLeastOnceProcessing() && !offsetManagers.containsKey(newTp)) { offsetManagers.put(newTp, new OffsetManager(newTp, fetchOffset)); } @@ -234,13 +235,13 @@ private void initialize(Collection partitions) { */ private long doSeek(TopicPartition newTp, OffsetAndMetadata committedOffset) { LOG.trace("Seeking offset for topic-partition [{}] with [{}] and committed offset [{}]", - newTp, firstPollOffsetStrategy, committedOffset); + newTp, firstPollOffsetStrategy, committedOffset); if (committedOffset != null) { // offset was previously committed for this consumer group and topic-partition, either by this or another topology. if (commitMetadataManager.isOffsetCommittedByThisTopology(newTp, - committedOffset, - Collections.unmodifiableMap(offsetManagers))) { + committedOffset, + Collections.unmodifiableMap(offsetManagers))) { // Another KafkaSpout instance (of this topology) already committed, therefore FirstPollOffsetStrategy does not apply. consumer.seek(newTp, committedOffset.offset()); } else { @@ -281,7 +282,7 @@ public void nextTuple() { commitOffsetsForAckedTuples(); } else if (kafkaSpoutConfig.getProcessingGuarantee() == ProcessingGuarantee.NO_GUARANTEE) { Map offsetsToCommit = - createFetchedOffsetsMetadata(consumer.assignment()); + createFetchedOffsetsMetadata(consumer.assignment()); consumer.commitAsync(offsetsToCommit, null); LOG.debug("Committed offsets {} to Kafka", offsetsToCommit); } @@ -336,7 +337,7 @@ private PollablePartitionsInfo getPollablePartitionsInfo() { pollablePartitions.add(tp); } else { LOG.debug("Not polling on partition [{}]. It has [{}] uncommitted offsets, which exceeds the limit of [{}]. ", tp, - numUncommittedOffsets, maxUncommittedOffsets); + numUncommittedOffsets, maxUncommittedOffsets); } } } @@ -345,7 +346,7 @@ private PollablePartitionsInfo getPollablePartitionsInfo() { private boolean isWaitingToEmit() { return waitingToEmit.values().stream() - .anyMatch(list -> !list.isEmpty()); + .anyMatch(list -> !list.isEmpty()); } private void setWaitingToEmit(ConsumerRecords consumerRecords) { @@ -365,11 +366,11 @@ private ConsumerRecords pollKafkaBroker(PollablePartitionsInfo pollablePar ackRetriableOffsetsIfCompactedAway(pollablePartitionsInfo.pollableEarliestRetriableOffsets, consumerRecords); final int numPolledRecords = consumerRecords.count(); LOG.debug("Polled [{}] records from Kafka", - numPolledRecords); + numPolledRecords); if (kafkaSpoutConfig.getProcessingGuarantee() == KafkaSpoutConfig.ProcessingGuarantee.AT_MOST_ONCE) { //Commit polled records immediately to ensure delivery is at-most-once. Map offsetsToCommit = - createFetchedOffsetsMetadata(consumer.assignment()); + createFetchedOffsetsMetadata(consumer.assignment()); consumer.commitSync(offsetsToCommit); LOG.debug("Committed offsets {} to Kafka", offsetsToCommit); } @@ -387,7 +388,7 @@ private void doSeekRetriableTopicPartitions(Map pollableEa } private void ackRetriableOffsetsIfCompactedAway(Map earliestRetriableOffsets, - ConsumerRecords consumerRecords) { + ConsumerRecords consumerRecords) { for (Entry entry : earliestRetriableOffsets.entrySet()) { TopicPartition tp = entry.getKey(); List> records = consumerRecords.records(tp); @@ -529,7 +530,7 @@ private void commitOffsetsForAckedTuples() { * to the committed offset. */ LOG.debug("Consumer fell behind committed offset. Catching up. Position was [{}], skipping to [{}]", - position, committedOffset); + position, committedOffset); consumer.seek(tp, committedOffset); } /** @@ -539,8 +540,8 @@ private void commitOffsetsForAckedTuples() { if (waitingToEmitForTp != null) { //Discard the pending records that are already committed waitingToEmit.put(tp, waitingToEmitForTp.stream() - .filter(record -> record.offset() >= committedOffset) - .collect(Collectors.toCollection(LinkedList::new))); + .filter(record -> record.offset() >= committedOffset) + .collect(Collectors.toCollection(LinkedList::new))); } final OffsetManager offsetManager = offsetManagers.get(tp); @@ -573,11 +574,11 @@ public void ack(Object messageId) { if (!emitted.contains(msgId)) { LOG.debug("Received ack for message [{}], associated with tuple emitted for a ConsumerRecord that " - + "came from a topic-partition that this consumer group instance is no longer tracking " - + "due to rebalance/partition reassignment. No action taken.", msgId); + + "came from a topic-partition that this consumer group instance is no longer tracking " + + "due to rebalance/partition reassignment. No action taken.", msgId); } else { Validate.isTrue(!retryService.isScheduled(msgId), "The message id " + msgId + " is queued for retry while being acked." - + " This should never occur barring errors in the RetryService implementation or the spout code."); + + " This should never occur barring errors in the RetryService implementation or the spout code."); offsetManagers.get(msgId.getTopicPartition()).addToAckMsgs(msgId); emitted.remove(msgId); } @@ -595,11 +596,11 @@ public void fail(Object messageId) { final KafkaSpoutMessageId msgId = (KafkaSpoutMessageId) messageId; if (!emitted.contains(msgId)) { LOG.debug("Received fail for tuple this spout is no longer tracking." - + " Partitions may have been reassigned. Ignoring message [{}]", msgId); + + " Partitions may have been reassigned. Ignoring message [{}]", msgId); return; } Validate.isTrue(!retryService.isScheduled(msgId), "The message id " + msgId + " is queued for retry while being failed." - + " This should never occur barring errors in the RetryService implementation or the spout code."); + + " This should never occur barring errors in the RetryService implementation or the spout code."); msgId.incrementNumFails(); @@ -630,7 +631,7 @@ private void refreshAssignment() { List allPartitionsSorted = new ArrayList<>(allPartitions); Collections.sort(allPartitionsSorted, TopicPartitionComparator.INSTANCE); Set assignedPartitions = kafkaSpoutConfig.getTopicPartitioner() - .getPartitionsForThisTask(allPartitionsSorted, context); + .getPartitionsForThisTask(allPartitionsSorted, context); boolean partitionChanged = topicAssigner.assignPartitions(consumer, assignedPartitions, rebalanceListener); if (partitionChanged && canRegisterMetrics()) { LOG.info("Partitions assignments has changed, updating metrics."); @@ -683,9 +684,9 @@ public void declareOutputFields(OutputFieldsDeclarer declarer) { @Override public String toString() { return "KafkaSpout{" - + "offsetManagers =" + offsetManagers - + ", emitted=" + emitted - + "}"; + + "offsetManagers =" + offsetManagers + + ", emitted=" + emitted + + "}"; } @Override @@ -718,8 +719,8 @@ private boolean isPrimitiveOrWrapper(Class type) { private boolean isWrapper(Class type) { return type == Double.class || type == Float.class || type == Long.class - || type == Integer.class || type == Short.class || type == Character.class - || type == Byte.class || type == Boolean.class || type == String.class; + || type == Integer.class || type == Short.class || type == Character.class + || type == Byte.class || type == Boolean.class || type == String.class; } private String getTopicsString() { @@ -735,8 +736,8 @@ private static class PollablePartitionsInfo { PollablePartitionsInfo(Set pollablePartitions, Map earliestRetriableOffsets) { this.pollablePartitions = pollablePartitions; this.pollableEarliestRetriableOffsets = earliestRetriableOffsets.entrySet().stream() - .filter(entry -> pollablePartitions.contains(entry.getKey())) - .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue())); + .filter(entry -> pollablePartitions.contains(entry.getKey())) + .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue())); } public boolean shouldPoll() { diff --git a/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/metrics2/KafkaOffsetMetricManager.java b/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/metrics2/KafkaOffsetMetricManager.java index 2b150797ffe..64c42832cf3 100644 --- a/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/metrics2/KafkaOffsetMetricManager.java +++ b/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/metrics2/KafkaOffsetMetricManager.java @@ -6,9 +6,9 @@ * 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. @@ -22,7 +22,6 @@ import java.util.Map; import java.util.Set; import java.util.function.Supplier; - import org.apache.kafka.clients.admin.Admin; import org.apache.kafka.common.TopicPartition; import org.apache.storm.kafka.spout.internal.OffsetManager; @@ -30,6 +29,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; + + /** * This class is used to manage both the partition and topic level offset metrics. */ @@ -55,6 +56,7 @@ public KafkaOffsetMetricManager(Supplier> off } public void registerMetricsForNewTopicPartitions(Set newAssignment) { + for (TopicPartition topicPartition : newAssignment) { if (!topicPartitionMetricsMap.containsKey(topicPartition)) { LOG.info("Registering metric for topicPartition: {}", topicPartition); @@ -62,13 +64,14 @@ public void registerMetricsForNewTopicPartitions(Set newAssignme String topic = topicPartition.topic(); KafkaOffsetTopicMetrics topicMetrics = topicMetricsMap.get(topic); if (topicMetrics == null) { - topicMetrics = new KafkaOffsetTopicMetrics(topic); + LOG.info("Registering metric for topic: {}", topic); + topicMetrics = new KafkaOffsetTopicMetrics(topic, offsetManagerSupplier, adminSupplier, newAssignment); topicMetricsMap.put(topic, topicMetrics); topologyContext.registerMetricSet("kafkaOffset", topicMetrics); } KafkaOffsetPartitionMetrics topicPartitionMetricSet - = new KafkaOffsetPartitionMetrics<>(offsetManagerSupplier, adminSupplier, topicPartition, topicMetrics); + = new KafkaOffsetPartitionMetrics<>(offsetManagerSupplier, adminSupplier, topicPartition); topicPartitionMetricsMap.put(topicPartition, topicPartitionMetricSet); topologyContext.registerMetricSet("kafkaOffset", topicPartitionMetricSet); } diff --git a/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/metrics2/KafkaOffsetPartitionMetrics.java b/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/metrics2/KafkaOffsetPartitionMetrics.java index 6b29eefc919..df5b0c8694e 100644 --- a/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/metrics2/KafkaOffsetPartitionMetrics.java +++ b/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/metrics2/KafkaOffsetPartitionMetrics.java @@ -6,9 +6,9 @@ * 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. @@ -18,27 +18,25 @@ package org.apache.storm.kafka.spout.metrics2; +import static org.apache.storm.kafka.spout.metrics2.KafkaOffsetUtil.getBeginningOffsets; +import static org.apache.storm.kafka.spout.metrics2.KafkaOffsetUtil.getEndOffsets; + import com.codahale.metrics.Gauge; import com.codahale.metrics.Metric; import com.codahale.metrics.MetricSet; - import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Set; -import java.util.concurrent.ExecutionException; import java.util.function.Supplier; - import org.apache.kafka.clients.admin.Admin; -import org.apache.kafka.clients.admin.ListOffsetsResult; -import org.apache.kafka.clients.admin.OffsetSpec; -import org.apache.kafka.common.KafkaFuture; import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.errors.RetriableException; import org.apache.storm.kafka.spout.internal.OffsetManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + + + /** * Partition level metrics. *

@@ -56,16 +54,13 @@ public class KafkaOffsetPartitionMetrics implements MetricSet { private final Supplier adminSupplier; private TopicPartition topicPartition; - private KafkaOffsetTopicMetrics topicMetrics; public KafkaOffsetPartitionMetrics(Supplier> offsetManagerSupplier, Supplier adminSupplier, - TopicPartition topicPartition, - KafkaOffsetTopicMetrics topicMetrics) { + TopicPartition topicPartition) { this.offsetManagerSupplier = offsetManagerSupplier; this.adminSupplier = adminSupplier; this.topicPartition = topicPartition; - this.topicMetrics = topicMetrics; LOG.info("Running KafkaOffsetMetricSet"); } @@ -74,93 +69,57 @@ public KafkaOffsetPartitionMetrics(Supplier> public Map getMetrics() { Map metrics = new HashMap(); - String metricPath = topicPartition.topic() + "/partition_" + topicPartition.partition(); - Gauge spoutLagGauge = new Gauge() { - @Override - public Long getValue() { - Map endOffsets = getEndOffsets(Collections.singleton(topicPartition)); - if (endOffsets == null || endOffsets.isEmpty()) { - LOG.error("Failed to get endOffsets from Kafka for topic partitions: {}.", topicPartition); - return 0L; - } - // add value to topic level metric - OffsetManager offsetManager = offsetManagerSupplier.get().get(topicPartition); - Long ret = endOffsets.get(topicPartition) - offsetManager.getCommittedOffset(); - topicMetrics.totalSpoutLag += ret; - return ret; + String metricPath = topicPartition.topic() + "/partition_" + topicPartition.partition(); + Gauge spoutLagGauge = () -> { + Map endOffsets = getEndOffsets(Collections.singleton(topicPartition), adminSupplier); + if (endOffsets == null || endOffsets.isEmpty()) { + LOG.error("Failed to get endOffsets from Kafka for topic partitions: {}.", topicPartition); + return 0L; } + OffsetManager offsetManager = offsetManagerSupplier.get().get(topicPartition); + return endOffsets.get(topicPartition) - offsetManager.getCommittedOffset(); }; - Gauge earliestTimeOffsetGauge = new Gauge() { - @Override - public Long getValue() { - Map beginningOffsets = getBeginningOffsets(Collections.singleton(topicPartition)); - if (beginningOffsets == null || beginningOffsets.isEmpty()) { - LOG.error("Failed to get beginningOffsets from Kafka for topic partitions: {}.", topicPartition); - return 0L; - } - // add value to topic level metric - Long ret = beginningOffsets.get(topicPartition); - topicMetrics.totalEarliestTimeOffset += beginningOffsets.get(topicPartition); - return ret; + Gauge earliestTimeOffsetGauge = () -> { + Map beginningOffsets = getBeginningOffsets(Collections.singleton(topicPartition), adminSupplier); + if (beginningOffsets == null || beginningOffsets.isEmpty()) { + LOG.error("Failed to get beginningOffsets from Kafka for topic partitions: {}.", topicPartition); + return 0L; } + return beginningOffsets.get(topicPartition); }; - Gauge latestTimeOffsetGauge = new Gauge() { - @Override - public Long getValue() { - Map endOffsets = getEndOffsets(Collections.singleton(topicPartition)); - if (endOffsets == null || endOffsets.isEmpty()) { - LOG.error("Failed to get endOffsets from Kafka for topic partitions: {}.", topicPartition); - return 0L; - } - // add value to topic level metric - Long ret = endOffsets.get(topicPartition); - topicMetrics.totalLatestTimeOffset += ret; - return ret; + Gauge latestTimeOffsetGauge = () -> { + Map endOffsets = getEndOffsets(Collections.singleton(topicPartition), adminSupplier); + if (endOffsets == null || endOffsets.isEmpty()) { + LOG.error("Failed to get endOffsets from Kafka for topic partitions: {}.", topicPartition); + return 0L; } + return endOffsets.get(topicPartition); }; - Gauge latestEmittedOffsetGauge = new Gauge() { - @Override - public Long getValue() { - // add value to topic level metric - OffsetManager offsetManager = offsetManagerSupplier.get().get(topicPartition); - Long ret = offsetManager.getLatestEmittedOffset(); - topicMetrics.totalLatestEmittedOffset += ret; - return ret; - } + Gauge latestEmittedOffsetGauge = () -> { + OffsetManager offsetManager = offsetManagerSupplier.get().get(topicPartition); + return offsetManager.getLatestEmittedOffset(); }; - Gauge latestCompletedOffsetGauge = new Gauge() { - @Override - public Long getValue() { - // add value to topic level metric - OffsetManager offsetManager = offsetManagerSupplier.get().get(topicPartition); - Long ret = offsetManager.getCommittedOffset(); - topicMetrics.totalLatestCompletedOffset += ret; - return ret; - } + Gauge latestCompletedOffsetGauge = () -> { + OffsetManager offsetManager = offsetManagerSupplier.get().get(topicPartition); + return offsetManager.getCommittedOffset(); }; - Gauge recordsInPartitionGauge = new Gauge() { - @Override - public Long getValue() { - Map endOffsets = getEndOffsets(Collections.singleton(topicPartition)); - if (endOffsets == null || endOffsets.isEmpty()) { - LOG.error("Failed to get endOffsets from Kafka for topic partitions: {}.", topicPartition); - return 0L; - } - Map beginningOffsets = getBeginningOffsets(Collections.singleton(topicPartition)); - if (beginningOffsets == null || beginningOffsets.isEmpty()) { - LOG.error("Failed to get beginningOffsets from Kafka for topic partitions: {}.", topicPartition); - return 0L; - } - // add value to topic level metric - Long ret = endOffsets.get(topicPartition) - beginningOffsets.get(topicPartition); - topicMetrics.totalRecordsInPartitions += ret; - return ret; + Gauge recordsInPartitionGauge = () -> { + Map endOffsets = getEndOffsets(Collections.singleton(topicPartition), adminSupplier); + if (endOffsets == null || endOffsets.isEmpty()) { + LOG.error("Failed to get endOffsets from Kafka for topic partitions: {}.", topicPartition); + return 0L; + } + Map beginningOffsets = getBeginningOffsets(Collections.singleton(topicPartition), adminSupplier); + if (beginningOffsets == null || beginningOffsets.isEmpty()) { + LOG.error("Failed to get beginningOffsets from Kafka for topic partitions: {}.", topicPartition); + return 0L; } + return endOffsets.get(topicPartition) - beginningOffsets.get(topicPartition); }; metrics.put(metricPath + "/" + "spoutLag", spoutLagGauge); @@ -172,56 +131,4 @@ public Long getValue() { return metrics; } - - private Map getBeginningOffsets(Set topicPartitions) { - Admin admin = adminSupplier.get(); - if (admin == null) { - LOG.error("Kafka admin object is null, returning 0."); - return Collections.EMPTY_MAP; - } - - Map beginningOffsets; - try { - beginningOffsets = getOffsets(admin, topicPartitions, OffsetSpec.earliest()); - } catch (RetriableException | ExecutionException | InterruptedException e) { - LOG.error("Failed to get offset from Kafka for topic partitions: {}.", topicPartition, e); - return Collections.EMPTY_MAP; - } - return beginningOffsets; - } - - private Map getEndOffsets(Set topicPartitions) { - Admin admin = adminSupplier.get(); - if (admin == null) { - LOG.error("Kafka admin object is null, returning 0."); - return Collections.EMPTY_MAP; - } - - Map endOffsets; - try { - endOffsets = getOffsets(admin, topicPartitions, OffsetSpec.latest()); - } catch (RetriableException | ExecutionException | InterruptedException e) { - LOG.error("Failed to get offset from Kafka for topic partitions: {}.", topicPartition, e); - return Collections.EMPTY_MAP; - } - return endOffsets; - } - - private static Map getOffsets(Admin admin, Set topicPartitions, OffsetSpec offsetSpec) - throws InterruptedException, ExecutionException { - - Map offsetSpecMap = new HashMap<>(); - for (TopicPartition topicPartition : topicPartitions) { - offsetSpecMap.put(topicPartition, offsetSpec); - } - Map ret = new HashMap<>(); - ListOffsetsResult listOffsetsResult = admin.listOffsets(offsetSpecMap); - KafkaFuture> all = listOffsetsResult.all(); - Map topicPartitionListOffsetsResultInfoMap = all.get(); - for (Map.Entry entry : - topicPartitionListOffsetsResultInfoMap.entrySet()) { - ret.put(entry.getKey(), entry.getValue().offset()); - } - return ret; - } } diff --git a/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/metrics2/KafkaOffsetTopicMetrics.java b/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/metrics2/KafkaOffsetTopicMetrics.java index cd0fbfc6b2c..8bdcef37485 100644 --- a/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/metrics2/KafkaOffsetTopicMetrics.java +++ b/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/metrics2/KafkaOffsetTopicMetrics.java @@ -6,9 +6,9 @@ * 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. @@ -18,14 +18,26 @@ package org.apache.storm.kafka.spout.metrics2; +import static org.apache.storm.kafka.spout.metrics2.KafkaOffsetUtil.getBeginningOffsets; +import static org.apache.storm.kafka.spout.metrics2.KafkaOffsetUtil.getEndOffsets; + import com.codahale.metrics.Gauge; import com.codahale.metrics.Metric; import com.codahale.metrics.MetricSet; + +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import org.apache.kafka.clients.admin.Admin; +import org.apache.kafka.common.TopicPartition; +import org.apache.storm.kafka.spout.internal.OffsetManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + /** * Topic level metrics. *

@@ -42,22 +54,18 @@ public class KafkaOffsetTopicMetrics implements MetricSet { private static final Logger LOG = LoggerFactory.getLogger(KafkaOffsetTopicMetrics.class); private String topic; - long totalSpoutLag; - long totalEarliestTimeOffset; - long totalLatestTimeOffset; - long totalLatestEmittedOffset; - long totalLatestCompletedOffset; - long totalRecordsInPartitions; - - - public KafkaOffsetTopicMetrics(String topic) { + Set assignment; + Supplier> offsetManagerSupplier; + Supplier adminSupplier; + + public KafkaOffsetTopicMetrics(String topic, Supplier> offsetManagerSupplier, + Supplier adminSupplier, + Set newAssignment) { this.topic = topic; - this.totalSpoutLag = 0L; - this.totalEarliestTimeOffset = 0L; - this.totalLatestTimeOffset = 0L; - this.totalLatestEmittedOffset = 0L; - this.totalLatestCompletedOffset = 0L; - this.totalRecordsInPartitions = 0L; + this.assignment = newAssignment; + this.offsetManagerSupplier = offsetManagerSupplier; + this.adminSupplier = adminSupplier; LOG.info("Create KafkaOffsetTopicMetrics for topic: {}", topic); } @@ -65,46 +73,117 @@ public KafkaOffsetTopicMetrics(String topic) { public Map getMetrics() { Map metrics = new HashMap(); - Gauge totalSpoutLagGauge = new Gauge() { - @Override - public Long getValue() { - return totalSpoutLag; + Gauge totalSpoutLagGauge = () -> { + Long totalSpoutLag = 0L; + for (TopicPartition topicPartition : assignment) { + String topicOfPartition = topicPartition.topic(); + if (topicOfPartition.equals(topic)) { + Map endOffsets = getEndOffsets(Collections.singleton(topicPartition), adminSupplier); + if (endOffsets == null || endOffsets.isEmpty()) { + LOG.error("Failed to get endOffsets from Kafka for topic partitions: {}.", topicPartition); + return 0L; + } + // add value to topic level metric + OffsetManager offsetManager = offsetManagerSupplier.get().get(topicPartition); + Long ret = endOffsets.get(topicPartition) - offsetManager.getCommittedOffset(); + totalSpoutLag += ret; + } } + return totalSpoutLag; }; - Gauge totalEarliestTimeOffsetGauge = new Gauge() { - @Override - public Long getValue() { - return totalEarliestTimeOffset; + Gauge totalEarliestTimeOffsetGauge = () -> { + + Long totalEarliestTimeOffset = 0L; + + for (TopicPartition topicPartition : assignment) { + String topicOfPartition = topicPartition.topic(); + if (topicOfPartition.equals(topic)) { + Map beginningOffsets = getBeginningOffsets(Collections.singleton(topicPartition), adminSupplier); + if (beginningOffsets == null || beginningOffsets.isEmpty()) { + LOG.error("Failed to get beginningOffsets from Kafka for topic partitions: {}.", topicPartition); + return 0L; + } + // add value to topic level metric + Long ret = beginningOffsets.get(topicPartition); + totalEarliestTimeOffset += ret; + } + } + return totalEarliestTimeOffset; }; - Gauge totalLatestTimeOffsetGauge = new Gauge() { - @Override - public Long getValue() { - return totalLatestTimeOffset; + Gauge totalLatestTimeOffsetGauge = () -> { + + Long totalLatestTimeOffset = 0L; + for (TopicPartition topicPartition : assignment) { + String topicOfPartition = topicPartition.topic(); + if (topicOfPartition.equals(topic)) { + Map endOffsets = getEndOffsets(Collections.singleton(topicPartition), adminSupplier); + if (endOffsets == null || endOffsets.isEmpty()) { + LOG.error("Failed to get endOffsets from Kafka for topic partitions: {}.", topicPartition); + return 0L; + } + // add value to topic level metric + Long ret = endOffsets.get(topicPartition); + totalLatestTimeOffset += ret; + } } + return totalLatestTimeOffset; }; - Gauge totalLatestEmittedOffsetGauge = new Gauge() { - @Override - public Long getValue() { - return totalLatestEmittedOffset; + Gauge totalLatestEmittedOffsetGauge = () -> { + + Long totalLatestEmittedOffset = 0L; + for (TopicPartition topicPartition : assignment) { + String topicOfPartition = topicPartition.topic(); + if (topicOfPartition.equals(topic)) { + OffsetManager offsetManager = offsetManagerSupplier.get().get(topicPartition); + Long ret = offsetManager.getLatestEmittedOffset(); + totalLatestEmittedOffset += ret; + } + } + return totalLatestEmittedOffset; }; - Gauge totalLatestCompletedOffsetGauge = new Gauge() { - @Override - public Long getValue() { - return totalLatestCompletedOffset; + Gauge totalLatestCompletedOffsetGauge = () -> { + + Long totalLatestCompletedOffset = 0L; + for (TopicPartition topicPartition : assignment) { + String topicOfPartition = topicPartition.topic(); + if (topicOfPartition.equals(topic)) { + // add value to topic level metric + OffsetManager offsetManager = offsetManagerSupplier.get().get(topicPartition); + Long ret = offsetManager.getCommittedOffset(); + totalLatestCompletedOffset += ret; + } } + + return totalLatestCompletedOffset; }; - Gauge totalRecordsInPartitionsGauge = new Gauge() { - @Override - public Long getValue() { - return totalRecordsInPartitions; + Gauge totalRecordsInPartitionsGauge = () -> { + Long totalRecordsInPartitions = 0L; + for (TopicPartition topicPartition : assignment) { + String topicOfPartition = topicPartition.topic(); + if (topicOfPartition.equals(topic)) { + Map endOffsets = getEndOffsets(Collections.singleton(topicPartition), adminSupplier); + if (endOffsets == null || endOffsets.isEmpty()) { + LOG.error("Failed to get endOffsets from Kafka for topic partitions: {}.", topicPartition); + return 0L; + } + Map beginningOffsets = getBeginningOffsets(Collections.singleton(topicPartition), adminSupplier); + if (beginningOffsets == null || beginningOffsets.isEmpty()) { + LOG.error("Failed to get beginningOffsets from Kafka for topic partitions: {}.", topicPartition); + return 0L; + } + // add value to topic level metric + Long ret = endOffsets.get(topicPartition) - beginningOffsets.get(topicPartition); + totalRecordsInPartitions += ret; + } } + return totalRecordsInPartitions; }; metrics.put(topic + "/" + "totalSpoutLag", totalSpoutLagGauge); @@ -115,25 +194,4 @@ public Long getValue() { metrics.put(topic + "/" + "totalRecordsInPartitions", totalRecordsInPartitionsGauge); return metrics; } - - private class TopicMetrics { - long totalSpoutLag = 0L; - long totalEarliestTimeOffset = 0L; - long totalLatestTimeOffset = 0L; - long totalLatestEmittedOffset = 0L; - long totalLatestCompletedOffset = 0L; - long totalRecordsInPartitions = 0L; - - public void incrementTotalSpoutLag(long offset) { - totalSpoutLag += offset; - } - - public void incrementTotalEarliestTimeOffset(long offset) { - totalEarliestTimeOffset += offset; - } - - public void incrementTotalLatestTimeOffset(long offset) { - totalLatestTimeOffset += offset; - } - } } diff --git a/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/metrics2/KafkaOffsetUtil.java b/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/metrics2/KafkaOffsetUtil.java new file mode 100644 index 00000000000..c7f9fabc470 --- /dev/null +++ b/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/metrics2/KafkaOffsetUtil.java @@ -0,0 +1,92 @@ +/** + * 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.storm.kafka.spout.metrics2; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; +import org.apache.kafka.clients.admin.Admin; +import org.apache.kafka.clients.admin.ListOffsetsResult; +import org.apache.kafka.clients.admin.OffsetSpec; +import org.apache.kafka.common.KafkaFuture; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.errors.RetriableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class KafkaOffsetUtil { + + private static final Logger LOG = LoggerFactory.getLogger(KafkaOffsetUtil.class); + + public static Map getBeginningOffsets(Set topicPartitions, Supplier adminSupplier) { + Admin admin = adminSupplier.get(); + if (admin == null) { + LOG.error("Kafka admin object is null, returning 0."); + return Collections.EMPTY_MAP; + } + + Map beginningOffsets; + try { + beginningOffsets = getOffsets(admin, topicPartitions, OffsetSpec.earliest()); + } catch (RetriableException | ExecutionException | InterruptedException e) { + LOG.error("Failed to get offset from Kafka for topic partitions: {}.", topicPartitions, e); + return Collections.EMPTY_MAP; + } + return beginningOffsets; + } + + public static Map getEndOffsets(Set topicPartitions, Supplier adminSupplier) { + Admin admin = adminSupplier.get(); + if (admin == null) { + LOG.error("Kafka admin object is null, returning 0."); + return Collections.EMPTY_MAP; + } + + Map endOffsets; + try { + endOffsets = getOffsets(admin, topicPartitions, OffsetSpec.latest()); + } catch (RetriableException | ExecutionException | InterruptedException e) { + LOG.error("Failed to get offset from Kafka for topic partitions: {}.", topicPartitions, e); + return Collections.EMPTY_MAP; + } + return endOffsets; + } + + public static Map getOffsets(Admin admin, Set topicPartitions, OffsetSpec offsetSpec) + throws InterruptedException, ExecutionException { + + Map offsetSpecMap = new HashMap<>(); + for (TopicPartition topicPartition : topicPartitions) { + offsetSpecMap.put(topicPartition, offsetSpec); + } + Map ret = new HashMap<>(); + ListOffsetsResult listOffsetsResult = admin.listOffsets(offsetSpecMap); + KafkaFuture> all = listOffsetsResult.all(); + Map topicPartitionListOffsetsResultInfoMap = all.get(); + for (Map.Entry entry : + topicPartitionListOffsetsResultInfoMap.entrySet()) { + ret.put(entry.getKey(), entry.getValue().offset()); + } + return ret; + } +} diff --git a/external/storm-kafka-client/src/test/java/org/apache/storm/kafka/spout/KafkaSpoutReactivationTest.java b/external/storm-kafka-client/src/test/java/org/apache/storm/kafka/spout/KafkaSpoutReactivationTest.java index b4cab2ace70..b7ae2824b7f 100644 --- a/external/storm-kafka-client/src/test/java/org/apache/storm/kafka/spout/KafkaSpoutReactivationTest.java +++ b/external/storm-kafka-client/src/test/java/org/apache/storm/kafka/spout/KafkaSpoutReactivationTest.java @@ -29,9 +29,12 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Metric; import java.util.HashMap; import java.util.Map; +import org.apache.kafka.clients.admin.Admin; import org.apache.kafka.clients.consumer.Consumer; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.OffsetAndMetadata; @@ -65,6 +68,7 @@ public class KafkaSpoutReactivationTest { private final SpoutOutputCollector collector = mock(SpoutOutputCollector.class); private final long commitOffsetPeriodMs = 2_000; private Consumer consumerSpy; + private Admin adminSpy; private KafkaSpout spout; private final int maxPollRecords = 10; @@ -78,9 +82,12 @@ public void prepareSpout(int messageCount, FirstPollOffsetStrategy firstPollOffs .build(); ClientFactory clientFactory = new ClientFactoryDefault<>(); this.consumerSpy = spy(clientFactory.createConsumer(spoutConfig.getKafkaProps())); + this.adminSpy = spy(clientFactory.createAdmin(spoutConfig.getKafkaProps())); ClientFactory clientFactoryMock = mock(ClientFactory.class); when(clientFactoryMock.createConsumer(any())) .thenReturn(consumerSpy); + when(clientFactoryMock.createAdmin(any())) + .thenReturn(adminSpy); this.spout = new KafkaSpout<>(spoutConfig, clientFactoryMock, new TopicAssigner()); SingleTopicKafkaUnitSetupHelper.populateTopicData(kafkaUnitExtension.getKafkaUnit(), SingleTopicKafkaSpoutConfiguration.TOPIC, messageCount); SingleTopicKafkaUnitSetupHelper.initializeSpout(spout, conf, topologyContext, collector); @@ -147,4 +154,24 @@ public void testSpoutShouldResumeWhereItLeftOffWithEarliestStrategy() throws Exc //With earliest, the spout should also resume where it left off, rather than restart at the earliest offset. doReactivationTest(FirstPollOffsetStrategy.EARLIEST); } + + @Test + public void testSpoutMustHandleGettingMetricsWhileDeactivated() throws Exception { + //Storm will try to get metrics from the spout even while deactivated, the spout must be able to handle this + prepareSpout(10, FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST); + + for (int i = 0; i < 5; i++) { + KafkaSpoutMessageId msgId = emitOne(); + spout.ack(msgId); + } + spout.deactivate(); + + Map partitionsOffsetMetric = spout.getKafkaOffsetMetricManager().getTopicPartitionMetricsMap().get(new TopicPartition(SingleTopicKafkaSpoutConfiguration.TOPIC ,0)).getMetrics(); + Long partitionLag = (Long) ((Gauge) partitionsOffsetMetric.get(SingleTopicKafkaSpoutConfiguration.TOPIC + "/partition_0/spoutLag")).getValue(); + assertThat(partitionLag, is(5L)); + + Map topicOffsetMetric = spout.getKafkaOffsetMetricManager().getTopicMetricsMap().get(SingleTopicKafkaSpoutConfiguration.TOPIC).getMetrics(); + Long totalSpoutLag = (Long) ((Gauge) topicOffsetMetric.get(SingleTopicKafkaSpoutConfiguration.TOPIC + "/totalSpoutLag")).getValue(); + assertThat(totalSpoutLag, is(5L)); + } } diff --git a/external/storm-kafka-client/src/test/java/org/apache/storm/kafka/spout/metric2/KafkaOffsetPartitionMetricsTest.java b/external/storm-kafka-client/src/test/java/org/apache/storm/kafka/spout/metric2/KafkaOffsetPartitionMetricsTest.java new file mode 100644 index 00000000000..ee085858e86 --- /dev/null +++ b/external/storm-kafka-client/src/test/java/org/apache/storm/kafka/spout/metric2/KafkaOffsetPartitionMetricsTest.java @@ -0,0 +1,163 @@ +/* + * 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.storm.kafka.spout.metric2; + + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Metric; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import org.apache.kafka.clients.admin.Admin; +import org.apache.kafka.clients.admin.ListOffsetsResult; +import org.apache.kafka.common.KafkaFuture; +import org.apache.kafka.common.TopicPartition; +import org.apache.storm.kafka.spout.internal.OffsetManager; +import org.apache.storm.kafka.spout.metrics2.KafkaOffsetPartitionMetrics; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class KafkaOffsetPartitionMetricsTest { + + private Set assignment; + private Admin admin = mock(Admin.class); + private HashMap offsetManagers; + private KafkaFuture kafkaFuture = mock(KafkaFuture.class); + + @BeforeEach + public void initializeTests() { + reset(admin, kafkaFuture); + + } + + @Test + public void registerMetricsGetSpoutLagAndPartitionRecords() throws ExecutionException, InterruptedException { + + TopicPartition topicAPartition1 = new TopicPartition("topicA", 1); + + ListOffsetsResult.ListOffsetsResultInfo topicAPartition1LatestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(100, System.currentTimeMillis(), Optional.empty()); + + Map topicPartitionLatestListOffsetsResultInfoMap = new HashMap<>(); + topicPartitionLatestListOffsetsResultInfoMap.put(topicAPartition1, topicAPartition1LatestListOffsetsResultInfo); + + when(kafkaFuture.get()).thenReturn(topicPartitionLatestListOffsetsResultInfoMap); + + ListOffsetsResult listOffsetsResult = mock(ListOffsetsResult.class); + when(listOffsetsResult.all()).thenReturn(kafkaFuture); + + admin = mock(Admin.class); + when(admin.listOffsets(anyMap())).thenReturn(listOffsetsResult); + + OffsetManager offsetManagertopicAPartition1 = mock(OffsetManager.class); + when(offsetManagertopicAPartition1.getCommittedOffset()).thenReturn(90L); + + + offsetManagers = new HashMap<>(); + offsetManagers.put(topicAPartition1, offsetManagertopicAPartition1); + + KafkaOffsetPartitionMetrics kafkaOffsetPartitionAndTopicMetrics = new KafkaOffsetPartitionMetrics(() -> Collections.unmodifiableMap(offsetManagers), () -> admin, topicAPartition1); + Map result = kafkaOffsetPartitionAndTopicMetrics.getMetrics(); + Gauge g1 = (Gauge) result.get("topicA/partition_1/spoutLag"); + assertEquals(10L, g1.getValue()); + + + //get partition records + + ListOffsetsResult.ListOffsetsResultInfo topicAPartition1EarliestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(1, System.currentTimeMillis(), Optional.empty()); + + Map topicPartitionEarliestListOffsetsResultInfoMap = new HashMap<>(); + topicPartitionEarliestListOffsetsResultInfoMap.put(topicAPartition1, topicAPartition1EarliestListOffsetsResultInfo); + + //mock consecutive calls. Each call to the recordsInPartition gauge will call kafkaFuture.get() twice + when(kafkaFuture.get()).thenReturn(topicPartitionLatestListOffsetsResultInfoMap, topicPartitionEarliestListOffsetsResultInfoMap); + + result = kafkaOffsetPartitionAndTopicMetrics.getMetrics(); + g1 = (Gauge) result.get("topicA/partition_1/recordsInPartition"); + assertEquals(99L, g1.getValue()); + } + + @Test + public void registerMetricsGetEarliestAndLatest() throws ExecutionException, InterruptedException { + + TopicPartition topicAPartition1 = new TopicPartition("topicA", 1); + + ListOffsetsResult.ListOffsetsResultInfo topicAPartition1EarliestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(1, System.currentTimeMillis(), Optional.empty()); + + Map topicPartitionEarliestListOffsetsResultInfoMap = new HashMap<>(); + topicPartitionEarliestListOffsetsResultInfoMap.put(topicAPartition1, topicAPartition1EarliestListOffsetsResultInfo); + + when(kafkaFuture.get()).thenReturn(topicPartitionEarliestListOffsetsResultInfoMap); + + ListOffsetsResult listOffsetsResult = mock(ListOffsetsResult.class); + when(listOffsetsResult.all()).thenReturn(kafkaFuture); + + admin = mock(Admin.class); + when(admin.listOffsets(anyMap())).thenReturn(listOffsetsResult); + + OffsetManager offsetManagertopicAPartition1 = mock(OffsetManager.class); + when(offsetManagertopicAPartition1.getLatestEmittedOffset()).thenReturn(50L); + when(offsetManagertopicAPartition1.getCommittedOffset()).thenReturn(40L); + + + offsetManagers = new HashMap<>(); + offsetManagers.put(topicAPartition1, offsetManagertopicAPartition1); + + assignment = new HashSet<>(); + assignment.add(topicAPartition1); + + KafkaOffsetPartitionMetrics kafkaOffsetPartitionAndTopicMetrics = new KafkaOffsetPartitionMetrics(() -> Collections.unmodifiableMap(offsetManagers), () -> admin, topicAPartition1); + Map result = kafkaOffsetPartitionAndTopicMetrics.getMetrics(); + Gauge g1 = (Gauge) result.get("topicA/partition_1/earliestTimeOffset"); + + assertEquals(g1.getValue(), 1L); + + //get the latest offsets + + ListOffsetsResult.ListOffsetsResultInfo topicAPartition1LatestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(100, System.currentTimeMillis(), Optional.empty()); + + + Map topicPartitionLatestListOffsetsResultInfoMap = new HashMap<>(); + topicPartitionLatestListOffsetsResultInfoMap.put(topicAPartition1, topicAPartition1LatestListOffsetsResultInfo); + + when(kafkaFuture.get()).thenReturn(topicPartitionLatestListOffsetsResultInfoMap); + + result = kafkaOffsetPartitionAndTopicMetrics.getMetrics(); + + g1 = (Gauge) result.get("topicA/partition_1/latestTimeOffset"); + assertEquals(100L, g1.getValue()); + + g1 = (Gauge) result.get("topicA/partition_1/latestEmittedOffset"); + assertEquals(50L, g1.getValue()); + + + g1 = (Gauge) result.get("topicA/partition_1/latestCompletedOffset"); + assertEquals(40L, g1.getValue()); + + } +} diff --git a/external/storm-kafka-client/src/test/java/org/apache/storm/kafka/spout/metric2/KafkaOffsetTopicMetricsTest.java b/external/storm-kafka-client/src/test/java/org/apache/storm/kafka/spout/metric2/KafkaOffsetTopicMetricsTest.java new file mode 100644 index 00000000000..26462503389 --- /dev/null +++ b/external/storm-kafka-client/src/test/java/org/apache/storm/kafka/spout/metric2/KafkaOffsetTopicMetricsTest.java @@ -0,0 +1,243 @@ +/** + * 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.storm.kafka.spout.metric2; + +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Metric; +import org.apache.kafka.clients.admin.Admin; +import org.apache.kafka.clients.admin.ListOffsetsResult; +import org.apache.kafka.common.KafkaFuture; +import org.apache.kafka.common.TopicPartition; +import org.apache.storm.kafka.spout.internal.OffsetManager; +import org.apache.storm.kafka.spout.metrics2.KafkaOffsetTopicMetrics; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.*; +import java.util.concurrent.ExecutionException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class KafkaOffsetTopicMetricsTest { + + private Set assignment; + private Admin admin = mock(Admin.class); + private HashMap offsetManagers; + private KafkaFuture kafkaFuture = mock(KafkaFuture.class); + + @BeforeEach + public void initializeTests() { + reset(admin, kafkaFuture); + } + + @Test + public void registerMetricsGetSpoutLagAndPartitionRecords() throws ExecutionException, InterruptedException { + + TopicPartition topicAPartition1 = new TopicPartition("topicA", 1); + TopicPartition topicAPartition2 = new TopicPartition("topicA", 2); + TopicPartition topicBPartition1 = new TopicPartition("topicB", 1); + TopicPartition topicBPartition2 = new TopicPartition("topicB", 2); + + ListOffsetsResult.ListOffsetsResultInfo topicAPartition1LatestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(100, System.currentTimeMillis(), Optional.empty()); + ListOffsetsResult.ListOffsetsResultInfo topicAPartition2LatestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(200, System.currentTimeMillis(), Optional.empty()); + ListOffsetsResult.ListOffsetsResultInfo topicBPartition1LatestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(300, System.currentTimeMillis(), Optional.empty()); + ListOffsetsResult.ListOffsetsResultInfo topicBPartition2LatestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(400, System.currentTimeMillis(), Optional.empty()); + + Map topicPartitionLatestListOffsetsResultInfoMap = new HashMap<>(); + topicPartitionLatestListOffsetsResultInfoMap.put(topicAPartition1, topicAPartition1LatestListOffsetsResultInfo); + topicPartitionLatestListOffsetsResultInfoMap.put(topicAPartition2, topicAPartition2LatestListOffsetsResultInfo); + topicPartitionLatestListOffsetsResultInfoMap.put(topicBPartition1, topicBPartition1LatestListOffsetsResultInfo); + topicPartitionLatestListOffsetsResultInfoMap.put(topicBPartition2, topicBPartition2LatestListOffsetsResultInfo); + + when(kafkaFuture.get()).thenReturn(topicPartitionLatestListOffsetsResultInfoMap); + + ListOffsetsResult listOffsetsResult = mock(ListOffsetsResult.class); + when(listOffsetsResult.all()).thenReturn(kafkaFuture); + + admin = mock(Admin.class); + when(admin.listOffsets(anyMap())).thenReturn(listOffsetsResult); + + OffsetManager offsetManagertopicAPartition1 = mock(OffsetManager.class); + when(offsetManagertopicAPartition1.getCommittedOffset()).thenReturn(90L); + + OffsetManager offsetManagertopicAPartition2 = mock(OffsetManager.class); + when(offsetManagertopicAPartition2.getCommittedOffset()).thenReturn(170L); + + offsetManagers = new HashMap<>(); + offsetManagers.put(topicAPartition1, offsetManagertopicAPartition1); + offsetManagers.put(topicAPartition2, offsetManagertopicAPartition2); + + assignment = new HashSet<>(); + assignment.add(topicAPartition1); + assignment.add(topicAPartition2); + assignment.add(topicBPartition1); + assignment.add(topicBPartition2); + + + KafkaOffsetTopicMetrics kafkaOffsetTopicMetricsA = new KafkaOffsetTopicMetrics("topicA", () -> Collections.unmodifiableMap(offsetManagers), () -> admin, assignment); + Map result = kafkaOffsetTopicMetricsA.getMetrics(); + Gauge g1 = (Gauge) result.get("topicA/totalSpoutLag"); + assertEquals(40L, g1.getValue()); + + //get again the values from the Gauge. Values cannot change + g1 = (Gauge) result.get("topicA/totalSpoutLag"); + assertEquals(40L, g1.getValue()); + + assertNull(result.get("topicB/totalSpoutLag")); + + //get topic records + + ListOffsetsResult.ListOffsetsResultInfo topicAPartition1EarliestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(1, System.currentTimeMillis(), Optional.empty()); + ListOffsetsResult.ListOffsetsResultInfo topicAPartition2EarliestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(2, System.currentTimeMillis(), Optional.empty()); + ListOffsetsResult.ListOffsetsResultInfo topicBPartition1EarliestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(3, System.currentTimeMillis(), Optional.empty()); + ListOffsetsResult.ListOffsetsResultInfo topicBPartition2EarliestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(4, System.currentTimeMillis(), Optional.empty()); + + Map topicPartitionEarliestListOffsetsResultInfoMap = new HashMap<>(); + topicPartitionEarliestListOffsetsResultInfoMap.put(topicAPartition1, topicAPartition1EarliestListOffsetsResultInfo); + topicPartitionEarliestListOffsetsResultInfoMap.put(topicAPartition2, topicAPartition2EarliestListOffsetsResultInfo); + topicPartitionEarliestListOffsetsResultInfoMap.put(topicBPartition1, topicBPartition1EarliestListOffsetsResultInfo); + topicPartitionEarliestListOffsetsResultInfoMap.put(topicBPartition2, topicBPartition2EarliestListOffsetsResultInfo); + + //mock consecutive calls. Each call to the recordsInPartition gauge will call kafkaFuture.get() twice + when(kafkaFuture.get()).thenReturn( + topicPartitionLatestListOffsetsResultInfoMap, topicPartitionEarliestListOffsetsResultInfoMap, + topicPartitionLatestListOffsetsResultInfoMap, topicPartitionEarliestListOffsetsResultInfoMap, + topicPartitionLatestListOffsetsResultInfoMap, topicPartitionEarliestListOffsetsResultInfoMap, + topicPartitionLatestListOffsetsResultInfoMap, topicPartitionEarliestListOffsetsResultInfoMap); + + Gauge gATotal = (Gauge) result.get("topicA/totalRecordsInPartitions"); + assertEquals(297L, gATotal.getValue()); + + //get again the values from the Gauge. Values cannot change + gATotal = (Gauge) result.get("topicA/totalRecordsInPartitions"); + assertEquals(297L, gATotal.getValue()); + + assertNull(result.get("topicB/totalRecordsInPartitions")); + } + + @Test + public void registerMetricsGetEarliestAndLatest() throws ExecutionException, InterruptedException { + + TopicPartition topicAPartition1 = new TopicPartition("topicA", 1); + TopicPartition topicAPartition2 = new TopicPartition("topicA", 2); + TopicPartition topicBPartition1 = new TopicPartition("topicB", 1); + TopicPartition topicBPartition2 = new TopicPartition("topicB", 2); + + ListOffsetsResult.ListOffsetsResultInfo topicAPartition1EarliestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(1, System.currentTimeMillis(), Optional.empty()); + ListOffsetsResult.ListOffsetsResultInfo topicAPartition2EarliestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(1, System.currentTimeMillis(), Optional.empty()); + ListOffsetsResult.ListOffsetsResultInfo topicBPartition1EarliestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(1, System.currentTimeMillis(), Optional.empty()); + ListOffsetsResult.ListOffsetsResultInfo topicBPartition2EarliestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(1, System.currentTimeMillis(), Optional.empty()); + + Map topicPartitionEarliestListOffsetsResultInfoMap = new HashMap<>(); + topicPartitionEarliestListOffsetsResultInfoMap.put(topicAPartition1, topicAPartition1EarliestListOffsetsResultInfo); + topicPartitionEarliestListOffsetsResultInfoMap.put(topicAPartition2, topicAPartition2EarliestListOffsetsResultInfo); + topicPartitionEarliestListOffsetsResultInfoMap.put(topicBPartition1, topicBPartition1EarliestListOffsetsResultInfo); + topicPartitionEarliestListOffsetsResultInfoMap.put(topicBPartition2, topicBPartition2EarliestListOffsetsResultInfo); + + when(kafkaFuture.get()).thenReturn(topicPartitionEarliestListOffsetsResultInfoMap); + + ListOffsetsResult listOffsetsResult = mock(ListOffsetsResult.class); + when(listOffsetsResult.all()).thenReturn(kafkaFuture); + + admin = mock(Admin.class); + when(admin.listOffsets(anyMap())).thenReturn(listOffsetsResult); + + OffsetManager offsetManagertopicAPartition1 = mock(OffsetManager.class); + when(offsetManagertopicAPartition1.getLatestEmittedOffset()).thenReturn(50L); + when(offsetManagertopicAPartition1.getCommittedOffset()).thenReturn(40L); + + OffsetManager offsetManagertopicAPartition2 = mock(OffsetManager.class); + when(offsetManagertopicAPartition2.getLatestEmittedOffset()).thenReturn(100L); + when(offsetManagertopicAPartition2.getCommittedOffset()).thenReturn(90L); + + + offsetManagers = new HashMap<>(); + offsetManagers.put(topicAPartition1, offsetManagertopicAPartition1); + offsetManagers.put(topicAPartition2, offsetManagertopicAPartition2); + + assignment = new HashSet<>(); + assignment.add(topicAPartition1); + assignment.add(topicAPartition2); + assignment.add(topicBPartition1); + assignment.add(topicBPartition2); + + KafkaOffsetTopicMetrics kafkaOffsetPartitionAndTopicMetrics = new KafkaOffsetTopicMetrics("topicA",() -> Collections.unmodifiableMap(offsetManagers), () -> admin, assignment); + Map result = kafkaOffsetPartitionAndTopicMetrics.getMetrics(); + + Gauge gATotal = (Gauge) result.get("topicA/totalEarliestTimeOffset"); + assertEquals(2L, gATotal.getValue()); + assertNull(result.get("topicB/totalEarliestTimeOffset")); + + //get the metrics a second time. Values should be the same + + gATotal = (Gauge) result.get("topicA/totalEarliestTimeOffset"); + assertEquals(2L, gATotal.getValue()); + assertNull(result.get("topicB/totalEarliestTimeOffset")); + + //get the latest offsets + + ListOffsetsResult.ListOffsetsResultInfo topicAPartition1LatestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(100, System.currentTimeMillis(), Optional.empty()); + ListOffsetsResult.ListOffsetsResultInfo topicAPartition2LatestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(200, System.currentTimeMillis(), Optional.empty()); + ListOffsetsResult.ListOffsetsResultInfo topicBPartition1LatestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(300, System.currentTimeMillis(), Optional.empty()); + ListOffsetsResult.ListOffsetsResultInfo topicBPartition2LatestListOffsetsResultInfo = new ListOffsetsResult.ListOffsetsResultInfo(400, System.currentTimeMillis(), Optional.empty()); + + Map topicPartitionLatestListOffsetsResultInfoMap = new HashMap<>(); + topicPartitionLatestListOffsetsResultInfoMap.put(topicAPartition1, topicAPartition1LatestListOffsetsResultInfo); + topicPartitionLatestListOffsetsResultInfoMap.put(topicAPartition2, topicAPartition2LatestListOffsetsResultInfo); + topicPartitionLatestListOffsetsResultInfoMap.put(topicBPartition1, topicBPartition1LatestListOffsetsResultInfo); + topicPartitionLatestListOffsetsResultInfoMap.put(topicBPartition2, topicBPartition2LatestListOffsetsResultInfo); + + when(kafkaFuture.get()).thenReturn(topicPartitionLatestListOffsetsResultInfoMap); + + gATotal = (Gauge) result.get("topicA/totalLatestTimeOffset"); + assertEquals(300L, gATotal.getValue()); + assertNull((result.get("topicB/totalLatestTimeOffset"))); + + gATotal = (Gauge) result.get("topicA/totalLatestEmittedOffset"); + assertEquals(150L, gATotal.getValue()); + assertNull( result.get("topicB/totalLatestEmittedOffset")); + + + gATotal = (Gauge) result.get("topicA/totalLatestCompletedOffset"); + assertEquals(130L, gATotal.getValue()); + assertNull(result.get("topiBA/totalLatestCompletedOffset")); + + //get the metrics a second time. Values should be the same + + gATotal = (Gauge) result.get("topicA/totalLatestTimeOffset"); + assertEquals(300L, gATotal.getValue()); + assertNull((result.get("topicB/totalLatestTimeOffset"))); + + gATotal = (Gauge) result.get("topicA/totalLatestEmittedOffset"); + assertEquals(150L, gATotal.getValue()); + assertNull( result.get("topicB/totalLatestEmittedOffset")); + + + gATotal = (Gauge) result.get("topicA/totalLatestCompletedOffset"); + assertEquals(130L, gATotal.getValue()); + assertNull(result.get("topiBA/totalLatestCompletedOffset")); + + } +}