com.networknt.kafka.producer.SidecarProducer Maven / Gradle / Ivy
package com.networknt.kafka.producer;
import com.fasterxml.jackson.databind.node.NullNode;
import com.google.protobuf.ByteString;
import com.networknt.config.Config;
import com.networknt.exception.FrameworkException;
import com.networknt.kafka.common.KafkaProducerConfig;
import com.networknt.kafka.entity.*;
import com.networknt.status.Status;
import com.networknt.utility.Constants;
import com.networknt.utility.ObjectUtils;
import com.networknt.utility.Util;
import io.confluent.kafka.schemaregistry.avro.AvroSchemaProvider;
import io.confluent.kafka.schemaregistry.client.CachedSchemaRegistryClient;
import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient;
import io.confluent.kafka.schemaregistry.client.rest.RestService;
import io.confluent.kafka.schemaregistry.json.JsonSchemaProvider;
import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchemaProvider;
import io.confluent.kafka.serializers.subject.TopicNameStrategy;
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.apache.kafka.common.KafkaException;
import org.apache.kafka.common.errors.AuthenticationException;
import org.apache.kafka.common.errors.AuthorizationException;
import org.apache.kafka.common.errors.RetriableException;
import org.apache.kafka.common.header.Headers;
import org.apache.kafka.common.header.internals.RecordHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static java.util.Collections.singletonList;
/**
 * This is the guaranteed producer to ensure that the message is acknowledged from the Kafka brokers
 * before the service is respond to the consumer call. Although this producer is not the best one
 * for high throughput batch producing, it is the safest one. Once the caller receives the successful
 * response from the service, it can make sure that the message is on the Kafka cluster.
 *
 * @author Steve Hu
 */
public class SidecarProducer implements NativeLightProducer {
    static private final Logger logger = LoggerFactory.getLogger(SidecarProducer.class);
    public static final KafkaProducerConfig config = (KafkaProducerConfig) Config.getInstance().getJsonObjectConfig(KafkaProducerConfig.CONFIG_NAME, KafkaProducerConfig.class);
    public static Map> schemaCache = new ConcurrentHashMap<>();
    private static String FAILED_TO_GET_SCHEMA = "ERR12208";
    private SchemaManager schemaManager;
    private SchemaRecordSerializer schemaRecordSerializer;
    private NoSchemaRecordSerializer noSchemaRecordSerializer;
    public Producer producer;
    @Override
    public void open() {
        producer = new KafkaProducer<>(config.getProperties());
        Map configs = new HashMap<>();
        configs.putAll(config.getProperties());
        String url = (String) config.getProperties().get("schema.registry.url");
        Object cacheObj = config.getProperties().get("schema.registry.cache");
        int cache = 100;
        if (cacheObj != null && cacheObj instanceof String) {
            cache = Integer.valueOf((String) cacheObj);
        }
        SchemaRegistryClient schemaRegistryClient = new CachedSchemaRegistryClient(
                new RestService(singletonList(url)),
                cache,
                Arrays.asList(new AvroSchemaProvider(), new JsonSchemaProvider(), new ProtobufSchemaProvider()),
                configs,
                null
        );
        noSchemaRecordSerializer = new NoSchemaRecordSerializer(new HashMap<>());
        schemaRecordSerializer = new SchemaRecordSerializer(schemaRegistryClient, configs, configs, configs);
        schemaManager = new SchemaManagerImpl(schemaRegistryClient, new TopicNameStrategy());
        // register the config to the module registry to output in server info.
        registerModule();
    }
    @Override
    public Producer getProducer() {
        return producer;
    }
    @Override
    public void close() {
        if(producer != null) {
            producer.close();
        }
    }
    public final CompletableFuture produceWithSchema(
            String topicName,
            String serviceId,
            Optional partition,
            ProduceRequest request,
            Headers headers, List auditRecords,
            boolean isReplay) {
        // get key schema based on different scenarios.
        long startSchema = System.currentTimeMillis();
        Optional keySchema = Optional.empty();
        if(null != request.getKeySchemaId() && request.getKeySchemaId().isPresent()) {
            // get from the cache first if keySchemaId is not empty.
            keySchema = schemaCache.get(topicName + "k" + request.getKeySchemaId().get());
        } else if (null !=request.getKeySchemaVersion() &&  request.getKeySchemaVersion().isPresent()) {
            // get form the cache first if KeySchemaVersion is not empty
            if(null != request.getKeySchemaSubject() && request.getKeySchemaSubject().isPresent()) {
                // use the supplied subject
                keySchema = schemaCache.get(request.getKeySchemaSubject().get() + request.getKeySchemaVersion().get());
            } else {
                // default to topic + isKey
                keySchema = schemaCache.get(topicName + "k" + request.getKeySchemaVersion().get());
            }
        }else if (isReplay) {
            //get the schema with topic name and v- value type
            keySchema = schemaCache.get(topicName + "k");
        }
        // reset the KeySchema as the cache will return null if the entry doesn't exist.
        if(keySchema == null) keySchema = Optional.empty();
        if(keySchema.isEmpty() && request.getKeyFormat().isPresent() && request.getKeyFormat().get().requiresSchema()) {
            keySchema =
                    getSchema(
                            topicName,
                            request.getKeyFormat(),
                            request.getKeySchemaSubject(),
                            request.getKeySchemaId(),
                            request.getKeySchemaVersion(),
                            request.getKeySchema(),
                            /* isKey= */ true);
            if(keySchema.isPresent()) {
                if(request.getKeySchemaId().isPresent()) {
                    schemaCache.put(topicName + "k" + request.getKeySchemaId().get(), keySchema);
                } else if(request.getKeySchemaVersion().isPresent()) {
                    if(request.getKeySchemaSubject().isPresent()) {
                        schemaCache.put(request.getKeySchemaSubject().get() + request.getKeySchemaVersion().get(), keySchema);
                    } else {
                        schemaCache.put(topicName + "k" + request.getKeySchemaVersion().get(), keySchema);
                    }
                } else if(isReplay){
                    schemaCache.put(topicName + "k" , keySchema);
                } else {
                    logger.error("Could not put key schema into the cache. It means that neither keySchemaId nor keySchemaVersion is supplied and Kafka Schema Registry will be overloaded.");
                }
            }
        }
        Optional keyFormat =
                keySchema.map(schema -> Optional.of(schema.getFormat()))
                        .orElse(request.getKeyFormat());
        // get value schema based on different scenarios.
        Optional valueSchema = Optional.empty();
        if(null !=request.getValueSchemaId() &&  request.getValueSchemaId().isPresent()) {
            // get from the cache first if ValueSchemaId is not empty
            valueSchema = schemaCache.get(topicName + "v" + request.getValueSchemaId().get());
        } else if (null !=request.getValueSchemaVersion() && request.getValueSchemaVersion().isPresent()) {
            // get from the cache first if ValueSchemaVersion is not empty
            if(null != request.getValueSchemaSubject() && request.getValueSchemaSubject().isPresent()) {
                // use the supplied subject
                valueSchema = schemaCache.get(request.getValueSchemaSubject().get() + request.getValueSchemaVersion().get());
            } else {
                // default to topic + isKey
                valueSchema = schemaCache.get(topicName + "v" + request.getValueSchemaVersion().get());
            }
        }else if (isReplay) {
            //get the schema with topic name and v- value type
            valueSchema = schemaCache.get(topicName + "v");
        }
        // reset the valueSchema as the cache will return null if the entry doesn't exist.
        if(valueSchema == null) valueSchema = Optional.empty();
        if(valueSchema.isEmpty() && request.getValueFormat().isPresent() && request.getValueFormat().get().requiresSchema()) {
            valueSchema =
                    getSchema(
                            topicName,
                            request.getValueFormat(),
                            request.getValueSchemaSubject(),
                            request.getValueSchemaId(),
                            request.getValueSchemaVersion(),
                            request.getValueSchema(),
                            /* isKey= */ false);
            if(valueSchema.isPresent()) {
                if(request.getValueSchemaId().isPresent()) {
                    schemaCache.put(topicName + "v" + request.getValueSchemaId().get(), valueSchema);
                } else if(request.getValueSchemaVersion().isPresent()) {
                    if(request.getValueSchemaSubject().isPresent()) {
                        schemaCache.put(request.getValueSchemaSubject().get() + request.getValueSchemaVersion().get(), valueSchema);
                    } else {
                        schemaCache.put(topicName + "v" + request.getValueSchemaVersion().get(), valueSchema);
                    }
                } else if(isReplay){
                        schemaCache.put(topicName + "v" , valueSchema);
                }else {
                    logger.error("Could not put value schema into the cache. It means that neither valueSchemaId nor valueSchemaVersion is supplied and Kafka Schema Registry will be overloaded.");
                }
            }
        }
        Optional valueFormat =
                valueSchema.map(schema -> Optional.of(schema.getFormat()))
                        .orElse(request.getValueFormat());
        List serialized =
                serialize(
                        keyFormat,
                        valueFormat,
                        topicName,
                        partition,
                        keySchema,
                        valueSchema,
                        request.getRecords());
        if(logger.isDebugEnabled()) {
            logger.debug("Serializing key and value with schema registry takes " + (System.currentTimeMillis() - startSchema));
        }
        long startProduce = System.currentTimeMillis();
        List> resultFutures = doProduce(topicName, serviceId, serialized, headers, auditRecords);
        if(logger.isDebugEnabled()) {
            logger.debug("Producing the entire batch to Kafka takes " + (System.currentTimeMillis() - startProduce));
        }
        return produceResultsToResponse(keySchema, valueSchema, resultFutures);
    }
    public final CompletableFuture produceWithSchema(
            String topicName,
            String serviceId,
            Optional partition,
            ProduceRequest request,
            Headers headers, List auditRecords) {
        return produceWithSchema(topicName, serviceId, partition, request, headers, auditRecords, false);
    }
    private List serialize(
            Optional keyFormat,
            Optional valueFormat,
            String topicName,
            Optional partition,
            Optional keySchema,
            Optional valueSchema,
            List records) {
        AtomicInteger atomicInteger= new AtomicInteger(0);
        return records.stream()
                .map(
                        record ->
                        {
                            int atomicIntegerNew=atomicInteger.getAndIncrement();
                            return new SerializedKeyAndValue(
                                    record.getPartition().map(Optional::of).orElse(partition),
                                    record.getTraceabilityId(),
                                    record.getCorrelationId(),
                                    keyFormat.isPresent() && keyFormat.get().requiresSchema() ?
                                            schemaRecordSerializer
                                                    .serialize(
                                                            atomicIntegerNew,
                                                            keyFormat.get(),
                                                            topicName,
                                                            keySchema,
                                                            record.getKey().orElse(NullNode.getInstance()),
                                                            /* isKey= */ true) :
                                            noSchemaRecordSerializer
                                                    .serialize(atomicIntegerNew, keyFormat.orElse(EmbeddedFormat.valueOf(config.getKeyFormat().toUpperCase())), record.getKey().orElse(NullNode.getInstance())),
                                    valueFormat.isPresent() && valueFormat.get().requiresSchema() ?
                                            schemaRecordSerializer
                                                    .serialize(
                                                            atomicIntegerNew,
                                                            valueFormat.get(),
                                                            topicName,
                                                            valueSchema,
                                                            record.getValue().orElse(NullNode.getInstance()),
                                                            /* isKey= */ false) :
                                            noSchemaRecordSerializer
                                                    .serialize(atomicIntegerNew, valueFormat.orElse(EmbeddedFormat.valueOf(config.getValueFormat().toUpperCase())), record.getValue().orElse(NullNode.getInstance())),
                                    record.getHeaders(),
                                    record.getTimestamp());
                        })
                .collect(Collectors.toList());
    }
    private Optional getSchema(
            String topicName,
            Optional format,
            Optional subject,
            Optional schemaId,
            Optional schemaVersion,
            Optional schema,
            boolean isKey) {
        try {
            return Optional.of(
                    schemaManager.getSchema(
                            /* topicName= */ topicName,
                            /* format= */ format,
                            /* subject= */ subject,
                            /* subjectNameStrategy= */ Optional.empty(),
                            /* schemaId= */ schemaId,
                            /* schemaVersion= */ schemaVersion,
                            /* rawSchema= */ schema,
                            /* isKey= */ isKey));
        } catch (IllegalStateException e) {
            logger.error("IllegalStateException:", e);
            Status status = new Status(FAILED_TO_GET_SCHEMA);
            throw new FrameworkException(status, e);
        } catch (RuntimeException e) {
            return Optional.empty();
        }
    }
    private List> doProduce(
            String topicName, String serviceId, List serialized, Headers headers, List auditRecords) {
        return serialized.stream()
                .map(
                        record ->
                                produce(
                                topicName,
                                record.getPartitionId(),
                                record.getTraceabilityId(),
                                record.getCorrelationId().isPresent() ? record.getCorrelationId() : Optional.of(Util.getUUID()),
                                serviceId,
                                headers,
                                auditRecords,
                                record.getKey(),
                                record.getValue(),
                                record.getHeaders(),
                                /* timestamp= */ (!ObjectUtils.isEmpty(record.getTimestamp()) && record.getTimestamp().isPresent() && record.getTimestamp().get()>0) ? Instant.ofEpochMilli(record.getTimestamp().get()) : Instant.now()))
                .collect(Collectors.toList());
    }
    public CompletableFuture produce(
            String topicName,
            Optional partitionId,
            Optional traceabilityId,
            Optional correlationId,
            String serviceId,
            Headers headers,
            List auditRecords,
            Optional key,
            Optional value,
            Optional                                          © 2015 - 2025 Weber Informatics LLC | Privacy Policy