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

io.streamthoughts.jikkou.kafka.internals.producer.DefaultProducerFactory Maven / Gradle / Ivy

The newest version!
/*
 * SPDX-License-Identifier: Apache-2.0
 * Copyright (c) The original authors
 *
 * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
 */
package io.streamthoughts.jikkou.kafka.internals.producer;

import io.streamthoughts.jikkou.common.memory.OpaqueMemoryResource;
import io.streamthoughts.jikkou.common.memory.ResourceDisposer;
import io.streamthoughts.jikkou.common.memory.SharedResources;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.function.Supplier;
import org.apache.kafka.clients.consumer.ConsumerGroupMetadata;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.Metric;
import org.apache.kafka.common.MetricName;
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.Uuid;
import org.apache.kafka.common.errors.ProducerFencedException;
import org.apache.kafka.common.serialization.Serializer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class DefaultProducerFactory implements ProducerFactory, AutoCloseable {

    private static final Logger LOG = LoggerFactory.getLogger(DefaultProducerFactory.class);

    private final Supplier> configSupplier;
    private Serializer keySerializer;
    private Serializer valueSerializer;
    private Duration closeTimeout = Duration.ofSeconds(30);
    /* share producer instance **/
    private final SharedResources resources = new SharedResources();
    private final List leaseHolders = new ArrayList<>();

    /**
     * Creates a new {@link DefaultProducerFactory} instance.
     *
     * @param config the Producer client properties.
     */
    public DefaultProducerFactory(@NotNull Map config) {
        Map immutable = Collections.unmodifiableMap(config);
        this.configSupplier = () -> immutable;
    }

    /**
     * Creates a new {@link DefaultProducerFactory} instance.
     *
     * @param config          the Producer client properties.
     * @param keySerializer   the Serializer for the record-key.
     * @param valueSerializer the Serializer for the record-value.
     */
    public DefaultProducerFactory(@NotNull Map config,
                                  @Nullable Serializer keySerializer,
                                  @Nullable Serializer valueSerializer) {
        Map immutable = Collections.unmodifiableMap(config);
        this.configSupplier = () -> immutable;
        this.keySerializer = keySerializer;
        this.valueSerializer = valueSerializer;
    }

    /**
     * Creates a new {@link DefaultProducerFactory} instance.
     *
     * @param configSupplier  the Producer client properties supplier
     * @param keySerializer   the Serializer for the record-key.
     * @param valueSerializer the Serializer for the record-value.
     */
    public DefaultProducerFactory(@NotNull Supplier> configSupplier,
                                  @Nullable Serializer keySerializer,
                                  @Nullable Serializer valueSerializer) {
        this.configSupplier = configSupplier;
        this.keySerializer = keySerializer;
        this.valueSerializer = valueSerializer;
    }

    public DefaultProducerFactory setKeySerializer(Serializer keySerializer) {
        this.keySerializer = keySerializer;
        return this;
    }

    public DefaultProducerFactory setValueSerializer(Serializer valueSerializer) {
        this.valueSerializer = valueSerializer;
        return this;
    }

    public DefaultProducerFactory setCloseTimeout(Duration closeTimeout) {
        this.closeTimeout = closeTimeout;
        return this;
    }

    /**
     * {@inheritDoc}
     **/
    @Override
    public Producer createProducer() {
        final Object leaseHolder = new Object();
        Producer producer = resources.getOrCreateSharedResource(
                "kafka-producer",
                this::createKafkaProducer,
                leaseHolder
        );

        ResourceDisposer disposer = createResourceDisposer(leaseHolder);

        leaseHolders.add(leaseHolder);
        return new OpaqueProducer<>(new OpaqueMemoryResource<>(producer, disposer));
    }

    @NotNull
    private ResourceDisposer createResourceDisposer(@NotNull final Object leaseHolder) {
        return () -> resources.release("kafka-producer", leaseHolder, this::closeKafkaProducer);
    }

    @NotNull
    private Producer createKafkaProducer() {
        LOG.info("Creating new kafka producer instance.");
        return new KafkaProducer<>(
                loadConfigs(),
                keySerializer,
                valueSerializer
        );
    }

    private void closeKafkaProducer(Producer producer) {
        producer.close(closeTimeout);
    }

    private Map loadConfigs() {
        return configSupplier.get();
    }

    /**
     * {@inheritDoc}
     **/
    @Override
    public void close() {
        ListIterator iterator = leaseHolders.listIterator();
        while (iterator.hasNext()) {
            Object leaseHolder = iterator.next();
            try {
                createResourceDisposer(leaseHolder).dispose();
            } catch (Throwable e) {
                LOG.error("Error while closing resource", e);
            }
            iterator.remove();
        }
    }

    /**
     * A {@code ProducerDisposer} can be used to dispose a shared resource after it is not used anymore.
     */

    private static class OpaqueProducer implements Producer {

        private final OpaqueMemoryResource> delegate;

        /**
         * Creates a new {@link OpaqueMemoryResource} instance.
         *
         * @param delegate the delegate Producer.
         */
        public OpaqueProducer(OpaqueMemoryResource> delegate) {
            this.delegate = delegate;
        }

        /**
         * @see KafkaProducer#initTransactions()
         **/
        @Override
        public void initTransactions() {
            delegate.getResourceHandle().initTransactions();
        }

        /**
         * @see KafkaProducer#beginTransaction()
         **/
        @Override
        public void beginTransaction() throws ProducerFencedException {
            delegate.getResourceHandle().beginTransaction();
        }

        /**
         * @see KafkaProducer#sendOffsetsToTransaction(Map, String)
         **/
        @Deprecated
        @Override
        public void sendOffsetsToTransaction(Map offsets,
                                             String consumerGroupId) throws ProducerFencedException {
            delegate.getResourceHandle().sendOffsetsToTransaction(offsets, consumerGroupId);
        }

        /**
         * @see KafkaProducer#sendOffsetsToTransaction(Map, ConsumerGroupMetadata)
         **/
        @Override
        public void sendOffsetsToTransaction(Map offsets,
                                             ConsumerGroupMetadata groupMetadata) throws ProducerFencedException {
            delegate.getResourceHandle().sendOffsetsToTransaction(offsets, groupMetadata);
        }

        /**
         * @see KafkaProducer#commitTransaction()
         **/
        @Override
        public void commitTransaction() throws ProducerFencedException {
            delegate.getResourceHandle().commitTransaction();
        }

        /**
         * @see KafkaProducer#abortTransaction()
         **/
        @Override
        public void abortTransaction() throws ProducerFencedException {
            delegate.getResourceHandle().abortTransaction();
        }

        /**
         * @see KafkaProducer#send(ProducerRecord)
         **/
        @Override
        public Future send(ProducerRecord record) {
            return delegate.getResourceHandle().send(record);
        }

        /**
         * @see KafkaProducer#send(ProducerRecord, Callback)
         **/
        @Override
        public Future send(ProducerRecord record, Callback callback) {
            return delegate.getResourceHandle().send(record, callback);
        }

        /**
         * @see KafkaProducer#flush()
         **/
        @Override
        public void flush() {
            delegate.getResourceHandle().flush();
        }

        /**
         * @see KafkaProducer#partitionsFor(String)
         **/
        @Override
        public List partitionsFor(String topic) {
            return delegate.getResourceHandle().partitionsFor(topic);
        }

        /**
         * @see KafkaProducer#metrics()
         **/
        @Override
        public Map metrics() {
            return delegate.getResourceHandle().metrics();
        }

        /**
         * @see KafkaProducer#clientInstanceId(Duration)
         **/
        @Override
        public Uuid clientInstanceId(final Duration duration) {
            return delegate.getResourceHandle().clientInstanceId(duration);
        }

        /**
         * @see KafkaProducer#close()
         **/
        @Override
        public void close() {
            close(null);
        }

        /**
         * @see KafkaProducer#close(Duration)
         **/
        @Override
        public void close(Duration timeout) {
            try {
                delegate.close();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}