/* * Copyright (c) 2010-2018. Axon Framework * * Licensed 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.axonframework.extensions.kafka.eventhandling.producer; 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.ProducerConfig; 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.errors.ProducerFencedException; import org.axonframework.common.AxonConfigurationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Duration; import java.time.temporal.TemporalUnit; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import static org.axonframework.common.BuilderUtils.assertNonNull; import static org.axonframework.common.BuilderUtils.assertThat; /** * The {@link ProducerFactory} implementation to produce a {@code singleton} shared {@link Producer} instance. * <p> * The {@link Producer} instance is freed from the external {@link Producer#close()} invocation with the internal * wrapper. The real {@link Producer#close()} is called on the target {@link Producer} during the {@link #shutDown()}. * <p> * Setting {@link Builder#confirmationMode(ConfirmationMode)} to transactional produces a transactional producer; in * which case, a cache of producers is maintained; closing the producer returns it to the cache. If cache is full the * producer will be closed through {@link KafkaProducer#close(Duration)} and evicted from cache. * * @author Nakul Mishra * @since 4.0 */ public class DefaultProducerFactory<K, V> implements ProducerFactory<K, V> { private static final Logger logger = LoggerFactory.getLogger(DefaultProducerFactory.class); private final Duration closeTimeout; private final BlockingQueue<PoolableProducer<K, V>> cache; private final Map<String, Object> configuration; private final ConfirmationMode confirmationMode; private final String transactionIdPrefix; private final AtomicInteger transactionIdSuffix; private volatile ShareableProducer<K, V> nonTransactionalProducer; /** * Instantiate a {@link DefaultProducerFactory} based on the fields contained in the {@link Builder}. * <p> * Will assert that the {@code configuration} is not {@code null}, and will throw an * {@link AxonConfigurationException} if it is {@code null}. * * @param builder the {@link Builder} used to instantiate a {@link DefaultProducerFactory} instance */ @SuppressWarnings("WeakerAccess") protected DefaultProducerFactory(Builder<K, V> builder) { builder.validate(); this.closeTimeout = builder.closeTimeout; this.cache = new ArrayBlockingQueue<>(builder.producerCacheSize); this.configuration = builder.configuration; this.confirmationMode = builder.confirmationMode; this.transactionIdPrefix = builder.transactionIdPrefix; this.transactionIdSuffix = new AtomicInteger(); } /** * Instantiate a Builder to be able to create a {@link DefaultProducerFactory}. * <p> * The {@code closeTimeout} is defaulted to a {@link Duration#ofSeconds(long)} of {@code 30}, the * {@code producerCacheSize} defaults to {@code 10} and the {@link ConfirmationMode} is defaulted to * {@link ConfirmationMode#NONE}. The {@code configuration} is a <b>hard requirement</b> and as such should be * provided. * * @param <K> a generic type for the key of the {@link Producer} this {@link ProducerFactory} will create * @param <V> a generic type for the value of the {@link Producer} this {@link ProducerFactory} will create * @return a Builder to be able to create a {@link DefaultProducerFactory} */ public static <K, V> Builder<K, V> builder() { return new Builder<>(); } @Override public Producer<K, V> createProducer() { if (confirmationMode.isTransactional()) { return createTransactionalProducer(); } if (this.nonTransactionalProducer == null) { synchronized (this) { if (this.nonTransactionalProducer == null) { this.nonTransactionalProducer = new ShareableProducer<>(createKafkaProducer(configuration)); } } } return this.nonTransactionalProducer; } @Override public ConfirmationMode confirmationMode() { return confirmationMode; } /** * Return an unmodifiable reference to the configuration map for this factory. Useful for cloning to make a similar * factory. * * @return a configuration {@link Map} used by this {@link ProducerFactory} to build {@link Producer}s */ public Map<String, Object> configurationProperties() { return Collections.unmodifiableMap(configuration); } /** * The {@code transactionalIdPrefix} used to mark all {@link Producer} instances. * * @return the {@code transactionalIdPrefix} used to mark all {@link Producer} instances */ public String transactionIdPrefix() { return transactionIdPrefix; } @Override public void shutDown() { ProducerDecorator<K, V> producer = this.nonTransactionalProducer; this.nonTransactionalProducer = null; if (producer != null) { producer.delegate.close(this.closeTimeout); } producer = this.cache.poll(); while (producer != null) { try { producer.delegate.close(this.closeTimeout); } catch (Exception e) { logger.error("Exception closing producer", e); } producer = this.cache.poll(); } } private Producer<K, V> createTransactionalProducer() { Producer<K, V> producer = this.cache.poll(); if (producer != null) { return producer; } Map<String, Object> configs = new HashMap<>(this.configuration); configs.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, this.transactionIdPrefix + this.transactionIdSuffix.getAndIncrement()); producer = new PoolableProducer<>(createKafkaProducer(configs), cache, closeTimeout); producer.initTransactions(); return producer; } private Producer<K, V> createKafkaProducer(Map<String, Object> configs) { return new KafkaProducer<>(configs); } /** * Abstract base class to apply the decorator pattern to a Kafka {@link Producer}. Implements all methods in the {@link Producer} interface by calling the * wrapped delegate. Subclasses can override any of the methods to add their specific behaviour. * * @param <K> record key type * @param <V> record value type */ private abstract static class ProducerDecorator<K, V> implements Producer<K, V> { private final Producer<K, V> delegate; ProducerDecorator(Producer<K, V> delegate) { this.delegate = delegate; } @Override public Future<RecordMetadata> send(ProducerRecord<K, V> record) { return this.delegate.send(record); } @Override public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) { return this.delegate.send(record, callback); } @Override public void flush() { this.delegate.flush(); } @Override public List<PartitionInfo> partitionsFor(String topic) { return this.delegate.partitionsFor(topic); } @Override public Map<MetricName, ? extends Metric> metrics() { return this.delegate.metrics(); } @Override public void initTransactions() { this.delegate.initTransactions(); } @Override public void beginTransaction() throws ProducerFencedException { this.delegate.beginTransaction(); } @Override public void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets, String consumerGroupId) throws ProducerFencedException { this.delegate.sendOffsetsToTransaction(offsets, consumerGroupId); } @Override public void commitTransaction() throws ProducerFencedException { this.delegate.commitTransaction(); } @Override public void abortTransaction() throws ProducerFencedException { this.delegate.abortTransaction(); } @Override public void close() { this.delegate.close(); } @Override public void close(Duration timeout) { this.delegate.close(timeout); } @Override public String toString() { return this.getClass().getSimpleName() + " [delegate=" + this.delegate + "]"; } } /** * A decorator for a Kafka {@link Producer} that returns itself to an instance pool when {@link #close()} is called instead of actually closing the wrapped * {@link Producer}. If the pool is already full (i.e. has the configured amount of idle producers), the wrapped producer is closed instead. * * @param <K> record key type * @param <V> record value type */ private static final class PoolableProducer<K, V> extends ProducerDecorator<K, V> { private final BlockingQueue<PoolableProducer<K, V>> pool; private final Duration closeTimeout; PoolableProducer(Producer<K, V> delegate, BlockingQueue<PoolableProducer<K, V>> pool, Duration closeTimeout) { super(delegate); this.pool = pool; this.closeTimeout = closeTimeout; } @Override public void close() { close(closeTimeout); } @Override public void close(Duration timeout) { boolean isAdded = this.pool.offer(this); if (!isAdded) { super.close(timeout); } } } /** * A decorator for a Kafka {@link Producer} that ignores any calls to {@link #close()} so it can be reused and closed by any number of clients. * * @param <K> record key type * @param <V> record value type */ private static final class ShareableProducer<K, V> extends ProducerDecorator<K, V> { ShareableProducer(Producer<K, V> delegate) { super(delegate); } @Override public void close() { // Do nothing } @Override public void close(Duration timeout) { // Do nothing } } /** * Builder class to instantiate a {@link DefaultProducerFactory}. * <p> * The {@code closeTimeout} is defaulted to a {@link Duration#ofSeconds(long)} of {@code 30}, the * {@code producerCacheSize} defaults to {@code 10} and the {@link ConfirmationMode} is defaulted to * {@link ConfirmationMode#NONE}. The {@code configuration} is a <b>hard requirement</b> and as such should be * provided. * * @param <K> a generic type for the key of the {@link Producer} this {@link ProducerFactory} will create * @param <V> a generic type for the value of the {@link Producer} this {@link ProducerFactory} will create */ public static final class Builder<K, V> { private Duration closeTimeout = Duration.ofSeconds(30); private int producerCacheSize = 10; private Map<String, Object> configuration; private ConfirmationMode confirmationMode = ConfirmationMode.NONE; private String transactionIdPrefix; /** * Set the {@code closeTimeout} specifying how long to wait when {@link Producer#close(Duration)} is * invoked. Defaults to a {@link Duration#ofSeconds(long)} of {@code 30}. * * @param timeout the time to wait before invoking {@link Producer#close(Duration)} in units of * {@code temporalUnit}. * @param temporalUnit a {@link TemporalUnit} determining how to interpret the {@code timeout} parameter * @return the current Builder instance, for fluent interfacing */ public Builder<K, V> closeTimeout(int timeout, TemporalUnit temporalUnit) { assertNonNull(temporalUnit, "The temporalUnit may not be null"); return closeTimeout(Duration.of(timeout, temporalUnit)); } /** * Set the {@code closeTimeout} specifying how long to wait when {@link Producer#close(Duration)} is * invoked. Defaults to a {@link Duration#ofSeconds(long)} of {@code 30}. * * @param closeTimeout the {@link Duration} to wait before invoking {@link Producer#close(Duration)} * @return the current Builder instance, for fluent interfacing */ @SuppressWarnings("WeakerAccess") public Builder<K, V> closeTimeout(Duration closeTimeout) { assertThat( closeTimeout, timeoutDuration -> !timeoutDuration.isNegative(), "The closeTimeout should be a positive duration" ); assertNonNull(closeTimeout, "The closeTimeout may not be null"); this.closeTimeout = closeTimeout; return this; } /** * Sets the number of {@link Producer} instances to cache. Defaults to {@code 10}. * <p> * Will instantiate an {@link ArrayBlockingQueue} based on this number. * * @param producerCacheSize an {@code int} specifying the number of {@link Producer} instances to cache * @return the current Builder instance, for fluent interfacing */ public Builder<K, V> producerCacheSize(int producerCacheSize) { assertThat(producerCacheSize, size -> size > 0, "The producerCacheSize should be a positive number"); this.producerCacheSize = producerCacheSize; return this; } /** * Sets the {@code configuration} properties for creating {@link Producer} instances. * * @param configuration a {@link Map} of {@link String} to {@link Object} containing Kafka properties for * creating {@link Producer} instances * @return the current Builder instance, for fluent interfacing */ public Builder<K, V> configuration(Map<String, Object> configuration) { assertNonNull(configuration, "The configuration may not be null"); this.configuration = Collections.unmodifiableMap(new HashMap<>(configuration)); return this; } /** * Sets the {@link ConfirmationMode} for producing {@link Producer} instances. Defaults to * {@link ConfirmationMode#NONE}. * * @param confirmationMode the {@link ConfirmationMode} for producing {@link Producer} instances * @return the current Builder instance, for fluent interfacing */ public Builder<K, V> confirmationMode(ConfirmationMode confirmationMode) { assertNonNull(confirmationMode, "ConfirmationMode may not be null"); this.confirmationMode = confirmationMode; return this; } /** * Sets the prefix to generate the {@code transactional.id} required for transactional {@link Producer}s. * * @param transactionIdPrefix a {@link String} specifying the prefix used to generate the * {@code transactional.id} required for transactional {@link Producer}s * @return the current Builder instance, for fluent interfacing */ public Builder<K, V> transactionalIdPrefix(String transactionIdPrefix) { assertNonNull(transactionIdPrefix, "The transactionalIdPrefix may not be null"); this.transactionIdPrefix = transactionIdPrefix; return this.confirmationMode(ConfirmationMode.TRANSACTIONAL); } /** * Initializes a {@link DefaultProducerFactory} as specified through this Builder. * * @return a {@link DefaultProducerFactory} as specified through this Builder */ public DefaultProducerFactory<K, V> build() { return new DefaultProducerFactory<>(this); } /** * Validates whether the fields contained in this Builder are set accordingly. * * @throws AxonConfigurationException if one field is asserted to be incorrect according to the Builder's * specifications */ @SuppressWarnings("WeakerAccess") protected void validate() throws AxonConfigurationException { assertNonNull(configuration, "The configuration is a hard requirement and should be provided"); } } }