All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sitewhere.microservice.kafka.MicroserviceKafkaProducer Maven / Gradle / Ivy

There is a newer version: 3.0.13
Show newest version
/*
 * Copyright (c) SiteWhere, LLC. All rights reserved. http://www.sitewhere.com
 *
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */
package com.sitewhere.microservice.kafka;

import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.admin.CreateTopicsResult;
import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.clients.admin.TopicDescription;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.config.ConfigException;
import org.apache.kafka.common.errors.InvalidReplicationFactorException;
import org.apache.kafka.common.errors.RetriableException;
import org.apache.kafka.common.errors.TopicExistsException;
import org.apache.kafka.common.errors.UnknownTopicOrPartitionException;
import org.apache.kafka.common.serialization.ByteArraySerializer;
import org.apache.kafka.common.serialization.StringSerializer;

import com.sitewhere.microservice.lifecycle.TenantEngineLifecycleComponent;
import com.sitewhere.spi.SiteWhereException;
import com.sitewhere.spi.microservice.instance.IInstanceSettings;
import com.sitewhere.spi.microservice.kafka.IMicroserviceKafkaProducer;
import com.sitewhere.spi.microservice.lifecycle.ILifecycleProgressMonitor;

/**
 * Base class for components that produce messages that are forwarded to a Kafka
 * topic.
 */
public abstract class MicroserviceKafkaProducer extends TenantEngineLifecycleComponent
	implements IMicroserviceKafkaProducer {

    /** Kafka availability check interval */
    private static final int KAFKA_RETRY_INTERVAL_MS = 10 * 1000;

    /** Producer */
    private KafkaProducer producer;

    /** Kafka admin client */
    private AdminClient kafkaAdmin;

    /** Kafka acknowledgement policy */
    private AckPolicy ackPolicy;

    /** Indicator for whether Kafka is available */
    private CountDownLatch kafkaAvailable;

    /** Executor service for waiter thread */
    ExecutorService waiterService;

    public MicroserviceKafkaProducer(AckPolicy ackPolicy) {
	this.ackPolicy = ackPolicy;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.sitewhere.server.lifecycle.LifecycleComponent#start(com.sitewhere.spi
     * .server.lifecycle.ILifecycleProgressMonitor)
     */
    @Override
    public void start(ILifecycleProgressMonitor monitor) throws SiteWhereException {
	getLogger().info(
		"Producer connecting to Kafka: " + getMicroservice().getInstanceSettings().getKafkaBootstrapServers());
	getLogger().info("Will be producing messages for: " + getTargetTopicName());
	this.kafkaAvailable = new CountDownLatch(1);
	this.waiterService = Executors.newSingleThreadExecutor();
	getWaiterService().execute(new KafkaWaiter());
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.sitewhere.server.lifecycle.LifecycleComponent#stop(com.sitewhere.spi.
     * server.lifecycle.ILifecycleProgressMonitor)
     */
    @Override
    public void stop(ILifecycleProgressMonitor monitor) throws SiteWhereException {
	if (getProducer() != null) {
	    getProducer().close();
	}
	if (getWaiterService() != null) {
	    getWaiterService().shutdown();
	}
    }

    /*
     * @see
     * com.sitewhere.spi.microservice.kafka.IMicroserviceKafkaProducer#send(java.
     * lang.String, byte[])
     */
    @Override
    public Future send(String key, byte[] message) throws SiteWhereException {
	while (true) {
	    ProducerRecord record = new ProducerRecord(getTargetTopicName(), key,
		    message);
	    try {
		if (getKafkaAvailable().getCount() != 0) {
		    getLogger().info("Producer waiting on Kafka to become available...");
		    getKafkaAvailable().await();
		}
		if (getProducer() == null) {
		    this.producer = new KafkaProducer(buildConfiguration());
		}
		return getProducer().send(record);
	    } catch (RetriableException e) {
		// Wait before attempting to send again.
		try {
		    getLogger().info(
			    String.format("Got retriable exception [%s] while sending Kafka payload. Waiting to retry.",
				    e.getMessage()));
		    Thread.sleep(5000);
		} catch (InterruptedException e1) {
		    getLogger().info("Interrupted while waiting to send Kafka payload.");
		}
	    } catch (InterruptedException e) {
		throw new SiteWhereException("Producer interrupted while waiting for Kafka.", e);
	    } catch (IllegalStateException e) {
		throw new SiteWhereException("Producer unable to send record.", e);
	    } catch (Throwable e) {
		throw new SiteWhereException("Unhandled exception in producer while sending record.", e);
	    }
	}
    }

    /**
     * Build configuration settings used by producer.
     * 
     * @return
     * @throws SiteWhereException
     */
    protected Properties buildConfiguration() throws SiteWhereException {
	Properties config = new Properties();
	config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
		getMicroservice().getInstanceSettings().getKafkaBootstrapServers());
	config.put(ProducerConfig.ACKS_CONFIG, getAckPolicy().getConfig());
	config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
	config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class.getName());
	return config;
    }

    /**
     * Build configuration settings used by admin client.
     * 
     * @return
     * @throws SiteWhereException
     */
    protected Properties buildAdminConfiguration() throws SiteWhereException {
	Properties config = new Properties();
	config.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,
		getMicroservice().getInstanceSettings().getKafkaBootstrapServers());
	return config;
    }

    /**
     * Thread that waits for Kafka to become available.
     */
    private class KafkaWaiter implements Runnable {

	@Override
	public void run() {
	    getLogger().info("Attempting to connect to Kafka...");
	    while (true) {
		try {
		    MicroserviceKafkaProducer.this.kafkaAdmin = AdminClient.create(buildAdminConfiguration());
		    Map topicMap = getKafkaAdmin()
			    .describeTopics(Arrays.asList(getTargetTopicName())).all().get();
		    TopicDescription topic = topicMap.get(getTargetTopicName());
		    if (topic != null) {
			getLogger().info("Kafka detected as available.");
			getKafkaAvailable().countDown();
			return;
		    }
		} catch (ExecutionException e) {
		    Throwable t = e.getCause();
		    if (t instanceof UnknownTopicOrPartitionException) {
			try {
			    IInstanceSettings settings = getMicroservice().getInstanceSettings();
			    NewTopic newTopic = new NewTopic(getTargetTopicName(),
				    settings.getKafkaDefaultTopicPartitions(),
				    (short) settings.getKafkaDefaultTopicReplicationFactor().intValue());
			    CreateTopicsResult result = getKafkaAdmin()
				    .createTopics(Collections.singletonList(newTopic));
			    result.all().get();
			    getLogger().info(String.format("Kafka topic '%s' created.", getTargetTopicName()));
			} catch (SiteWhereException e1) {
			    getLogger().error("Exception creating topic.", e1);
			} catch (ExecutionException e1) {
			    if (e1.getCause() instanceof TopicExistsException) {
				getLogger().debug("Topic already existed.");
			    } else if (e1.getCause() instanceof InvalidReplicationFactorException) {
				getLogger().info("Not enough replicas are available to create topic. Waiting.");
				try {
				    Thread.sleep(1000);
				} catch (InterruptedException e2) {
				    getLogger().error("Interrupted while waiting for replicas.");
				    return;
				}
			    } else {
				getLogger().error("Kakfa exception creating topic.", e1);
			    }
			} catch (InterruptedException e1) {
			    getLogger().error("Interrupted while creating topic.");
			    return;
			}
		    } else {
			getLogger()
				.warn("Execution exception connecting to Kafka. Will continue attempting to connect. ("
					+ e.getMessage() + ")", t);
		    }
		} catch (ConfigException e) {
		    getLogger().warn("Configuration issue connecting to Kafka. Will continue attempting to connect.",
			    e);
		} catch (Throwable t) {
		    getLogger().warn("Exception while connecting to Kafka. Will continue attempting to connect.", t);
		}
		try {
		    Thread.sleep(KAFKA_RETRY_INTERVAL_MS);
		} catch (InterruptedException e) {
		    getLogger().warn("Interrupted while waiting for Kafka to become available.");
		    return;
		}
	    }
	}
    }

    protected KafkaProducer getProducer() {
	return producer;
    }

    protected void setProducer(KafkaProducer producer) {
	this.producer = producer;
    }

    protected AdminClient getKafkaAdmin() {
	return kafkaAdmin;
    }

    protected void setKafkaAdmin(AdminClient kafkaAdmin) {
	this.kafkaAdmin = kafkaAdmin;
    }

    protected AckPolicy getAckPolicy() {
	return ackPolicy;
    }

    protected void setAckPolicy(AckPolicy ackPolicy) {
	this.ackPolicy = ackPolicy;
    }

    protected CountDownLatch getKafkaAvailable() {
	return kafkaAvailable;
    }

    protected void setKafkaAvailable(CountDownLatch kafkaAvailable) {
	this.kafkaAvailable = kafkaAvailable;
    }

    protected ExecutorService getWaiterService() {
	return waiterService;
    }

    protected void setWaiterService(ExecutorService waiterService) {
	this.waiterService = waiterService;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy