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

org.radarbase.producer.BatchedKafkaSender Maven / Gradle / Ivy

/*
 * Copyright 2017 The Hyve and King's College London
 *
 * 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.radarbase.producer;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import org.apache.avro.SchemaValidationException;
import org.radarbase.data.AvroRecordData;
import org.radarbase.data.RecordData;
import org.radarbase.topic.AvroTopic;

/**
 * A Kafka REST Proxy sender that batches up records. It will send data once the batch size is
 * exceeded, or when at a send call the first record in the batch is older than given age. If send,
 * flush or close are not called within this given age, the data will also not be sent. Calling
 * {@link #close()} will not flush or close the KafkaTopicSender that were created. That must be
 * done separately.
 */
public class BatchedKafkaSender implements KafkaSender {
    private final KafkaSender wrappedSender;
    private final long ageNanos;
    private final int maxBatchSize;

    /**
     * Kafka sender that sends data along.
     * @param sender kafka sender to send data with.
     * @param ageMillis threshold time after which a record should be sent.
     * @param maxBatchSize threshold batch size over which records should be sent.
     */
    public BatchedKafkaSender(KafkaSender sender, int ageMillis, int maxBatchSize) {
        this.wrappedSender = sender;
        this.ageNanos = TimeUnit.MILLISECONDS.toNanos(ageMillis);
        this.maxBatchSize = maxBatchSize;
    }

    @Override
    public  KafkaTopicSender sender(final AvroTopic topic)
            throws IOException, SchemaValidationException {
        return new BatchedKafkaTopicSender<>(topic);
    }

    @Override
    public boolean isConnected() throws AuthenticationException {
        return wrappedSender.isConnected();
    }

    @Override
    public boolean resetConnection() throws AuthenticationException {
        return wrappedSender.resetConnection();
    }

    @Override
    public synchronized void close() throws IOException {
        wrappedSender.close();
    }

    /** Batched kafka topic sender. This does the actual data batching. */
    private class BatchedKafkaTopicSender implements KafkaTopicSender {
        private long nanoAdded;
        private K cachedKey;
        private final List cache;
        private final KafkaTopicSender topicSender;
        private final AvroTopic topic;

        private BatchedKafkaTopicSender(AvroTopic topic)
                throws IOException, SchemaValidationException {
            cache = new ArrayList<>();
            this.topic = topic;
            topicSender = wrappedSender.sender(topic);
        }

        @Override
        public void send(K key, V value) throws IOException, SchemaValidationException {
            if (!isConnected()) {
                throw new IOException("Cannot send records to unconnected producer.");
            }
            trySend(key, value);
        }

        @Override
        public void send(RecordData records) throws IOException, SchemaValidationException {
            if (records.isEmpty()) {
                return;
            }
            K key = records.getKey();
            for (V value : records) {
                trySend(key, value);
            }
        }

        private void trySend(K key, V record) throws IOException, SchemaValidationException {
            boolean keysMatch;

            if (cache.isEmpty()) {
                cachedKey = key;
                nanoAdded = System.nanoTime();
                keysMatch = true;
            } else {
                keysMatch = Objects.equals(key, cachedKey);
            }

            if (keysMatch) {
                cache.add(record);
                if (exceedsBuffer(cache)) {
                    doSend();
                }
            } else {
                doSend();
                trySend(key, record);
            }
        }

        private void doSend() throws IOException, SchemaValidationException {
            topicSender.send(new AvroRecordData<>(topic, cachedKey, cache));
            cache.clear();
            cachedKey = null;
        }

        @Override
        public void clear() {
            cache.clear();
            topicSender.clear();
        }

        @Override
        public void flush() throws IOException {
            if (!cache.isEmpty()) {
                try {
                    doSend();
                } catch (SchemaValidationException ex) {
                    throw new IOException("Schemas do not match", ex);
                }
            }
            topicSender.flush();
        }

        @Override
        public void close() throws IOException {
            try {
                flush();
            } finally {
                wrappedSender.close();
            }
        }

        private boolean exceedsBuffer(List records) {
            return records.size() >= maxBatchSize
                    || System.nanoTime() - nanoAdded >= ageNanos;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy