kafka-health-check/src/main/java/com/deviceinsight/kafka/health/KafkaConsumingHealthIndicat...

222 lines
7.3 KiB
Java
Raw Normal View History

2019-03-28 18:35:14 +03:00
package com.deviceinsight.kafka.health;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
2019-06-03 15:04:34 +03:00
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
2019-03-28 18:35:14 +03:00
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
2019-06-03 15:54:27 +03:00
import org.apache.kafka.clients.consumer.ConsumerRecords;
2019-03-28 18:35:14 +03:00
import org.apache.kafka.clients.consumer.KafkaConsumer;
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.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
2019-06-03 15:54:27 +03:00
import org.springframework.beans.factory.BeanInitializationException;
2019-03-28 18:35:14 +03:00
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import java.net.InetAddress;
import java.net.UnknownHostException;
2019-04-01 19:18:24 +03:00
import java.time.Duration;
2019-03-28 18:35:14 +03:00
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
2019-05-16 10:25:22 +03:00
import java.util.concurrent.Executors;
2019-03-28 18:35:14 +03:00
import java.util.concurrent.RejectedExecutionException;
2019-06-03 15:04:34 +03:00
import java.util.concurrent.TimeUnit;
2019-03-28 18:35:14 +03:00
import java.util.concurrent.TimeoutException;
2019-05-16 10:25:22 +03:00
import java.util.concurrent.atomic.AtomicBoolean;
2019-03-28 18:35:14 +03:00
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
public class KafkaConsumingHealthIndicator extends AbstractHealthIndicator {
2019-06-03 15:04:34 +03:00
private static final Logger logger = LoggerFactory.getLogger(KafkaConsumingHealthIndicator.class);
2019-05-24 14:02:45 +03:00
private static final String CONSUMER_GROUP_PREFIX = "health-check-";
2019-03-28 18:35:14 +03:00
private final Consumer<String, String> consumer;
private final Producer<String, String> producer;
private final String topic;
private final long sendReceiveTimeoutMs;
private final long pollTimeoutMs;
private final long subscriptionTimeoutMs;
private final ExecutorService executor;
2019-05-16 10:25:22 +03:00
private final AtomicBoolean running;
2019-06-03 15:04:34 +03:00
private final Cache<String, String> cache;
2019-03-28 18:35:14 +03:00
2019-05-16 10:25:22 +03:00
private KafkaCommunicationResult kafkaCommunicationResult;
2019-03-28 18:35:14 +03:00
public KafkaConsumingHealthIndicator(KafkaHealthProperties kafkaHealthProperties,
Map<String, Object> kafkaConsumerProperties, Map<String, Object> kafkaProducerProperties) {
this.topic = kafkaHealthProperties.getTopic();
this.sendReceiveTimeoutMs = kafkaHealthProperties.getSendReceiveTimeoutMs();
this.pollTimeoutMs = kafkaHealthProperties.getPollTimeoutMs();
this.subscriptionTimeoutMs = kafkaHealthProperties.getSubscriptionTimeoutMs();
Map<String, Object> kafkaConsumerPropertiesCopy = new HashMap<>(kafkaConsumerProperties);
setConsumerGroup(kafkaConsumerPropertiesCopy);
StringDeserializer deserializer = new StringDeserializer();
StringSerializer serializer = new StringSerializer();
this.consumer = new KafkaConsumer<>(kafkaConsumerPropertiesCopy, deserializer, deserializer);
this.producer = new KafkaProducer<>(kafkaProducerProperties, serializer, serializer);
2019-06-03 15:54:27 +03:00
this.executor = Executors.newSingleThreadExecutor();
2019-05-16 10:25:22 +03:00
this.running = new AtomicBoolean(true);
2019-06-03 15:54:27 +03:00
this.cache = Caffeine.newBuilder().expireAfterWrite(sendReceiveTimeoutMs, TimeUnit.MILLISECONDS).build();
2019-05-16 10:25:22 +03:00
2019-06-03 15:54:27 +03:00
this.kafkaCommunicationResult =
KafkaCommunicationResult.failure(new RejectedExecutionException("Kafka Health Check is starting."));
2019-03-28 18:35:14 +03:00
}
@PostConstruct
void subscribeAndSendMessage() throws InterruptedException {
subscribeToTopic();
2019-05-16 10:25:22 +03:00
2019-03-28 18:35:14 +03:00
if (kafkaCommunicationResult.isFailure()) {
2019-06-03 15:54:27 +03:00
throw new BeanInitializationException("Kafka health check failed", kafkaCommunicationResult.getException());
2019-03-28 18:35:14 +03:00
}
2019-05-16 10:25:22 +03:00
executor.submit(() -> {
while (running.get()) {
2019-06-03 15:54:27 +03:00
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(pollTimeoutMs));
records.forEach(record -> cache.put(record.key(), record.value()));
2019-05-16 10:25:22 +03:00
}
});
2019-03-28 18:35:14 +03:00
}
@PreDestroy
void shutdown() {
2019-05-16 10:25:22 +03:00
running.set(false);
2019-06-03 15:54:27 +03:00
executor.shutdownNow();
2019-03-28 18:35:14 +03:00
producer.close();
consumer.close();
}
private void setConsumerGroup(Map<String, Object> kafkaConsumerProperties) {
try {
2019-05-15 17:23:27 +03:00
String groupId = (String) kafkaConsumerProperties.getOrDefault(ConsumerConfig.GROUP_ID_CONFIG,
UUID.randomUUID().toString());
kafkaConsumerProperties.put(ConsumerConfig.GROUP_ID_CONFIG,
2019-05-24 14:02:45 +03:00
CONSUMER_GROUP_PREFIX + groupId + "-" + InetAddress.getLocalHost().getHostAddress());
2019-03-28 18:35:14 +03:00
} catch (UnknownHostException e) {
throw new IllegalStateException(e);
}
}
2019-06-03 15:54:27 +03:00
private void subscribeToTopic() throws InterruptedException {
2019-03-28 18:35:14 +03:00
final CountDownLatch subscribed = new CountDownLatch(1);
logger.info("Subscribe to health check topic={}", topic);
consumer.subscribe(Collections.singleton(topic), new ConsumerRebalanceListener() {
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
// nothing to do her
}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
logger.debug("Got partitions = {}", partitions);
if (!partitions.isEmpty()) {
subscribed.countDown();
}
}
});
2019-04-01 19:18:24 +03:00
consumer.poll(Duration.ofMillis(pollTimeoutMs));
2019-03-28 18:35:14 +03:00
if (!subscribed.await(subscriptionTimeoutMs, MILLISECONDS)) {
2019-06-03 15:54:27 +03:00
throw new BeanInitializationException("Subscription to kafka failed, topic=" + topic);
2019-03-28 18:35:14 +03:00
}
2019-06-03 15:54:27 +03:00
this.kafkaCommunicationResult = KafkaCommunicationResult.success();
2019-03-28 18:35:14 +03:00
}
2019-06-03 15:54:27 +03:00
private String sendMessage() {
2019-03-28 18:35:14 +03:00
try {
2019-06-03 15:54:27 +03:00
return sendKafkaMessage();
2019-03-28 18:35:14 +03:00
} catch (ExecutionException e) {
logger.warn("Kafka health check execution failed.", e);
2019-06-03 15:54:27 +03:00
this.kafkaCommunicationResult = KafkaCommunicationResult.failure(e);
2019-03-28 18:35:14 +03:00
} catch (TimeoutException | InterruptedException e) {
logger.warn("Kafka health check timed out.", e);
2019-06-03 15:54:27 +03:00
this.kafkaCommunicationResult = KafkaCommunicationResult.failure(e);
2019-03-28 18:35:14 +03:00
} catch (RejectedExecutionException e) {
logger.debug("Ignore health check, already running...");
}
2019-06-03 15:54:27 +03:00
return null;
2019-03-28 18:35:14 +03:00
}
2019-06-03 15:54:27 +03:00
private String sendKafkaMessage() throws InterruptedException, ExecutionException, TimeoutException {
2019-03-28 18:35:14 +03:00
String message = UUID.randomUUID().toString();
2019-09-10 11:45:21 +03:00
logger.trace("Send health check message = {}", message);
2019-03-28 18:35:14 +03:00
2019-05-16 10:25:22 +03:00
producer.send(new ProducerRecord<>(topic, message, message)).get(sendReceiveTimeoutMs, MILLISECONDS);
2019-03-28 18:35:14 +03:00
2019-06-03 15:54:27 +03:00
return message;
2019-03-28 18:35:14 +03:00
}
2019-06-03 15:54:27 +03:00
@Override
protected void doHealthCheck(Health.Builder builder) {
String expectedMessage = sendMessage();
if (expectedMessage == null) {
goDown(builder);
return;
}
2019-03-28 18:35:14 +03:00
2019-06-03 15:54:27 +03:00
long startTime = System.currentTimeMillis();
while (true) {
String receivedMessage = cache.getIfPresent(expectedMessage);
if (expectedMessage.equals(receivedMessage)) {
2019-03-28 18:35:14 +03:00
2019-06-03 15:54:27 +03:00
builder.up();
return;
2019-03-28 18:35:14 +03:00
2019-06-03 15:54:27 +03:00
} else if (System.currentTimeMillis() - startTime > sendReceiveTimeoutMs) {
2019-03-28 18:35:14 +03:00
2019-06-03 15:54:27 +03:00
if (kafkaCommunicationResult.isFailure()) {
goDown(builder);
} else {
builder.down(new TimeoutException(
"Sending and receiving took longer than " + sendReceiveTimeoutMs + " ms"))
.withDetail("topic", topic);
}
return;
}
2019-03-28 18:35:14 +03:00
}
2019-06-03 15:54:27 +03:00
2019-03-28 18:35:14 +03:00
}
2019-05-16 10:25:22 +03:00
2019-06-03 15:54:27 +03:00
private void goDown(Health.Builder builder) {
builder.down(kafkaCommunicationResult.getException()).withDetail("topic", topic);
2019-05-16 10:25:22 +03:00
}
2019-06-03 15:54:27 +03:00
2019-03-28 18:35:14 +03:00
}