You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@storm.apache.org by hmcl <gi...@git.apache.org> on 2016/02/21 23:18:40 UTC

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

GitHub user hmcl opened a pull request:

    https://github.com/apache/storm/pull/1131

    STORM-822: Kafka Spout New Consumer API

    This patch is still under development and was uploaded at this moment for early testing. Please read README.
    
    There may be a bug in the offsets management, because of diff o 1. I am looking into it. 
    Currently polling from an arbitrary offset is possible but it will come in the next patch, today or tomorrow
    I refactored the code a bit and left, maybe, some unnecessary locking. I am also looking into it.
    
    @connieyang @jianbzhou @tgravescs please let me know of any other requirements you may have and I will address them soon.
    
    Thanks

You can merge this pull request into a Git repository by running:

    $ git pull https://github.com/hmcl/storm-apache STORM-822

Alternatively you can review and apply these changes as the patch at:

    https://github.com/apache/storm/pull/1131.patch

To close this pull request, make a commit to your master/trunk branch
with (at least) the following in the commit message:

    This closes #1131
    
----
commit bcabde29a0df7713b71d1807afec3be5b0147664
Author: Hugo Louro <hm...@gmail.com>
Date:   2015-12-14T18:16:42Z

    STORM-822
    - Storm Kafka Spout new consumer API

commit 92ae8a6aff4a1bd66c9153dcfd2c44d16de37637
Author: Hugo Louro <hm...@gmail.com>
Date:   2016-02-21T22:11:12Z

    STORM-822
    - Storm Kafka Spout new consumer API

----


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54661412
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig.toString());
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if(commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.expired();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +
    +                    kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                    LOG.debug("Emitted tuple [{}] for record [{}]", tuple, record);
    +                }
    +            }
    +        }
    +    }
    +
    +    // ======== Ack =======
    +    @Override
    +    public void ack(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            acked.get(tp).add(msgId);
    +            LOG.debug("Adding acked message to [{}] to list of messages to be committed to Kafka", msgId);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.debug("Retried tuple with message id [{}]", messageId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message being marked as acked.");
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        if(!consumerAutoCommitMode) {
    +            commitOffsetsForAckedTuples();
    +        }
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            if(!consumerAutoCommitMode) {
    +                commitOffsetsForAckedTuples();
    +            }
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    // TODO must declare multiple output streams
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        kafkaSpoutStreams.declareOutputFields(declarer);
    +    }
    +
    +    // ====== Private helper methods ======
    +
    +    private void commitOffsetsForAckedTuples() {
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +
    +        try {
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetAndMetadata offsetAndMetadata = tpOffset.getValue().findNextCommitOffset();
    +                if (offsetAndMetadata != null) {
    +                    nextCommitOffsets.put(tpOffset.getKey(), offsetAndMetadata);
    +                }
    +            }
    +
    +            if (!nextCommitOffsets.isEmpty()) {
    +                kafkaConsumer.commitSync(nextCommitOffsets);
    +                LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +                // Instead of iterating again, we could commit and update state for each TopicPartition in the prior loop,
    +                // but the multiple network calls should be more expensive than iterating twice over a small loop
    +                for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                    final OffsetEntry offsetEntry = tpOffset.getValue();
    +                    offsetEntry.updateAckedState(nextCommitOffsets.get(tpOffset.getKey()));
    +                }
    +            } else {
    +                LOG.trace("No offsets to commit. {}", toString());
    +            }
    +        } catch (Exception e) {
    +            LOG.error("Exception occurred while committing to Kafka offsets of acked tuples", e);
    +        }
    +    }
    +
    +    @Override
    +    public String toString() {
    +        return "{acked=" + acked + "} ";
    +    }
    +
    +    // ======= Offsets Commit Management ==========
    +
    +    private static class OffsetComparator implements Comparator<MessageId> {
    +        public int compare(MessageId m1, MessageId m2) {
    +            return m1.offset() < m2.offset() ? -1 : m1.offset() == m2.offset() ? 0 : 1;
    +        }
    +    }
    +
    +    /** This class is not thread safe */
    +    private class OffsetEntry {
    +        private final TopicPartition tp;
    +        private long committedOffset;               // last offset committed to Kafka, or initial fetching offset (initial value depends on offset strategy. See KafkaSpoutConsumerRebalanceListener)
    +        private final NavigableSet<MessageId> ackedMsgs = new TreeSet<>(OFFSET_COMPARATOR);     // acked messages sorted by ascending order of offset
    +
    +        public OffsetEntry(TopicPartition tp, long committedOffset) {
    +            this.tp = tp;
    +            this.committedOffset = committedOffset;
    +            LOG.debug("Created OffsetEntry for [topic-partition={}, committed-or-initial-fetch-offset={}]", tp, committedOffset);
    +        }
    +
    +        public void add(MessageId msgId) {          // O(Log N)
    +            ackedMsgs.add(msgId);
    +        }
    +
    +        /**
    +         * This method has side effects. The method updateAckedState should be called after this method.
    --- End diff --
    
    The comment was outdated. Before the refactoring, that was the case, but no longer is.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by knusbaum <gi...@git.apache.org>.
Github user knusbaum commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53668891
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    --- End diff --
    
    Can probably just get rid of this method and inline `blackList.contains()`


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-217781379
  
    Just think another scenario \u2013 for example we polled 10k message and emitted \u2013 say from 10001 to 20000, so the numUncommittedOffsets is 10000, if the 10500 msg failed, which caused all the following message 10501~20000 will not got commited until the 10500 message was reemitted and acked.
    If a rebalance happened during this time, the offset will seek back to the last commited offiset +1, possibly will seek back to offset 10001, then all the message from 10001 to 2000 will be polled and emitted again \u2013 which will cause numUncommittedOffsets be incremented by another 10000 again.
    After successfully commit to kafka, numUncommittedOffsets will substract 10000 and leave 10000 value there.
    Seems this will also gradually cause numUncommittedOffsets be a bigger value than we expect, Is this a possible scenario?



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm issue #1131: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the issue:

    https://github.com/apache/storm/pull/1131
  
    @hmcl and all, we have communicated via email for a while and going forward let's talk in this thread so everyone is in same page.
    Base on the spout from the community(written by you), we have several fixes and it worked quite stable in our production for about 6 months.
    
    We want to share the latest spout to you and could you please kindly help review and merge to the community version if any fix is reasonable? we want to avoid diverging too much from the community version.
    
    Below are our major fixes:
    1.	For failed message, in next tuple method, originally the spout seek back to the non-continuous offset, so the failed message will be polled again for retry, say we seek back to message 10 for retry, now if kafka log file was purged, earliest offset is 1000, it means we will seek to 10 but reset to 1000 as per the reset policy, and we cannot poll the message 10, so spout not work.
    Our fix is: we manually catch the out of range exception, commit the offset to earliest offset first, then seek to the earliest offset
    
    2.	Currently the way to find next committed offset is very complex, under some edge cases \u2013 a), if no message acked back because bolt has some issue or cannot catch up with the spout emit; b) seek back is happened frequently and it is much faster than the message be acked back
    We give each message a status \u2013 None, emit, acked, failed(if failed number is bigger than the maximum retry, set to acked)
    3.	One of our use cases need ordering in partition level, so after seek back for retry, we re-emit all the follow messages again no matter they have emitted or not, if possible, maybe you can give an option here to configure it \u2013 either re-emit all the message from the failed one, or just emit the failed one, same as current version.
    4.	We record the message count for acked, failed, emitted, just for statistics.
    
    Could you please kindly help review and let us know if you can merge it into the community version? Any comments/concern pls feel free to let us know. Btw, I just send the latest code to you via email. 



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm issue #1131: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the issue:

    https://github.com/apache/storm/pull/1131
  
    @jianbzhou Thanks for filing the JIRA. I have assigned it to me, such that it's easier to keep track and follow up on it. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55189816
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,454 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called following this poll, upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // do nothing - by default polling starts at the last committed offset
    +                    fetchOffset = committedOffset.offset();
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.isExpiredResetOnTrue();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);
    +
    +                    kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                    LOG.debug("Emitted tuple [{}] for record [{}]", tuple, record);
    +                }
    +            }
    +        }
    +    }
    +
    +    private void commitOffsetsForAckedTuples() {
    +        // Find offsets that are ready to be committed for every topic partition
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +        for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +            final OffsetAndMetadata nextCommitOffset = tpOffset.getValue().findNextCommitOffset();
    +            if (nextCommitOffset != null) {
    +                nextCommitOffsets.put(tpOffset.getKey(), nextCommitOffset);
    +            }
    +        }
    +
    +        // Commit offsets that are ready to be committed for every topic partition
    +        if (!nextCommitOffsets.isEmpty()) {
    +            kafkaConsumer.commitSync(nextCommitOffsets);
    +            LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +            // Instead of iterating again, it would be possible to commit and update the state for each TopicPartition
    +            // in the prior loop, but the multiple network calls should be more expensive than iterating twice over a small loop
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetEntry offsetEntry = tpOffset.getValue();
    +                offsetEntry.commit(nextCommitOffsets.get(tpOffset.getKey()));
    +            }
    +        } else {
    +            LOG.trace("No offsets to commit. {}", toString());
    +        }
    +    }
    +
    +    // ======== Ack =======
    +    @Override
    +    public void ack(Object messageId) {
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            final MessageId msgId = (MessageId) messageId;
    +            acked.get(msgId.getTopicPartition()).add(msgId);
    +            LOG.debug("Added acked message [{}] to list of messages to be committed to Kafka", msgId);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.debug("Retried tuple with message id [{}]", msgId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message [{}] being marked as acked.", msgId);
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate / Close / Declare Outputs =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    --- End diff --
    
    @revans2 the logic that is now in the method subscribeKafkaConsumer() was previously done in open() is now called by activate().


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm issue #1131: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the issue:

    https://github.com/apache/storm/pull/1131
  
    @jianbzhou thanks. Looking at it.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by knusbaum <gi...@git.apache.org>.
Github user knusbaum commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53659933
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaRecordTupleBuilder.java ---
    @@ -0,0 +1,34 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.storm.tuple.Values;
    +
    +public class KafkaRecordTupleBuilder<K,V> implements org.apache.storm.kafka.spout.KafkaTupleBuilder<K, V> {
    +    @Override
    +    public Values buildTuple(final ConsumerRecord<K, V> consumerRecord) {
    +        return new Values() {{
    --- End diff --
    
    It would probably be clearer to just use the constructor rather than an anonymous inner class with an initializer block.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55318130
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaRecordTupleBuilder.java ---
    @@ -0,0 +1,34 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.storm.tuple.Values;
    +
    +import java.util.List;
    +
    +public class KafkaRecordTupleBuilder<K,V> implements org.apache.storm.kafka.spout.KafkaTupleBuilder<K, V> {
    --- End diff --
    
    you can import org.apache.storm.kafka.spout.KafkaTupleBuilder


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131
  
    @jianbzhou **Storm** guarantees that all the messages are either acked or failed. There is the property "topology.message.timeout.secs" https://github.com/apache/storm/blob/master/storm-core/src/jvm/org/apache/storm/Config.java#L1669
    
    If Storm is configured to use acks, and the acks don't arrive in a certain amount of time, the tuple will be retired. You don't have to worry about the scenario you described, and basically implement the timeout yourself.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56237461
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,517 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.PollStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<KafkaSpoutMessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private transient KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaSpoutTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer commitTimer;                                    // timer == null for auto commit mode
    +    private transient Timer logTimer;
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +    private transient long numUncommittedOffsets;   // Number of offsets that have been polled and emitted but not yet been committed
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +    private transient PollStrategy pollStrategy;
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaSpoutTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +        numUncommittedOffsets = 0;
    --- End diff --
    
    How is this different from topology.max.spout.pending?  If it were a maximum distance from commit instead of a count that would seem OK, but a count makes it seem like it is identical to topology.max.spout.pending.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-190870605
  
    @jianbzhou if tuples 1-5 have been emitted, if 3 fails, currently only 2 is re-emitted. Offsets 1,2 are committed, and the offsets 3-5 are only committed once 2 is acked. 
    
    I think that It should not be too hard to send again tuples 3-5, the problem is that the downstream component would receive once the tuples 4 and 5 twice. We try avoiding duplicates a much as possible, although in some failure scenarios it can still occur. 
    
    The problem that you are describing isn't well solved by trident? Isn't it one of the purposes it was built for? 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55333414
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,458 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    --- End diff --
    
    can this be LOG.info


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55319067
  
    --- Diff: external/storm-kafka-client/src/test/java/org/apache/storm/kafka/spout/test/KafkaSpoutTopologyMain.java ---
    @@ -0,0 +1,112 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout.test;
    +
    +import org.apache.storm.Config;
    +import org.apache.storm.LocalCluster;
    +import org.apache.storm.StormSubmitter;
    +import org.apache.storm.generated.StormTopology;
    +import org.apache.storm.kafka.spout.KafkaRecordTupleBuilder;
    +import org.apache.storm.kafka.spout.KafkaSpout;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig;
    +import org.apache.storm.kafka.spout.KafkaSpoutStreams;
    +import org.apache.storm.kafka.spout.KafkaTupleBuilder;
    +import org.apache.storm.topology.TopologyBuilder;
    +import org.apache.storm.tuple.Fields;
    +
    +import java.io.BufferedReader;
    +import java.io.IOException;
    +import java.io.InputStreamReader;
    +import java.util.HashMap;
    +import java.util.Map;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +
    +public class KafkaSpoutTopologyMain {
    --- End diff --
    
    This can be moved to examples within the same module. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r57960723
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,547 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Collections;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Iterator;
    +import java.util.LinkedList;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<KafkaSpoutMessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private transient KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +
    +
    +    // Bookkeeping
    +    private transient int maxRetries;                                   // Max number of times a tuple is retried
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;  // Strategy to determine the fetch offset of the first realized by the spout upon activation
    +    private transient KafkaSpoutRetryService retryService;              // Class that has the logic to handle tuple failure
    +    private transient Timer commitTimer;                                // timer == null for auto commit mode
    +    private transient boolean initialized;                              // Flag indicating that the spout is still undergoing initialization process.
    +    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +    private KafkaSpoutStreams kafkaSpoutStreams;                        // Object that wraps all the logic to declare output fields and emit tuples
    +    private transient KafkaSpoutTuplesBuilder<K, V> tuplesBuilder;      // Object that contains the logic to build tuples for each ConsumerRecord
    +
    +    private transient Map<TopicPartition, OffsetEntry> acked;           // Tuples that were successfully acked. These tuples will be committed periodically when the commit timer expires, after consumer rebalance, or on close/deactivate
    +    private transient Set<KafkaSpoutMessageId> emitted;                 // Tuples that have been emitted but that are "on the wire", i.e. pending being acked or failed
    +    private transient Iterator<ConsumerRecord<K, V>> waitingToEmit;         // Records that have been polled and are queued to be emitted in the nextTuple() call. One record is emitted per nextTuple()
    +    private transient long numUncommittedOffsets;                       // Number of offsets that have been polled and emitted but not yet been committed
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +        numUncommittedOffsets = 0;
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        // Retries management
    +        retryService = kafkaSpoutConfig.getRetryService();
    +
    +        // Tuples builder delegate
    +        tuplesBuilder = kafkaSpoutConfig.getTuplesBuilder();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            commitTimer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitPeriodMs(), TimeUnit.MILLISECONDS);
    +        }
    +
    +        acked = new HashMap<>();
    +        emitted = new HashSet<>();
    +        waitingToEmit = Collections.emptyListIterator();
    +
    +        LOG.info("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            if (!consumerAutoCommitMode) {
    +                acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            }
    +
    +            retryService.remove(partitions);
    +
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // By default polling starts at the last committed offset. +1 to point fetch to the first uncommitted offset.
    +                    fetchOffset = committedOffset.offset() + 1;
    +                    kafkaConsumer.seek(tp, fetchOffset);
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            }
    +
    +            if (poll()) {
    +                setWaitingToEmit(pollKafkaBroker());
    +            }
    +
    +            if (waitingToEmit()) {
    +                emit();
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && commitTimer.isExpiredResetOnTrue();    // timer != null for non auto commit mode
    +    }
    +
    +    private boolean poll() {
    +        return !waitingToEmit() && numUncommittedOffsets < kafkaSpoutConfig.getMaxUncommittedOffsets();
    +    }
    +
    +    private boolean waitingToEmit() {
    +        return waitingToEmit != null && waitingToEmit.hasNext();
    +    }
    +
    +    public void setWaitingToEmit(ConsumerRecords<K,V> consumerRecords) {
    +        List<ConsumerRecord<K,V>> waitingToEmitList = new LinkedList<>();
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            waitingToEmitList.addAll(consumerRecords.records(tp));
    +        }
    +        waitingToEmit = waitingToEmitList.iterator();
    +        LOG.trace("Records waiting to be emitted {}", waitingToEmitList);
    +    }
    +
    +    // ======== poll =========
    +    private ConsumerRecords<K, V> pollKafkaBroker() {
    +        doSeekRetriableTopicPartitions();
    +
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        final int numPolledRecords = consumerRecords.count();
    +        LOG.debug("Polled [{}] records from Kafka. NumUncommittedOffsets=[{}]", numPolledRecords, numUncommittedOffsets);
    +        return consumerRecords;
    +    }
    +
    +    private void doSeekRetriableTopicPartitions() {
    +        final Set<TopicPartition> retriableTopicPartitions = retryService.retriableTopicPartitions();
    +
    +        for (TopicPartition rtp : retriableTopicPartitions) {
    +            final OffsetAndMetadata offsetAndMeta = acked.get(rtp).findNextCommitOffset();
    +            if (offsetAndMeta != null) {
    +                kafkaConsumer.seek(rtp, offsetAndMeta.offset() + 1);  // seek to the next offset that is ready to commit in next commit cycle
    +            } else {
    +                kafkaConsumer.seekToEnd(rtp);    // Seek to last committed offset
    +            }
    +        }
    +    }
    +
    +    // ======== emit  =========
    +    private void emit() {
    +        emitTupleIfNotEmitted(waitingToEmit.next());
    +        waitingToEmit.remove();
    --- End diff --
    
    I want to add that I am still +1 on this, even without this change.  This is minor.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm issue #1131: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the issue:

    https://github.com/apache/storm/pull/1131
  
    @jianbzhou you can get obtain at most once semantics by setting maxRetries to zero. Here is the method to do so.
    
    https://github.com/apache/storm/blob/master/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java#L206


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56399116
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    @abhishekagarwal87 thanks for the clarification.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54661514
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig.toString());
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if(commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.expired();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +
    +                    kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                    LOG.debug("Emitted tuple [{}] for record [{}]", tuple, record);
    +                }
    +            }
    +        }
    +    }
    +
    +    // ======== Ack =======
    +    @Override
    +    public void ack(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            acked.get(tp).add(msgId);
    +            LOG.debug("Adding acked message to [{}] to list of messages to be committed to Kafka", msgId);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.debug("Retried tuple with message id [{}]", messageId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message being marked as acked.");
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        if(!consumerAutoCommitMode) {
    +            commitOffsetsForAckedTuples();
    +        }
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            if(!consumerAutoCommitMode) {
    +                commitOffsetsForAckedTuples();
    +            }
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    // TODO must declare multiple output streams
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        kafkaSpoutStreams.declareOutputFields(declarer);
    +    }
    +
    +    // ====== Private helper methods ======
    +
    +    private void commitOffsetsForAckedTuples() {
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +
    +        try {
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetAndMetadata offsetAndMetadata = tpOffset.getValue().findNextCommitOffset();
    +                if (offsetAndMetadata != null) {
    +                    nextCommitOffsets.put(tpOffset.getKey(), offsetAndMetadata);
    +                }
    +            }
    +
    +            if (!nextCommitOffsets.isEmpty()) {
    +                kafkaConsumer.commitSync(nextCommitOffsets);
    +                LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +                // Instead of iterating again, we could commit and update state for each TopicPartition in the prior loop,
    +                // but the multiple network calls should be more expensive than iterating twice over a small loop
    +                for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                    final OffsetEntry offsetEntry = tpOffset.getValue();
    +                    offsetEntry.updateAckedState(nextCommitOffsets.get(tpOffset.getKey()));
    +                }
    +            } else {
    +                LOG.trace("No offsets to commit. {}", toString());
    +            }
    +        } catch (Exception e) {
    +            LOG.error("Exception occurred while committing to Kafka offsets of acked tuples", e);
    +        }
    +    }
    +
    +    @Override
    +    public String toString() {
    +        return "{acked=" + acked + "} ";
    +    }
    +
    +    // ======= Offsets Commit Management ==========
    +
    +    private static class OffsetComparator implements Comparator<MessageId> {
    +        public int compare(MessageId m1, MessageId m2) {
    +            return m1.offset() < m2.offset() ? -1 : m1.offset() == m2.offset() ? 0 : 1;
    +        }
    +    }
    +
    +    /** This class is not thread safe */
    +    private class OffsetEntry {
    +        private final TopicPartition tp;
    +        private long committedOffset;               // last offset committed to Kafka, or initial fetching offset (initial value depends on offset strategy. See KafkaSpoutConsumerRebalanceListener)
    +        private final NavigableSet<MessageId> ackedMsgs = new TreeSet<>(OFFSET_COMPARATOR);     // acked messages sorted by ascending order of offset
    +
    +        public OffsetEntry(TopicPartition tp, long committedOffset) {
    +            this.tp = tp;
    +            this.committedOffset = committedOffset;
    +            LOG.debug("Created OffsetEntry for [topic-partition={}, committed-or-initial-fetch-offset={}]", tp, committedOffset);
    +        }
    +
    +        public void add(MessageId msgId) {          // O(Log N)
    +            ackedMsgs.add(msgId);
    +        }
    +
    +        /**
    +         * This method has side effects. The method updateAckedState should be called after this method.
    +         * @return the next OffsetAndMetadata to commit, or null if no offset is ready to commit.
    +         */
    +        public OffsetAndMetadata findNextCommitOffset() {
    +            boolean found = false;
    +            long currOffset;
    +            long nextCommitOffset = committedOffset;
    +            MessageId nextCommitMsg = null;     // this is a convenience field to make it faster to create OffsetAndMetadata
    +
    +            for (MessageId currAckedMsg : ackedMsgs) {  // complexity is that of a linear scan on a TreeMap
    +                if ((currOffset = currAckedMsg.offset()) == 0 || currOffset != nextCommitOffset) {     // this is to void duplication because the first message polled is the last message acked.
    +                    if (currOffset == nextCommitOffset || currOffset == nextCommitOffset + 1) {            // found the next offset to commit
    +                        found = true;
    +                        nextCommitMsg = currAckedMsg;
    +                        nextCommitOffset = currOffset;
    +                        LOG.trace("Found offset to commit [{}]. {}", currOffset, toString());
    +                    } else if (currAckedMsg.offset() > nextCommitOffset + 1) {    // offset found is not continuous to the offsets listed to go in the next commit, so stop search
    +                        LOG.debug("Non continuous offset found [{}]. It will be processed in a subsequent batch. {}", currOffset, toString());
    +                        break;
    +                    } else {
    +                        LOG.debug("Unexpected offset found [{}]. {}", currOffset, toString());
    +                        break;
    +                    }
    +                }
    +            }
    +
    +            OffsetAndMetadata offsetAndMetadata = null;
    +            if (found) {
    +                offsetAndMetadata = new OffsetAndMetadata(nextCommitOffset, nextCommitMsg.getMetadata(Thread.currentThread()));
    +                LOG.trace("Offset to be committed next: [{}] {}", offsetAndMetadata.offset(), toString());
    +            } else {
    +                LOG.debug("No offsets ready to commit", toString());
    +            }
    +            return offsetAndMetadata;
    +        }
    +
    +        /**
    +         * This method has side effects and should be called after findNextCommitOffset
    +         */
    +        public void updateAckedState(OffsetAndMetadata offsetAndMetadata) {
    +            if (offsetAndMetadata != null) {
    +                committedOffset = offsetAndMetadata.offset();
    +                for (Iterator<MessageId> iterator1 = ackedMsgs.iterator();
    +                     iterator1.hasNext(); ) {
    +                    if (iterator1.next().offset() <= offsetAndMetadata.offset()) {
    +                        iterator1.remove();
    +                    } else {
    +                        break;
    +                    }
    +                }
    +            }
    +            LOG.trace("Object state after update: {}", toString());
    +        }
    +
    +        public boolean isEmpty() {
    +            return ackedMsgs.isEmpty();
    +        }
    +
    +        @Override
    +        public String toString() {
    +            return "OffsetEntry{" +
    +                    "topic-partition=" + tp +
    +                    ", committedOffset=" + committedOffset +
    +                    ", ackedMsgs=" + ackedMsgs +
    +                    '}';
    +        }
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            for (TopicPartition tp: partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /** sets the cursor to the location dictated by the first poll strategy and returns the fetch offset */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null)  {     // fetchOffset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // do nothing - by default polling starts at the lat committed fetchOffset
    +                    fetchOffset = committedOffset.offset();
    +                }
    +            } else {    // no previous commit occurred, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +
    +        private void setAcked(TopicPartition tp, long fetchOffset) {
    +            if (!consumerAutoCommitMode && !acked.containsKey(tp)) {           // If this partition was previously assigned, leave the acked offsets as they were to resume where it left off
    +                acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +            }
    +        }
    +    }
    +
    +    // =========== Timer ===========
    +
    +    private class Timer {
    +        private final long frequency;
    +        private final long delay;
    +        private final TimeUnit timeUnit;
    +        private final long frequencyNanos;
    +        private long start;
    +
    +        /** Creates a timer to expire at the given frequency and starting with the specified time delay.
    +         *  Frequency and delay must be specified in the same TimeUnit */
    +        public Timer(long frequency, long delay, TimeUnit timeUnit) {
    +            this.frequency = frequency;
    +            this.delay = delay;
    +            this.timeUnit = timeUnit;
    +
    +            frequencyNanos = timeUnit.toNanos(frequency);
    +            start = System.nanoTime() + timeUnit.toNanos(delay);
    +        }
    +
    +        public long frequency() {
    +            return frequency;
    +        }
    +
    +        public long delay() {
    +            return delay;
    +        }
    +
    +        public TimeUnit getTimeUnit() {
    +            return timeUnit;
    +        }
    +
    +        /**
    +         * If this method returns true, a new timer cycle will start.
    +         * @return true if the elapsed time since the last true value call to this method is greater or
    +         * equal to the frequency specified upon creation of this timer. Returns false otherwise.
    +         */
    +        public boolean expired() {
    --- End diff --
    
    Done. Renamed to isExpiredResetOnTrue()


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by knusbaum <gi...@git.apache.org>.
Github user knusbaum commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-187366632
  
    I don't think the blacklist is going to work in general since it is possible in Storm to get multiple ack or fail messages for the same messageId. I think it would be better to empty `emittedTuples` and `failedTuples` on rebalance and determine if a message should be ignored based on its membership in those maps. 
    
    Everything else looks pretty good besides normal cleanup.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by connieyang <gi...@git.apache.org>.
Github user connieyang commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-187287074
  
    +1 on Trident support.  Thanks!


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56399639
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    Throughput will definitely take a dip with the Batch config and may not be desirable for any use case. Actually if we can just abstract out the part of * fetching tuple object from messageId * in an interface, we can have two implementations. In case of retry, one implementation will fetch the failed message from in-memory. other implementation can fetch the failed message directly from kafka similar to older spout. I have not thought it through completely yet. But it should be doable. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-190775616
  
    @jianbzhou That would be possible, but is not typically what storm guarantees.  Additionally you would have to configure a lot of other components to maintain that ordering.  Kafka itself does not guarantee this unless you only have a single partition.  Storm cannot guarantee it internally unless you are sending downstream to a single bolt instance, or you have a custom grouping that is smart enough to be able to take it into account.
    
    Ordering guarantees are very difficult in distributed systems and if you can design your system handle out of order data it will be much more robust and scale better.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by harshach <gi...@git.apache.org>.
Github user harshach commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-202656832
  
    @connieyang we can file trident work as another jira. 
    @revans2 can you please review the patch . Hopefully we can include this for 1.0 release.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-221143192
  
    @jianbzhou can you please email me what you have such that we can provide with a fix? Is there a way you can share the kafka setup that causes the issues that you mention ?
    
    I should upload my test cases later today, and that should help us address any possible issues. Thanks.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54661283
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig.toString());
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if(commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.expired();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    --- End diff --
    
    Done


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56041205
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutStreams.java ---
    @@ -0,0 +1,142 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.utils.Utils;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * Represents the output streams associated with each topic, and provides a public API to
    + * declare output streams and emmit tuples, on the appropriate stream, for all the topics specified.
    + */
    +public class KafkaSpoutStreams implements Serializable {
    +    private final Map<String, KafkaSpoutStream> topicToStream;
    +
    +    private KafkaSpoutStreams(Builder builder) {
    +        this.topicToStream = builder.topicToStream;
    +    }
    +
    +    /**
    +     * @param topic the topic for which to get output fields
    +     * @return the output fields declared
    +     */
    +    public Fields getOutputFields(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getOutputFields();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @param topic the topic to for which to get the stream id
    +     * @return the id of the stream to where the tuples are emitted
    +     */
    +    public String getStreamId(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getStreamId();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @return list of topics subscribed and emitting tuples to a stream as configured by {@link KafkaSpoutStream}
    +     */
    +    public List<String> getTopics() {
    +        return new ArrayList<>(topicToStream.keySet());
    +    }
    +
    +    void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        for (KafkaSpoutStream stream : topicToStream.values()) {
    +            declarer.declareStream(stream.getStreamId(), stream.getOutputFields());
    +        }
    +    }
    +
    +    void emit(SpoutOutputCollector collector, MessageId messageId) {
    +        collector.emit(getStreamId(messageId.topic()), messageId.getTuple(), messageId);
    --- End diff --
    
    @abhishekagarwal87 @harshach @revans2 I have a patch I am about to upload that I believe addresses most or all of these points. It will give an option to have a behavior identical to the previous Kafka Spout, and an option to keep an upper limit on the amount of memory (i.e. pending records in memory) used by each spout.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by harshach <gi...@git.apache.org>.
Github user harshach commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56409148
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    @revans2 makes sense. Yes one of the things considered was to make network call vs  in-memory replay. We all agree on failures not-norm and I am +1 on removing the in-memory replay and instead make a call to broker to replay the message. 
    BATCH can go away as well once we fix the in-memory replay. BATCH is there to mimic the old spout behavior.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55190400
  
    --- Diff: pom.xml ---
    @@ -836,14 +839,39 @@
                     <scope>test</scope>
                 </dependency>
                 <dependency>
    -              <groupId>org.apache.calcite</groupId>
    -              <artifactId>calcite-core</artifactId>
    -              <version>${calcite.version}</version>
    +                <groupId>org.apache.calcite</groupId>
    +                <artifactId>calcite-core</artifactId>
    +                <version>${calcite.version}</version>
    +            </dependency>
    +            <dependency>
    +                <groupId>com.fasterxml.jackson.core</groupId>
    +                <artifactId>jackson-databind</artifactId>
    +                <version>${jackson.version}</version>
    +            </dependency>
    +            <dependency>
    +                <groupId>org.apache.kafka</groupId>
    +                <artifactId>${kafka.artifact.id}</artifactId>
    +                <version>${kafka.version}</version>
    +                <exclusions>
    +                    <exclusion>
    +                        <groupId>org.apache.zookeeper</groupId>
    +                        <artifactId>zookeeper</artifactId>
    +                    </exclusion>
    +                    <exclusion>
    +                        <groupId>log4j</groupId>
    +                        <artifactId>log4j</artifactId>
    +                    </exclusion>
    +                    <exclusion>
    +                        <groupId>org.slf4j</groupId>
    +                        <artifactId>slf4j-log4j12</artifactId>
    +                    </exclusion>
    +                </exclusions>
                 </dependency>
                 <dependency>
    -              <groupId>com.fasterxml.jackson.core</groupId>
    -              <artifactId>jackson-databind</artifactId>
    -              <version>${jackson.version}</version>
    +                <groupId>org.apache.kafka</groupId>
    +                <artifactId>kafka-clients</artifactId>
    +                <version>${kafka.version}</version>
    +                <scope>provided</scope>
    --- End diff --
    
    @revans2 I left the provided unchanged with my last changes, and will take care of fixing all of this when I submit a patch for STORM-1599.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56239019
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,517 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.PollStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<KafkaSpoutMessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private transient KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaSpoutTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer commitTimer;                                    // timer == null for auto commit mode
    +    private transient Timer logTimer;
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +    private transient long numUncommittedOffsets;   // Number of offsets that have been polled and emitted but not yet been committed
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +    private transient PollStrategy pollStrategy;
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaSpoutTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +        numUncommittedOffsets = 0;
    +        logTimer = new Timer(500, Math.min(1000, kafkaSpoutConfig.getOffsetsCommitPeriodMs()/2), TimeUnit.MILLISECONDS);
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        pollStrategy = kafkaSpoutConfig.getPollStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            commitTimer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitPeriodMs(), TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        LOG.info("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            if (!consumerAutoCommitMode) {
    +                acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            }
    +
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // By default polling starts at the last committed offset. +1 to point fetch to the first uncommitted offset.
    +                    fetchOffset = committedOffset.offset() + 1;
    +                    kafkaConsumer.seek(tp, fetchOffset);
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else if (poll()) {
    +                emitTuples(pollKafkaBroker());
    +            } else if (logTimer.isExpiredResetOnTrue()){
    +                log();
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private void log() {
    +        switch(pollStrategy) {
    +            case STREAM:
    +                LOG.trace("Reached the maximum number number of uncommitted records [{}]. " +
    +                        "No more polls will occur until a sequence of commits sets the count under the [{}] threshold ",
    +                        numUncommittedOffsets, kafkaSpoutConfig.getMaxUncommittedOffsets());
    +                break;
    +            case BATCH:
    +                LOG.trace("No more polls will occur until the last batch completes. [{}] emitted tuples pending", numUncommittedOffsets);
    +                break;
    +            default:
    +                throw new IllegalStateException("No implementation defined for polling strategy " + pollStrategy);
    +        }
    +
    +    }
    +
    +    // always poll in auto commit mode because no state is kept and therefore there is no need to set an upper limit in memory
    +    private boolean poll()  {
    +        switch(pollStrategy) {
    +            case STREAM:
    +                return consumerAutoCommitMode || numUncommittedOffsets < kafkaSpoutConfig.getMaxUncommittedOffsets();
    +            case BATCH:
    +                return consumerAutoCommitMode || numUncommittedOffsets <= 0;
    +            default:
    +                throw new IllegalStateException("No implementation defined for polling strategy " + pollStrategy);
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && commitTimer.isExpiredResetOnTrue();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> pollKafkaBroker() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        final int numPolledRecords = consumerRecords.count();
    +        numUncommittedOffsets+= numPolledRecords;
    +        LOG.debug("Polled [{}] records from Kafka. NumUncommittedOffsets=[{}]", numPolledRecords, numUncommittedOffsets);
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                final List<Object> tuple = tupleBuilder.buildTuple(record, kafkaSpoutStreams);
    +                final KafkaSpoutMessageId messageId = new KafkaSpoutMessageId(record, tuple);
    +
    +                kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                LOG.trace("Emitted tuple [{}] for record [{}]", tuple, record);
    +            }
    +        }
    +    }
    +
    +    private void commitOffsetsForAckedTuples() {
    +        // Find offsets that are ready to be committed for every topic partition
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +        for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +            final OffsetAndMetadata nextCommitOffset = tpOffset.getValue().findNextCommitOffset();
    +            if (nextCommitOffset != null) {
    +                nextCommitOffsets.put(tpOffset.getKey(), nextCommitOffset);
    +            }
    +        }
    +
    +        // Commit offsets that are ready to be committed for every topic partition
    +        if (!nextCommitOffsets.isEmpty()) {
    +            kafkaConsumer.commitSync(nextCommitOffsets);
    +            LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +            // Instead of iterating again, it would be possible to commit and update the state for each TopicPartition
    +            // in the prior loop, but the multiple network calls should be more expensive than iterating twice over a small loop
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetEntry offsetEntry = tpOffset.getValue();
    +                offsetEntry.commit(nextCommitOffsets.get(tpOffset.getKey()));
    +            }
    +        } else {
    +            LOG.trace("No offsets to commit. {}", this);
    +        }
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            final KafkaSpoutMessageId msgId = (KafkaSpoutMessageId) messageId;
    +            acked.get(msgId.getTopicPartition()).add(msgId);
    +            LOG.trace("Acked message [{}]. Messages acked and pending commit [{}]", msgId, acked);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {   // TODO poll all tuples after the failed tuple
    +        final KafkaSpoutMessageId msgId = (KafkaSpoutMessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.trace("Retried tuple with message id [{}]", msgId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message [{}] being marked as acked.", msgId);
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate / Close / Declare Outputs =======
    +
    +    @Override
    +    public void activate() {
    +        subscribeKafkaConsumer();
    +    }
    +
    +    private void subscribeKafkaConsumer() {
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called following this poll, upon partition registration
    +        kafkaConsumer.poll(0);
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        shutdown();
    +    }
    +
    +    @Override
    +    public void close() {
    +        shutdown();
    +    }
    +
    +    private void shutdown() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            if (!consumerAutoCommitMode) {
    +                commitOffsetsForAckedTuples();
    +            }
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        kafkaSpoutStreams.declareOutputFields(declarer);
    +    }
    +
    +    @Override
    +    public String toString() {
    +        return "{acked=" + acked + "} ";
    +    }
    +
    +    // ======= Offsets Commit Management ==========
    +
    +    private static class OffsetComparator implements Comparator<KafkaSpoutMessageId> {
    +        public int compare(KafkaSpoutMessageId m1, KafkaSpoutMessageId m2) {
    +            return m1.offset() < m2.offset() ? -1 : m1.offset() == m2.offset() ? 0 : 1;
    +        }
    +    }
    +
    +    /**
    +     * This class is not thread safe
    +     */
    +    private class OffsetEntry {
    +        private final TopicPartition tp;
    +        private final long initialFetchOffset;  /* First offset to be fetched. It is either set to the beginning, end, or to the first uncommitted offset.
    +                                                 * Initial value depends on offset strategy. See KafkaSpoutConsumerRebalanceListener */
    +        private long committedOffset;     // last offset committed to Kafka. Initially it is set to fetchOffset - 1
    +        private final NavigableSet<KafkaSpoutMessageId> ackedMsgs = new TreeSet<>(OFFSET_COMPARATOR);     // acked messages sorted by ascending order of offset
    +
    +        public OffsetEntry(TopicPartition tp, long initialFetchOffset) {
    +            this.tp = tp;
    +            this.initialFetchOffset = initialFetchOffset;
    +            this.committedOffset = initialFetchOffset - 1;
    +            LOG.debug("Created OffsetEntry. {}", this);
    +        }
    +
    +        public void add(KafkaSpoutMessageId msgId) {          // O(Log N)
    +            ackedMsgs.add(msgId);
    +        }
    +
    +        /**
    +         * @return the next OffsetAndMetadata to commit, or null if no offset is ready to commit.
    +         */
    +        public OffsetAndMetadata findNextCommitOffset() {
    +            boolean found = false;
    +            long currOffset;
    +            long nextCommitOffset = committedOffset;
    +            KafkaSpoutMessageId nextCommitMsg = null;     // this is a convenience variable to make it faster to create OffsetAndMetadata
    +
    +            for (KafkaSpoutMessageId currAckedMsg : ackedMsgs) {  // complexity is that of a linear scan on a TreeMap
    +                if ((currOffset = currAckedMsg.offset()) == initialFetchOffset || currOffset == nextCommitOffset + 1) {            // found the next offset to commit
    +                    found = true;
    +                    nextCommitMsg = currAckedMsg;
    +                    nextCommitOffset = currOffset;
    +                    LOG.trace("Found offset to commit [{}]. {}", currOffset, this);
    +                } else if (currAckedMsg.offset() > nextCommitOffset + 1) {    // offset found is not continuous to the offsets listed to go in the next commit, so stop search
    +                    LOG.debug("Non continuous offset found [{}]. It will be processed in a subsequent batch. {}", currOffset, this);
    +                    break;
    +                } else {
    +                    LOG.debug("Unexpected offset found [{}]. {}", currOffset, this);
    +                    break;
    +                }
    +            }
    +
    +            OffsetAndMetadata nextCommitOffsetAndMetadata = null;
    +            if (found) {
    +                nextCommitOffsetAndMetadata = new OffsetAndMetadata(nextCommitOffset, nextCommitMsg.getMetadata(Thread.currentThread()));
    +                LOG.debug("Offset to be committed next: [{}] {}", nextCommitOffsetAndMetadata.offset(), this);
    +            } else {
    +                LOG.debug("No offsets ready to commit. {}", this);
    +            }
    +            return nextCommitOffsetAndMetadata;
    +        }
    +
    +        /**
    +         * Marks an offset has committed. This method has side effects - it sets the internal state in such a way that future
    +         * calls to {@link #findNextCommitOffset()} will return offsets greater than the offset specified, if any.
    +         *
    +         * @param committedOffset offset to be marked as committed
    +         */
    +        public void commit(OffsetAndMetadata committedOffset) {
    +            if (committedOffset != null) {
    +                /*final long numCommittedOffsets = this.committedOffset < initialFetchOffset
    +                        ? committedOffset.offset() - this.committedOffset + 1       // +1 because fetchOffset is pointing to the first uncommitted offset
    +                        : committedOffset.offset() - this.committedOffset;
    +*/
    --- End diff --
    
    Lest delete commented out code.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55889085
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,458 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    --- End diff --
    
    Done


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54661279
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig.toString());
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if(commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.expired();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    --- End diff --
    
    Done


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55737411
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutStreams.java ---
    @@ -0,0 +1,142 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.utils.Utils;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * Represents the output streams associated with each topic, and provides a public API to
    + * declare output streams and emmit tuples, on the appropriate stream, for all the topics specified.
    + */
    +public class KafkaSpoutStreams implements Serializable {
    +    private final Map<String, KafkaSpoutStream> topicToStream;
    +
    +    private KafkaSpoutStreams(Builder builder) {
    +        this.topicToStream = builder.topicToStream;
    +    }
    +
    +    /**
    +     * @param topic the topic for which to get output fields
    +     * @return the output fields declared
    +     */
    +    public Fields getOutputFields(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getOutputFields();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @param topic the topic to for which to get the stream id
    +     * @return the id of the stream to where the tuples are emitted
    +     */
    +    public String getStreamId(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getStreamId();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @return list of topics subscribed and emitting tuples to a stream as configured by {@link KafkaSpoutStream}
    +     */
    +    public List<String> getTopics() {
    +        return new ArrayList<>(topicToStream.keySet());
    +    }
    +
    +    void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        for (KafkaSpoutStream stream : topicToStream.values()) {
    +            declarer.declareStream(stream.getStreamId(), stream.getOutputFields());
    +        }
    +    }
    +
    +    void emit(SpoutOutputCollector collector, MessageId messageId) {
    +        collector.emit(getStreamId(messageId.topic()), messageId.getTuple(), messageId);
    --- End diff --
    
    I am okay with that. I have some ideas around how to make retry logic pluggable like current kafka spout (wherein user can get both flavors). I will share them in the follow up JIRA. We also have to file a JIRA to add metrics in this spout which I will do. I could see some places in the code where metrics will be very helpful. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-203019780
  
    @harshach sorry about the delay I have been swamped.  Looking at it now.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55318083
  
    --- Diff: external/storm-kafka-client/pom.xml ---
    @@ -0,0 +1,86 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!--/**
    +        * Licensed to the Apache Software Foundation (ASF) under one
    +        * or more contributor license agreements.  See the NOTICE file
    +        * distributed with this work for additional information
    +        * regarding copyright ownership.  The ASF licenses this file
    +        * to you 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.
    +        */-->
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +    <modelVersion>4.0.0</modelVersion>
    +
    +    <parent>
    +        <artifactId>storm</artifactId>
    +        <groupId>org.apache.storm</groupId>
    +        <version>2.0.0-SNAPSHOT</version>
    +        <relativePath>../../pom.xml</relativePath>
    +    </parent>
    +
    +    <artifactId>storm-kafka-client</artifactId>
    +    <name>storm-kafka-client</name>
    +
    +    <packaging>jar</packaging>
    +
    +    <developers>
    --- End diff --
    
    should this be removed?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56232096
  
    --- Diff: external/storm-kafka-client/pom.xml ---
    @@ -0,0 +1,86 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!--/**
    +        * Licensed to the Apache Software Foundation (ASF) under one
    +        * or more contributor license agreements.  See the NOTICE file
    +        * distributed with this work for additional information
    +        * regarding copyright ownership.  The ASF licenses this file
    +        * to you 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.
    +        */-->
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +    <modelVersion>4.0.0</modelVersion>
    +
    +    <parent>
    +        <artifactId>storm</artifactId>
    +        <groupId>org.apache.storm</groupId>
    +        <version>2.0.0-SNAPSHOT</version>
    +        <relativePath>../../pom.xml</relativePath>
    +    </parent>
    +
    +    <artifactId>storm-kafka-client</artifactId>
    --- End diff --
    
    +1 I like that idea better then putting them all in a single package.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54905188
  
    --- Diff: external/storm-kafka-new-consumer-api/pom.xml ---
    @@ -0,0 +1,91 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!--/**
    +        * Licensed to the Apache Software Foundation (ASF) under one
    +        * or more contributor license agreements.  See the NOTICE file
    +        * distributed with this work for additional information
    +        * regarding copyright ownership.  The ASF licenses this file
    +        * to you 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.
    +        */-->
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +    <modelVersion>4.0.0</modelVersion>
    +
    +    <parent>
    +        <artifactId>storm</artifactId>
    +        <groupId>org.apache.storm</groupId>
    +        <version>2.0.0-SNAPSHOT</version>
    +        <relativePath>../../pom.xml</relativePath>
    +    </parent>
    +
    +    <artifactId>storm-kafka-new-consumer-api</artifactId>
    --- End diff --
    
    renamed this to storm-kafka-client


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56469003
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaRecordTupleBuilder.java ---
    @@ -0,0 +1,44 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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,
    + *   See the License for the specific language governing permissions and
    + *   limitations under the License.
    + */
    +package org.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.tuple.Values;
    +
    +import java.util.List;
    +
    +public class KafkaRecordTupleBuilder<K, V> implements KafkaSpoutTupleBuilder<K, V> {
    +    @Override
    +    public List<Object> buildTuple(final ConsumerRecord<K, V> consumerRecord, KafkaSpoutStreams kafkaSpoutStreams) {
    --- End diff --
    
    There are three entities being constructed 
    1. Tuple - This is a list of objects being emitted by the spout e.g. ["electronics", "abhishek", "router"]
    2. OutputFields - this indicates the position of fields by their name e.g. [CATEGORY_NAME, USER_NAME, ITEM_NAME]
    The bolt uses the field name in OutputFields to access the value in tuple. e.g. one might access category using `String category = tuple.getStringByField(CATEGORY_NAME)`
    Person who is constructing the tuple, knows which positions of different fields in tuple. That's why I suggested to have tuple building and output fields in single interface because these two are tied and need to be consistent with each other.
    
    3. Schema - *a subset of the output fields* - I am not sure what it is. can you elaborate more on this?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-218453241
  
    HI Hcml, 
    Just fyi - 
    In method doSeekRetriableTopicPartitions, we find below code:
    **kafkaConsumer.seekToEnd(rtp);    // Seek to last committed offset**
    Above code is contradictory with the comments, we replaced above line to below:
    **OffsetAndMetadata commitOffset = kafkaConsumer.committed(rtp);
    kafkaConsumer.seek(rtp, commitOffset.offset());    // Seek to last committed offset**
    Any comments please let me know. Thanks!
    
    For all above identified issues, we applied some quick and dirty fix and the testing is in progress, we will let you know the final testing result later.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56402570
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    @harshach You are combining two different features/issues here.  The first one is limiting the maximum number of replay.  +1 this is a great feature.  The second one is how we do a replay on a failure when we have not hit the replay limit yet.
    
    This code keeps the entire tuple in memory along with its offset so when a failure happens we can replay the message without talking to kafka.  The original KafkaSpout would only keep the offset in memory and if it needed to replay the message it would go back to kafka, fetch an entire batch of messages and drop the ones that didn't need to be replayed.
    
    My opinion is that failure is not the common case, so lets not optimize for it.  The current code is much simpler, but also much more memory intensive, so lets keep the in-memory replay for now, but file a JIRA and go back to reduce the memory usage by adding in the more complex code that uses less memory.
    
    If the entire point of the BATCH config is to reduce the memory footprint of the spout, and at the same time make the throughput really bad, lets not do it.  Instead I would prefer to release a memory hungry spout sooner and spend our efforts on this second JIRA that would reduce the memory usage without impacting the common case throughput.  If there is something else about the BATCH config that I am missing, or is going to be important in the future please let me know. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm issue #1131: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the issue:

    https://github.com/apache/storm/pull/1131
  
    @hmcl , fired a Jira ticket STORM-2292 and attached the code to the Jira...please let me know if any comments.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r57770291
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutRetryService.java ---
    @@ -0,0 +1,69 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.TopicPartition;
    +
    +import java.io.Serializable;
    +import java.util.Collection;
    +import java.util.Set;
    +
    +public interface KafkaSpoutRetryService extends Serializable {
    +    /**
    +     * Schedules this {@link KafkaSpoutMessageId} if not yet scheduled, or updates retry time if it has already been scheduled.
    +     * @param msgId message to schedule for retrial
    +     */
    +    void schedule(KafkaSpoutMessageId msgId);
    +
    +    /**
    +     * Removes a message from the list of messages scheduled for retrial
    +     * @param msgId message to remove from retrial
    +     */
    +    boolean remove(KafkaSpoutMessageId msgId);
    +
    +    /**
    +     * Removes all the messages whose {@link TopicPartition} does NOT belong to the specified {@code Collection<TopicPartition>}.
    +     * All messages that come from a {@link TopicPartition} existing in the collection will be kept.
    +     * This method is useful to cleanup state following partition rebalance.
    +     * @param topicPartitions Collection of {@link TopicPartition} for which to keep messages
    +     * @return true if at least one message was removed, false otherwise
    +     */
    +    boolean remove(Collection<TopicPartition> topicPartitions);
    +
    +    /**
    +     * @return set of topic partitions that have offsets that are ready to be retried, i.e.,
    +     * for which a tuple has failed and has retry time less than current time
    +     */
    +    Set<TopicPartition> retriableTopicPartitions();
    +
    +    /**
    +     * Checks if a specific failed {@link KafkaSpoutMessageId} is is ready to be retried,
    +     * i.e is scheduled and has retry time that is less than current time.
    +     * @return true if message is ready to be retried, false otherwise
    +     */
    +    boolean retry(KafkaSpoutMessageId msgId);
    +
    +    /**
    +     * Checks if a specific failed {@link KafkaSpoutMessageId} is scheduled to be retried.
    +     * The message may or may not be ready to be retried yet.
    +     * @return true if the message is scheduled to be retried, regardless of being or not ready to be retried.
    +     * Returns false is this message is not scheduled for retrial
    +     */
    +    boolean scheduled(KafkaSpoutMessageId msgId);
    --- End diff --
    
    can we rename this to a question to like isScheduled?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by connieyang <gi...@git.apache.org>.
Github user connieyang commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-190326611
  
    @revans2 @tgravescs , comments?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by harshach <gi...@git.apache.org>.
Github user harshach commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-205435683
  
    Thanks @connieyang filed a JIRA here https://issues.apache.org/jira/browse/STORM-1684


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56397965
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    *blocks* == *not polls kafka*


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by harshach <gi...@git.apache.org>.
Github user harshach commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55942709
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutStreams.java ---
    @@ -0,0 +1,142 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.utils.Utils;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * Represents the output streams associated with each topic, and provides a public API to
    + * declare output streams and emmit tuples, on the appropriate stream, for all the topics specified.
    + */
    +public class KafkaSpoutStreams implements Serializable {
    +    private final Map<String, KafkaSpoutStream> topicToStream;
    +
    +    private KafkaSpoutStreams(Builder builder) {
    +        this.topicToStream = builder.topicToStream;
    +    }
    +
    +    /**
    +     * @param topic the topic for which to get output fields
    +     * @return the output fields declared
    +     */
    +    public Fields getOutputFields(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getOutputFields();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @param topic the topic to for which to get the stream id
    +     * @return the id of the stream to where the tuples are emitted
    +     */
    +    public String getStreamId(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getStreamId();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @return list of topics subscribed and emitting tuples to a stream as configured by {@link KafkaSpoutStream}
    +     */
    +    public List<String> getTopics() {
    +        return new ArrayList<>(topicToStream.keySet());
    +    }
    +
    +    void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        for (KafkaSpoutStream stream : topicToStream.values()) {
    +            declarer.declareStream(stream.getStreamId(), stream.getOutputFields());
    +        }
    +    }
    +
    +    void emit(SpoutOutputCollector collector, MessageId messageId) {
    +        collector.emit(getStreamId(messageId.topic()), messageId.getTuple(), messageId);
    --- End diff --
    
    @abhishekagarwal87 Yes with new consumer api this is limited by max.partition.fetch.bytes , so the max will be no.of partitions x max.partition.fetch.bytes. Given that we ask users to set the parallelism of kafkaSpout to that of no.of partitions in a topic.  So at max it will be partitions * max.partition.fetch.bytes in a single instance of kafka spout and in avg it wll max.partition.fetch.bytes per instance of kafka spout ( if users configures parallelism == topic.partitions)


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55694717
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutStreams.java ---
    @@ -0,0 +1,142 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.utils.Utils;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * Represents the output streams associated with each topic, and provides a public API to
    + * declare output streams and emmit tuples, on the appropriate stream, for all the topics specified.
    + */
    +public class KafkaSpoutStreams implements Serializable {
    +    private final Map<String, KafkaSpoutStream> topicToStream;
    +
    +    private KafkaSpoutStreams(Builder builder) {
    +        this.topicToStream = builder.topicToStream;
    +    }
    +
    +    /**
    +     * @param topic the topic for which to get output fields
    +     * @return the output fields declared
    +     */
    +    public Fields getOutputFields(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getOutputFields();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @param topic the topic to for which to get the stream id
    +     * @return the id of the stream to where the tuples are emitted
    +     */
    +    public String getStreamId(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getStreamId();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @return list of topics subscribed and emitting tuples to a stream as configured by {@link KafkaSpoutStream}
    +     */
    +    public List<String> getTopics() {
    +        return new ArrayList<>(topicToStream.keySet());
    +    }
    +
    +    void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        for (KafkaSpoutStream stream : topicToStream.values()) {
    +            declarer.declareStream(stream.getStreamId(), stream.getOutputFields());
    +        }
    +    }
    +
    +    void emit(SpoutOutputCollector collector, MessageId messageId) {
    +        collector.emit(getStreamId(messageId.topic()), messageId.getTuple(), messageId);
    --- End diff --
    
    I forgot to add that the only feature that I think might interfere with a change like this is the auto-commit feature.  Perhaps we should remove that option from the config until we understand how auto-commit and seek work together.
    
    If everyone is OK with doing this on another JIRA perhaps @hmcl you can clean up the other comments that @abhishekagarwal87 has, I will file a follow up JIRA to reduce the memory pressure in the spout and then we can merge this in.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by harshach <gi...@git.apache.org>.
Github user harshach commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55932605
  
    --- Diff: external/storm-kafka-client/pom.xml ---
    @@ -0,0 +1,86 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!--/**
    +        * Licensed to the Apache Software Foundation (ASF) under one
    +        * or more contributor license agreements.  See the NOTICE file
    +        * distributed with this work for additional information
    +        * regarding copyright ownership.  The ASF licenses this file
    +        * to you 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.
    +        */-->
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +    <modelVersion>4.0.0</modelVersion>
    +
    +    <parent>
    +        <artifactId>storm</artifactId>
    +        <groupId>org.apache.storm</groupId>
    +        <version>2.0.0-SNAPSHOT</version>
    +        <relativePath>../../pom.xml</relativePath>
    +    </parent>
    +
    +    <artifactId>storm-kafka-client</artifactId>
    --- End diff --
    
    can we call this as storm-kafka-connector. Ideally this should be part of storm-kafka. We do want to maintain storm-kafka and since its on older consumer api and which going to deprecated from kafka 0.10 release. we can move this to storm-kafka once thats done.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-217776015
  
    Hi Hmcl,
    
    During our testing we found sometime the poll method was not called for long time, I suspect it is caused by below condition:
    
    private boolean poll() {
        return !waitingToEmit() && **numUncommittedOffsets < kafkaSpoutConfig.getMaxUncommittedOffsets()**;
    }
    
    I found numUncommittedOffsets will be incremented in either of the below situation:
    1.	(!retryService.isScheduled(msgId) \u2013 this is the most common situation \u2013 one message was polled and it is not in the toRetryMsg \u2013 it is a normal emit instead of a retry;
    2.	retryService.isReady(msgId) \u2013 this means the message was emitted previously \u2013 now will be re-emitted as per the retry logic.
    
    As per below logic \u2013 for one message 50, in the first time it was polled and emiited, numUncommittedOffsets will be incremented by 1, then this message failed and retried for 10 times, so totally numUncommittedOffsets will be increamented by 11.
    
    
    private void emitTupleIfNotEmitted(ConsumerRecord<K, V> record) {
        \u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026
        else if (!retryService.isScheduled(msgId) || retryService.isReady(msgId)) {   // not scheduled <=> never failed (i.e. never emitted) or ready to be retried
            final List<Object> tuple = tuplesBuilder.buildTuple(record);
            kafkaSpoutStreams.emit(collector, tuple, msgId);
            emitted.add(msgId);
            **numUncommittedOffsets++;**
        \u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026
    }
    
    But as per below logic \u2013 after successful commit, numUncommittedOffsets will subtract the actual number of message that got commited. If it commit one message 50, then will only substract 1 instead of 11.
    
    public void commit(OffsetAndMetadata committedOffset) {
        \u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026
            **numUncommittedOffsets-= numCommittedOffsets;**
        \u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026
    }
    
    Under some circumstances \u2013 say a rebalance happened and we seek back to a very small/early offset, seems this would cause emitTupleIfNotEmitted have a quite big number \u2013 finally this will be greater than kafkaSpoutConfig.getMaxUncommittedOffsets, and got poll() method not be called.
    
    
    I am not sure if I corrrectly understand your code or miss anything \u2013 could you please kindly help confirm if above situtaion is possible or not?
    
    Please feel free to let me know if you need any further info and thanks for your help.



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by harshach <gi...@git.apache.org>.
Github user harshach commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55893379
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutStreams.java ---
    @@ -0,0 +1,142 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.utils.Utils;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * Represents the output streams associated with each topic, and provides a public API to
    + * declare output streams and emmit tuples, on the appropriate stream, for all the topics specified.
    + */
    +public class KafkaSpoutStreams implements Serializable {
    +    private final Map<String, KafkaSpoutStream> topicToStream;
    +
    +    private KafkaSpoutStreams(Builder builder) {
    +        this.topicToStream = builder.topicToStream;
    +    }
    +
    +    /**
    +     * @param topic the topic for which to get output fields
    +     * @return the output fields declared
    +     */
    +    public Fields getOutputFields(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getOutputFields();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @param topic the topic to for which to get the stream id
    +     * @return the id of the stream to where the tuples are emitted
    +     */
    +    public String getStreamId(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getStreamId();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @return list of topics subscribed and emitting tuples to a stream as configured by {@link KafkaSpoutStream}
    +     */
    +    public List<String> getTopics() {
    +        return new ArrayList<>(topicToStream.keySet());
    +    }
    +
    +    void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        for (KafkaSpoutStream stream : topicToStream.values()) {
    +            declarer.declareStream(stream.getStreamId(), stream.getOutputFields());
    +        }
    +    }
    +
    +    void emit(SpoutOutputCollector collector, MessageId messageId) {
    +        collector.emit(getStreamId(messageId.topic()), messageId.getTuple(), messageId);
    --- End diff --
    
    The current kafka spout keeps lot more book keeping than what @hmcl doing in this PR. We never seen any big memory consumption in KafkaSpout. I think this approach only improves what we've. Don't think memory consumption is an issue. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-189004616
  
    I agree that we need to keep track of outstanding tuples + offsets, and the code you have around acked feels OK, but we could possibly clean it up a little.  I called it markAsDone instead of addAckedTuples because I didn't really like having a tuple that failed too many times be marked as acked, but thinking about it more it is not that big of a deal.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56386686
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,517 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.PollStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<KafkaSpoutMessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private transient KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaSpoutTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer commitTimer;                                    // timer == null for auto commit mode
    +    private transient Timer logTimer;
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +    private transient long numUncommittedOffsets;   // Number of offsets that have been polled and emitted but not yet been committed
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +    private transient PollStrategy pollStrategy;
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaSpoutTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +        numUncommittedOffsets = 0;
    +        logTimer = new Timer(500, Math.min(1000, kafkaSpoutConfig.getOffsetsCommitPeriodMs()/2), TimeUnit.MILLISECONDS);
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        pollStrategy = kafkaSpoutConfig.getPollStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            commitTimer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitPeriodMs(), TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        LOG.info("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            if (!consumerAutoCommitMode) {
    +                acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            }
    +
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // By default polling starts at the last committed offset. +1 to point fetch to the first uncommitted offset.
    +                    fetchOffset = committedOffset.offset() + 1;
    +                    kafkaConsumer.seek(tp, fetchOffset);
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else if (poll()) {
    +                emitTuples(pollKafkaBroker());
    +            } else if (logTimer.isExpiredResetOnTrue()){
    +                log();
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private void log() {
    +        switch(pollStrategy) {
    +            case STREAM:
    +                LOG.trace("Reached the maximum number number of uncommitted records [{}]. " +
    +                        "No more polls will occur until a sequence of commits sets the count under the [{}] threshold ",
    +                        numUncommittedOffsets, kafkaSpoutConfig.getMaxUncommittedOffsets());
    +                break;
    +            case BATCH:
    +                LOG.trace("No more polls will occur until the last batch completes. [{}] emitted tuples pending", numUncommittedOffsets);
    +                break;
    +            default:
    +                throw new IllegalStateException("No implementation defined for polling strategy " + pollStrategy);
    +        }
    +
    +    }
    +
    +    // always poll in auto commit mode because no state is kept and therefore there is no need to set an upper limit in memory
    +    private boolean poll()  {
    +        switch(pollStrategy) {
    +            case STREAM:
    +                return consumerAutoCommitMode || numUncommittedOffsets < kafkaSpoutConfig.getMaxUncommittedOffsets();
    +            case BATCH:
    +                return consumerAutoCommitMode || numUncommittedOffsets <= 0;
    +            default:
    +                throw new IllegalStateException("No implementation defined for polling strategy " + pollStrategy);
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && commitTimer.isExpiredResetOnTrue();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> pollKafkaBroker() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        final int numPolledRecords = consumerRecords.count();
    +        numUncommittedOffsets+= numPolledRecords;
    +        LOG.debug("Polled [{}] records from Kafka. NumUncommittedOffsets=[{}]", numPolledRecords, numUncommittedOffsets);
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                final List<Object> tuple = tupleBuilder.buildTuple(record, kafkaSpoutStreams);
    +                final KafkaSpoutMessageId messageId = new KafkaSpoutMessageId(record, tuple);
    +
    +                kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                LOG.trace("Emitted tuple [{}] for record [{}]", tuple, record);
    +            }
    +        }
    +    }
    +
    +    private void commitOffsetsForAckedTuples() {
    +        // Find offsets that are ready to be committed for every topic partition
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +        for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +            final OffsetAndMetadata nextCommitOffset = tpOffset.getValue().findNextCommitOffset();
    +            if (nextCommitOffset != null) {
    +                nextCommitOffsets.put(tpOffset.getKey(), nextCommitOffset);
    +            }
    +        }
    +
    +        // Commit offsets that are ready to be committed for every topic partition
    +        if (!nextCommitOffsets.isEmpty()) {
    +            kafkaConsumer.commitSync(nextCommitOffsets);
    +            LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +            // Instead of iterating again, it would be possible to commit and update the state for each TopicPartition
    +            // in the prior loop, but the multiple network calls should be more expensive than iterating twice over a small loop
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetEntry offsetEntry = tpOffset.getValue();
    +                offsetEntry.commit(nextCommitOffsets.get(tpOffset.getKey()));
    +            }
    +        } else {
    +            LOG.trace("No offsets to commit. {}", this);
    +        }
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            final KafkaSpoutMessageId msgId = (KafkaSpoutMessageId) messageId;
    +            acked.get(msgId.getTopicPartition()).add(msgId);
    --- End diff --
    
    So in non-auto commit mode, we keep adding message ids to the acked messages. That list is cleared only when the commit timer expires. Correct me if I am wrong but there is no cap on the number of messages in the acked? 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56521036
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaRecordTupleBuilder.java ---
    @@ -0,0 +1,44 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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,
    + *   See the License for the specific language governing permissions and
    + *   limitations under the License.
    + */
    +package org.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.tuple.Values;
    +
    +import java.util.List;
    +
    +public class KafkaRecordTupleBuilder<K, V> implements KafkaSpoutTupleBuilder<K, V> {
    +    @Override
    +    public List<Object> buildTuple(final ConsumerRecord<K, V> consumerRecord, KafkaSpoutStreams kafkaSpoutStreams) {
    --- End diff --
    
    @abhishekagarwal87 So what is the use case you are trying to solve, that this does not work for?  
    
    The current code has a many to one relationship from topics to output stream.  But the KafkaSpoutTupleBuilder is a singleton.  If we keep this the same moving the output fields to the KafkaSpoutTupleBuilder would result in all output streams being required to have the same set of fields.
    
    To me it feels like the issue is that the KafkSpoutTupleBuilder is a singleton.  If it had a one to one relationship with the output stream instead, the fields would fit into it better.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56377111
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    Maybe I have misread the code for the original kafka spout, but I don't think that either of these even come close to doing what the original spout did so at a minimum we need to change the explanation.  What is the point of having this config at all?  BATCH is equivalent of setting maxUncommittedOffsets to 0, unless we decide to make it work more like the original spout.
    
    For clarification the original `KafkaSpout` calls into the `PartitionManager` to emit a tuple.  It keeps a queue of messages that need to be emitted, but have not been emitted yet.  If that buffer were empty it attempts to refill the buffer with another batch from kafka.  Then it will pop off the first message try to transform it into a tuple and emit that tuple.  Only if the transformation turns into an empty tuple or something like that does it try again with another tuple form the queue.
    
    https://github.com/apache/storm/blob/master/external/storm-kafka/src/jvm/org/apache/storm/kafka/PartitionManager.java#L136-L173
    
    https://github.com/apache/storm/blob/master/external/storm-kafka/src/jvm/org/apache/storm/kafka/KafkaSpout.java#L139-L141
    
    So the spout will only emit at most one event from kafka per call to nextTuple ever.  From looking at the code it looks like there is a bug in there if we do try to transform one event into multiple tuples where the tracking gets off.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-222280056
  
    @jianbzhou any updates on 
    
    ``` One customer of us who also use the spout they found some other issues:
    1. Work load is not distributed to all spout tasks(as per the storm topology) 
    2. No progress on some partitions (as per the log)
    3. No commit on some partitions (as per the log)
    I will start looking into that tomorrow once I get the detail log file. Also if you have any clue on this please kindly advise.```


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-190868282
  
    @knusbaum can you please let me know how you are trying to run it. I am uploading shortly the changes addressing the code review. So, if you tell me how you are trying to run it, I can address any problems there may be. Thanks.
    



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55334218
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,458 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            if(!consumerAutoCommitMode) {
    +                acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            }
    +
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // do nothing - by default polling starts at the last committed offset
    +                    fetchOffset = committedOffset.offset();
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.isExpiredResetOnTrue();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || consumerAutoCommitMode || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);
    +
    +                    kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                    LOG.debug("Emitted tuple [{}] for record [{}]", tuple, record);
    +                }
    +            }
    +        }
    +    }
    +
    +    private void commitOffsetsForAckedTuples() {
    +        // Find offsets that are ready to be committed for every topic partition
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +        for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +            final OffsetAndMetadata nextCommitOffset = tpOffset.getValue().findNextCommitOffset();
    +            if (nextCommitOffset != null) {
    +                nextCommitOffsets.put(tpOffset.getKey(), nextCommitOffset);
    +            }
    +        }
    +
    +        // Commit offsets that are ready to be committed for every topic partition
    +        if (!nextCommitOffsets.isEmpty()) {
    +            kafkaConsumer.commitSync(nextCommitOffsets);
    +            LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +            // Instead of iterating again, it would be possible to commit and update the state for each TopicPartition
    +            // in the prior loop, but the multiple network calls should be more expensive than iterating twice over a small loop
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetEntry offsetEntry = tpOffset.getValue();
    +                offsetEntry.commit(nextCommitOffsets.get(tpOffset.getKey()));
    +            }
    +        } else {
    +            LOG.trace("No offsets to commit. {}", toString());
    +        }
    +    }
    +
    +    // ======== Ack =======
    +    @Override
    +    public void ack(Object messageId) {
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            final MessageId msgId = (MessageId) messageId;
    +            acked.get(msgId.getTopicPartition()).add(msgId);
    +            LOG.debug("Added acked message [{}] to list of messages to be committed to Kafka", msgId);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.debug("Retried tuple with message id [{}]", msgId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message [{}] being marked as acked.", msgId);
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate / Close / Declare Outputs =======
    +
    +    @Override
    +    public void activate() {
    +        subscribeKafkaConsumer();
    +    }
    +
    +    private void subscribeKafkaConsumer() {
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called following this poll, upon partition registration
    +        kafkaConsumer.poll(0);
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        shutdown();
    +    }
    +
    +    @Override
    +    public void close() {
    +        shutdown();
    +    }
    +
    +    private void shutdown() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            if (!consumerAutoCommitMode) {
    +                commitOffsetsForAckedTuples();
    +            }
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        kafkaSpoutStreams.declareOutputFields(declarer);
    +    }
    +
    +    @Override
    +    public String toString() {
    +        return "{acked=" + acked + "} ";
    +    }
    +
    +    // ======= Offsets Commit Management ==========
    +
    +    private static class OffsetComparator implements Comparator<MessageId> {
    +        public int compare(MessageId m1, MessageId m2) {
    +            return m1.offset() < m2.offset() ? -1 : m1.offset() == m2.offset() ? 0 : 1;
    +        }
    +    }
    +
    +    /**
    +     * This class is not thread safe
    +     */
    +    private class OffsetEntry {
    +        private final TopicPartition tp;
    +        private long committedOffset;               // last offset committed to Kafka, or initial fetching offset (initial value depends on offset strategy. See KafkaSpoutConsumerRebalanceListener)
    +        private final NavigableSet<MessageId> ackedMsgs = new TreeSet<>(OFFSET_COMPARATOR);     // acked messages sorted by ascending order of offset
    +
    +        public OffsetEntry(TopicPartition tp, long committedOffset) {
    +            this.tp = tp;
    +            this.committedOffset = committedOffset;
    +            LOG.debug("Created OffsetEntry for [topic-partition={}, committed-or-initial-fetch-offset={}]", tp, committedOffset);
    +        }
    +
    +        public void add(MessageId msgId) {          // O(Log N)
    +            ackedMsgs.add(msgId);
    +        }
    +
    +        /**
    +         * @return the next OffsetAndMetadata to commit, or null if no offset is ready to commit.
    +         */
    +        public OffsetAndMetadata findNextCommitOffset() {
    +            boolean found = false;
    +            long currOffset;
    +            long nextCommitOffset = committedOffset;
    +            MessageId nextCommitMsg = null;     // this is a convenience variable to make it faster to create OffsetAndMetadata
    +
    +            for (MessageId currAckedMsg : ackedMsgs) {  // complexity is that of a linear scan on a TreeMap
    +                if ((currOffset = currAckedMsg.offset()) == 0 || currOffset != nextCommitOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    if (currOffset == nextCommitOffset || currOffset == nextCommitOffset + 1) {            // found the next offset to commit
    +                        found = true;
    +                        nextCommitMsg = currAckedMsg;
    +                        nextCommitOffset = currOffset;
    +                        LOG.trace("Found offset to commit [{}]. {}", currOffset, toString());
    --- End diff --
    
    can you pass `this` instead of toString() so that toString() is called only if trace is enabled. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54890490
  
    --- Diff: pom.xml ---
    @@ -836,14 +839,39 @@
                     <scope>test</scope>
                 </dependency>
                 <dependency>
    -              <groupId>org.apache.calcite</groupId>
    -              <artifactId>calcite-core</artifactId>
    -              <version>${calcite.version}</version>
    +                <groupId>org.apache.calcite</groupId>
    +                <artifactId>calcite-core</artifactId>
    +                <version>${calcite.version}</version>
    +            </dependency>
    +            <dependency>
    +                <groupId>com.fasterxml.jackson.core</groupId>
    +                <artifactId>jackson-databind</artifactId>
    +                <version>${jackson.version}</version>
    +            </dependency>
    +            <dependency>
    +                <groupId>org.apache.kafka</groupId>
    +                <artifactId>${kafka.artifact.id}</artifactId>
    +                <version>${kafka.version}</version>
    +                <exclusions>
    +                    <exclusion>
    +                        <groupId>org.apache.zookeeper</groupId>
    +                        <artifactId>zookeeper</artifactId>
    +                    </exclusion>
    +                    <exclusion>
    +                        <groupId>log4j</groupId>
    +                        <artifactId>log4j</artifactId>
    +                    </exclusion>
    +                    <exclusion>
    +                        <groupId>org.slf4j</groupId>
    +                        <artifactId>slf4j-log4j12</artifactId>
    +                    </exclusion>
    +                </exclusions>
                 </dependency>
                 <dependency>
    -              <groupId>com.fasterxml.jackson.core</groupId>
    -              <artifactId>jackson-databind</artifactId>
    -              <version>${jackson.version}</version>
    +                <groupId>org.apache.kafka</groupId>
    +                <artifactId>kafka-clients</artifactId>
    +                <version>${kafka.version}</version>
    +                <scope>provided</scope>
    --- End diff --
    
    I filed STORM-1599 for it.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm issue #1131: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the issue:

    https://github.com/apache/storm/pull/1131
  
    @hmcl, just fyi, we found one new issue:
    when rebalance happened, in method onPartitionsAssigned->initialize(Collection<TopicPartition> partitions)
    we can see below two lines:
    **final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    final long fetchOffset = doSeek(tp, committedOffset);**
    
    If the committedOffset is out of range(say kafka log file removed), the when the poll() method is called, the offset will reset as the property auto.offset.reset. This will cause the newly polled message has bigger offset, so there is a break between committed offset and the acked offset, no continuous offset will be found.
    We will apply a quick fix for this.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r57960479
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,547 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Collections;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Iterator;
    +import java.util.LinkedList;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<KafkaSpoutMessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private transient KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +
    +
    +    // Bookkeeping
    +    private transient int maxRetries;                                   // Max number of times a tuple is retried
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;  // Strategy to determine the fetch offset of the first realized by the spout upon activation
    +    private transient KafkaSpoutRetryService retryService;              // Class that has the logic to handle tuple failure
    +    private transient Timer commitTimer;                                // timer == null for auto commit mode
    +    private transient boolean initialized;                              // Flag indicating that the spout is still undergoing initialization process.
    +    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +    private KafkaSpoutStreams kafkaSpoutStreams;                        // Object that wraps all the logic to declare output fields and emit tuples
    +    private transient KafkaSpoutTuplesBuilder<K, V> tuplesBuilder;      // Object that contains the logic to build tuples for each ConsumerRecord
    +
    +    private transient Map<TopicPartition, OffsetEntry> acked;           // Tuples that were successfully acked. These tuples will be committed periodically when the commit timer expires, after consumer rebalance, or on close/deactivate
    +    private transient Set<KafkaSpoutMessageId> emitted;                 // Tuples that have been emitted but that are "on the wire", i.e. pending being acked or failed
    +    private transient Iterator<ConsumerRecord<K, V>> waitingToEmit;         // Records that have been polled and are queued to be emitted in the nextTuple() call. One record is emitted per nextTuple()
    +    private transient long numUncommittedOffsets;                       // Number of offsets that have been polled and emitted but not yet been committed
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +        numUncommittedOffsets = 0;
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        // Retries management
    +        retryService = kafkaSpoutConfig.getRetryService();
    +
    +        // Tuples builder delegate
    +        tuplesBuilder = kafkaSpoutConfig.getTuplesBuilder();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            commitTimer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitPeriodMs(), TimeUnit.MILLISECONDS);
    +        }
    +
    +        acked = new HashMap<>();
    +        emitted = new HashSet<>();
    +        waitingToEmit = Collections.emptyListIterator();
    +
    +        LOG.info("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            if (!consumerAutoCommitMode) {
    +                acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            }
    +
    +            retryService.remove(partitions);
    +
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // By default polling starts at the last committed offset. +1 to point fetch to the first uncommitted offset.
    +                    fetchOffset = committedOffset.offset() + 1;
    +                    kafkaConsumer.seek(tp, fetchOffset);
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            }
    +
    +            if (poll()) {
    +                setWaitingToEmit(pollKafkaBroker());
    +            }
    +
    +            if (waitingToEmit()) {
    +                emit();
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && commitTimer.isExpiredResetOnTrue();    // timer != null for non auto commit mode
    +    }
    +
    +    private boolean poll() {
    +        return !waitingToEmit() && numUncommittedOffsets < kafkaSpoutConfig.getMaxUncommittedOffsets();
    +    }
    +
    +    private boolean waitingToEmit() {
    +        return waitingToEmit != null && waitingToEmit.hasNext();
    +    }
    +
    +    public void setWaitingToEmit(ConsumerRecords<K,V> consumerRecords) {
    +        List<ConsumerRecord<K,V>> waitingToEmitList = new LinkedList<>();
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            waitingToEmitList.addAll(consumerRecords.records(tp));
    +        }
    +        waitingToEmit = waitingToEmitList.iterator();
    +        LOG.trace("Records waiting to be emitted {}", waitingToEmitList);
    +    }
    +
    +    // ======== poll =========
    +    private ConsumerRecords<K, V> pollKafkaBroker() {
    +        doSeekRetriableTopicPartitions();
    +
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        final int numPolledRecords = consumerRecords.count();
    +        LOG.debug("Polled [{}] records from Kafka. NumUncommittedOffsets=[{}]", numPolledRecords, numUncommittedOffsets);
    +        return consumerRecords;
    +    }
    +
    +    private void doSeekRetriableTopicPartitions() {
    +        final Set<TopicPartition> retriableTopicPartitions = retryService.retriableTopicPartitions();
    +
    +        for (TopicPartition rtp : retriableTopicPartitions) {
    +            final OffsetAndMetadata offsetAndMeta = acked.get(rtp).findNextCommitOffset();
    +            if (offsetAndMeta != null) {
    +                kafkaConsumer.seek(rtp, offsetAndMeta.offset() + 1);  // seek to the next offset that is ready to commit in next commit cycle
    +            } else {
    +                kafkaConsumer.seekToEnd(rtp);    // Seek to last committed offset
    +            }
    +        }
    +    }
    +
    +    // ======== emit  =========
    +    private void emit() {
    +        emitTupleIfNotEmitted(waitingToEmit.next());
    +        waitingToEmit.remove();
    --- End diff --
    
    The way storm throttles spouts is if nextTuple is called and it does not emit anything storm will sleep for 1 ms before trying to tall nextTuple again.  If something was emitted nextTuple is called without any pause, assuming that the transfer queue etc can keep up with everything.
    
    This code will throw away at most 1000 messages a second.  Which for me is not the end of the world, but it would be nice to recover from a failure faster.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54458052
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig.toString());
    --- End diff --
    
    Minor nit. The .toString() is not needed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by harshach <gi...@git.apache.org>.
Github user harshach commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56542210
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    @revans2 I disagree with using existing kafkaspout just because it works. Its been a nightmare to maintain and support the existing kafkaspout.  Its goes through way too much delegation to achieve simple tasks. I am +1 for going away from existing kafkaspout design.
    
    _[0] - We fetch (poll) a new set of records from Kafka when we don't have something to emit from our buffer. The internal spout flow control assumes that we will emit around one tuple at a time when it calls .nextTuple(). So we have a buffer of messages to emit and emit them one at a time_
    The above is not true. 
    https://github.com/apache/storm/blob/master/external/storm-kafka/src/jvm/org/apache/storm/kafka/PartitionManager.java#L140 
    
    
    
    It emits all the available tuples in one nextTuple call.
    
    _[1] - If offsets 1 2 3 4 5 6 get fetched (polled)
 and the following occurs: 1 2 committed, 4 fails
 , 3 5 6 pending and the exponential back-off for 4 has expired and 4 has been retired less then the maximum number of retries.
    
    [1.1] - We want to fetch again starting on 4, i.e 4 5 6 ... and then filter out any messages that are not ready to be replayed.
    
    [1.2] - Let's say 3 that was pending fails after the retry fetch described in [1.1] (4 5 6 ... ) has happened. What do we do? We fetch again starting with 3 this time (3 4 5 ...)_
    
    This is unnecessarily complex. If its time to fetch again. We fetch from last committed offset + 1. If 3 fails it will be available and we don't need to fetch again.
    
    
    _[5] - Does it make sense to even support retrying an offset forever? This can potentially cause an infinite cycle of fetch (poll) calls to Kafka accross  sahe network... but it will guarantee delivery up to that point (offset)._
    probably not but storm core api + ackers contract is at least once delivery. So as long as we get retriable exception from kafka we should provide this capability. If its something like OffsetNotFound exception than we can move as the data on the kafka is purged.
    
    _We do NOT want at all to have the feature to retry from memory. That means that there is no need to ever keep tuples in memory, only offsets._
    Given we agreed on not having in-memory replays as failures are rare. Supporting this won't give us much benefit and having one more config for the user won't help either.
    
    
    
    
    



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by connieyang <gi...@git.apache.org>.
Github user connieyang commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-204615040
  
    +1 for merging and thanks for work @hmcl.
    
    @hmcl The previous KafkaSpout implementation publishes a kafkaOffset metrics to track spout lag, latest time offset and earliest time offset.  I can't seem to find the same or the equivalent metrics in the new implementation.  Am I missing something.  Thanks.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r57770218
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutRetryService.java ---
    @@ -0,0 +1,69 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.TopicPartition;
    +
    +import java.io.Serializable;
    +import java.util.Collection;
    +import java.util.Set;
    +
    +public interface KafkaSpoutRetryService extends Serializable {
    +    /**
    +     * Schedules this {@link KafkaSpoutMessageId} if not yet scheduled, or updates retry time if it has already been scheduled.
    +     * @param msgId message to schedule for retrial
    +     */
    +    void schedule(KafkaSpoutMessageId msgId);
    +
    +    /**
    +     * Removes a message from the list of messages scheduled for retrial
    +     * @param msgId message to remove from retrial
    +     */
    +    boolean remove(KafkaSpoutMessageId msgId);
    +
    +    /**
    +     * Removes all the messages whose {@link TopicPartition} does NOT belong to the specified {@code Collection<TopicPartition>}.
    +     * All messages that come from a {@link TopicPartition} existing in the collection will be kept.
    +     * This method is useful to cleanup state following partition rebalance.
    +     * @param topicPartitions Collection of {@link TopicPartition} for which to keep messages
    +     * @return true if at least one message was removed, false otherwise
    +     */
    +    boolean remove(Collection<TopicPartition> topicPartitions);
    +
    +    /**
    +     * @return set of topic partitions that have offsets that are ready to be retried, i.e.,
    +     * for which a tuple has failed and has retry time less than current time
    +     */
    +    Set<TopicPartition> retriableTopicPartitions();
    +
    +    /**
    +     * Checks if a specific failed {@link KafkaSpoutMessageId} is is ready to be retried,
    +     * i.e is scheduled and has retry time that is less than current time.
    +     * @return true if message is ready to be retried, false otherwise
    +     */
    +    boolean retry(KafkaSpoutMessageId msgId);
    --- End diff --
    
    Can we rename this to a question like canBeRetried, or isReadyForRetry?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by harshach <gi...@git.apache.org>.
Github user harshach commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56399307
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    @abhishekagarwal87 @revans2 not sure I follow your comments on the blocked strategy. If a tuple is failed in the old spout it won't be committing that offset into zookeeper. So we will be polling the same data again and again and causing more duplicates.
    With this new approach we are not causing unnecessary duplicates and if someone wants to limit this behavior they can set the maxretries to 1 and we proceed with polling.
    " If we want to save memory lets fix the spout so it acts more like the original kafkaSpout for replay instead of putting in a config that I don't want to support long term."
    @revans2 Failures in real topology won't happen that often hence its better we support fast poll mechanism that we've instead waiting for the whole batch to emitted.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by tgravescs <gi...@git.apache.org>.
Github user tgravescs commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56233752
  
    --- Diff: external/storm-kafka-client/pom.xml ---
    @@ -0,0 +1,86 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!--/**
    +        * Licensed to the Apache Software Foundation (ASF) under one
    +        * or more contributor license agreements.  See the NOTICE file
    +        * distributed with this work for additional information
    +        * regarding copyright ownership.  The ASF licenses this file
    +        * to you 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.
    +        */-->
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +    <modelVersion>4.0.0</modelVersion>
    +
    +    <parent>
    +        <artifactId>storm</artifactId>
    +        <groupId>org.apache.storm</groupId>
    +        <version>2.0.0-SNAPSHOT</version>
    +        <relativePath>../../pom.xml</relativePath>
    +    </parent>
    +
    +    <artifactId>storm-kafka-client</artifactId>
    --- End diff --
    
    the spouts stay where they are, the KafkaBolt moves to this storm-kafka-client.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-203031147
  
    I had a few minor comments, but nothing too big.  Overall I am +1 on this.  At some point we need to run some profiling on it and see what type of a throughput we can support with it.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by tgravescs <gi...@git.apache.org>.
Github user tgravescs commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-206967402
  
    @harshach @hmcl  did we file a jira to add the trident support?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54459488
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig.toString());
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if(commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.expired();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +
    +                    kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                    LOG.debug("Emitted tuple [{}] for record [{}]", tuple, record);
    +                }
    +            }
    +        }
    +    }
    +
    +    // ======== Ack =======
    +    @Override
    +    public void ack(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            acked.get(tp).add(msgId);
    +            LOG.debug("Adding acked message to [{}] to list of messages to be committed to Kafka", msgId);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.debug("Retried tuple with message id [{}]", messageId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message being marked as acked.");
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        if(!consumerAutoCommitMode) {
    +            commitOffsetsForAckedTuples();
    +        }
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            if(!consumerAutoCommitMode) {
    +                commitOffsetsForAckedTuples();
    +            }
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    // TODO must declare multiple output streams
    --- End diff --
    
    Do we need this now?  Or should we have a follow on JIRA for it?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by harshach <gi...@git.apache.org>.
Github user harshach commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56234828
  
    --- Diff: external/storm-kafka-client/pom.xml ---
    @@ -0,0 +1,86 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!--/**
    +        * Licensed to the Apache Software Foundation (ASF) under one
    +        * or more contributor license agreements.  See the NOTICE file
    +        * distributed with this work for additional information
    +        * regarding copyright ownership.  The ASF licenses this file
    +        * to you 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.
    +        */-->
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +    <modelVersion>4.0.0</modelVersion>
    +
    +    <parent>
    +        <artifactId>storm</artifactId>
    +        <groupId>org.apache.storm</groupId>
    +        <version>2.0.0-SNAPSHOT</version>
    +        <relativePath>../../pom.xml</relativePath>
    +    </parent>
    +
    +    <artifactId>storm-kafka-client</artifactId>
    --- End diff --
    
    +1 on above approach. We can do it in a follow-up JIRA.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-188859042
  
    @tgravescs probably there is a bit of miscommunication here. I didn't complete the spout part yet because I have been waiting on getting answers to some follow up questions I made to the code review comments, such that I can try to address them completely. I will push what I have, and trident can go in separate if that fits your timeline better. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54460067
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig.toString());
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if(commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.expired();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +
    +                    kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                    LOG.debug("Emitted tuple [{}] for record [{}]", tuple, record);
    +                }
    +            }
    +        }
    +    }
    +
    +    // ======== Ack =======
    +    @Override
    +    public void ack(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            acked.get(tp).add(msgId);
    +            LOG.debug("Adding acked message to [{}] to list of messages to be committed to Kafka", msgId);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.debug("Retried tuple with message id [{}]", messageId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message being marked as acked.");
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        if(!consumerAutoCommitMode) {
    +            commitOffsetsForAckedTuples();
    +        }
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            if(!consumerAutoCommitMode) {
    +                commitOffsetsForAckedTuples();
    +            }
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    // TODO must declare multiple output streams
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        kafkaSpoutStreams.declareOutputFields(declarer);
    +    }
    +
    +    // ====== Private helper methods ======
    +
    +    private void commitOffsetsForAckedTuples() {
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +
    +        try {
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetAndMetadata offsetAndMetadata = tpOffset.getValue().findNextCommitOffset();
    +                if (offsetAndMetadata != null) {
    +                    nextCommitOffsets.put(tpOffset.getKey(), offsetAndMetadata);
    +                }
    +            }
    +
    +            if (!nextCommitOffsets.isEmpty()) {
    +                kafkaConsumer.commitSync(nextCommitOffsets);
    +                LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +                // Instead of iterating again, we could commit and update state for each TopicPartition in the prior loop,
    +                // but the multiple network calls should be more expensive than iterating twice over a small loop
    +                for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                    final OffsetEntry offsetEntry = tpOffset.getValue();
    +                    offsetEntry.updateAckedState(nextCommitOffsets.get(tpOffset.getKey()));
    +                }
    +            } else {
    +                LOG.trace("No offsets to commit. {}", toString());
    +            }
    +        } catch (Exception e) {
    +            LOG.error("Exception occurred while committing to Kafka offsets of acked tuples", e);
    --- End diff --
    
    This is especially concerning because findNextCommitOffset says that it has side effects.  If we ignore an exception after calling something with side effects what will the results be?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55190214
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutStream.java ---
    @@ -0,0 +1,60 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.utils.Utils;
    +
    +import java.io.Serializable;
    +
    +public class KafkaSpoutStream implements Serializable {
    +    public static final String DEFAULT_TOPIC = "DEFAULT_TOPIC_KAFKA_SPOUT_INTERNAL";
    +    private final Fields outputFields;
    +    private final String streamId;
    +    private final String topic;
    +
    +    /** Declare specified outputFields with default stream and default topic */
    +    public KafkaSpoutStream(Fields outputFields) {
    +        this(outputFields, Utils.DEFAULT_STREAM_ID);
    +    }
    +
    +    /** Declare specified outputFields with specified stream and default topic */
    +    public KafkaSpoutStream(Fields outputFields, String streamId) {
    +        this(outputFields, streamId, DEFAULT_TOPIC);
    +    }
    +
    +    /** Declare specified outputFields with specified stream and specified topic */
    +    public KafkaSpoutStream(Fields outputFields, String streamId, String topic) {
    --- End diff --
    
    @revans2 I agree with you but I put the builder logic in `KafkaSpoutStreams`, and the `KafkaSpoutConfig` constructor is now as follows: 
    
    `public Builder(Map<String, Object> kafkaProps, KafkaSpoutStreams kafkaSpoutStreams)`
    
    The reason for this is that topics and output streams are not optional, but required for the spout to work. The same applies to the kafkaProps parameters that go. All required parameters go in the constructor, and the set methods are only for parameters that are optional.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-191399473
  
    @hmcl I went through the code again.  It looks good.  There are a few issues with activate and deactivate and The APIs for KafkaSpoutStream(s) does not seem as clean as I would like it to be.  Especially if this is an API we want to support long term.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by knusbaum <gi...@git.apache.org>.
Github user knusbaum commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53679765
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    +    }
    +
    +    private void removeFromBlacklist(org.apache.storm.kafka.spout.MessageId msgId) {
    +        blackList.remove(msgId);
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            addAckedTuples(tp, msgId);
    +            // Removed acked tuples from the emittedTuples data structure
    +            emittedTuples.remove(msgId);
    +            // if this acked msg is a retry, remove it from failed data structure
    +            removeFromFailed(tp, msgId);
    +        }
    +    }
    +
    +    private void addAckedTuples(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (!acked.containsKey(tp)) {
    +                acked.put(tp, new OffsetEntry(tp));
    +            }
    +            acked.get(tp).add(msgId);
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            final TopicPartition tp = msgId.getTopicPartition();
    +            // limit to max number of retries
    +            if (msgId.numFails() >= maxRetries) {
    +                LOG.debug("Reached the maximum number of retries. Adding [{]} to list of messages to be committed to kafka", msgId);
    --- End diff --
    
    More bracket formatting


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55936769
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutStreams.java ---
    @@ -0,0 +1,142 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.utils.Utils;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * Represents the output streams associated with each topic, and provides a public API to
    + * declare output streams and emmit tuples, on the appropriate stream, for all the topics specified.
    + */
    +public class KafkaSpoutStreams implements Serializable {
    +    private final Map<String, KafkaSpoutStream> topicToStream;
    +
    +    private KafkaSpoutStreams(Builder builder) {
    +        this.topicToStream = builder.topicToStream;
    +    }
    +
    +    /**
    +     * @param topic the topic for which to get output fields
    +     * @return the output fields declared
    +     */
    +    public Fields getOutputFields(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getOutputFields();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @param topic the topic to for which to get the stream id
    +     * @return the id of the stream to where the tuples are emitted
    +     */
    +    public String getStreamId(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getStreamId();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @return list of topics subscribed and emitting tuples to a stream as configured by {@link KafkaSpoutStream}
    +     */
    +    public List<String> getTopics() {
    +        return new ArrayList<>(topicToStream.keySet());
    +    }
    +
    +    void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        for (KafkaSpoutStream stream : topicToStream.values()) {
    +            declarer.declareStream(stream.getStreamId(), stream.getOutputFields());
    +        }
    +    }
    +
    +    void emit(SpoutOutputCollector collector, MessageId messageId) {
    +        collector.emit(getStreamId(messageId.topic()), messageId.getTuple(), messageId);
    --- End diff --
    
    @harshach current kafka spout only keeps offsets in memory. This new spout keeps the actual record as well in memory. Offsets are 64 bytes long but there is no limit on the record size. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-221161594
  
    @jianbzhou please email me to
    
    ![image](https://cloud.githubusercontent.com/assets/10284328/15491930/97f1a368-212a-11e6-8dd0-da092f040818.png)



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56462751
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    @revans2 @abhishekagarwal87 @harshach thanks a lot for your comments. I do agree that this code is optimized for fails as I was a bit concerned on the time spent fetching data across the network. I also agree that failure should be sporadic, and therefore it's not the best tradeoff to optimize for it. Therefore, I am going to change the implementation as follows (please let me know if this what you have in mind such that I can get this right). 
    
    [0] - We fetch (poll) a new set of records from Kafka as soon as the previous batch has been emitted. We fetch without worrying about acks or fails.  What I mean is, if offsets `1 2 3 4 5 6` get fetched, after I have done collector.emit(1) ... collector.emit(6), I can fetch `6 7 8 9`. At this point 1-6 may have been acked, failed, both, or still be in the wire but we don't care, we simply fetch `6 7 8 9` and then deal with potencial fails through retry.
    
    [1] - If offsets `1 2 3 4 5 6` get fetched (polled)
 and the following occurs:
    `1 2` committed
      
    `4` fails
        
    `3 5 6` pending
      [1.1] - We want to fetch again starting on `3`, i.e `3 4 5 6 ...`
          This would result in `3 5 6` being duplicate unless [2]
      [1.2] - Let's say `3` that was pending now fails after the fetch `3 4 5 6 ...` described in [1.1] has happened. What do we do? Do we retry again `3 4 5 6 ...` and deal with dups as described in [2]? Otherwise we can have lots of dups.
    
    [2] - Do we want THIS PATCH to have some logic to avoid re-emitting `3 5 6` if they were polled again but have been acked in the meantime? Or do we want to support this later?
    
    [3] - Do we want THIS PATCH to support a limited number of retrials for a particular offset. Let's say `4` keeps on failing. Do we want to limit the number of fails (i.e repeat what is done in [1.1]) to e.g. `3` times, and once the limit is reached, we consider `4` has comitted, and fetch from offset `5` onwards, i.e. `5 6 7 8 ...`. 
    [3.1] - In this scenario, in case of spout failure, all the offsets that potentially fail before `4` will not be retried once we have moved past `4`. We may sacrifice guarantee of delivery in this case, or [3.2]
    [3.2] - Do we want to stop retrying `4` (even if it has reached max retries) only if all the offsets < `4` have been acked or all have failed more than `numMaxRetries`. In this case we won't miss any offsets before `4` unless they reached `numMaxRetries`, which is the contract the user says he agrees in terms of not having a certain record delivered if it failed more than `numMaxRetries`.
    
    [4] - We don't want at all to have the feature to retry from memory. That means that there is no need to ever keep tuples in memory, only offsets. 
      [4.1] - Or do we want extract the retry strategy into one interface, and have implementations for both?
    
    [5] - Does it make sense to even support retrying forever? This can potentially cause an infinite cycle of fetch (poll) calls to Kafka accross the network... but it will guarantee delievery up to that point.
    
    @revans2 I understand your urgency. I will make a push to have this done by today or tomorrow.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54460997
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig.toString());
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if(commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.expired();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +
    +                    kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                    LOG.debug("Emitted tuple [{}] for record [{}]", tuple, record);
    +                }
    +            }
    +        }
    +    }
    +
    +    // ======== Ack =======
    +    @Override
    +    public void ack(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            acked.get(tp).add(msgId);
    +            LOG.debug("Adding acked message to [{}] to list of messages to be committed to Kafka", msgId);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.debug("Retried tuple with message id [{}]", messageId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message being marked as acked.");
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        if(!consumerAutoCommitMode) {
    +            commitOffsetsForAckedTuples();
    +        }
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            if(!consumerAutoCommitMode) {
    +                commitOffsetsForAckedTuples();
    +            }
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    // TODO must declare multiple output streams
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        kafkaSpoutStreams.declareOutputFields(declarer);
    +    }
    +
    +    // ====== Private helper methods ======
    +
    +    private void commitOffsetsForAckedTuples() {
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +
    +        try {
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetAndMetadata offsetAndMetadata = tpOffset.getValue().findNextCommitOffset();
    +                if (offsetAndMetadata != null) {
    +                    nextCommitOffsets.put(tpOffset.getKey(), offsetAndMetadata);
    +                }
    +            }
    +
    +            if (!nextCommitOffsets.isEmpty()) {
    +                kafkaConsumer.commitSync(nextCommitOffsets);
    +                LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +                // Instead of iterating again, we could commit and update state for each TopicPartition in the prior loop,
    +                // but the multiple network calls should be more expensive than iterating twice over a small loop
    +                for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                    final OffsetEntry offsetEntry = tpOffset.getValue();
    +                    offsetEntry.updateAckedState(nextCommitOffsets.get(tpOffset.getKey()));
    +                }
    +            } else {
    +                LOG.trace("No offsets to commit. {}", toString());
    +            }
    +        } catch (Exception e) {
    +            LOG.error("Exception occurred while committing to Kafka offsets of acked tuples", e);
    +        }
    +    }
    +
    +    @Override
    +    public String toString() {
    +        return "{acked=" + acked + "} ";
    +    }
    +
    +    // ======= Offsets Commit Management ==========
    +
    +    private static class OffsetComparator implements Comparator<MessageId> {
    +        public int compare(MessageId m1, MessageId m2) {
    +            return m1.offset() < m2.offset() ? -1 : m1.offset() == m2.offset() ? 0 : 1;
    +        }
    +    }
    +
    +    /** This class is not thread safe */
    +    private class OffsetEntry {
    +        private final TopicPartition tp;
    +        private long committedOffset;               // last offset committed to Kafka, or initial fetching offset (initial value depends on offset strategy. See KafkaSpoutConsumerRebalanceListener)
    +        private final NavigableSet<MessageId> ackedMsgs = new TreeSet<>(OFFSET_COMPARATOR);     // acked messages sorted by ascending order of offset
    +
    +        public OffsetEntry(TopicPartition tp, long committedOffset) {
    +            this.tp = tp;
    +            this.committedOffset = committedOffset;
    +            LOG.debug("Created OffsetEntry for [topic-partition={}, committed-or-initial-fetch-offset={}]", tp, committedOffset);
    +        }
    +
    +        public void add(MessageId msgId) {          // O(Log N)
    +            ackedMsgs.add(msgId);
    +        }
    +
    +        /**
    +         * This method has side effects. The method updateAckedState should be called after this method.
    +         * @return the next OffsetAndMetadata to commit, or null if no offset is ready to commit.
    +         */
    +        public OffsetAndMetadata findNextCommitOffset() {
    +            boolean found = false;
    +            long currOffset;
    +            long nextCommitOffset = committedOffset;
    +            MessageId nextCommitMsg = null;     // this is a convenience field to make it faster to create OffsetAndMetadata
    +
    +            for (MessageId currAckedMsg : ackedMsgs) {  // complexity is that of a linear scan on a TreeMap
    +                if ((currOffset = currAckedMsg.offset()) == 0 || currOffset != nextCommitOffset) {     // this is to void duplication because the first message polled is the last message acked.
    +                    if (currOffset == nextCommitOffset || currOffset == nextCommitOffset + 1) {            // found the next offset to commit
    +                        found = true;
    +                        nextCommitMsg = currAckedMsg;
    +                        nextCommitOffset = currOffset;
    +                        LOG.trace("Found offset to commit [{}]. {}", currOffset, toString());
    +                    } else if (currAckedMsg.offset() > nextCommitOffset + 1) {    // offset found is not continuous to the offsets listed to go in the next commit, so stop search
    +                        LOG.debug("Non continuous offset found [{}]. It will be processed in a subsequent batch. {}", currOffset, toString());
    +                        break;
    +                    } else {
    +                        LOG.debug("Unexpected offset found [{}]. {}", currOffset, toString());
    +                        break;
    +                    }
    +                }
    +            }
    +
    +            OffsetAndMetadata offsetAndMetadata = null;
    +            if (found) {
    +                offsetAndMetadata = new OffsetAndMetadata(nextCommitOffset, nextCommitMsg.getMetadata(Thread.currentThread()));
    +                LOG.trace("Offset to be committed next: [{}] {}", offsetAndMetadata.offset(), toString());
    +            } else {
    +                LOG.debug("No offsets ready to commit", toString());
    +            }
    +            return offsetAndMetadata;
    +        }
    +
    +        /**
    +         * This method has side effects and should be called after findNextCommitOffset
    +         */
    +        public void updateAckedState(OffsetAndMetadata offsetAndMetadata) {
    +            if (offsetAndMetadata != null) {
    +                committedOffset = offsetAndMetadata.offset();
    +                for (Iterator<MessageId> iterator1 = ackedMsgs.iterator();
    +                     iterator1.hasNext(); ) {
    +                    if (iterator1.next().offset() <= offsetAndMetadata.offset()) {
    +                        iterator1.remove();
    +                    } else {
    +                        break;
    +                    }
    +                }
    +            }
    +            LOG.trace("Object state after update: {}", toString());
    +        }
    +
    +        public boolean isEmpty() {
    +            return ackedMsgs.isEmpty();
    +        }
    +
    +        @Override
    +        public String toString() {
    +            return "OffsetEntry{" +
    +                    "topic-partition=" + tp +
    +                    ", committedOffset=" + committedOffset +
    +                    ", ackedMsgs=" + ackedMsgs +
    +                    '}';
    +        }
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            for (TopicPartition tp: partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /** sets the cursor to the location dictated by the first poll strategy and returns the fetch offset */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null)  {     // fetchOffset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // do nothing - by default polling starts at the lat committed fetchOffset
    +                    fetchOffset = committedOffset.offset();
    +                }
    +            } else {    // no previous commit occurred, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +
    +        private void setAcked(TopicPartition tp, long fetchOffset) {
    +            if (!consumerAutoCommitMode && !acked.containsKey(tp)) {           // If this partition was previously assigned, leave the acked offsets as they were to resume where it left off
    +                acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +            }
    +        }
    +    }
    +
    +    // =========== Timer ===========
    +
    +    private class Timer {
    +        private final long frequency;
    +        private final long delay;
    +        private final TimeUnit timeUnit;
    +        private final long frequencyNanos;
    +        private long start;
    +
    +        /** Creates a timer to expire at the given frequency and starting with the specified time delay.
    +         *  Frequency and delay must be specified in the same TimeUnit */
    +        public Timer(long frequency, long delay, TimeUnit timeUnit) {
    +            this.frequency = frequency;
    +            this.delay = delay;
    +            this.timeUnit = timeUnit;
    +
    +            frequencyNanos = timeUnit.toNanos(frequency);
    +            start = System.nanoTime() + timeUnit.toNanos(delay);
    +        }
    +
    +        public long frequency() {
    +            return frequency;
    +        }
    +
    +        public long delay() {
    +            return delay;
    +        }
    +
    +        public TimeUnit getTimeUnit() {
    +            return timeUnit;
    +        }
    +
    +        /**
    +         * If this method returns true, a new timer cycle will start.
    +         * @return true if the elapsed time since the last true value call to this method is greater or
    +         * equal to the frequency specified upon creation of this timer. Returns false otherwise.
    +         */
    +        public boolean expired() {
    --- End diff --
    
    Can we rename this to indicate that expired is being reset?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53697397
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    --- End diff --
    
    Can we replace this with a static Timer?  We might have multiple instances of the spout in a single worker, and having a thread per each just to set a volatile boolean to true, feels like overkill.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56534191
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaRecordTupleBuilder.java ---
    @@ -0,0 +1,44 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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,
    + *   See the License for the specific language governing permissions and
    + *   limitations under the License.
    + */
    +package org.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.tuple.Values;
    +
    +import java.util.List;
    +
    +public class KafkaRecordTupleBuilder<K, V> implements KafkaSpoutTupleBuilder<K, V> {
    +    @Override
    +    public List<Object> buildTuple(final ConsumerRecord<K, V> consumerRecord, KafkaSpoutStreams kafkaSpoutStreams) {
    --- End diff --
    
    Yes I can see that.  I am just saying that if we want to do that, we also need to make it so KafkaSpoutTupleBuilder is not a singleton.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56548912
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    If you think the replay is too complex then OK feel free to propose something that is more simpler.  But replaying an entire batch of tuples because I don't want to try and keep track of which I need to do and which I don't seems like a waste.
    
    And OK for [5] you convinced me that in order to truly keep the contract having replay forever be the default option is probably the best.
    
    Like I said before I am happy to write this if you want me to.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-222278070
  
    @jianbzhou I confirm that your suggested fix for doSeekRetriableTopicPartitions is correct. I am going to include that in the next patch.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by lujinhong <gi...@git.apache.org>.
Github user lujinhong commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-186986537
  
    In KafkaUtil.java, NOT_LEADER_FOR_PARTITION happend, may catch the NOT_LEADER_FOR_PARTITION execption will be a good idea.
    
    This exception should be catch and then log & ignored Or invole the ZkCoordinator#refresh().
    



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55334289
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,458 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            if(!consumerAutoCommitMode) {
    +                acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            }
    +
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // do nothing - by default polling starts at the last committed offset
    +                    fetchOffset = committedOffset.offset();
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.isExpiredResetOnTrue();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || consumerAutoCommitMode || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);
    +
    +                    kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                    LOG.debug("Emitted tuple [{}] for record [{}]", tuple, record);
    +                }
    +            }
    +        }
    +    }
    +
    +    private void commitOffsetsForAckedTuples() {
    +        // Find offsets that are ready to be committed for every topic partition
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +        for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +            final OffsetAndMetadata nextCommitOffset = tpOffset.getValue().findNextCommitOffset();
    +            if (nextCommitOffset != null) {
    +                nextCommitOffsets.put(tpOffset.getKey(), nextCommitOffset);
    +            }
    +        }
    +
    +        // Commit offsets that are ready to be committed for every topic partition
    +        if (!nextCommitOffsets.isEmpty()) {
    +            kafkaConsumer.commitSync(nextCommitOffsets);
    +            LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +            // Instead of iterating again, it would be possible to commit and update the state for each TopicPartition
    +            // in the prior loop, but the multiple network calls should be more expensive than iterating twice over a small loop
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetEntry offsetEntry = tpOffset.getValue();
    +                offsetEntry.commit(nextCommitOffsets.get(tpOffset.getKey()));
    +            }
    +        } else {
    +            LOG.trace("No offsets to commit. {}", toString());
    +        }
    +    }
    +
    +    // ======== Ack =======
    +    @Override
    +    public void ack(Object messageId) {
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            final MessageId msgId = (MessageId) messageId;
    +            acked.get(msgId.getTopicPartition()).add(msgId);
    +            LOG.debug("Added acked message [{}] to list of messages to be committed to Kafka", msgId);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.debug("Retried tuple with message id [{}]", msgId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message [{}] being marked as acked.", msgId);
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate / Close / Declare Outputs =======
    +
    +    @Override
    +    public void activate() {
    +        subscribeKafkaConsumer();
    +    }
    +
    +    private void subscribeKafkaConsumer() {
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called following this poll, upon partition registration
    +        kafkaConsumer.poll(0);
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        shutdown();
    +    }
    +
    +    @Override
    +    public void close() {
    +        shutdown();
    +    }
    +
    +    private void shutdown() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            if (!consumerAutoCommitMode) {
    +                commitOffsetsForAckedTuples();
    +            }
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        kafkaSpoutStreams.declareOutputFields(declarer);
    +    }
    +
    +    @Override
    +    public String toString() {
    +        return "{acked=" + acked + "} ";
    +    }
    +
    +    // ======= Offsets Commit Management ==========
    +
    +    private static class OffsetComparator implements Comparator<MessageId> {
    +        public int compare(MessageId m1, MessageId m2) {
    +            return m1.offset() < m2.offset() ? -1 : m1.offset() == m2.offset() ? 0 : 1;
    +        }
    +    }
    +
    +    /**
    +     * This class is not thread safe
    +     */
    +    private class OffsetEntry {
    +        private final TopicPartition tp;
    +        private long committedOffset;               // last offset committed to Kafka, or initial fetching offset (initial value depends on offset strategy. See KafkaSpoutConsumerRebalanceListener)
    +        private final NavigableSet<MessageId> ackedMsgs = new TreeSet<>(OFFSET_COMPARATOR);     // acked messages sorted by ascending order of offset
    +
    +        public OffsetEntry(TopicPartition tp, long committedOffset) {
    +            this.tp = tp;
    +            this.committedOffset = committedOffset;
    +            LOG.debug("Created OffsetEntry for [topic-partition={}, committed-or-initial-fetch-offset={}]", tp, committedOffset);
    +        }
    +
    +        public void add(MessageId msgId) {          // O(Log N)
    +            ackedMsgs.add(msgId);
    +        }
    +
    +        /**
    +         * @return the next OffsetAndMetadata to commit, or null if no offset is ready to commit.
    +         */
    +        public OffsetAndMetadata findNextCommitOffset() {
    +            boolean found = false;
    +            long currOffset;
    +            long nextCommitOffset = committedOffset;
    +            MessageId nextCommitMsg = null;     // this is a convenience variable to make it faster to create OffsetAndMetadata
    +
    +            for (MessageId currAckedMsg : ackedMsgs) {  // complexity is that of a linear scan on a TreeMap
    +                if ((currOffset = currAckedMsg.offset()) == 0 || currOffset != nextCommitOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    if (currOffset == nextCommitOffset || currOffset == nextCommitOffset + 1) {            // found the next offset to commit
    +                        found = true;
    +                        nextCommitMsg = currAckedMsg;
    +                        nextCommitOffset = currOffset;
    +                        LOG.trace("Found offset to commit [{}]. {}", currOffset, toString());
    --- End diff --
    
    The above can be done in other places where toString() is being called. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54144192
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    +    }
    +
    +    private void removeFromBlacklist(org.apache.storm.kafka.spout.MessageId msgId) {
    +        blackList.remove(msgId);
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            addAckedTuples(tp, msgId);
    +            // Removed acked tuples from the emittedTuples data structure
    +            emittedTuples.remove(msgId);
    +            // if this acked msg is a retry, remove it from failed data structure
    +            removeFromFailed(tp, msgId);
    +        }
    +    }
    +
    +    private void addAckedTuples(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (!acked.containsKey(tp)) {
    +                acked.put(tp, new OffsetEntry(tp));
    +            }
    +            acked.get(tp).add(msgId);
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            final TopicPartition tp = msgId.getTopicPartition();
    +            // limit to max number of retries
    +            if (msgId.numFails() >= maxRetries) {
    +                LOG.debug("Reached the maximum number of retries. Adding [{]} to list of messages to be committed to kafka", msgId);
    +                ack(msgId);
    +                removeFromFailed(tp, msgId);
    +            } else {
    +                addToFailed(tp, msgId);
    +            }
    +        }
    +    }
    +
    +    private void addToFailed(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        if (!failed.containsKey(tp)) {
    +            failed.put(tp, new HashSet<org.apache.storm.kafka.spout.MessageId>());
    +        }
    +        final Set<org.apache.storm.kafka.spout.MessageId> msgIds = failed.get(tp);
    +        if (msgIds.contains(msgId)) {      // do this to update the counter of the message
    +            msgIds.remove(msgId);
    +        }
    +        msgId.incrementNumFails();        // increment number of failures counter
    +        msgIds.add(msgId);
    +    }
    +
    +    private void removeFromFailed(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        if (failed.containsKey(tp)) {
    +            final Set<org.apache.storm.kafka.spout.MessageId> msgIds = failed.get(tp);
    +            msgIds.remove(msgId);
    +            if (msgIds.isEmpty()) {
    +                failed.remove(tp);
    +            }
    +            LOG.debug("Removing from failed list [{}, {}]", tp, msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        commitAckedTuples();
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            commitAckedTuples();
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        declarer.declareStream(kafkaSpoutStream.getStreamId(), kafkaSpoutStream.getOutputFields());
    +    }
    +
    +    // ====== Private helper methods ======
    +
    +    private void commitAckedTuples() {
    +        final Map<TopicPartition, OffsetAndMetadata> toCommitOffsets = new HashMap<>();
    +
    +        LOG.debug("Committing acked offsets to Kafka");
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            for (TopicPartition tp : acked.keySet()) {
    --- End diff --
    
    DONE


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54457973
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    --- End diff --
    
    minor not spelling "foloowing"


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by tgravescs <gi...@git.apache.org>.
Github user tgravescs commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56231321
  
    --- Diff: external/storm-kafka-client/pom.xml ---
    @@ -0,0 +1,86 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!--/**
    +        * Licensed to the Apache Software Foundation (ASF) under one
    +        * or more contributor license agreements.  See the NOTICE file
    +        * distributed with this work for additional information
    +        * regarding copyright ownership.  The ASF licenses this file
    +        * to you 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.
    +        */-->
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +    <modelVersion>4.0.0</modelVersion>
    +
    +    <parent>
    +        <artifactId>storm</artifactId>
    +        <groupId>org.apache.storm</groupId>
    +        <version>2.0.0-SNAPSHOT</version>
    +        <relativePath>../../pom.xml</relativePath>
    +    </parent>
    +
    +    <artifactId>storm-kafka-client</artifactId>
    --- End diff --
    
    We talked about this some more and it seems to me it would make more sense to leave this new spout in storm-kafka-client (or whatever you want to call it) and move the KafkaBolt which uses the new producer api over here also.  That way this component only needs to depend on the new kafka-clients java api and not on the entire scala kafka core.  We can make the old storm-kafka depend on this component so it still picks up the bolt so if anyone is using that its still works.  We can deprecate the old KafkaSpout but keep it around for people using older versions of Kafka.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-221153889
  
    @hmcl, could you share your email address? I will send our latest spout so you can have a quick review - this version is working in our testing env for about a week. Our customer faced one issue which seems that  the load is not well distributed across all partition in 0.9 KafkaSpout, some partitions have no commit, progress...I am still waiting for the kafka setup from the customer and shall send to you once i have.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by kishorvpatil <gi...@git.apache.org>.
Github user kishorvpatil commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-194567970
  
    @hmcl, the new `KafkaSpoutConfig` does not take in `securityProtocol` unlike `SpoutConfig`. Is this intentional? will `storm-kafka-client` module work with secure kafka instance?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56239392
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,517 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.PollStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<KafkaSpoutMessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private transient KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaSpoutTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer commitTimer;                                    // timer == null for auto commit mode
    +    private transient Timer logTimer;
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +    private transient long numUncommittedOffsets;   // Number of offsets that have been polled and emitted but not yet been committed
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +    private transient PollStrategy pollStrategy;
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaSpoutTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +        numUncommittedOffsets = 0;
    --- End diff --
    
    Never mind it is the distance.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-197979792
  
    @revans2 @abhishekagarwal87 I am still going over all your comments and I will think through them thoroughly and I am welcome to incorporate everything that makes this better. However, one thing I would like to suggest is that we try to gather requirements and identify the best possible solution for all of those requirements, rather than simply trying to fit some of the existing KafkaSpout's solutions into this KafkaSpout. There are a lot of design decisions I don't agree with in the previous KafkaSpout that among other things made the code very complex and verbose. We should use the previous code to identify requirements, but we shouldn't simply follow it.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56396492
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    What do you mean by "blocks", especially with emitted?
    
    For me I just don't understand why we need a separate config just to override `maxUncommittedOffsets` and set it to 0.  The amount of memory used by the spout still follows the same pattern `O(max(maxUncommittedOffsets, kafkaBatchSizeInMessages) * avgMessageSize)`  and the maximum throughput still follows `O(max(kafkaBatchSizeInMessages, maxUncommittedOffsets)/offsetCommitPeriodInSeconds)`
    
    So the higher throughput we want the higher the memory that will be used.  I just don't see a reason for a config that purposely sets it to one extreme that is going to give you really really crappy performance.  If we want to save memory lets fix the spout so it acts more like the original kafkaSpout for replay instead of putting in a config that I don't want to support long term.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-188879736
  
    @hmcl sorry I missed your questions in all my e-mail


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-189457987
  
    @tgravescs @revans2 I am just finalizing some testing and I will push in the patch after lunch. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by ptgoetz <gi...@git.apache.org>.
Github user ptgoetz commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-203693035
  
    I think this wins the record for the most comments on a pull request.
    
    Thanks @hmcl for persevering and putting up with the epic review. Nice work.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm issue #1131: STORM-822: Kafka Spout New Consumer API

Posted by erikdw <gi...@git.apache.org>.
Github user erikdw commented on the issue:

    https://github.com/apache/storm/pull/1131
  
    @jianbzhou : you can file a STORM JIRA ticket yourself actually -- you just need to [create an Apache JIRA account](https://issues.apache.org/jira/secure/Signup!default.jspa).


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56692645
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    @revans2 I understand your requirement to send only one tuple per call to nextTuple(), hence the need for the waitingToEmit queue. However, I would like to ask what is your opinion on the scenario the currently existing KafkaSpout has that using the MultiScheme you can fan out one record into multiple tuples, which implies that we will be sending multiple tuples in one call to nextTuple
    
    https://github.com/apache/storm/blob/master/external/storm-kafka/src/jvm/org/apache/storm/kafka/PartitionManager.java#L155-L157
    
    https://github.com/apache/storm/blob/master/external/storm-kafka/src/jvm/org/apache/storm/kafka/PartitionManager.java#L159-L161
    
    Do we want to support this exact behavior? Or de we want to really send just one tuple per call to nextTuple in every possible scenario, including these fan out scnearios? 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-190345957
  
    For the most part things look good.  My biggest concern is around catching Exception and ignoring it when we commit offsets.
    
    I am also a little concerned about having auto-commit be the default, as it violates some of storms guarantees.  I would prefer it to honor storms guarantees by default.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by knusbaum <gi...@git.apache.org>.
Github user knusbaum commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53679898
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    +    }
    +
    +    private void removeFromBlacklist(org.apache.storm.kafka.spout.MessageId msgId) {
    +        blackList.remove(msgId);
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            addAckedTuples(tp, msgId);
    +            // Removed acked tuples from the emittedTuples data structure
    +            emittedTuples.remove(msgId);
    +            // if this acked msg is a retry, remove it from failed data structure
    +            removeFromFailed(tp, msgId);
    +        }
    +    }
    +
    +    private void addAckedTuples(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (!acked.containsKey(tp)) {
    +                acked.put(tp, new OffsetEntry(tp));
    +            }
    +            acked.get(tp).add(msgId);
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    --- End diff --
    
    This looks like it can cause problems. I believe it's possible that fail() can be called more than once with the same `messageId`.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54128306
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    +    }
    +
    +    private void removeFromBlacklist(org.apache.storm.kafka.spout.MessageId msgId) {
    +        blackList.remove(msgId);
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            addAckedTuples(tp, msgId);
    +            // Removed acked tuples from the emittedTuples data structure
    +            emittedTuples.remove(msgId);
    +            // if this acked msg is a retry, remove it from failed data structure
    +            removeFromFailed(tp, msgId);
    +        }
    +    }
    +
    +    private void addAckedTuples(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (!acked.containsKey(tp)) {
    +                acked.put(tp, new OffsetEntry(tp));
    +            }
    +            acked.get(tp).add(msgId);
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            final TopicPartition tp = msgId.getTopicPartition();
    +            // limit to max number of retries
    +            if (msgId.numFails() >= maxRetries) {
    +                LOG.debug("Reached the maximum number of retries. Adding [{]} to list of messages to be committed to kafka", msgId);
    +                ack(msgId);
    +                removeFromFailed(tp, msgId);
    +            } else {
    +                addToFailed(tp, msgId);
    --- End diff --
    
    I agree that we need an upper limit to the times we replay a tuple, but we can do this differently.  See https://github.com/apache/storm/pull/1131#issuecomment-187425242 as an example of how to do this.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54661270
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig.toString());
    --- End diff --
    
    Done


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53723733
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    --- End diff --
    
    @revans2 it may be possible but depends on what we want to do. My initial implementation used this thread to do the commits to Kafka. However that causes ConcurrentModificationException on the Kafka side, as it is single threaded. The immediate fix was to just create this volatile variable and leave it as is. This was meant to be temporary all along, an just pushed it like this for an early review attending to the urgency of the patch.
    
    Nevertheless, we should discuss if we would like to consider in the future using the Kafka option commitAsync, which receives as parameter OffsetCommitCallback, which could be used to manage the bookkeeping state. I suppose even if we consider this it won't be in this patch... but I wonder if this piece of functionality would helpful for that scenario, if we ever want to consider it.
    
    I am referring to this comment as C_1 in another comment bellow.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-220878934
  
    @hmcl sorry for the late reply. we have made some quick and dirty fix for above issues, I will share the new spout to you via email so you can do a quick comparison. Now it seems working for our project. Please help review and let us know your comments/concern on the fix.
    
    One customer of us who also use the spout they found some other issues:
    1.	Work load is not distributed to all spout tasks(as per the storm topology) 
    2.	No progress on some partitions (as per the log)
    3.	No commit on some partitions (as per the log)
    I will start looking into that tomorrow once I get the detail log file. Also if you have any clue on this please kindly advise.



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54768912
  
    --- Diff: pom.xml ---
    @@ -836,14 +839,39 @@
                     <scope>test</scope>
                 </dependency>
                 <dependency>
    -              <groupId>org.apache.calcite</groupId>
    -              <artifactId>calcite-core</artifactId>
    -              <version>${calcite.version}</version>
    +                <groupId>org.apache.calcite</groupId>
    +                <artifactId>calcite-core</artifactId>
    +                <version>${calcite.version}</version>
    +            </dependency>
    +            <dependency>
    +                <groupId>com.fasterxml.jackson.core</groupId>
    +                <artifactId>jackson-databind</artifactId>
    +                <version>${jackson.version}</version>
    +            </dependency>
    +            <dependency>
    +                <groupId>org.apache.kafka</groupId>
    +                <artifactId>${kafka.artifact.id}</artifactId>
    +                <version>${kafka.version}</version>
    +                <exclusions>
    +                    <exclusion>
    +                        <groupId>org.apache.zookeeper</groupId>
    +                        <artifactId>zookeeper</artifactId>
    +                    </exclusion>
    +                    <exclusion>
    +                        <groupId>log4j</groupId>
    +                        <artifactId>log4j</artifactId>
    +                    </exclusion>
    +                    <exclusion>
    +                        <groupId>org.slf4j</groupId>
    +                        <artifactId>slf4j-log4j12</artifactId>
    +                    </exclusion>
    +                </exclusions>
                 </dependency>
                 <dependency>
    -              <groupId>com.fasterxml.jackson.core</groupId>
    -              <artifactId>jackson-databind</artifactId>
    -              <version>${jackson.version}</version>
    +                <groupId>org.apache.kafka</groupId>
    +                <artifactId>kafka-clients</artifactId>
    +                <version>${kafka.version}</version>
    +                <scope>provided</scope>
    --- End diff --
    
    Minor nit.
    I would prefer to see the `provided` moved out of `dependencyManagement`, and into the `external/storm-kafka*/pom.xml` file


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by lujinhong <gi...@git.apache.org>.
Github user lujinhong commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-187582981
  
    +1 on trident spout


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-221928986
  
    @jianbzhou looking at it.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-213522367
  
    @jianbzhou done!  Please see https://github.com/apache/storm/pull/1357. Thanks once again.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54825460
  
    --- Diff: external/storm-kafka-new-consumer-api/pom.xml ---
    @@ -0,0 +1,91 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!--/**
    +        * Licensed to the Apache Software Foundation (ASF) under one
    +        * or more contributor license agreements.  See the NOTICE file
    +        * distributed with this work for additional information
    +        * regarding copyright ownership.  The ASF licenses this file
    +        * to you 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.
    +        */-->
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +    <modelVersion>4.0.0</modelVersion>
    +
    +    <parent>
    +        <artifactId>storm</artifactId>
    +        <groupId>org.apache.storm</groupId>
    +        <version>2.0.0-SNAPSHOT</version>
    +        <relativePath>../../pom.xml</relativePath>
    +    </parent>
    +
    +    <artifactId>storm-kafka-new-consumer-api</artifactId>
    +    <name>storm-kafka-new-consumer-api</name>
    +
    +    <packaging>jar</packaging>
    +
    +    <developers>
    +        <developer>
    +            <id>hmcl</id>
    +            <name>Hugo Louro</name>
    +            <email>hmclouro@gmail.com</email>
    +        </developer>
    +    </developers>
    +
    +    <dependencies>
    +        <!--parent module dependency-->
    +        <dependency>
    +            <groupId>org.apache.storm</groupId>
    +            <artifactId>storm-core</artifactId>
    +            <version>${project.version}</version>
    +            <scope>provided</scope>
    +        </dependency>
    +        <!--kafka libraries-->
    +        <dependency>
    +            <groupId>org.apache.kafka</groupId>
    +            <artifactId>${kafka.artifact.id}</artifactId>
    +            <scope>provided</scope>
    +        </dependency>
    --- End diff --
    
    Removed


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56390890
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    Yes that is the behavior of original kafka spout. The buffer size is equal to number of records fetched in single call to kafka broker. Since next fetch call is made only when the whole buffer has been emitted, there is a cap on the number of messages pending in memory. I think @hmcl  has tried to enforce that cap using Batch poll strategy. apart from that, two spouts are very different. 
    
    In this spout, Batch poll strategy essentially means that spout won't move forward till all the polled records have been acked and committed. So even if only one message has not been acked, spout won't fetch more data. That will result in very poor throughput. 
    
    The key difference between the two spouts is that older blocks till the whole buffer is emitted. Newer spout blocks till the whole buffer has been acked and committed.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-221465746
  
    @hmcl , sorry for the late reply, i was on leave and just now i send the updated spout to you, pls help review. Below is the major changes:
    1. In poll method, change numUncommittedOffsets < kafkaSpoutConfig.getMaxUncommittedOffsets()
    to emitted.size() < kafkaSpoutConfig.getMaxUncommittedOffsets();
    2. In method doSeekRetriableTopicPartitions, seems your code is contradicted with the comment, i changed 
    **from:**
        else {
              kafkaConsumer.seekToEnd(rtp);    // Seek to last committed offset
         }
    **To:** 
                  else {
                        //  kafkaConsumer.seekToEnd(rtp);    // Seek to last committed offset
                        OffsetAndMetadata commitOffset = kafkaConsumer.committed(rtp);
                        kafkaConsumer.seek(rtp, commitOffset.offset());  // Seek to last committed offset
                    }
    
    3. in ack method, we found acked.get(msgId.getTopicPartition()) might return null so we add some defensive validation - possibly due to kafka consumer rebalance, the partition doesn't belongs to this spout anymore
    4. in OffsetEntry.add method, we add one condition, only add the message when condition is met -             if (msgId.offset() > committedOffset). This change was also applied in method doSeekRetriableTopicPartitions.
    
    



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53723835
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    --- End diff --
    
    OK


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54128129
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    +    }
    +
    +    private void removeFromBlacklist(org.apache.storm.kafka.spout.MessageId msgId) {
    +        blackList.remove(msgId);
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            addAckedTuples(tp, msgId);
    +            // Removed acked tuples from the emittedTuples data structure
    +            emittedTuples.remove(msgId);
    +            // if this acked msg is a retry, remove it from failed data structure
    +            removeFromFailed(tp, msgId);
    +        }
    +    }
    +
    +    private void addAckedTuples(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (!acked.containsKey(tp)) {
    +                acked.put(tp, new OffsetEntry(tp));
    +            }
    +            acked.get(tp).add(msgId);
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            final TopicPartition tp = msgId.getTopicPartition();
    +            // limit to max number of retries
    +            if (msgId.numFails() >= maxRetries) {
    +                LOG.debug("Reached the maximum number of retries. Adding [{]} to list of messages to be committed to kafka", msgId);
    +                ack(msgId);
    +                removeFromFailed(tp, msgId);
    +            } else {
    +                addToFailed(tp, msgId);
    +            }
    +        }
    +    }
    +
    +    private void addToFailed(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        if (!failed.containsKey(tp)) {
    +            failed.put(tp, new HashSet<org.apache.storm.kafka.spout.MessageId>());
    +        }
    +        final Set<org.apache.storm.kafka.spout.MessageId> msgIds = failed.get(tp);
    +        if (msgIds.contains(msgId)) {      // do this to update the counter of the message
    +            msgIds.remove(msgId);
    +        }
    +        msgId.incrementNumFails();        // increment number of failures counter
    +        msgIds.add(msgId);
    +    }
    +
    +    private void removeFromFailed(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        if (failed.containsKey(tp)) {
    +            final Set<org.apache.storm.kafka.spout.MessageId> msgIds = failed.get(tp);
    +            msgIds.remove(msgId);
    +            if (msgIds.isEmpty()) {
    +                failed.remove(tp);
    +            }
    +            LOG.debug("Removing from failed list [{}, {}]", tp, msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        commitAckedTuples();
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            commitAckedTuples();
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        declarer.declareStream(kafkaSpoutStream.getStreamId(), kafkaSpoutStream.getOutputFields());
    +    }
    +
    +    // ====== Private helper methods ======
    +
    +    private void commitAckedTuples() {
    +        final Map<TopicPartition, OffsetAndMetadata> toCommitOffsets = new HashMap<>();
    +
    +        LOG.debug("Committing acked offsets to Kafka");
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            for (TopicPartition tp : acked.keySet()) {
    +                final OffsetEntry offsetEntry = acked.get(tp);
    +                OffsetAndMetadata offsetAndMetadata = offsetEntry.findOffsetToCommit();
    +                if (offsetAndMetadata != null) {
    +                    toCommitOffsets.put(tp, offsetAndMetadata);
    +                }
    +            }
    +
    +            if (!toCommitOffsets.isEmpty()) {
    +                kafkaConsumer.commitSync(toCommitOffsets);
    +                LOG.debug("Offsets successfully committed to Kafka [{]}", toCommitOffsets);
    +            }
    +
    +            // Instead of iterating again, we could commit each TopicPartition in the prior loop,
    +            // but the multiple networks calls should be more expensive than iteration twice over a small loop
    +            for (TopicPartition tp : acked.keySet()) {
    +                OffsetEntry offsetEntry = acked.get(tp);
    +                offsetEntry.updateAckedState(toCommitOffsets.get(tp));
    +                updateAckedState(tp, offsetEntry);
    +            }
    +        } catch (Exception e) {
    +            LOG.error("Exception occurred while committing to Kafka offsets for acked tuples ", e);
    +        } finally {
    +            commit = false;
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    private void updateAckedState(TopicPartition tp, OffsetEntry offsetEntry) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (offsetEntry.isEmpty()) {
    +                acked.remove(tp);
    +            }
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======= Offsets Commit Management ==========
    +
    +    private static class OffsetComparator implements Comparator<org.apache.storm.kafka.spout.MessageId> {
    +        public int compare(org.apache.storm.kafka.spout.MessageId m1, org.apache.storm.kafka.spout.MessageId m2) {
    +            return m1.offset() < m2.offset() ? -1 : m1.offset() == m2.offset() ? 0 : 1;
    +        }
    +    }
    +
    +    /** This class is not thread safe */
    +    // Although this class is called by multiple (2) threads, all the calling methods are properly synchronized
    +    private class OffsetEntry {
    +        private long committedOffset;          // last offset committed to Kafka
    +        private long toCommitOffset;                // last offset to be committed in the next commit operation
    +        private final Set<org.apache.storm.kafka.spout.MessageId> ackedMsgs = new TreeSet<>(OFFSET_COMPARATOR);     // sort messages by ascending order of offset
    +        private Set<org.apache.storm.kafka.spout.MessageId> toCommitMsgs = new TreeSet<>(OFFSET_COMPARATOR);        // Messages that contain the offsets to be committed in the next commit operation
    +
    +        public OffsetEntry(TopicPartition tp) {
    +            OffsetAndMetadata committed = kafkaConsumer.committed(tp);
    +            committedOffset = committed == null ? -1 : committed.offset();
    +            LOG.debug("Created OffsetEntry for [topic-partition={}, last-committed-offset={}]", tp, committedOffset);
    +        }
    +
    +        public void add(org.apache.storm.kafka.spout.MessageId msgId) {          // O(Log N)
    +            ackedMsgs.add(msgId);
    +        }
    +
    +        /**
    +         * This method has side effects. The method updateAckedState should be called after this method.
    +         */
    +        public OffsetAndMetadata findOffsetToCommit() {
    +            long currOffset;
    +            OffsetAndMetadata offsetAndMetadata = null;
    +            toCommitMsgs = new TreeSet<>(OFFSET_COMPARATOR);
    +            toCommitOffset = committedOffset;
    +            org.apache.storm.kafka.spout.MessageId toCommitMsg = null;
    +
    +            for (org.apache.storm.kafka.spout.MessageId ackedMsg : ackedMsgs) {  // for K matching messages complexity is K*(Log*N). K <= N
    +                if ((currOffset = ackedMsg.offset()) == toCommitOffset || currOffset == toCommitOffset + 1) {    // found the next offset to commit
    +                    toCommitMsgs.add(ackedMsg);
    +                    toCommitMsg = ackedMsg;
    +                    toCommitOffset = currOffset;
    +                } else if (ackedMsg.offset() > toCommitOffset + 1) {    // offset found is not continuous to the offsets listed to go in the next commit, so stop search
    +                    break;
    +                } else {
    +                    LOG.debug("Unexpected offset found [{]}. {}", ackedMsg.offset(), toString());
    +                    break;
    +                }
    +            }
    +
    +            if (!toCommitMsgs.isEmpty()) {
    +                offsetAndMetadata = new OffsetAndMetadata(toCommitOffset, toCommitMsg.getMetadata(Thread.currentThread()));
    +                LOG.debug("Last offset to be committed in the next commit call: [{]}", offsetAndMetadata.offset());
    +                LOG.trace(toString());
    +            } else {
    +                LOG.debug("No offsets found ready to commit");
    +                LOG.trace(toString());
    +            }
    +            return offsetAndMetadata;
    +        }
    +
    +        /**
    +         * This method has side effects and should be called after findOffsetToCommit
    +         */
    +        public void updateAckedState(OffsetAndMetadata offsetAndMetadata) {
    +            if (offsetAndMetadata != null) {
    +                committedOffset = offsetAndMetadata.offset();
    +                toCommitMsgs = new TreeSet<>(OFFSET_COMPARATOR);
    +                ackedMsgs.removeAll(toCommitMsgs);
    +            }
    +        }
    +
    +        public boolean isEmpty() {
    +            return ackedMsgs.isEmpty();
    +        }
    +
    +        @Override
    +        public String toString() {
    +            return "OffsetEntry{" +
    +                    "committedOffset=" + committedOffset +
    +                    ", toCommitOffset=" + toCommitOffset +
    +                    ", ackedMsgs=" + ackedMsgs +
    +                    ", toCommitMsgs=" + toCommitMsgs +
    +                    '}';
    +        }
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            commitAckedTuples();  // commit acked records,
    +            copyEmittedTuplesToBlackList(); // all the tuples that are in traffic when the rebalance occurs will be added to black list to avoid duplication
    +            clearFailed();   // remove all failed tuples form list to avoid duplication
    +            clearEmittedTuples();    // clear emitted tuples
    --- End diff --
    
    I am not familiar with the Kafka API, if it is truly single threaded too, then we do not need any locking.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55957034
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutStreams.java ---
    @@ -0,0 +1,142 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.utils.Utils;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * Represents the output streams associated with each topic, and provides a public API to
    + * declare output streams and emmit tuples, on the appropriate stream, for all the topics specified.
    + */
    +public class KafkaSpoutStreams implements Serializable {
    +    private final Map<String, KafkaSpoutStream> topicToStream;
    +
    +    private KafkaSpoutStreams(Builder builder) {
    +        this.topicToStream = builder.topicToStream;
    +    }
    +
    +    /**
    +     * @param topic the topic for which to get output fields
    +     * @return the output fields declared
    +     */
    +    public Fields getOutputFields(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getOutputFields();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @param topic the topic to for which to get the stream id
    +     * @return the id of the stream to where the tuples are emitted
    +     */
    +    public String getStreamId(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getStreamId();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @return list of topics subscribed and emitting tuples to a stream as configured by {@link KafkaSpoutStream}
    +     */
    +    public List<String> getTopics() {
    +        return new ArrayList<>(topicToStream.keySet());
    +    }
    +
    +    void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        for (KafkaSpoutStream stream : topicToStream.values()) {
    +            declarer.declareStream(stream.getStreamId(), stream.getOutputFields());
    +        }
    +    }
    +
    +    void emit(SpoutOutputCollector collector, MessageId messageId) {
    +        collector.emit(getStreamId(messageId.topic()), messageId.getTuple(), messageId);
    --- End diff --
    
    older consumer didn't call the fetch again till the _waitingToEmit queue became empty. And records were only stored in _waitingToEmit. Here however, they are also stored as a part of message id. The message id in turn is stored in two data structures - 
    1. pending map of spout - It has an upper ceiling of topology.max.spout.pending * sizeof(message id) bytes
    2. acked map - This stores the message ids that have been acked. It is cleared on a time basis and doesn't have any upper ceiling on number of entries per TopicPartition.
    @hmcl @harshach Please let me know if above two are valid inferences. Nonetheless, I am +1 on resolving this discussion later without blocking the PR. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53697785
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    --- End diff --
    
    Can we change TupleBuilder to return a List and not a Values?  Values is a convinenece class. The actual APIs all take Lists and I would like to keep it the same here if possible.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56232318
  
    --- Diff: external/storm-kafka-client/pom.xml ---
    @@ -0,0 +1,86 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!--/**
    +        * Licensed to the Apache Software Foundation (ASF) under one
    +        * or more contributor license agreements.  See the NOTICE file
    +        * distributed with this work for additional information
    +        * regarding copyright ownership.  The ASF licenses this file
    +        * to you 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.
    +        */-->
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +    <modelVersion>4.0.0</modelVersion>
    +
    +    <parent>
    +        <artifactId>storm</artifactId>
    +        <groupId>org.apache.storm</groupId>
    +        <version>2.0.0-SNAPSHOT</version>
    +        <relativePath>../../pom.xml</relativePath>
    +    </parent>
    +
    +    <artifactId>storm-kafka-client</artifactId>
    --- End diff --
    
    So that means we would leave everything in its own module, as it currently is, right? I am +1 for this approach.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54661378
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig.toString());
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if(commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.expired();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +
    +                    kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                    LOG.debug("Emitted tuple [{}] for record [{}]", tuple, record);
    +                }
    +            }
    +        }
    +    }
    +
    +    // ======== Ack =======
    +    @Override
    +    public void ack(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            acked.get(tp).add(msgId);
    +            LOG.debug("Adding acked message to [{}] to list of messages to be committed to Kafka", msgId);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.debug("Retried tuple with message id [{}]", messageId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message being marked as acked.");
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        if(!consumerAutoCommitMode) {
    +            commitOffsetsForAckedTuples();
    +        }
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            if(!consumerAutoCommitMode) {
    +                commitOffsetsForAckedTuples();
    +            }
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    // TODO must declare multiple output streams
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        kafkaSpoutStreams.declareOutputFields(declarer);
    +    }
    +
    +    // ====== Private helper methods ======
    +
    +    private void commitOffsetsForAckedTuples() {
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +
    +        try {
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetAndMetadata offsetAndMetadata = tpOffset.getValue().findNextCommitOffset();
    +                if (offsetAndMetadata != null) {
    +                    nextCommitOffsets.put(tpOffset.getKey(), offsetAndMetadata);
    +                }
    +            }
    +
    +            if (!nextCommitOffsets.isEmpty()) {
    +                kafkaConsumer.commitSync(nextCommitOffsets);
    +                LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +                // Instead of iterating again, we could commit and update state for each TopicPartition in the prior loop,
    +                // but the multiple network calls should be more expensive than iterating twice over a small loop
    +                for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                    final OffsetEntry offsetEntry = tpOffset.getValue();
    +                    offsetEntry.updateAckedState(nextCommitOffsets.get(tpOffset.getKey()));
    +                }
    +            } else {
    +                LOG.trace("No offsets to commit. {}", toString());
    +            }
    +        } catch (Exception e) {
    +            LOG.error("Exception occurred while committing to Kafka offsets of acked tuples", e);
    --- End diff --
    
    Done. Catch removed.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54661265
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    --- End diff --
    
    Done


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54661341
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig.toString());
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if(commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.expired();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +
    +                    kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                    LOG.debug("Emitted tuple [{}] for record [{}]", tuple, record);
    +                }
    +            }
    +        }
    +    }
    +
    +    // ======== Ack =======
    +    @Override
    +    public void ack(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            acked.get(tp).add(msgId);
    +            LOG.debug("Adding acked message to [{}] to list of messages to be committed to Kafka", msgId);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.debug("Retried tuple with message id [{}]", messageId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message being marked as acked.");
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        if(!consumerAutoCommitMode) {
    +            commitOffsetsForAckedTuples();
    +        }
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            if(!consumerAutoCommitMode) {
    +                commitOffsetsForAckedTuples();
    +            }
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    // TODO must declare multiple output streams
    --- End diff --
    
    It's already in place. It was just a note that I forgot to remove.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-203631616
  
    CI Failure looks unrelated, If no one has any other comments by tomorrow morning I think we should merge this in.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r57803200
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutRetryService.java ---
    @@ -0,0 +1,69 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.TopicPartition;
    +
    +import java.io.Serializable;
    +import java.util.Collection;
    +import java.util.Set;
    +
    +public interface KafkaSpoutRetryService extends Serializable {
    +    /**
    +     * Schedules this {@link KafkaSpoutMessageId} if not yet scheduled, or updates retry time if it has already been scheduled.
    +     * @param msgId message to schedule for retrial
    +     */
    +    void schedule(KafkaSpoutMessageId msgId);
    +
    +    /**
    +     * Removes a message from the list of messages scheduled for retrial
    +     * @param msgId message to remove from retrial
    +     */
    +    boolean remove(KafkaSpoutMessageId msgId);
    +
    +    /**
    +     * Removes all the messages whose {@link TopicPartition} does NOT belong to the specified {@code Collection<TopicPartition>}.
    +     * All messages that come from a {@link TopicPartition} existing in the collection will be kept.
    +     * This method is useful to cleanup state following partition rebalance.
    +     * @param topicPartitions Collection of {@link TopicPartition} for which to keep messages
    +     * @return true if at least one message was removed, false otherwise
    +     */
    +    boolean remove(Collection<TopicPartition> topicPartitions);
    +
    +    /**
    +     * @return set of topic partitions that have offsets that are ready to be retried, i.e.,
    +     * for which a tuple has failed and has retry time less than current time
    +     */
    +    Set<TopicPartition> retriableTopicPartitions();
    +
    +    /**
    +     * Checks if a specific failed {@link KafkaSpoutMessageId} is is ready to be retried,
    +     * i.e is scheduled and has retry time that is less than current time.
    +     * @return true if message is ready to be retried, false otherwise
    +     */
    +    boolean retry(KafkaSpoutMessageId msgId);
    --- End diff --
    
    Renamed to `isReady()` to be consistent to `isScheduled()`


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-187425242
  
    Looking through the code I think we are keeping track of way too much state.  In some places we cannot help it, but in most places we can probably rely on storm to keep track of the state for us.
    
    First of all we probably don't need failed, emittedTuples, nor blacklist.  Just put the information we need in the message id and let storm keep track of it for you.  The Message ID that you pass in never leaves the spout and is never read by storm, except to check if it is null or not, so it become very convenient in implementing a best effort reply cache.
    
    ```
    public class MessageId {
        private final TopicPartition topicPart;
        private final long offset;
        private final long epoc;
        private final List<Object> values;
        private int numFails;
    
       ...
    }
    ```
    
    To implement blacklisting instead of clearing state, and shifting things around, just increment an epoc counter for the entire spout.  Then in the logic for fail (where we do the replay) we would have something like.
    
    ```
    public void fail(Object mid) {
        MessageId id = (MesageId)mid;
        id.incNumFails();
        if (id.getEpoc() == currentEpic.get()) {
          if (id.getNumFails > CUT_OFF) {
            markAsDone(id);
          } else {
            collector.emit(id.getValues(), id);
          }
        }
    }
    ```
    
    And ack would be similar
    
    
    ```
    public void ack(Object mid) {
        MessageId id = (MesageId)mid;
        if (id.getEpoc() == currentEpic.get()) {
          markAsDone(id);
        }
    }
    ```
    
    What do you think?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56524564
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaRecordTupleBuilder.java ---
    @@ -0,0 +1,44 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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,
    + *   See the License for the specific language governing permissions and
    + *   limitations under the License.
    + */
    +package org.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.tuple.Values;
    +
    +import java.util.List;
    +
    +public class KafkaRecordTupleBuilder<K, V> implements KafkaSpoutTupleBuilder<K, V> {
    +    @Override
    +    public List<Object> buildTuple(final ConsumerRecord<K, V> consumerRecord, KafkaSpoutStreams kafkaSpoutStreams) {
    --- End diff --
    
    @revans2 current code solves the use cases. My suggestion was purely for the API exposed to end user. To me, having *tuple construction* and *tuple schema* in one interface felt more natural. That is how it is in older kafka spout
    ```
    public interface MultiScheme extends Serializable {
      public Iterable<List<Object>> deserialize(byte[] ser);
      public Fields getOutputFields();
    }
    ```
    There is no additional functionality per se. It just simplifies the API in my opinion.
    
    I would vote for making KafkaSpoutTupleBuilder non-singleton. Since if you have different schema of output fields for each topic, you would also *build the tuple differently* for each topic. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by knusbaum <gi...@git.apache.org>.
Github user knusbaum commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53668951
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    +    }
    +
    +    private void removeFromBlacklist(org.apache.storm.kafka.spout.MessageId msgId) {
    --- End diff --
    
    Same here. Can remove.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54460535
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig.toString());
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if(commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.expired();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +
    +                    kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                    LOG.debug("Emitted tuple [{}] for record [{}]", tuple, record);
    +                }
    +            }
    +        }
    +    }
    +
    +    // ======== Ack =======
    +    @Override
    +    public void ack(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            acked.get(tp).add(msgId);
    +            LOG.debug("Adding acked message to [{}] to list of messages to be committed to Kafka", msgId);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.debug("Retried tuple with message id [{}]", messageId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message being marked as acked.");
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        if(!consumerAutoCommitMode) {
    +            commitOffsetsForAckedTuples();
    +        }
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            if(!consumerAutoCommitMode) {
    +                commitOffsetsForAckedTuples();
    +            }
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    // TODO must declare multiple output streams
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        kafkaSpoutStreams.declareOutputFields(declarer);
    +    }
    +
    +    // ====== Private helper methods ======
    +
    +    private void commitOffsetsForAckedTuples() {
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +
    +        try {
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetAndMetadata offsetAndMetadata = tpOffset.getValue().findNextCommitOffset();
    +                if (offsetAndMetadata != null) {
    +                    nextCommitOffsets.put(tpOffset.getKey(), offsetAndMetadata);
    +                }
    +            }
    +
    +            if (!nextCommitOffsets.isEmpty()) {
    +                kafkaConsumer.commitSync(nextCommitOffsets);
    +                LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +                // Instead of iterating again, we could commit and update state for each TopicPartition in the prior loop,
    +                // but the multiple network calls should be more expensive than iterating twice over a small loop
    +                for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                    final OffsetEntry offsetEntry = tpOffset.getValue();
    +                    offsetEntry.updateAckedState(nextCommitOffsets.get(tpOffset.getKey()));
    +                }
    +            } else {
    +                LOG.trace("No offsets to commit. {}", toString());
    +            }
    +        } catch (Exception e) {
    +            LOG.error("Exception occurred while committing to Kafka offsets of acked tuples", e);
    +        }
    +    }
    +
    +    @Override
    +    public String toString() {
    +        return "{acked=" + acked + "} ";
    +    }
    +
    +    // ======= Offsets Commit Management ==========
    +
    +    private static class OffsetComparator implements Comparator<MessageId> {
    +        public int compare(MessageId m1, MessageId m2) {
    +            return m1.offset() < m2.offset() ? -1 : m1.offset() == m2.offset() ? 0 : 1;
    +        }
    +    }
    +
    +    /** This class is not thread safe */
    +    private class OffsetEntry {
    +        private final TopicPartition tp;
    +        private long committedOffset;               // last offset committed to Kafka, or initial fetching offset (initial value depends on offset strategy. See KafkaSpoutConsumerRebalanceListener)
    +        private final NavigableSet<MessageId> ackedMsgs = new TreeSet<>(OFFSET_COMPARATOR);     // acked messages sorted by ascending order of offset
    +
    +        public OffsetEntry(TopicPartition tp, long committedOffset) {
    +            this.tp = tp;
    +            this.committedOffset = committedOffset;
    +            LOG.debug("Created OffsetEntry for [topic-partition={}, committed-or-initial-fetch-offset={}]", tp, committedOffset);
    +        }
    +
    +        public void add(MessageId msgId) {          // O(Log N)
    +            ackedMsgs.add(msgId);
    +        }
    +
    +        /**
    +         * This method has side effects. The method updateAckedState should be called after this method.
    --- End diff --
    
    Actually where are the side effects?  I don't see any member variable being modified by the method.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56387329
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,517 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.PollStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<KafkaSpoutMessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private transient KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaSpoutTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer commitTimer;                                    // timer == null for auto commit mode
    +    private transient Timer logTimer;
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +    private transient long numUncommittedOffsets;   // Number of offsets that have been polled and emitted but not yet been committed
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +    private transient PollStrategy pollStrategy;
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaSpoutTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +        numUncommittedOffsets = 0;
    +        logTimer = new Timer(500, Math.min(1000, kafkaSpoutConfig.getOffsetsCommitPeriodMs()/2), TimeUnit.MILLISECONDS);
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        pollStrategy = kafkaSpoutConfig.getPollStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            commitTimer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitPeriodMs(), TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        LOG.info("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            if (!consumerAutoCommitMode) {
    +                acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            }
    +
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // By default polling starts at the last committed offset. +1 to point fetch to the first uncommitted offset.
    +                    fetchOffset = committedOffset.offset() + 1;
    +                    kafkaConsumer.seek(tp, fetchOffset);
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else if (poll()) {
    +                emitTuples(pollKafkaBroker());
    +            } else if (logTimer.isExpiredResetOnTrue()){
    +                log();
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private void log() {
    +        switch(pollStrategy) {
    +            case STREAM:
    +                LOG.trace("Reached the maximum number number of uncommitted records [{}]. " +
    +                        "No more polls will occur until a sequence of commits sets the count under the [{}] threshold ",
    +                        numUncommittedOffsets, kafkaSpoutConfig.getMaxUncommittedOffsets());
    +                break;
    +            case BATCH:
    +                LOG.trace("No more polls will occur until the last batch completes. [{}] emitted tuples pending", numUncommittedOffsets);
    +                break;
    +            default:
    +                throw new IllegalStateException("No implementation defined for polling strategy " + pollStrategy);
    +        }
    +
    +    }
    +
    +    // always poll in auto commit mode because no state is kept and therefore there is no need to set an upper limit in memory
    +    private boolean poll()  {
    +        switch(pollStrategy) {
    +            case STREAM:
    +                return consumerAutoCommitMode || numUncommittedOffsets < kafkaSpoutConfig.getMaxUncommittedOffsets();
    +            case BATCH:
    +                return consumerAutoCommitMode || numUncommittedOffsets <= 0;
    +            default:
    +                throw new IllegalStateException("No implementation defined for polling strategy " + pollStrategy);
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && commitTimer.isExpiredResetOnTrue();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> pollKafkaBroker() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        final int numPolledRecords = consumerRecords.count();
    +        numUncommittedOffsets+= numPolledRecords;
    +        LOG.debug("Polled [{}] records from Kafka. NumUncommittedOffsets=[{}]", numPolledRecords, numUncommittedOffsets);
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                final List<Object> tuple = tupleBuilder.buildTuple(record, kafkaSpoutStreams);
    +                final KafkaSpoutMessageId messageId = new KafkaSpoutMessageId(record, tuple);
    +
    +                kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                LOG.trace("Emitted tuple [{}] for record [{}]", tuple, record);
    +            }
    +        }
    +    }
    +
    +    private void commitOffsetsForAckedTuples() {
    +        // Find offsets that are ready to be committed for every topic partition
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +        for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +            final OffsetAndMetadata nextCommitOffset = tpOffset.getValue().findNextCommitOffset();
    +            if (nextCommitOffset != null) {
    +                nextCommitOffsets.put(tpOffset.getKey(), nextCommitOffset);
    +            }
    +        }
    +
    +        // Commit offsets that are ready to be committed for every topic partition
    +        if (!nextCommitOffsets.isEmpty()) {
    +            kafkaConsumer.commitSync(nextCommitOffsets);
    +            LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +            // Instead of iterating again, it would be possible to commit and update the state for each TopicPartition
    +            // in the prior loop, but the multiple network calls should be more expensive than iterating twice over a small loop
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetEntry offsetEntry = tpOffset.getValue();
    +                offsetEntry.commit(nextCommitOffsets.get(tpOffset.getKey()));
    +            }
    +        } else {
    +            LOG.trace("No offsets to commit. {}", this);
    +        }
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            final KafkaSpoutMessageId msgId = (KafkaSpoutMessageId) messageId;
    +            acked.get(msgId.getTopicPartition()).add(msgId);
    --- End diff --
    
    The limit is numUncommittedOffsets.  acked will never be larger than that.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm issue #1131: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the issue:

    https://github.com/apache/storm/pull/1131
  
    @hmcl and all, the new spout is fit for the at least once semantics and works fine for us, thanks a lot! Very recently one of our key customers asked to use a at most once implementation. Do we have any plan to have a at-most-once implementation? They set the topology.acker.executors=0 and found the spout is not working. Could you please help to evaluate - 1, will we implement this? 2. roughly how long time needed? Thanks
    
    **Requirement from customer** - \u201ctopology.acker.executors\u201d is a Storm parameter, which refers to at-least-once when it\u2019s not 0, and at-most-once if it\u2019s 0. We want to know do we have a at-most-once implementation?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm issue #1131: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the issue:

    https://github.com/apache/storm/pull/1131
  
    @hmcl, today we found one NullpointerException and i applied a fix as below:
    
    In method doSeekRetriableTopicPartitions, if one partition was never committed before one message is failed back, we will encounter below issue. Could you please help review and let me know if this fix is oaky?
    
    **Code change is as below -from**
    
    //  kafkaConsumer.seekToEnd(rtp);    // Seek to last committed offset
    OffsetAndMetadata commitOffset = kafkaConsumer.committed(rtp);
    kafkaConsumer.seek(rtp, **commitOffset.offset()**);  // Seek to last committed offset
    
    **to**
    //  kafkaConsumer.seekToEnd(rtp);    // Seek to last committed offset
    OffsetAndMetadata commitOffset = kafkaConsumer.committed(rtp);
    if(commitOffset != null){
        kafkaConsumer.seek(rtp, commitOffset.offset());  // Seek to last committed offset
    } else{
        LOG.info("In doSeekRetriableTopicPartitions, topic partition is {}, no offset was committed for this partition, will seek back to the initial offset {}", rtp, offsetEntry.initialFetchOffset);
        kafkaConsumer.seek(rtp, offsetEntry.initialFetchOffset); //no offset committed for this partition, seek to the original fetch offset
    }



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm issue #1131: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the issue:

    https://github.com/apache/storm/pull/1131
  
    @erikdw @hmcl , sorry for the late reply.
    include only part of my fixes is okie for me, we hope finally the community version has the proper fixes so we can use the community version. I don't have the access, could you please help me create the Jira and pull request(Pls feel free to let me know if you need any help from me)? many thanks. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm issue #1131: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the issue:

    https://github.com/apache/storm/pull/1131
  
    Thanks erikdw! sorry i was out of office in the last couple of days. I just created a jira account and will create a jira ticket asap and assign to hmcl. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56238105
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,517 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.PollStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<KafkaSpoutMessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private transient KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaSpoutTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer commitTimer;                                    // timer == null for auto commit mode
    +    private transient Timer logTimer;
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +    private transient long numUncommittedOffsets;   // Number of offsets that have been polled and emitted but not yet been committed
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +    private transient PollStrategy pollStrategy;
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaSpoutTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +        numUncommittedOffsets = 0;
    +        logTimer = new Timer(500, Math.min(1000, kafkaSpoutConfig.getOffsetsCommitPeriodMs()/2), TimeUnit.MILLISECONDS);
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        pollStrategy = kafkaSpoutConfig.getPollStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            commitTimer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitPeriodMs(), TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        LOG.info("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            if (!consumerAutoCommitMode) {
    +                acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            }
    +
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // By default polling starts at the last committed offset. +1 to point fetch to the first uncommitted offset.
    +                    fetchOffset = committedOffset.offset() + 1;
    +                    kafkaConsumer.seek(tp, fetchOffset);
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else if (poll()) {
    --- End diff --
    
    OK I misunderstood what log was doing.  It is saying what state we are in with trace logging.  I am not sure we really need that.  I would rather see metrics about the values we care about instead of trace logging.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by knusbaum <gi...@git.apache.org>.
Github user knusbaum commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53680233
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    +    }
    +
    +    private void removeFromBlacklist(org.apache.storm.kafka.spout.MessageId msgId) {
    +        blackList.remove(msgId);
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            addAckedTuples(tp, msgId);
    +            // Removed acked tuples from the emittedTuples data structure
    +            emittedTuples.remove(msgId);
    +            // if this acked msg is a retry, remove it from failed data structure
    +            removeFromFailed(tp, msgId);
    +        }
    +    }
    +
    +    private void addAckedTuples(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    --- End diff --
    
    `ackCommitLock` can be replaced with a synchronized block or method. There's no reason in this class to do explicit lock/unlock operations.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53659580
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaRecordTupleBuilder.java ---
    @@ -0,0 +1,34 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.storm.tuple.Values;
    +
    +public class KafkaRecordTupleBuilder<K,V> implements org.apache.storm.kafka.spout.KafkaTupleBuilder<K, V> {
    --- End diff --
    
    @knusbaum agree. When I was moving stuff around to organize the packages this happened. It has already been cleaned up and will be in the next patch.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55321087
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutStreams.java ---
    @@ -0,0 +1,142 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.utils.Utils;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * Represents the output streams associated with each topic, and provides a public API to
    + * declare output streams and emmit tuples, on the appropriate stream, for all the topics specified.
    + */
    +public class KafkaSpoutStreams implements Serializable {
    +    private final Map<String, KafkaSpoutStream> topicToStream;
    +
    +    private KafkaSpoutStreams(Builder builder) {
    +        this.topicToStream = builder.topicToStream;
    +    }
    +
    +    /**
    +     * @param topic the topic for which to get output fields
    +     * @return the output fields declared
    +     */
    +    public Fields getOutputFields(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getOutputFields();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @param topic the topic to for which to get the stream id
    +     * @return the id of the stream to where the tuples are emitted
    +     */
    +    public String getStreamId(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getStreamId();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @return list of topics subscribed and emitting tuples to a stream as configured by {@link KafkaSpoutStream}
    +     */
    +    public List<String> getTopics() {
    +        return new ArrayList<>(topicToStream.keySet());
    +    }
    +
    +    void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        for (KafkaSpoutStream stream : topicToStream.values()) {
    +            declarer.declareStream(stream.getStreamId(), stream.getOutputFields());
    +        }
    +    }
    +
    +    void emit(SpoutOutputCollector collector, MessageId messageId) {
    +        collector.emit(getStreamId(messageId.topic()), messageId.getTuple(), messageId);
    --- End diff --
    
    My bad. the message id is only sent during the event logging and not otherwise. Still would it be better to declare tuple in MessageId as transient (or some other annotation) to ignore it during serialization? And of course, memory pressure will significantly increase in the spout. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53723864
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    --- End diff --
    
    DONE


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54459225
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig.toString());
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if(commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.expired();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    --- End diff --
    
    In non acking mode ack is called instantly.
    
    https://github.com/apache/storm/blob/master/storm-core/src/clj/org/apache/storm/daemon/executor.clj#L592-L595
    
    So we already support it.  Please remove the comment.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53724264
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    +    }
    +
    +    private void removeFromBlacklist(org.apache.storm.kafka.spout.MessageId msgId) {
    +        blackList.remove(msgId);
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            addAckedTuples(tp, msgId);
    +            // Removed acked tuples from the emittedTuples data structure
    +            emittedTuples.remove(msgId);
    +            // if this acked msg is a retry, remove it from failed data structure
    +            removeFromFailed(tp, msgId);
    +        }
    +    }
    +
    +    private void addAckedTuples(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (!acked.containsKey(tp)) {
    +                acked.put(tp, new OffsetEntry(tp));
    +            }
    +            acked.get(tp).add(msgId);
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            final TopicPartition tp = msgId.getTopicPartition();
    +            // limit to max number of retries
    +            if (msgId.numFails() >= maxRetries) {
    +                LOG.debug("Reached the maximum number of retries. Adding [{]} to list of messages to be committed to kafka", msgId);
    +                ack(msgId);
    +                removeFromFailed(tp, msgId);
    +            } else {
    +                addToFailed(tp, msgId);
    +            }
    +        }
    +    }
    +
    +    private void addToFailed(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        if (!failed.containsKey(tp)) {
    +            failed.put(tp, new HashSet<org.apache.storm.kafka.spout.MessageId>());
    +        }
    +        final Set<org.apache.storm.kafka.spout.MessageId> msgIds = failed.get(tp);
    +        if (msgIds.contains(msgId)) {      // do this to update the counter of the message
    +            msgIds.remove(msgId);
    +        }
    +        msgId.incrementNumFails();        // increment number of failures counter
    +        msgIds.add(msgId);
    +    }
    +
    +    private void removeFromFailed(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        if (failed.containsKey(tp)) {
    +            final Set<org.apache.storm.kafka.spout.MessageId> msgIds = failed.get(tp);
    +            msgIds.remove(msgId);
    +            if (msgIds.isEmpty()) {
    +                failed.remove(tp);
    +            }
    +            LOG.debug("Removing from failed list [{}, {}]", tp, msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        commitAckedTuples();
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            commitAckedTuples();
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        declarer.declareStream(kafkaSpoutStream.getStreamId(), kafkaSpoutStream.getOutputFields());
    +    }
    +
    +    // ====== Private helper methods ======
    +
    +    private void commitAckedTuples() {
    +        final Map<TopicPartition, OffsetAndMetadata> toCommitOffsets = new HashMap<>();
    +
    +        LOG.debug("Committing acked offsets to Kafka");
    +        // lock because ack and commit happen in different threads
    --- End diff --
    
    @revans2 if what I said in C2 is true, we do not need any locking, right?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56237896
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,517 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.PollStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<KafkaSpoutMessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private transient KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaSpoutTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer commitTimer;                                    // timer == null for auto commit mode
    +    private transient Timer logTimer;
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +    private transient long numUncommittedOffsets;   // Number of offsets that have been polled and emitted but not yet been committed
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +    private transient PollStrategy pollStrategy;
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaSpoutTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +        numUncommittedOffsets = 0;
    +        logTimer = new Timer(500, Math.min(1000, kafkaSpoutConfig.getOffsetsCommitPeriodMs()/2), TimeUnit.MILLISECONDS);
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        pollStrategy = kafkaSpoutConfig.getPollStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            commitTimer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitPeriodMs(), TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        LOG.info("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            if (!consumerAutoCommitMode) {
    +                acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            }
    +
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // By default polling starts at the last committed offset. +1 to point fetch to the first uncommitted offset.
    +                    fetchOffset = committedOffset.offset() + 1;
    +                    kafkaConsumer.seek(tp, fetchOffset);
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else if (poll()) {
    --- End diff --
    
    Minor nit. I don't think poll, commit, and logging should be mutually exclusive.  It slows things down if we don't emit anything, and it also means that we may never log if we always emit something.
    
    ```
    if (commit()) {
        commitOffsetsForAckedTuples();
    }
    
    if (poll()) {
      emitTuples(pollKafkaBroker());
    }
    
    if (logTimer.isExpiredResetOnTrue()) {
      log()
    }
    ```


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54779292
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutStream.java ---
    @@ -0,0 +1,60 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.utils.Utils;
    +
    +import java.io.Serializable;
    +
    +public class KafkaSpoutStream implements Serializable {
    +    public static final String DEFAULT_TOPIC = "DEFAULT_TOPIC_KAFKA_SPOUT_INTERNAL";
    +    private final Fields outputFields;
    +    private final String streamId;
    +    private final String topic;
    +
    +    /** Declare specified outputFields with default stream and default topic */
    +    public KafkaSpoutStream(Fields outputFields) {
    +        this(outputFields, Utils.DEFAULT_STREAM_ID);
    +    }
    +
    +    /** Declare specified outputFields with specified stream and default topic */
    +    public KafkaSpoutStream(Fields outputFields, String streamId) {
    +        this(outputFields, streamId, DEFAULT_TOPIC);
    +    }
    +
    +    /** Declare specified outputFields with specified stream and specified topic */
    +    public KafkaSpoutStream(Fields outputFields, String streamId, String topic) {
    --- End diff --
    
    I am a bit confused by having both KafkaSpoutStream(s) and KafkaSpoutConfig.topics.
    
    It feels like they really should be together in one user facing API, especially if we want to support a Pattern topic in the future.  Perhaps something that would let me do something like the following in the KafkaSpoutConfig.Builder
    
    ```
    public Builder addTopics(String... topics, Fields outputFileds);
    ```


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-213468155
  
    @jianbzhou Thanks for the catch and for suggesting a fix. I was taking a look and the best way to fix this is to replace the `TreeMap` with a `HashMap`, i.e. do `final Set tps = new HashSet<>();`. 
    `TopicPartition` implements `equals()` and `hashCode()` so if you change the line as suggested nothing else needs to be done. Ordering is not important here, so `HashMap` is indeed the most appropriate data structure here, and the one I meant to use to begin with.
    
    The appropriate way to do code contributions is to create a Pull Request and push it for review by the community. Please find the documentation on how to do so in here:
    * http://storm.apache.org/contribute/Contributing-to-Storm.html
    * https://github.com/apache/storm/blob/master/DEVELOPER.md#contribute-code
    * https://github.com/apache/storm/blob/master/DEVELOPER.md#pull-requests
    
    I am in the process of submitting a patch with an updated README and a few more things. Since this is such a small fix, I can include this fix there, and mention that the credit should go to you. However, if you with to create the pull request yourself to receive the deserved credit, please go ahead and I will review it right away.
    
    Please let me know and thanks once again. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56239098
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,517 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.PollStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<KafkaSpoutMessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private transient KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaSpoutTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer commitTimer;                                    // timer == null for auto commit mode
    +    private transient Timer logTimer;
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +    private transient long numUncommittedOffsets;   // Number of offsets that have been polled and emitted but not yet been committed
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +    private transient PollStrategy pollStrategy;
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaSpoutTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +        numUncommittedOffsets = 0;
    +        logTimer = new Timer(500, Math.min(1000, kafkaSpoutConfig.getOffsetsCommitPeriodMs()/2), TimeUnit.MILLISECONDS);
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        pollStrategy = kafkaSpoutConfig.getPollStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            commitTimer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitPeriodMs(), TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        LOG.info("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            if (!consumerAutoCommitMode) {
    +                acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            }
    +
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // By default polling starts at the last committed offset. +1 to point fetch to the first uncommitted offset.
    +                    fetchOffset = committedOffset.offset() + 1;
    +                    kafkaConsumer.seek(tp, fetchOffset);
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else if (poll()) {
    +                emitTuples(pollKafkaBroker());
    +            } else if (logTimer.isExpiredResetOnTrue()){
    +                log();
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private void log() {
    +        switch(pollStrategy) {
    +            case STREAM:
    +                LOG.trace("Reached the maximum number number of uncommitted records [{}]. " +
    +                        "No more polls will occur until a sequence of commits sets the count under the [{}] threshold ",
    +                        numUncommittedOffsets, kafkaSpoutConfig.getMaxUncommittedOffsets());
    +                break;
    +            case BATCH:
    +                LOG.trace("No more polls will occur until the last batch completes. [{}] emitted tuples pending", numUncommittedOffsets);
    +                break;
    +            default:
    +                throw new IllegalStateException("No implementation defined for polling strategy " + pollStrategy);
    +        }
    +
    +    }
    +
    +    // always poll in auto commit mode because no state is kept and therefore there is no need to set an upper limit in memory
    +    private boolean poll()  {
    +        switch(pollStrategy) {
    +            case STREAM:
    +                return consumerAutoCommitMode || numUncommittedOffsets < kafkaSpoutConfig.getMaxUncommittedOffsets();
    +            case BATCH:
    +                return consumerAutoCommitMode || numUncommittedOffsets <= 0;
    +            default:
    +                throw new IllegalStateException("No implementation defined for polling strategy " + pollStrategy);
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && commitTimer.isExpiredResetOnTrue();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> pollKafkaBroker() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        final int numPolledRecords = consumerRecords.count();
    +        numUncommittedOffsets+= numPolledRecords;
    +        LOG.debug("Polled [{}] records from Kafka. NumUncommittedOffsets=[{}]", numPolledRecords, numUncommittedOffsets);
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                final List<Object> tuple = tupleBuilder.buildTuple(record, kafkaSpoutStreams);
    +                final KafkaSpoutMessageId messageId = new KafkaSpoutMessageId(record, tuple);
    +
    +                kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                LOG.trace("Emitted tuple [{}] for record [{}]", tuple, record);
    +            }
    +        }
    +    }
    +
    +    private void commitOffsetsForAckedTuples() {
    +        // Find offsets that are ready to be committed for every topic partition
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +        for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +            final OffsetAndMetadata nextCommitOffset = tpOffset.getValue().findNextCommitOffset();
    +            if (nextCommitOffset != null) {
    +                nextCommitOffsets.put(tpOffset.getKey(), nextCommitOffset);
    +            }
    +        }
    +
    +        // Commit offsets that are ready to be committed for every topic partition
    +        if (!nextCommitOffsets.isEmpty()) {
    +            kafkaConsumer.commitSync(nextCommitOffsets);
    +            LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +            // Instead of iterating again, it would be possible to commit and update the state for each TopicPartition
    +            // in the prior loop, but the multiple network calls should be more expensive than iterating twice over a small loop
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetEntry offsetEntry = tpOffset.getValue();
    +                offsetEntry.commit(nextCommitOffsets.get(tpOffset.getKey()));
    +            }
    +        } else {
    +            LOG.trace("No offsets to commit. {}", this);
    +        }
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            final KafkaSpoutMessageId msgId = (KafkaSpoutMessageId) messageId;
    +            acked.get(msgId.getTopicPartition()).add(msgId);
    +            LOG.trace("Acked message [{}]. Messages acked and pending commit [{}]", msgId, acked);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {   // TODO poll all tuples after the failed tuple
    +        final KafkaSpoutMessageId msgId = (KafkaSpoutMessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.trace("Retried tuple with message id [{}]", msgId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message [{}] being marked as acked.", msgId);
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate / Close / Declare Outputs =======
    +
    +    @Override
    +    public void activate() {
    +        subscribeKafkaConsumer();
    +    }
    +
    +    private void subscribeKafkaConsumer() {
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called following this poll, upon partition registration
    +        kafkaConsumer.poll(0);
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        shutdown();
    +    }
    +
    +    @Override
    +    public void close() {
    +        shutdown();
    +    }
    +
    +    private void shutdown() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            if (!consumerAutoCommitMode) {
    +                commitOffsetsForAckedTuples();
    +            }
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        kafkaSpoutStreams.declareOutputFields(declarer);
    +    }
    +
    +    @Override
    +    public String toString() {
    +        return "{acked=" + acked + "} ";
    +    }
    +
    +    // ======= Offsets Commit Management ==========
    +
    +    private static class OffsetComparator implements Comparator<KafkaSpoutMessageId> {
    +        public int compare(KafkaSpoutMessageId m1, KafkaSpoutMessageId m2) {
    +            return m1.offset() < m2.offset() ? -1 : m1.offset() == m2.offset() ? 0 : 1;
    +        }
    +    }
    +
    +    /**
    +     * This class is not thread safe
    +     */
    +    private class OffsetEntry {
    +        private final TopicPartition tp;
    +        private final long initialFetchOffset;  /* First offset to be fetched. It is either set to the beginning, end, or to the first uncommitted offset.
    +                                                 * Initial value depends on offset strategy. See KafkaSpoutConsumerRebalanceListener */
    +        private long committedOffset;     // last offset committed to Kafka. Initially it is set to fetchOffset - 1
    +        private final NavigableSet<KafkaSpoutMessageId> ackedMsgs = new TreeSet<>(OFFSET_COMPARATOR);     // acked messages sorted by ascending order of offset
    +
    +        public OffsetEntry(TopicPartition tp, long initialFetchOffset) {
    +            this.tp = tp;
    +            this.initialFetchOffset = initialFetchOffset;
    +            this.committedOffset = initialFetchOffset - 1;
    +            LOG.debug("Created OffsetEntry. {}", this);
    +        }
    +
    +        public void add(KafkaSpoutMessageId msgId) {          // O(Log N)
    +            ackedMsgs.add(msgId);
    +        }
    +
    +        /**
    +         * @return the next OffsetAndMetadata to commit, or null if no offset is ready to commit.
    +         */
    +        public OffsetAndMetadata findNextCommitOffset() {
    +            boolean found = false;
    +            long currOffset;
    +            long nextCommitOffset = committedOffset;
    +            KafkaSpoutMessageId nextCommitMsg = null;     // this is a convenience variable to make it faster to create OffsetAndMetadata
    +
    +            for (KafkaSpoutMessageId currAckedMsg : ackedMsgs) {  // complexity is that of a linear scan on a TreeMap
    +                if ((currOffset = currAckedMsg.offset()) == initialFetchOffset || currOffset == nextCommitOffset + 1) {            // found the next offset to commit
    +                    found = true;
    +                    nextCommitMsg = currAckedMsg;
    +                    nextCommitOffset = currOffset;
    +                    LOG.trace("Found offset to commit [{}]. {}", currOffset, this);
    +                } else if (currAckedMsg.offset() > nextCommitOffset + 1) {    // offset found is not continuous to the offsets listed to go in the next commit, so stop search
    +                    LOG.debug("Non continuous offset found [{}]. It will be processed in a subsequent batch. {}", currOffset, this);
    +                    break;
    +                } else {
    +                    LOG.debug("Unexpected offset found [{}]. {}", currOffset, this);
    +                    break;
    +                }
    +            }
    +
    +            OffsetAndMetadata nextCommitOffsetAndMetadata = null;
    +            if (found) {
    +                nextCommitOffsetAndMetadata = new OffsetAndMetadata(nextCommitOffset, nextCommitMsg.getMetadata(Thread.currentThread()));
    +                LOG.debug("Offset to be committed next: [{}] {}", nextCommitOffsetAndMetadata.offset(), this);
    +            } else {
    +                LOG.debug("No offsets ready to commit. {}", this);
    +            }
    +            return nextCommitOffsetAndMetadata;
    +        }
    +
    +        /**
    +         * Marks an offset has committed. This method has side effects - it sets the internal state in such a way that future
    +         * calls to {@link #findNextCommitOffset()} will return offsets greater than the offset specified, if any.
    +         *
    +         * @param committedOffset offset to be marked as committed
    +         */
    +        public void commit(OffsetAndMetadata committedOffset) {
    +            if (committedOffset != null) {
    +                /*final long numCommittedOffsets = this.committedOffset < initialFetchOffset
    +                        ? committedOffset.offset() - this.committedOffset + 1       // +1 because fetchOffset is pointing to the first uncommitted offset
    +                        : committedOffset.offset() - this.committedOffset;
    +*/
    +                final long numCommittedOffsets = committedOffset.offset() - this.committedOffset;
    +
    +                this.committedOffset = committedOffset.offset();
    +                for (Iterator<KafkaSpoutMessageId> iterator = ackedMsgs.iterator(); iterator.hasNext(); ) {
    +                    if (iterator.next().offset() <= committedOffset.offset()) {
    +                        iterator.remove();
    +                    } else {
    +                        break;
    +                    }
    +                }
    +                numUncommittedOffsets-= numCommittedOffsets;
    +            }
    +            LOG.trace("Object state after update: {}, numUncommittedOffsets [{}]", this, numUncommittedOffsets);
    +        }
    +
    +        public boolean isEmpty() {
    +            return ackedMsgs.isEmpty();
    +        }
    +
    +        /*@Override
    +        public String toString() {
    +            return "OffsetEntry{" +
    +                    "topic-partition=" + tp +
    +                    ", committedOffset=" + committedOffset +
    +                    ", ackedMsgs=" + ackedMsgs +
    +                    '}';
    +        }*/
    --- End diff --
    
    again lest remove commented out code.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by harshach <gi...@git.apache.org>.
Github user harshach commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-206968509
  
    @tgravescs we don't have JIRA yet on it.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-213501159
  
    Thanks hmcl for the documentation.
    For this time, please kindly help me to apply the suggested fix, I will create pull request myself going forward.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by tgravescs <gi...@git.apache.org>.
Github user tgravescs commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-188834473
  
    @hmcl Its obvious people are also looking for trident support but I'm wondering if it would make sense to split that into separate jira?  That way we could get the regular Spout in so people waiting on that would be unblocked and then the trident after.
    
    Maybe it depends on how far along you are or how much overlap they have.  What do you think?
    
    Do you want help with any parts of this?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54905145
  
    --- Diff: pom.xml ---
    @@ -836,14 +839,39 @@
                     <scope>test</scope>
                 </dependency>
                 <dependency>
    -              <groupId>org.apache.calcite</groupId>
    -              <artifactId>calcite-core</artifactId>
    -              <version>${calcite.version}</version>
    +                <groupId>org.apache.calcite</groupId>
    +                <artifactId>calcite-core</artifactId>
    +                <version>${calcite.version}</version>
    +            </dependency>
    +            <dependency>
    +                <groupId>com.fasterxml.jackson.core</groupId>
    +                <artifactId>jackson-databind</artifactId>
    +                <version>${jackson.version}</version>
    +            </dependency>
    +            <dependency>
    +                <groupId>org.apache.kafka</groupId>
    +                <artifactId>${kafka.artifact.id}</artifactId>
    +                <version>${kafka.version}</version>
    +                <exclusions>
    +                    <exclusion>
    +                        <groupId>org.apache.zookeeper</groupId>
    +                        <artifactId>zookeeper</artifactId>
    +                    </exclusion>
    +                    <exclusion>
    +                        <groupId>log4j</groupId>
    +                        <artifactId>log4j</artifactId>
    +                    </exclusion>
    +                    <exclusion>
    +                        <groupId>org.slf4j</groupId>
    +                        <artifactId>slf4j-log4j12</artifactId>
    +                    </exclusion>
    +                </exclusions>
                 </dependency>
                 <dependency>
    -              <groupId>com.fasterxml.jackson.core</groupId>
    -              <artifactId>jackson-databind</artifactId>
    -              <version>${jackson.version}</version>
    +                <groupId>org.apache.kafka</groupId>
    +                <artifactId>kafka-clients</artifactId>
    +                <version>${kafka.version}</version>
    +                <scope>provided</scope>
    --- End diff --
    
    @revans2 I assigned it to me and will take care of it.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53700693
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    +    }
    +
    +    private void removeFromBlacklist(org.apache.storm.kafka.spout.MessageId msgId) {
    +        blackList.remove(msgId);
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            addAckedTuples(tp, msgId);
    +            // Removed acked tuples from the emittedTuples data structure
    +            emittedTuples.remove(msgId);
    +            // if this acked msg is a retry, remove it from failed data structure
    +            removeFromFailed(tp, msgId);
    +        }
    +    }
    +
    +    private void addAckedTuples(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (!acked.containsKey(tp)) {
    +                acked.put(tp, new OffsetEntry(tp));
    +            }
    +            acked.get(tp).add(msgId);
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            final TopicPartition tp = msgId.getTopicPartition();
    +            // limit to max number of retries
    +            if (msgId.numFails() >= maxRetries) {
    +                LOG.debug("Reached the maximum number of retries. Adding [{]} to list of messages to be committed to kafka", msgId);
    +                ack(msgId);
    +                removeFromFailed(tp, msgId);
    +            } else {
    +                addToFailed(tp, msgId);
    +            }
    +        }
    +    }
    +
    +    private void addToFailed(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        if (!failed.containsKey(tp)) {
    +            failed.put(tp, new HashSet<org.apache.storm.kafka.spout.MessageId>());
    +        }
    +        final Set<org.apache.storm.kafka.spout.MessageId> msgIds = failed.get(tp);
    +        if (msgIds.contains(msgId)) {      // do this to update the counter of the message
    +            msgIds.remove(msgId);
    +        }
    +        msgId.incrementNumFails();        // increment number of failures counter
    +        msgIds.add(msgId);
    +    }
    +
    +    private void removeFromFailed(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        if (failed.containsKey(tp)) {
    +            final Set<org.apache.storm.kafka.spout.MessageId> msgIds = failed.get(tp);
    +            msgIds.remove(msgId);
    +            if (msgIds.isEmpty()) {
    +                failed.remove(tp);
    +            }
    +            LOG.debug("Removing from failed list [{}, {}]", tp, msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        commitAckedTuples();
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            commitAckedTuples();
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        declarer.declareStream(kafkaSpoutStream.getStreamId(), kafkaSpoutStream.getOutputFields());
    +    }
    +
    +    // ====== Private helper methods ======
    +
    +    private void commitAckedTuples() {
    +        final Map<TopicPartition, OffsetAndMetadata> toCommitOffsets = new HashMap<>();
    +
    +        LOG.debug("Committing acked offsets to Kafka");
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            for (TopicPartition tp : acked.keySet()) {
    --- End diff --
    
    If we are going to read the value each time, can we use the entrySet instead of the keySet?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55889098
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,458 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            if(!consumerAutoCommitMode) {
    +                acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            }
    +
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // do nothing - by default polling starts at the last committed offset
    +                    fetchOffset = committedOffset.offset();
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.isExpiredResetOnTrue();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || consumerAutoCommitMode || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);
    +
    +                    kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                    LOG.debug("Emitted tuple [{}] for record [{}]", tuple, record);
    +                }
    +            }
    +        }
    +    }
    +
    +    private void commitOffsetsForAckedTuples() {
    +        // Find offsets that are ready to be committed for every topic partition
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +        for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +            final OffsetAndMetadata nextCommitOffset = tpOffset.getValue().findNextCommitOffset();
    +            if (nextCommitOffset != null) {
    +                nextCommitOffsets.put(tpOffset.getKey(), nextCommitOffset);
    +            }
    +        }
    +
    +        // Commit offsets that are ready to be committed for every topic partition
    +        if (!nextCommitOffsets.isEmpty()) {
    +            kafkaConsumer.commitSync(nextCommitOffsets);
    +            LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +            // Instead of iterating again, it would be possible to commit and update the state for each TopicPartition
    +            // in the prior loop, but the multiple network calls should be more expensive than iterating twice over a small loop
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetEntry offsetEntry = tpOffset.getValue();
    +                offsetEntry.commit(nextCommitOffsets.get(tpOffset.getKey()));
    +            }
    +        } else {
    +            LOG.trace("No offsets to commit. {}", toString());
    +        }
    +    }
    +
    +    // ======== Ack =======
    +    @Override
    +    public void ack(Object messageId) {
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            final MessageId msgId = (MessageId) messageId;
    +            acked.get(msgId.getTopicPartition()).add(msgId);
    +            LOG.debug("Added acked message [{}] to list of messages to be committed to Kafka", msgId);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.debug("Retried tuple with message id [{}]", msgId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message [{}] being marked as acked.", msgId);
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate / Close / Declare Outputs =======
    +
    +    @Override
    +    public void activate() {
    +        subscribeKafkaConsumer();
    +    }
    +
    +    private void subscribeKafkaConsumer() {
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called following this poll, upon partition registration
    +        kafkaConsumer.poll(0);
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        shutdown();
    +    }
    +
    +    @Override
    +    public void close() {
    +        shutdown();
    +    }
    +
    +    private void shutdown() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            if (!consumerAutoCommitMode) {
    +                commitOffsetsForAckedTuples();
    +            }
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        kafkaSpoutStreams.declareOutputFields(declarer);
    +    }
    +
    +    @Override
    +    public String toString() {
    +        return "{acked=" + acked + "} ";
    +    }
    +
    +    // ======= Offsets Commit Management ==========
    +
    +    private static class OffsetComparator implements Comparator<MessageId> {
    +        public int compare(MessageId m1, MessageId m2) {
    +            return m1.offset() < m2.offset() ? -1 : m1.offset() == m2.offset() ? 0 : 1;
    +        }
    +    }
    +
    +    /**
    +     * This class is not thread safe
    +     */
    +    private class OffsetEntry {
    +        private final TopicPartition tp;
    +        private long committedOffset;               // last offset committed to Kafka, or initial fetching offset (initial value depends on offset strategy. See KafkaSpoutConsumerRebalanceListener)
    +        private final NavigableSet<MessageId> ackedMsgs = new TreeSet<>(OFFSET_COMPARATOR);     // acked messages sorted by ascending order of offset
    +
    +        public OffsetEntry(TopicPartition tp, long committedOffset) {
    +            this.tp = tp;
    +            this.committedOffset = committedOffset;
    +            LOG.debug("Created OffsetEntry for [topic-partition={}, committed-or-initial-fetch-offset={}]", tp, committedOffset);
    +        }
    +
    +        public void add(MessageId msgId) {          // O(Log N)
    +            ackedMsgs.add(msgId);
    +        }
    +
    +        /**
    +         * @return the next OffsetAndMetadata to commit, or null if no offset is ready to commit.
    +         */
    +        public OffsetAndMetadata findNextCommitOffset() {
    +            boolean found = false;
    +            long currOffset;
    +            long nextCommitOffset = committedOffset;
    +            MessageId nextCommitMsg = null;     // this is a convenience variable to make it faster to create OffsetAndMetadata
    +
    +            for (MessageId currAckedMsg : ackedMsgs) {  // complexity is that of a linear scan on a TreeMap
    +                if ((currOffset = currAckedMsg.offset()) == 0 || currOffset != nextCommitOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    if (currOffset == nextCommitOffset || currOffset == nextCommitOffset + 1) {            // found the next offset to commit
    +                        found = true;
    +                        nextCommitMsg = currAckedMsg;
    +                        nextCommitOffset = currOffset;
    +                        LOG.trace("Found offset to commit [{}]. {}", currOffset, toString());
    --- End diff --
    
    Done


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by bastiliu <gi...@git.apache.org>.
Github user bastiliu commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55833857
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutStreams.java ---
    @@ -0,0 +1,142 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.utils.Utils;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * Represents the output streams associated with each topic, and provides a public API to
    + * declare output streams and emmit tuples, on the appropriate stream, for all the topics specified.
    + */
    +public class KafkaSpoutStreams implements Serializable {
    +    private final Map<String, KafkaSpoutStream> topicToStream;
    +
    +    private KafkaSpoutStreams(Builder builder) {
    +        this.topicToStream = builder.topicToStream;
    +    }
    +
    +    /**
    +     * @param topic the topic for which to get output fields
    +     * @return the output fields declared
    +     */
    +    public Fields getOutputFields(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getOutputFields();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @param topic the topic to for which to get the stream id
    +     * @return the id of the stream to where the tuples are emitted
    +     */
    +    public String getStreamId(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getStreamId();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @return list of topics subscribed and emitting tuples to a stream as configured by {@link KafkaSpoutStream}
    +     */
    +    public List<String> getTopics() {
    +        return new ArrayList<>(topicToStream.keySet());
    +    }
    +
    +    void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        for (KafkaSpoutStream stream : topicToStream.values()) {
    +            declarer.declareStream(stream.getStreamId(), stream.getOutputFields());
    +        }
    +    }
    +
    +    void emit(SpoutOutputCollector collector, MessageId messageId) {
    +        collector.emit(getStreamId(messageId.topic()), messageId.getTuple(), messageId);
    --- End diff --
    
    @revans2 Actually, the values of tuple have already been stored in the pending map of spout. But currently, we only return the message id when ack() or fail(). If there is a follow up JIRA to reduce the memory pressure when replay, we can just add a extension interface of spout, if user implements it, both message id and tuple values will be returned when fail().


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56228825
  
    --- Diff: external/storm-kafka-client/pom.xml ---
    @@ -0,0 +1,86 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!--/**
    +        * Licensed to the Apache Software Foundation (ASF) under one
    +        * or more contributor license agreements.  See the NOTICE file
    +        * distributed with this work for additional information
    +        * regarding copyright ownership.  The ASF licenses this file
    +        * to you 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.
    +        */-->
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +    <modelVersion>4.0.0</modelVersion>
    +
    +    <parent>
    +        <artifactId>storm</artifactId>
    +        <groupId>org.apache.storm</groupId>
    +        <version>2.0.0-SNAPSHOT</version>
    +        <relativePath>../../pom.xml</relativePath>
    +    </parent>
    +
    +    <artifactId>storm-kafka-client</artifactId>
    --- End diff --
    
    In my opinion if we are going to eventually move it to storm-kafa eventually, lets just start out with it there now.
    
    The only downside is if someone wants to use the new spout they also get a dependency on all of kafka from the old spout, but that will be going away soon anyways.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56701828
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    I personally don't see a need for it, because we can always create a bolt after the spout to split one tuple into multiple.  If we do it in the spout we run the risk of double replaying things, or fully acking something that later fails, Which is what appears to be the case with the PartitionManger.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-217905885
  
    thanks Hmcl.
    
    Just found below log constantly show up, seems it constantly try to commit one offset which is actually committed to kafka already \u2013 it might be caused by group rebalance \u2013 so a smaller offset (smaller than the committed offset) is acked back lately.
    
    For example(it is our assumption, kindly correct me if wrong): one consumer commit offset 1000, polled 1001~1050 messages and emitted, also message was acked for 1001 ~ 1009, then a rebalance happened, another consumer poll message from 1000 to 1025, and commit the offset to 1010, then the message 1010(was emitted before the rebalance) was acked back. This will cause 1010 will never be committed as per the logic in findNextCommitOffset method \u2013 because this offset was already commited to kafka successfully.
    
    Log is:
    2016-05-09 03:02:14 io.ebay.rheos.KafkaSpout [INFO] Unexpected offset found [37137]. OffsetEntry{topic-partition=oradb.core4-lcr.caty.ebay-bids-3, fetchOffset=37138, committedOffset=37137, ackedMsgs={topic-partition=oradb.core4-lcr.caty.ebay-bids-3, offset=37137, numFails=0}|{topic-partition=oradb.core4-lcr.caty.ebay-bids-3, offset=37137, numFails=0}} 
    
    We applied below fix - For OffsetEntry.add(KafkaSpoutMessageId msgId) method, we changed the code as per below \u2013 only add acked message when its offset is bigger than the committed offset.
    
    public void add(KafkaSpoutMessageId msgId) {          // O(Log N)
        **_if(msgId.offset() > committedOffset)//this line is newly added_**
            ackedMsgs.add(msgId);
    }
    
    Could you please help take a look at the above and let me know your thoughts? Thanks.



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm issue #1131: STORM-822: Kafka Spout New Consumer API

Posted by erikdw <gi...@git.apache.org>.
Github user erikdw commented on the issue:

    https://github.com/apache/storm/pull/1131
  
    > Btw, I just send the latest code to you via email.
    
    @jianbzhou: can you please clarify the "you" in this statement?
    
    > we have communicated via email for a while and going forward let's talk in this thread so everyone is in same page.
    
    @jianbzhou : wouldn't it be more appropriate to open a new STORM-XXXX JIRA and then communicate via that JIRA?  As opposed to private emails with diffs, or adding comments into a merged and done PR?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by knusbaum <gi...@git.apache.org>.
Github user knusbaum commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53672283
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    --- End diff --
    
    Looks like some bracket salad.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by connieyang <gi...@git.apache.org>.
Github user connieyang commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-202527934
  
    @harshach @revans2 @abhishekagarwal87 Any comments?
    @hmcl Thanks for the work.  I assume that this does not have the required changes for Trident.  So, would it come in a separate patch? 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53698623
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    +    }
    +
    +    private void removeFromBlacklist(org.apache.storm.kafka.spout.MessageId msgId) {
    +        blackList.remove(msgId);
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            addAckedTuples(tp, msgId);
    +            // Removed acked tuples from the emittedTuples data structure
    +            emittedTuples.remove(msgId);
    +            // if this acked msg is a retry, remove it from failed data structure
    +            removeFromFailed(tp, msgId);
    +        }
    +    }
    +
    +    private void addAckedTuples(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (!acked.containsKey(tp)) {
    +                acked.put(tp, new OffsetEntry(tp));
    +            }
    +            acked.get(tp).add(msgId);
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    --- End diff --
    
    Actually I looked and no it cannot.  We guard against that with the rotating map.  We remove the tuple info before calling fail, and only call fail if we found the tuple in the map.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by tgravescs <gi...@git.apache.org>.
Github user tgravescs commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-187274041
  
    @hmcl  I haven't looked at code yet but don't see anything trident related so I was just curious if you are planning on adding trident support here or if that is separate jira?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53699257
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    +    }
    +
    +    private void removeFromBlacklist(org.apache.storm.kafka.spout.MessageId msgId) {
    +        blackList.remove(msgId);
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            addAckedTuples(tp, msgId);
    +            // Removed acked tuples from the emittedTuples data structure
    +            emittedTuples.remove(msgId);
    +            // if this acked msg is a retry, remove it from failed data structure
    +            removeFromFailed(tp, msgId);
    +        }
    +    }
    +
    +    private void addAckedTuples(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (!acked.containsKey(tp)) {
    +                acked.put(tp, new OffsetEntry(tp));
    +            }
    +            acked.get(tp).add(msgId);
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            final TopicPartition tp = msgId.getTopicPartition();
    +            // limit to max number of retries
    +            if (msgId.numFails() >= maxRetries) {
    +                LOG.debug("Reached the maximum number of retries. Adding [{]} to list of messages to be committed to kafka", msgId);
    +                ack(msgId);
    +                removeFromFailed(tp, msgId);
    +            } else {
    +                addToFailed(tp, msgId);
    --- End diff --
    
    Why don't we want to just re-emit that failed tuple right here?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-188995178
  
    @revans2 no worries. Concerning your code snippet suggestion, I agree with most of it. We can definitely keep a lot of the state in the MessageId object. I agree it is indeed the ideal solution. I considered it initially but was concerned of how expensive it would be to keep all of that state in the MessageId.
    
    However, I don't think that Using the markAsDone(id) strategy will suffice to address the cases where tuples get acked out of order, and the cases were a sequence of tuples will get acked in sequence, but shifted from the last offset committed. In this scenario we need to have a way to keep track of the offset sequences that are ready to be committed. I still think that keeping `acked Map<TopicPartition, OffsetEntry>` will take care of this in an efficient way. If there is a better way, of course, I would totally welcome a suggestion. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56517067
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    Now to answer your other question.  It seems like you have thought through this a lot.  Philosophically I think that modular code, with interfaces is usually a good idea, especially for making the code unit testable.  Exposing those to end users and making them pluggable without a really good use case make the code more difficult to maintain because I have to maintain that interface for good or bad.
    
    I personally would like to reuse a lot of the design from the original spout because we know it works.
    
    **All of the following describes what the follow on spout should look like unless otherwise stated. My changes/comments are in bold**
    
    [0] - We fetch (poll) a new set of records from Kafka **when we don't have something to emit from our buffer.  The internal spout flow control assumes that we will emit around one tuple at a time when it calls `.nextTuple()`.  So we have a buffer of messages to emit and emit them one at a time**  
    
    [1] - If offsets 1 2 3 4 5 6 get fetched (polled)
 and the following occurs: 1 2 committed, 4 fails
 , 3 5 6 pending **and the exponential back-off for 4 has expired and 4 has been retired less then the maximum number of retries**.
    
      * [1.1] - We want to fetch again starting on **4, i.e 4 5 6 ... and then filter out any messages that are not ready to be replayed.**
    
      * [1.2] - Let's say 3 that was pending fails after the retry fetch described in [1.1] **(4 5 6 ... )** has happened. What do we do? **We fetch again starting with 3 this time (3 4 5 ...)**
    
    [2] - **We want in the follow on JIRA** to have some logic to avoid re-emitting **any tuples that have been emitted and not had a corresponding `fail()` called on them yet.  Preferably with some sort of a back-off like the original code.**
    
    [3] - **We want this patch** to support a limited number of retrials for a particular offset**.** Let's say 4 keeps on failing. **W**e want to limit the number of fails (i.e repeat what is done in [1.1]) to e.g. 3 times, and once the limit is reached, we consider 4 has committed**. In the follow-on patch at a minimum we need a metric to count how many of these we have dropped.**
    
      * [3.1] - In this scenario, **we are only replaying failed messages by placing them into the same buffer/queue that we emit from. We will continue to fetch new messages into that queue so long as we have not violated the maxUncommitedOffsets, but we will never commit anything above 4 until it has failed the maximum number of times.  This means we will not violate the guarantee, even if the spout crashes and a new spout looses all of its state, unless we have kafka auto-commit turned on.** 
    
       * [3.2] - Do we want to stop retrying 4 (even if it has reached max retries) only if all the offsets less than 4 have been acked, or all have failed more than maxNumRetries**? maxNumRetires needs to be marked as a best effort.  If the spout fails before we can persist the data to kafka we may retry the message more often.**
    
    [4] - We do NOT want at all to have the feature to retry from memory. That means that there is no need to ever keep tuples in memory, only offsets. **Ultimately I think so.  I still have not seen a use case that warrants it.**
    
       * [4.1] - Or do we want extract the retry strategy into one interface, and have implementations for both? **Like I said above having an interface that allows us to make it pluggable, especially for unit tests is a great thing, but exposing it to the end user without a good use case is not.**
    
    [5] - Does it make sense to even support retrying an offset forever? This can potentially cause an infinite cycle of fetch (poll) calls to Kafka accross the network... but it will guarantee delivery up to that point (offset).  **I don't have one, but to work around this not being there I think this is why we have the FirstPollOffsetStrategy so we can pick up in different spot and skip something that is bad**
    
    
    The more I look at the original spout the more I see a lot of good design/features in it, despite its complexity I would suggest you look at how it handles replay.
    
    There is a manager that keeps track of what should/should not be replayed and when.
    
    The current implementation is https://github.com/apache/storm/blob/master/external/storm-kafka/src/jvm/org/apache/storm/kafka/ExponentialBackoffMsgRetryManager.java
    
    For it there is a pulggable interface, but it is not exposed to the end user.  Using that code when a new batch is fetch it will only replay tuples that have failed and have met the criteria to be replayed.  
    
    https://github.com/apache/storm/blob/master/external/storm-kafka/src/jvm/org/apache/storm/kafka/PartitionManager.java#L231-L241
    
    A lot of that code could be reused almost with no changes at all.
    



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53724182
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    +    }
    +
    +    private void removeFromBlacklist(org.apache.storm.kafka.spout.MessageId msgId) {
    +        blackList.remove(msgId);
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            addAckedTuples(tp, msgId);
    +            // Removed acked tuples from the emittedTuples data structure
    +            emittedTuples.remove(msgId);
    +            // if this acked msg is a retry, remove it from failed data structure
    +            removeFromFailed(tp, msgId);
    +        }
    +    }
    +
    +    private void addAckedTuples(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (!acked.containsKey(tp)) {
    +                acked.put(tp, new OffsetEntry(tp));
    +            }
    +            acked.get(tp).add(msgId);
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            final TopicPartition tp = msgId.getTopicPartition();
    +            // limit to max number of retries
    +            if (msgId.numFails() >= maxRetries) {
    +                LOG.debug("Reached the maximum number of retries. Adding [{]} to list of messages to be committed to kafka", msgId);
    +                ack(msgId);
    +                removeFromFailed(tp, msgId);
    +            } else {
    +                addToFailed(tp, msgId);
    +            }
    +        }
    +    }
    +
    +    private void addToFailed(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        if (!failed.containsKey(tp)) {
    +            failed.put(tp, new HashSet<org.apache.storm.kafka.spout.MessageId>());
    +        }
    +        final Set<org.apache.storm.kafka.spout.MessageId> msgIds = failed.get(tp);
    +        if (msgIds.contains(msgId)) {      // do this to update the counter of the message
    +            msgIds.remove(msgId);
    +        }
    +        msgId.incrementNumFails();        // increment number of failures counter
    +        msgIds.add(msgId);
    +    }
    +
    +    private void removeFromFailed(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        if (failed.containsKey(tp)) {
    +            final Set<org.apache.storm.kafka.spout.MessageId> msgIds = failed.get(tp);
    +            msgIds.remove(msgId);
    +            if (msgIds.isEmpty()) {
    +                failed.remove(tp);
    +            }
    +            LOG.debug("Removing from failed list [{}, {}]", tp, msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        commitAckedTuples();
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            commitAckedTuples();
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        declarer.declareStream(kafkaSpoutStream.getStreamId(), kafkaSpoutStream.getOutputFields());
    +    }
    +
    +    // ====== Private helper methods ======
    +
    +    private void commitAckedTuples() {
    +        final Map<TopicPartition, OffsetAndMetadata> toCommitOffsets = new HashMap<>();
    +
    +        LOG.debug("Committing acked offsets to Kafka");
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            for (TopicPartition tp : acked.keySet()) {
    +                final OffsetEntry offsetEntry = acked.get(tp);
    +                OffsetAndMetadata offsetAndMetadata = offsetEntry.findOffsetToCommit();
    +                if (offsetAndMetadata != null) {
    +                    toCommitOffsets.put(tp, offsetAndMetadata);
    +                }
    +            }
    +
    +            if (!toCommitOffsets.isEmpty()) {
    +                kafkaConsumer.commitSync(toCommitOffsets);
    +                LOG.debug("Offsets successfully committed to Kafka [{]}", toCommitOffsets);
    +            }
    +
    +            // Instead of iterating again, we could commit each TopicPartition in the prior loop,
    +            // but the multiple networks calls should be more expensive than iteration twice over a small loop
    +            for (TopicPartition tp : acked.keySet()) {
    +                OffsetEntry offsetEntry = acked.get(tp);
    +                offsetEntry.updateAckedState(toCommitOffsets.get(tp));
    +                updateAckedState(tp, offsetEntry);
    +            }
    +        } catch (Exception e) {
    +            LOG.error("Exception occurred while committing to Kafka offsets for acked tuples ", e);
    +        } finally {
    +            commit = false;
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    private void updateAckedState(TopicPartition tp, OffsetEntry offsetEntry) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (offsetEntry.isEmpty()) {
    +                acked.remove(tp);
    +            }
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======= Offsets Commit Management ==========
    +
    +    private static class OffsetComparator implements Comparator<org.apache.storm.kafka.spout.MessageId> {
    +        public int compare(org.apache.storm.kafka.spout.MessageId m1, org.apache.storm.kafka.spout.MessageId m2) {
    +            return m1.offset() < m2.offset() ? -1 : m1.offset() == m2.offset() ? 0 : 1;
    +        }
    +    }
    +
    +    /** This class is not thread safe */
    +    // Although this class is called by multiple (2) threads, all the calling methods are properly synchronized
    +    private class OffsetEntry {
    +        private long committedOffset;          // last offset committed to Kafka
    +        private long toCommitOffset;                // last offset to be committed in the next commit operation
    +        private final Set<org.apache.storm.kafka.spout.MessageId> ackedMsgs = new TreeSet<>(OFFSET_COMPARATOR);     // sort messages by ascending order of offset
    +        private Set<org.apache.storm.kafka.spout.MessageId> toCommitMsgs = new TreeSet<>(OFFSET_COMPARATOR);        // Messages that contain the offsets to be committed in the next commit operation
    +
    +        public OffsetEntry(TopicPartition tp) {
    +            OffsetAndMetadata committed = kafkaConsumer.committed(tp);
    +            committedOffset = committed == null ? -1 : committed.offset();
    +            LOG.debug("Created OffsetEntry for [topic-partition={}, last-committed-offset={}]", tp, committedOffset);
    +        }
    +
    +        public void add(org.apache.storm.kafka.spout.MessageId msgId) {          // O(Log N)
    +            ackedMsgs.add(msgId);
    +        }
    +
    +        /**
    +         * This method has side effects. The method updateAckedState should be called after this method.
    +         */
    +        public OffsetAndMetadata findOffsetToCommit() {
    +            long currOffset;
    +            OffsetAndMetadata offsetAndMetadata = null;
    +            toCommitMsgs = new TreeSet<>(OFFSET_COMPARATOR);
    +            toCommitOffset = committedOffset;
    +            org.apache.storm.kafka.spout.MessageId toCommitMsg = null;
    +
    +            for (org.apache.storm.kafka.spout.MessageId ackedMsg : ackedMsgs) {  // for K matching messages complexity is K*(Log*N). K <= N
    +                if ((currOffset = ackedMsg.offset()) == toCommitOffset || currOffset == toCommitOffset + 1) {    // found the next offset to commit
    +                    toCommitMsgs.add(ackedMsg);
    +                    toCommitMsg = ackedMsg;
    +                    toCommitOffset = currOffset;
    +                } else if (ackedMsg.offset() > toCommitOffset + 1) {    // offset found is not continuous to the offsets listed to go in the next commit, so stop search
    +                    break;
    +                } else {
    +                    LOG.debug("Unexpected offset found [{]}. {}", ackedMsg.offset(), toString());
    +                    break;
    +                }
    +            }
    +
    +            if (!toCommitMsgs.isEmpty()) {
    +                offsetAndMetadata = new OffsetAndMetadata(toCommitOffset, toCommitMsg.getMetadata(Thread.currentThread()));
    +                LOG.debug("Last offset to be committed in the next commit call: [{]}", offsetAndMetadata.offset());
    +                LOG.trace(toString());
    +            } else {
    +                LOG.debug("No offsets found ready to commit");
    +                LOG.trace(toString());
    +            }
    +            return offsetAndMetadata;
    +        }
    +
    +        /**
    +         * This method has side effects and should be called after findOffsetToCommit
    +         */
    +        public void updateAckedState(OffsetAndMetadata offsetAndMetadata) {
    +            if (offsetAndMetadata != null) {
    +                committedOffset = offsetAndMetadata.offset();
    +                toCommitMsgs = new TreeSet<>(OFFSET_COMPARATOR);
    +                ackedMsgs.removeAll(toCommitMsgs);
    +            }
    +        }
    +
    +        public boolean isEmpty() {
    +            return ackedMsgs.isEmpty();
    +        }
    +
    +        @Override
    +        public String toString() {
    +            return "OffsetEntry{" +
    +                    "committedOffset=" + committedOffset +
    +                    ", toCommitOffset=" + toCommitOffset +
    +                    ", ackedMsgs=" + ackedMsgs +
    +                    ", toCommitMsgs=" + toCommitMsgs +
    +                    '}';
    +        }
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            commitAckedTuples();  // commit acked records,
    +            copyEmittedTuplesToBlackList(); // all the tuples that are in traffic when the rebalance occurs will be added to black list to avoid duplication
    +            clearFailed();   // remove all failed tuples form list to avoid duplication
    +            clearEmittedTuples();    // clear emitted tuples
    --- End diff --
    
    This comment is a continuation of C1.
    
    @revans2 As I said in C1, the commits to Kafka, as well as all its callbacks are done in the callers thread. If I am not missing anything that means that all the calls are done in the same thread, which implies that in practice this class is single threaded, and therefore we do not need any locking whatsoever. Is there any reason why we may need to lock?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53697933
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    --- End diff --
    
    Please remove the "HMCL - " at least and ideally make it as debug, or just delete it all together.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56403733
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    @abhishekagarwal87 For me I personally don't want to make something configurable/pluggable unless I know there is a use case for it.  Being able to support lots and lots of failures with little impact on kafka does not feel like that important of a use case.  Mostly because lots and lots of failures seems to be an "oh crap I need to fix something" situation, not an "I do this all the time and failure is the expected behavior so stop pounding on kafka so much" situation.  


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54889025
  
    --- Diff: pom.xml ---
    @@ -836,14 +839,39 @@
                     <scope>test</scope>
                 </dependency>
                 <dependency>
    -              <groupId>org.apache.calcite</groupId>
    -              <artifactId>calcite-core</artifactId>
    -              <version>${calcite.version}</version>
    +                <groupId>org.apache.calcite</groupId>
    +                <artifactId>calcite-core</artifactId>
    +                <version>${calcite.version}</version>
    +            </dependency>
    +            <dependency>
    +                <groupId>com.fasterxml.jackson.core</groupId>
    +                <artifactId>jackson-databind</artifactId>
    +                <version>${jackson.version}</version>
    +            </dependency>
    +            <dependency>
    +                <groupId>org.apache.kafka</groupId>
    +                <artifactId>${kafka.artifact.id}</artifactId>
    +                <version>${kafka.version}</version>
    +                <exclusions>
    +                    <exclusion>
    +                        <groupId>org.apache.zookeeper</groupId>
    +                        <artifactId>zookeeper</artifactId>
    +                    </exclusion>
    +                    <exclusion>
    +                        <groupId>log4j</groupId>
    +                        <artifactId>log4j</artifactId>
    +                    </exclusion>
    +                    <exclusion>
    +                        <groupId>org.slf4j</groupId>
    +                        <artifactId>slf4j-log4j12</artifactId>
    +                    </exclusion>
    +                </exclusions>
                 </dependency>
                 <dependency>
    -              <groupId>com.fasterxml.jackson.core</groupId>
    -              <artifactId>jackson-databind</artifactId>
    -              <version>${jackson.version}</version>
    +                <groupId>org.apache.kafka</groupId>
    +                <artifactId>kafka-clients</artifactId>
    +                <version>${kafka.version}</version>
    +                <scope>provided</scope>
    --- End diff --
    
    I know that they all had provided before, and we can make packaging work with them marked as provided, but it requires extra steps by our customers which is not ideal.  If you want to do it as a separate JIRA/Pull that would probably be ideal because thinking about it we should make this change in at least 1.x and possibly in 0.10.x as well.
    
    I'll file a separate JIRA for it.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by harshach <gi...@git.apache.org>.
Github user harshach commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-206972371
  
    filed one here https://issues.apache.org/jira/browse/STORM-1694


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by knusbaum <gi...@git.apache.org>.
Github user knusbaum commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-190855726
  
    @hmcl 
    I can't run your `KafkaSpoutTopologyMain`. It complains about missing `org.apache.kafka.clients.consumer.ConsumerRebalanceListener`.
    
    Are you sure all of the things you removed from the poms are unnecessary?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56507684
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    @hmcl This is to me a performance issue.  I have urgency but I don't want to throw in complex code that is not very well tested.  It has taken us over a month to get to this point.  I don't want to spend another month trying to review a lot of new complex code. 
    
    I would very much prefer to merge this in as is.  Even with BATH/STREAM etc.  Then we can look at doing the more complicated/optimized version in a follow on JIRA.
    



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54822068
  
    --- Diff: pom.xml ---
    @@ -836,14 +839,39 @@
                     <scope>test</scope>
                 </dependency>
                 <dependency>
    -              <groupId>org.apache.calcite</groupId>
    -              <artifactId>calcite-core</artifactId>
    -              <version>${calcite.version}</version>
    +                <groupId>org.apache.calcite</groupId>
    +                <artifactId>calcite-core</artifactId>
    +                <version>${calcite.version}</version>
    +            </dependency>
    +            <dependency>
    +                <groupId>com.fasterxml.jackson.core</groupId>
    +                <artifactId>jackson-databind</artifactId>
    +                <version>${jackson.version}</version>
    +            </dependency>
    +            <dependency>
    +                <groupId>org.apache.kafka</groupId>
    +                <artifactId>${kafka.artifact.id}</artifactId>
    +                <version>${kafka.version}</version>
    +                <exclusions>
    +                    <exclusion>
    +                        <groupId>org.apache.zookeeper</groupId>
    +                        <artifactId>zookeeper</artifactId>
    +                    </exclusion>
    +                    <exclusion>
    +                        <groupId>log4j</groupId>
    +                        <artifactId>log4j</artifactId>
    +                    </exclusion>
    +                    <exclusion>
    +                        <groupId>org.slf4j</groupId>
    +                        <artifactId>slf4j-log4j12</artifactId>
    +                    </exclusion>
    +                </exclusions>
                 </dependency>
                 <dependency>
    -              <groupId>com.fasterxml.jackson.core</groupId>
    -              <artifactId>jackson-databind</artifactId>
    -              <version>${jackson.version}</version>
    +                <groupId>org.apache.kafka</groupId>
    +                <artifactId>kafka-clients</artifactId>
    +                <version>${kafka.version}</version>
    +                <scope>provided</scope>
    --- End diff --
    
    @revans2 the following pom files already had provided before and I left them in the same state. Should we leave it as provided, or change that to the default (compile) ?
    
    storm-starter/pom.xml
    flux-examples/pom.xml
    storm-sql-kafka/pom.xml


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-190494188
  
    Hi **@hmcl**, in some project we are tracking the DB activities, we may need the message to be emitted in same sequence as they were polled from Kafka - i.e. polled 5 messages(1~5), if 1, 3, 4, 5 was acked while 2 is failed, seems currently only 2 will be re-emitted , this will cause the sequence is like 1, 3, 4, 5, 2..is it possible to re-emit message from the position that was failed, so the flow will be: 1, 3, 4, 5, 2, 3, 4, 5...
    What is your view on this?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r57769058
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,547 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Collections;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Iterator;
    +import java.util.LinkedList;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<KafkaSpoutMessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private transient KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +
    +
    +    // Bookkeeping
    +    private transient int maxRetries;                                   // Max number of times a tuple is retried
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;  // Strategy to determine the fetch offset of the first realized by the spout upon activation
    +    private transient KafkaSpoutRetryService retryService;              // Class that has the logic to handle tuple failure
    +    private transient Timer commitTimer;                                // timer == null for auto commit mode
    +    private transient boolean initialized;                              // Flag indicating that the spout is still undergoing initialization process.
    +    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +    private KafkaSpoutStreams kafkaSpoutStreams;                        // Object that wraps all the logic to declare output fields and emit tuples
    +    private transient KafkaSpoutTuplesBuilder<K, V> tuplesBuilder;      // Object that contains the logic to build tuples for each ConsumerRecord
    +
    +    private transient Map<TopicPartition, OffsetEntry> acked;           // Tuples that were successfully acked. These tuples will be committed periodically when the commit timer expires, after consumer rebalance, or on close/deactivate
    +    private transient Set<KafkaSpoutMessageId> emitted;                 // Tuples that have been emitted but that are "on the wire", i.e. pending being acked or failed
    +    private transient Iterator<ConsumerRecord<K, V>> waitingToEmit;         // Records that have been polled and are queued to be emitted in the nextTuple() call. One record is emitted per nextTuple()
    +    private transient long numUncommittedOffsets;                       // Number of offsets that have been polled and emitted but not yet been committed
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +        numUncommittedOffsets = 0;
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        // Retries management
    +        retryService = kafkaSpoutConfig.getRetryService();
    +
    +        // Tuples builder delegate
    +        tuplesBuilder = kafkaSpoutConfig.getTuplesBuilder();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            commitTimer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitPeriodMs(), TimeUnit.MILLISECONDS);
    +        }
    +
    +        acked = new HashMap<>();
    +        emitted = new HashSet<>();
    +        waitingToEmit = Collections.emptyListIterator();
    +
    +        LOG.info("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            if (!consumerAutoCommitMode) {
    +                acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            }
    +
    +            retryService.remove(partitions);
    +
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // By default polling starts at the last committed offset. +1 to point fetch to the first uncommitted offset.
    +                    fetchOffset = committedOffset.offset() + 1;
    +                    kafkaConsumer.seek(tp, fetchOffset);
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            }
    +
    +            if (poll()) {
    +                setWaitingToEmit(pollKafkaBroker());
    +            }
    +
    +            if (waitingToEmit()) {
    +                emit();
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && commitTimer.isExpiredResetOnTrue();    // timer != null for non auto commit mode
    +    }
    +
    +    private boolean poll() {
    +        return !waitingToEmit() && numUncommittedOffsets < kafkaSpoutConfig.getMaxUncommittedOffsets();
    +    }
    +
    +    private boolean waitingToEmit() {
    +        return waitingToEmit != null && waitingToEmit.hasNext();
    +    }
    +
    +    public void setWaitingToEmit(ConsumerRecords<K,V> consumerRecords) {
    +        List<ConsumerRecord<K,V>> waitingToEmitList = new LinkedList<>();
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            waitingToEmitList.addAll(consumerRecords.records(tp));
    +        }
    +        waitingToEmit = waitingToEmitList.iterator();
    +        LOG.trace("Records waiting to be emitted {}", waitingToEmitList);
    +    }
    +
    +    // ======== poll =========
    +    private ConsumerRecords<K, V> pollKafkaBroker() {
    +        doSeekRetriableTopicPartitions();
    +
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        final int numPolledRecords = consumerRecords.count();
    +        LOG.debug("Polled [{}] records from Kafka. NumUncommittedOffsets=[{}]", numPolledRecords, numUncommittedOffsets);
    +        return consumerRecords;
    +    }
    +
    +    private void doSeekRetriableTopicPartitions() {
    +        final Set<TopicPartition> retriableTopicPartitions = retryService.retriableTopicPartitions();
    +
    +        for (TopicPartition rtp : retriableTopicPartitions) {
    +            final OffsetAndMetadata offsetAndMeta = acked.get(rtp).findNextCommitOffset();
    +            if (offsetAndMeta != null) {
    +                kafkaConsumer.seek(rtp, offsetAndMeta.offset() + 1);  // seek to the next offset that is ready to commit in next commit cycle
    +            } else {
    +                kafkaConsumer.seekToEnd(rtp);    // Seek to last committed offset
    +            }
    +        }
    +    }
    +
    +    // ======== emit  =========
    +    private void emit() {
    +        emitTupleIfNotEmitted(waitingToEmit.next());
    +        waitingToEmit.remove();
    --- End diff --
    
    This will be very slow.  It would be nice for us to loop until we emit something or there is truly nothing to emit.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-204081001
  
    @hmcl I merged this into master, but when I tried to backport it to 1.x there was a merge conflict.  It was very minor, pom.xml needed a version update, and remove the storm-clojure package listing.
    
    But because I am not really setup to manually test it I thought it might be best to wait for an official pull request.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54774473
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,454 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called following this poll, upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // do nothing - by default polling starts at the last committed offset
    +                    fetchOffset = committedOffset.offset();
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.isExpiredResetOnTrue();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);
    +
    +                    kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                    LOG.debug("Emitted tuple [{}] for record [{}]", tuple, record);
    +                }
    +            }
    +        }
    +    }
    +
    +    private void commitOffsetsForAckedTuples() {
    +        // Find offsets that are ready to be committed for every topic partition
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +        for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +            final OffsetAndMetadata nextCommitOffset = tpOffset.getValue().findNextCommitOffset();
    +            if (nextCommitOffset != null) {
    +                nextCommitOffsets.put(tpOffset.getKey(), nextCommitOffset);
    +            }
    +        }
    +
    +        // Commit offsets that are ready to be committed for every topic partition
    +        if (!nextCommitOffsets.isEmpty()) {
    +            kafkaConsumer.commitSync(nextCommitOffsets);
    +            LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +            // Instead of iterating again, it would be possible to commit and update the state for each TopicPartition
    +            // in the prior loop, but the multiple network calls should be more expensive than iterating twice over a small loop
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetEntry offsetEntry = tpOffset.getValue();
    +                offsetEntry.commit(nextCommitOffsets.get(tpOffset.getKey()));
    +            }
    +        } else {
    +            LOG.trace("No offsets to commit. {}", toString());
    +        }
    +    }
    +
    +    // ======== Ack =======
    +    @Override
    +    public void ack(Object messageId) {
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            final MessageId msgId = (MessageId) messageId;
    +            acked.get(msgId.getTopicPartition()).add(msgId);
    +            LOG.debug("Added acked message [{}] to list of messages to be committed to Kafka", msgId);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.debug("Retried tuple with message id [{}]", msgId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message [{}] being marked as acked.", msgId);
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate / Close / Declare Outputs =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    --- End diff --
    
    activate gets called after deactivate is called.  It can potentially go back and forth between the two states multiple times.  If we shutdown in deactivate, which I think is fine, then we need to reinitialize in activate.
    
    I assume that once the kafkaConsumer is closed poll will not work on it any more.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by connieyang <gi...@git.apache.org>.
Github user connieyang commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-222278362
  
    Following the Trident API support for the new KafkaSpout implementation... Is anyone working on this?  Thanks.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56384733
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaRecordTupleBuilder.java ---
    @@ -0,0 +1,44 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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,
    + *   See the License for the specific language governing permissions and
    + *   limitations under the License.
    + */
    +package org.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.tuple.Values;
    +
    +import java.util.List;
    +
    +public class KafkaRecordTupleBuilder<K, V> implements KafkaSpoutTupleBuilder<K, V> {
    +    @Override
    +    public List<Object> buildTuple(final ConsumerRecord<K, V> consumerRecord, KafkaSpoutStreams kafkaSpoutStreams) {
    --- End diff --
    
    if record tuple builder is constructing tuple object, the schema of the tuple should be in this class itself rather than KafkaSpoutStreams.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit closed the pull request at:

    https://github.com/apache/storm/pull/1131


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54459712
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig.toString());
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if(commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.expired();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +
    +                    kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                    LOG.debug("Emitted tuple [{}] for record [{}]", tuple, record);
    +                }
    +            }
    +        }
    +    }
    +
    +    // ======== Ack =======
    +    @Override
    +    public void ack(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    +            acked.get(tp).add(msgId);
    +            LOG.debug("Adding acked message to [{}] to list of messages to be committed to Kafka", msgId);
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final MessageId msgId = (MessageId) messageId;
    +        if (msgId.numFails() < maxRetries) {
    +            msgId.incrementNumFails();
    +            kafkaSpoutStreams.emit(collector, msgId);
    +            LOG.debug("Retried tuple with message id [{}]", messageId);
    +        } else { // limit to max number of retries
    +            LOG.debug("Reached maximum number of retries. Message being marked as acked.");
    +            ack(msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        if(!consumerAutoCommitMode) {
    +            commitOffsetsForAckedTuples();
    +        }
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            if(!consumerAutoCommitMode) {
    +                commitOffsetsForAckedTuples();
    +            }
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    // TODO must declare multiple output streams
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        kafkaSpoutStreams.declareOutputFields(declarer);
    +    }
    +
    +    // ====== Private helper methods ======
    +
    +    private void commitOffsetsForAckedTuples() {
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +
    +        try {
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetAndMetadata offsetAndMetadata = tpOffset.getValue().findNextCommitOffset();
    +                if (offsetAndMetadata != null) {
    +                    nextCommitOffsets.put(tpOffset.getKey(), offsetAndMetadata);
    +                }
    +            }
    +
    +            if (!nextCommitOffsets.isEmpty()) {
    +                kafkaConsumer.commitSync(nextCommitOffsets);
    +                LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +                // Instead of iterating again, we could commit and update state for each TopicPartition in the prior loop,
    +                // but the multiple network calls should be more expensive than iterating twice over a small loop
    +                for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                    final OffsetEntry offsetEntry = tpOffset.getValue();
    +                    offsetEntry.updateAckedState(nextCommitOffsets.get(tpOffset.getKey()));
    +                }
    +            } else {
    +                LOG.trace("No offsets to commit. {}", toString());
    +            }
    +        } catch (Exception e) {
    +            LOG.error("Exception occurred while committing to Kafka offsets of acked tuples", e);
    --- End diff --
    
    Is this common?  Which exceptions do we expect to see, and why do we want to ignore all Exceptions?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55693831
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutStreams.java ---
    @@ -0,0 +1,142 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.utils.Utils;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * Represents the output streams associated with each topic, and provides a public API to
    + * declare output streams and emmit tuples, on the appropriate stream, for all the topics specified.
    + */
    +public class KafkaSpoutStreams implements Serializable {
    +    private final Map<String, KafkaSpoutStream> topicToStream;
    +
    +    private KafkaSpoutStreams(Builder builder) {
    +        this.topicToStream = builder.topicToStream;
    +    }
    +
    +    /**
    +     * @param topic the topic for which to get output fields
    +     * @return the output fields declared
    +     */
    +    public Fields getOutputFields(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getOutputFields();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @param topic the topic to for which to get the stream id
    +     * @return the id of the stream to where the tuples are emitted
    +     */
    +    public String getStreamId(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getStreamId();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @return list of topics subscribed and emitting tuples to a stream as configured by {@link KafkaSpoutStream}
    +     */
    +    public List<String> getTopics() {
    +        return new ArrayList<>(topicToStream.keySet());
    +    }
    +
    +    void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        for (KafkaSpoutStream stream : topicToStream.values()) {
    +            declarer.declareStream(stream.getStreamId(), stream.getOutputFields());
    +        }
    +    }
    +
    +    void emit(SpoutOutputCollector collector, MessageId messageId) {
    +        collector.emit(getStreamId(messageId.topic()), messageId.getTuple(), messageId);
    --- End diff --
    
    @abhishekagarwal87 you are right that this adds a lot of memory pressure, and it would probably be good to go with a model that is closer to the original kafka spout.  I didn't spend much time looking at the original kafka spout before.
    
    But the message ID is never serialized, it will only ever remain in the spout as part of its internal book keeping.  When debug is enabled the internal spout book-keeping also keeps a copy of the tuple, but the user spout code has no way to access it.
    
    The original kafka spout kept track of failed/outstanding tuple offsets only.  When a failed tuple needed to be replayed (which was controlled by a FailedMsgRetryManager).  The entire partition would be rewound to that point and a new batch of messages would be pulled in from kafka, only tuples that had failed or had not been sent yet would be replayed.
    
    If we assume that failure are rare I don't see much of a problem with using extra Network/Disk IO to recover it, especially when compared to the common case of no failures needing to use significantly more memory.
    
    Perhaps we can make that change in a follow on JIRA.  We have had a lot of people waiting for this to come out, and this is going to be a major change to the architecture of the spout.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53700605
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    +    }
    +
    +    private void removeFromBlacklist(org.apache.storm.kafka.spout.MessageId msgId) {
    +        blackList.remove(msgId);
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            addAckedTuples(tp, msgId);
    +            // Removed acked tuples from the emittedTuples data structure
    +            emittedTuples.remove(msgId);
    +            // if this acked msg is a retry, remove it from failed data structure
    +            removeFromFailed(tp, msgId);
    +        }
    +    }
    +
    +    private void addAckedTuples(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (!acked.containsKey(tp)) {
    +                acked.put(tp, new OffsetEntry(tp));
    +            }
    +            acked.get(tp).add(msgId);
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            final TopicPartition tp = msgId.getTopicPartition();
    +            // limit to max number of retries
    +            if (msgId.numFails() >= maxRetries) {
    +                LOG.debug("Reached the maximum number of retries. Adding [{]} to list of messages to be committed to kafka", msgId);
    +                ack(msgId);
    +                removeFromFailed(tp, msgId);
    +            } else {
    +                addToFailed(tp, msgId);
    +            }
    +        }
    +    }
    +
    +    private void addToFailed(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        if (!failed.containsKey(tp)) {
    +            failed.put(tp, new HashSet<org.apache.storm.kafka.spout.MessageId>());
    +        }
    +        final Set<org.apache.storm.kafka.spout.MessageId> msgIds = failed.get(tp);
    +        if (msgIds.contains(msgId)) {      // do this to update the counter of the message
    +            msgIds.remove(msgId);
    +        }
    +        msgId.incrementNumFails();        // increment number of failures counter
    +        msgIds.add(msgId);
    +    }
    +
    +    private void removeFromFailed(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        if (failed.containsKey(tp)) {
    +            final Set<org.apache.storm.kafka.spout.MessageId> msgIds = failed.get(tp);
    +            msgIds.remove(msgId);
    +            if (msgIds.isEmpty()) {
    +                failed.remove(tp);
    +            }
    +            LOG.debug("Removing from failed list [{}, {}]", tp, msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        commitAckedTuples();
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            commitAckedTuples();
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        declarer.declareStream(kafkaSpoutStream.getStreamId(), kafkaSpoutStream.getOutputFields());
    +    }
    +
    +    // ====== Private helper methods ======
    +
    +    private void commitAckedTuples() {
    +        final Map<TopicPartition, OffsetAndMetadata> toCommitOffsets = new HashMap<>();
    +
    +        LOG.debug("Committing acked offsets to Kafka");
    +        // lock because ack and commit happen in different threads
    --- End diff --
    
    Only when the rebalance happens.  And even then we don't really need a separate lock object, we could just make this method synchronized.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-201568283
  
    @harshach @revans2 @abhishekagarwal87 I believe the current implementation addressed everything that we discussed. Can you please take a look and give some feedback.
    
    The changes to not keep ConsumerRecords state inside the Spout are in a new commit such that you can compare the two and have the flexibility to merge the initial commit (thoroughly reviewed) first, if one wants to do so.
    
    @abhishekagarwal87 I thought about this thoroughly and my conclusion is that `OutputFields` are are associated with `Streams` and the logic to `build tuples` is associated with `topics`. The current implementation supports multiple topics emitting to the same stream, whereas the previous KafkaSpout didn't. The logic to build tuples is related, but independent from the `OutputFields'. That is because we may declare 10 `OutputFields` but yet for particular topic we may wish to build tuples with only one `Field`. My current implementation follows this approach.
    
    Thanks!


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-197000251
  
    @revans2 @abhishekagarwal87 I was discussing with @harshach and for the condition if it's time to poll or not, I am going to try to impose a byte limit instead of a limit on the number of records (offsets) count.
    
    It would also be good to find better names for STREAM and BATCH, although this is what actually is happening, we could benefit of better names. Please feel free to suggest any ideas you may have. Thanks!


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by harshach <gi...@git.apache.org>.
Github user harshach commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55941248
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutStreams.java ---
    @@ -0,0 +1,142 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.utils.Utils;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * Represents the output streams associated with each topic, and provides a public API to
    + * declare output streams and emmit tuples, on the appropriate stream, for all the topics specified.
    + */
    +public class KafkaSpoutStreams implements Serializable {
    +    private final Map<String, KafkaSpoutStream> topicToStream;
    +
    +    private KafkaSpoutStreams(Builder builder) {
    +        this.topicToStream = builder.topicToStream;
    +    }
    +
    +    /**
    +     * @param topic the topic for which to get output fields
    +     * @return the output fields declared
    +     */
    +    public Fields getOutputFields(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getOutputFields();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @param topic the topic to for which to get the stream id
    +     * @return the id of the stream to where the tuples are emitted
    +     */
    +    public String getStreamId(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getStreamId();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @return list of topics subscribed and emitting tuples to a stream as configured by {@link KafkaSpoutStream}
    +     */
    +    public List<String> getTopics() {
    +        return new ArrayList<>(topicToStream.keySet());
    +    }
    +
    +    void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        for (KafkaSpoutStream stream : topicToStream.values()) {
    +            declarer.declareStream(stream.getStreamId(), stream.getOutputFields());
    +        }
    +    }
    +
    +    void emit(SpoutOutputCollector collector, MessageId messageId) {
    +        collector.emit(getStreamId(messageId.topic()), messageId.getTuple(), messageId);
    --- End diff --
    
    @abhishekagarwal87 it does keep both message and offset https://github.com/apache/storm/blob/master/external/storm-kafka/src/jvm/org/apache/storm/kafka/PartitionManager.java#L54


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54770496
  
    --- Diff: external/storm-kafka-new-consumer-api/pom.xml ---
    @@ -0,0 +1,91 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!--/**
    +        * Licensed to the Apache Software Foundation (ASF) under one
    +        * or more contributor license agreements.  See the NOTICE file
    +        * distributed with this work for additional information
    +        * regarding copyright ownership.  The ASF licenses this file
    +        * to you 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.
    +        */-->
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +    <modelVersion>4.0.0</modelVersion>
    +
    +    <parent>
    +        <artifactId>storm</artifactId>
    +        <groupId>org.apache.storm</groupId>
    +        <version>2.0.0-SNAPSHOT</version>
    +        <relativePath>../../pom.xml</relativePath>
    +    </parent>
    +
    +    <artifactId>storm-kafka-new-consumer-api</artifactId>
    +    <name>storm-kafka-new-consumer-api</name>
    +
    +    <packaging>jar</packaging>
    +
    +    <developers>
    +        <developer>
    +            <id>hmcl</id>
    +            <name>Hugo Louro</name>
    +            <email>hmclouro@gmail.com</email>
    +        </developer>
    +    </developers>
    +
    +    <dependencies>
    +        <!--parent module dependency-->
    +        <dependency>
    +            <groupId>org.apache.storm</groupId>
    +            <artifactId>storm-core</artifactId>
    +            <version>${project.version}</version>
    +            <scope>provided</scope>
    +        </dependency>
    +        <!--kafka libraries-->
    +        <dependency>
    +            <groupId>org.apache.kafka</groupId>
    +            <artifactId>${kafka.artifact.id}</artifactId>
    +            <scope>provided</scope>
    +        </dependency>
    --- End diff --
    
    This package is not actually used by the spout at all.  We can remove it and everything still compiles.  Why is it needed?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-190989626
  
    @revans2 I have made enable.auto.commit default to false.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-198048331
  
    @hmcl @harshach I have gone through the older kafka spout code in the past and I share the same thought that code is quite complex. Nothing great if it can be simplified even more. However, current kakfa spout works well and works for many. So it would be good idea to stick as much as possible to behavior of original kafka spout while at the same time leverage new consumer apis to simplify the code wherever we can. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53699919
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    +    }
    +
    +    private void removeFromBlacklist(org.apache.storm.kafka.spout.MessageId msgId) {
    +        blackList.remove(msgId);
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            addAckedTuples(tp, msgId);
    +            // Removed acked tuples from the emittedTuples data structure
    +            emittedTuples.remove(msgId);
    +            // if this acked msg is a retry, remove it from failed data structure
    +            removeFromFailed(tp, msgId);
    +        }
    +    }
    +
    +    private void addAckedTuples(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (!acked.containsKey(tp)) {
    +                acked.put(tp, new OffsetEntry(tp));
    +            }
    +            acked.get(tp).add(msgId);
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            final TopicPartition tp = msgId.getTopicPartition();
    +            // limit to max number of retries
    +            if (msgId.numFails() >= maxRetries) {
    +                LOG.debug("Reached the maximum number of retries. Adding [{]} to list of messages to be committed to kafka", msgId);
    +                ack(msgId);
    +                removeFromFailed(tp, msgId);
    +            } else {
    +                addToFailed(tp, msgId);
    +            }
    +        }
    +    }
    +
    +    private void addToFailed(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        if (!failed.containsKey(tp)) {
    +            failed.put(tp, new HashSet<org.apache.storm.kafka.spout.MessageId>());
    +        }
    +        final Set<org.apache.storm.kafka.spout.MessageId> msgIds = failed.get(tp);
    +        if (msgIds.contains(msgId)) {      // do this to update the counter of the message
    +            msgIds.remove(msgId);
    +        }
    +        msgId.incrementNumFails();        // increment number of failures counter
    +        msgIds.add(msgId);
    +    }
    +
    +    private void removeFromFailed(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        if (failed.containsKey(tp)) {
    +            final Set<org.apache.storm.kafka.spout.MessageId> msgIds = failed.get(tp);
    +            msgIds.remove(msgId);
    +            if (msgIds.isEmpty()) {
    +                failed.remove(tp);
    +            }
    +            LOG.debug("Removing from failed list [{}, {}]", tp, msgId);
    +        }
    +    }
    +
    +    // ======== Activate / Deactivate =======
    +
    +    @Override
    +    public void activate() {
    +        // Shouldn't have to do anything for now. If specific cases need to be handled logic will go here
    +    }
    +
    +    @Override
    +    public void deactivate() {
    +        commitAckedTuples();
    +    }
    +
    +    @Override
    +    public void close() {
    +        try {
    +            kafkaConsumer.wakeup();
    +            commitAckedTuples();
    +        } finally {
    +            //remove resources
    +            kafkaConsumer.close();
    +        }
    +    }
    +
    +    @Override
    +    public void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        declarer.declareStream(kafkaSpoutStream.getStreamId(), kafkaSpoutStream.getOutputFields());
    +    }
    +
    +    // ====== Private helper methods ======
    +
    +    private void commitAckedTuples() {
    +        final Map<TopicPartition, OffsetAndMetadata> toCommitOffsets = new HashMap<>();
    +
    +        LOG.debug("Committing acked offsets to Kafka");
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            for (TopicPartition tp : acked.keySet()) {
    +                final OffsetEntry offsetEntry = acked.get(tp);
    +                OffsetAndMetadata offsetAndMetadata = offsetEntry.findOffsetToCommit();
    +                if (offsetAndMetadata != null) {
    +                    toCommitOffsets.put(tp, offsetAndMetadata);
    +                }
    +            }
    +
    +            if (!toCommitOffsets.isEmpty()) {
    +                kafkaConsumer.commitSync(toCommitOffsets);
    +                LOG.debug("Offsets successfully committed to Kafka [{]}", toCommitOffsets);
    +            }
    +
    +            // Instead of iterating again, we could commit each TopicPartition in the prior loop,
    +            // but the multiple networks calls should be more expensive than iteration twice over a small loop
    +            for (TopicPartition tp : acked.keySet()) {
    +                OffsetEntry offsetEntry = acked.get(tp);
    +                offsetEntry.updateAckedState(toCommitOffsets.get(tp));
    +                updateAckedState(tp, offsetEntry);
    +            }
    +        } catch (Exception e) {
    +            LOG.error("Exception occurred while committing to Kafka offsets for acked tuples ", e);
    +        } finally {
    +            commit = false;
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    private void updateAckedState(TopicPartition tp, OffsetEntry offsetEntry) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (offsetEntry.isEmpty()) {
    +                acked.remove(tp);
    +            }
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======= Offsets Commit Management ==========
    +
    +    private static class OffsetComparator implements Comparator<org.apache.storm.kafka.spout.MessageId> {
    +        public int compare(org.apache.storm.kafka.spout.MessageId m1, org.apache.storm.kafka.spout.MessageId m2) {
    +            return m1.offset() < m2.offset() ? -1 : m1.offset() == m2.offset() ? 0 : 1;
    +        }
    +    }
    +
    +    /** This class is not thread safe */
    +    // Although this class is called by multiple (2) threads, all the calling methods are properly synchronized
    +    private class OffsetEntry {
    +        private long committedOffset;          // last offset committed to Kafka
    +        private long toCommitOffset;                // last offset to be committed in the next commit operation
    +        private final Set<org.apache.storm.kafka.spout.MessageId> ackedMsgs = new TreeSet<>(OFFSET_COMPARATOR);     // sort messages by ascending order of offset
    +        private Set<org.apache.storm.kafka.spout.MessageId> toCommitMsgs = new TreeSet<>(OFFSET_COMPARATOR);        // Messages that contain the offsets to be committed in the next commit operation
    +
    +        public OffsetEntry(TopicPartition tp) {
    +            OffsetAndMetadata committed = kafkaConsumer.committed(tp);
    +            committedOffset = committed == null ? -1 : committed.offset();
    +            LOG.debug("Created OffsetEntry for [topic-partition={}, last-committed-offset={}]", tp, committedOffset);
    +        }
    +
    +        public void add(org.apache.storm.kafka.spout.MessageId msgId) {          // O(Log N)
    +            ackedMsgs.add(msgId);
    +        }
    +
    +        /**
    +         * This method has side effects. The method updateAckedState should be called after this method.
    +         */
    +        public OffsetAndMetadata findOffsetToCommit() {
    +            long currOffset;
    +            OffsetAndMetadata offsetAndMetadata = null;
    +            toCommitMsgs = new TreeSet<>(OFFSET_COMPARATOR);
    +            toCommitOffset = committedOffset;
    +            org.apache.storm.kafka.spout.MessageId toCommitMsg = null;
    +
    +            for (org.apache.storm.kafka.spout.MessageId ackedMsg : ackedMsgs) {  // for K matching messages complexity is K*(Log*N). K <= N
    +                if ((currOffset = ackedMsg.offset()) == toCommitOffset || currOffset == toCommitOffset + 1) {    // found the next offset to commit
    +                    toCommitMsgs.add(ackedMsg);
    +                    toCommitMsg = ackedMsg;
    +                    toCommitOffset = currOffset;
    +                } else if (ackedMsg.offset() > toCommitOffset + 1) {    // offset found is not continuous to the offsets listed to go in the next commit, so stop search
    +                    break;
    +                } else {
    +                    LOG.debug("Unexpected offset found [{]}. {}", ackedMsg.offset(), toString());
    +                    break;
    +                }
    +            }
    +
    +            if (!toCommitMsgs.isEmpty()) {
    +                offsetAndMetadata = new OffsetAndMetadata(toCommitOffset, toCommitMsg.getMetadata(Thread.currentThread()));
    +                LOG.debug("Last offset to be committed in the next commit call: [{]}", offsetAndMetadata.offset());
    +                LOG.trace(toString());
    +            } else {
    +                LOG.debug("No offsets found ready to commit");
    +                LOG.trace(toString());
    +            }
    +            return offsetAndMetadata;
    +        }
    +
    +        /**
    +         * This method has side effects and should be called after findOffsetToCommit
    +         */
    +        public void updateAckedState(OffsetAndMetadata offsetAndMetadata) {
    +            if (offsetAndMetadata != null) {
    +                committedOffset = offsetAndMetadata.offset();
    +                toCommitMsgs = new TreeSet<>(OFFSET_COMPARATOR);
    +                ackedMsgs.removeAll(toCommitMsgs);
    +            }
    +        }
    +
    +        public boolean isEmpty() {
    +            return ackedMsgs.isEmpty();
    +        }
    +
    +        @Override
    +        public String toString() {
    +            return "OffsetEntry{" +
    +                    "committedOffset=" + committedOffset +
    +                    ", toCommitOffset=" + toCommitOffset +
    +                    ", ackedMsgs=" + ackedMsgs +
    +                    ", toCommitMsgs=" + toCommitMsgs +
    +                    '}';
    +        }
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            commitAckedTuples();  // commit acked records,
    +            copyEmittedTuplesToBlackList(); // all the tuples that are in traffic when the rebalance occurs will be added to black list to avoid duplication
    +            clearFailed();   // remove all failed tuples form list to avoid duplication
    +            clearEmittedTuples();    // clear emitted tuples
    --- End diff --
    
    Committing the acked tuples is done with a lock, but copying to the blacklist followed by clearing the others are not thread safe.  In fact the references to the maps are not volatile, so we could lose track of some tuples when this happens.
    
    I can see that adding locking to the critical path could be costly, but I would rather do that then risk having state get in an odd place.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55320414
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutStreams.java ---
    @@ -0,0 +1,142 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.utils.Utils;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * Represents the output streams associated with each topic, and provides a public API to
    + * declare output streams and emmit tuples, on the appropriate stream, for all the topics specified.
    + */
    +public class KafkaSpoutStreams implements Serializable {
    +    private final Map<String, KafkaSpoutStream> topicToStream;
    +
    +    private KafkaSpoutStreams(Builder builder) {
    +        this.topicToStream = builder.topicToStream;
    +    }
    +
    +    /**
    +     * @param topic the topic for which to get output fields
    +     * @return the output fields declared
    +     */
    +    public Fields getOutputFields(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getOutputFields();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @param topic the topic to for which to get the stream id
    +     * @return the id of the stream to where the tuples are emitted
    +     */
    +    public String getStreamId(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getStreamId();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @return list of topics subscribed and emitting tuples to a stream as configured by {@link KafkaSpoutStream}
    +     */
    +    public List<String> getTopics() {
    +        return new ArrayList<>(topicToStream.keySet());
    +    }
    +
    +    void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        for (KafkaSpoutStream stream : topicToStream.values()) {
    +            declarer.declareStream(stream.getStreamId(), stream.getOutputFields());
    +        }
    +    }
    +
    +    void emit(SpoutOutputCollector collector, MessageId messageId) {
    +        collector.emit(getStreamId(messageId.topic()), messageId.getTuple(), messageId);
    --- End diff --
    
    messageId contains the tuple object as well. This would almost double the data that was flowing in the older spout. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by harshach <gi...@git.apache.org>.
Github user harshach commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53700660
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,457 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.apache.storm.tuple.Values;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ScheduledExecutorService;
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.locks.Lock;
    +import java.util.concurrent.locks.ReentrantLock;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<org.apache.storm.kafka.spout.MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    private Map conf;
    +    private TopologyContext context;
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final org.apache.storm.kafka.spout.KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +
    +    // Bookkeeping
    +    private org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream;
    +    private org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient ScheduledExecutorService commitOffsetsTask;
    +    private transient Lock ackCommitLock;
    +    private transient volatile boolean commit;
    +    private transient Map<org.apache.storm.kafka.spout.MessageId, Values> emittedTuples;           // Keeps a list of emitted tuples that are pending being acked or failed
    +    private transient Map<TopicPartition, Set<org.apache.storm.kafka.spout.MessageId>> failed;     // failed tuples. They stay in this list until success or max retries is reached
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient Set<org.apache.storm.kafka.spout.MessageId> blackList;                       // all the tuples that are in traffic when the rebalance occurs will be added to black list to be disregarded when they are either acked or failed
    +    private transient int maxRetries;
    +
    +    public KafkaSpout(org.apache.storm.kafka.spout.KafkaSpoutConfig<K,V> kafkaSpoutConfig, org.apache.storm.kafka.spout.KafkaSpoutStream kafkaSpoutStream, org.apache.storm.kafka.spout.KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStream = kafkaSpoutStream;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        // Spout internals
    +        this.conf = conf;
    +        this.context = context;
    +        this.collector = collector;
    +
    +        // Bookkeeping objects
    +        emittedTuples = new HashMap<>();
    +        failed = new HashMap<>();
    +        acked = new HashMap<>();
    +        blackList = new HashSet<>();
    +        ackCommitLock = new ReentrantLock();
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +
    +        // Create commit offsets task
    +        if (!kafkaSpoutConfig.isConsumerAutoCommitMode()) {     // If it is auto commit, no need to commit offsets manually
    +            createCommitOffsetsTask();
    +        }
    +    }
    +
    +    // ======== Commit Offsets Task =======
    +
    +    private void createCommitOffsetsTask() {
    +        commitOffsetsTask = Executors.newSingleThreadScheduledExecutor(commitOffsetsThreadFactory());
    +        commitOffsetsTask.scheduleAtFixedRate(new Runnable() {
    +            @Override
    +            public void run() {
    +                commit = true;
    +            }
    +        }, 1000, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +    }
    +
    +    private ThreadFactory commitOffsetsThreadFactory() {
    +        return new ThreadFactory() {
    +            @Override
    +            public Thread newThread(Runnable r) {
    +                return new Thread(r, "kafka-spout-commit-offsets-thread");
    +            }
    +        };
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if(commit) {
    +            commitAckedTuples();
    +        } else if (retry()) {              // Don't process new tuples until the failed tuples have all been acked
    +            retryFailedTuples();
    +        } else {
    +            emitTuples(poll());
    +        }
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{]} records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    +            for (ConsumerRecord<K, V> record : records) {
    +                final Values tuple = tupleBuilder.buildTuple(record);
    +                final org.apache.storm.kafka.spout.MessageId messageId = new org.apache.storm.kafka.spout.MessageId(record);                                  // TODO don't create message for non acking mode. Should we support non acking mode?
    +                collector.emit(kafkaSpoutStream.getStreamId(), tuple, messageId);           // emits one tuple per record
    +                emittedTuples.put(messageId, tuple);
    +                LOG.info("HMCL - Emitted tuple for record {}", record);
    +            }
    +        }
    +    }
    +
    +    private boolean retry() {
    +        return failed.size() > 0;
    +    }
    +
    +    private void retryFailedTuples() {
    +        for (TopicPartition tp : failed.keySet()) {
    +            for (org.apache.storm.kafka.spout.MessageId msgId : failed.get(tp)) {
    +                if (isInBlackList(msgId)) {
    +                    removeFromBlacklist(msgId);
    +                    removeFromFailed(tp, msgId);
    +                } else {
    +                    final Values tuple = emittedTuples.get(msgId);
    +                    LOG.debug("Retrying tuple. [msgId={}, tuple={}]", msgId, tuple);
    +                    collector.emit(kafkaSpoutStream.getStreamId(), tuple, msgId);
    +                }
    +            }
    +        }
    +    }
    +
    +    // all the tuples that are in traffic when the rebalance occurs will be added
    +    // to black list to be disregarded when they are either acked or failed
    +    private boolean isInBlackList(org.apache.storm.kafka.spout.MessageId msgId) {
    +        return blackList.contains(msgId);
    +    }
    +
    +    private void removeFromBlacklist(org.apache.storm.kafka.spout.MessageId msgId) {
    +        blackList.remove(msgId);
    +    }
    +
    +    // ======== Ack =======
    +
    +    @Override
    +    public void ack(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +        final TopicPartition tp = msgId.getTopicPartition();
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            addAckedTuples(tp, msgId);
    +            // Removed acked tuples from the emittedTuples data structure
    +            emittedTuples.remove(msgId);
    +            // if this acked msg is a retry, remove it from failed data structure
    +            removeFromFailed(tp, msgId);
    +        }
    +    }
    +
    +    private void addAckedTuples(TopicPartition tp, org.apache.storm.kafka.spout.MessageId msgId) {
    +        // lock because ack and commit happen in different threads
    +        ackCommitLock.lock();
    +        try {
    +            if (!acked.containsKey(tp)) {
    +                acked.put(tp, new OffsetEntry(tp));
    +            }
    +            acked.get(tp).add(msgId);
    +        } finally {
    +            ackCommitLock.unlock();
    +        }
    +    }
    +
    +    // ======== Fail =======
    +
    +    @Override
    +    public void fail(Object messageId) {
    +        final org.apache.storm.kafka.spout.MessageId msgId = (org.apache.storm.kafka.spout.MessageId) messageId;
    +
    +        if (isInBlackList(msgId)) {
    +            removeFromBlacklist(msgId);
    +        } else {
    +            final TopicPartition tp = msgId.getTopicPartition();
    +            // limit to max number of retries
    +            if (msgId.numFails() >= maxRetries) {
    +                LOG.debug("Reached the maximum number of retries. Adding [{]} to list of messages to be committed to kafka", msgId);
    +                ack(msgId);
    +                removeFromFailed(tp, msgId);
    +            } else {
    +                addToFailed(tp, msgId);
    --- End diff --
    
    @revans2 I think we should just re-emit the tuples by default and provide an option to add retries. We've seen in several cases where a single failed tuple can get into long retry loop and stop the topology from progressing. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54771740
  
    --- Diff: pom.xml ---
    @@ -836,14 +839,39 @@
                     <scope>test</scope>
                 </dependency>
                 <dependency>
    -              <groupId>org.apache.calcite</groupId>
    -              <artifactId>calcite-core</artifactId>
    -              <version>${calcite.version}</version>
    +                <groupId>org.apache.calcite</groupId>
    +                <artifactId>calcite-core</artifactId>
    +                <version>${calcite.version}</version>
    +            </dependency>
    +            <dependency>
    +                <groupId>com.fasterxml.jackson.core</groupId>
    +                <artifactId>jackson-databind</artifactId>
    +                <version>${jackson.version}</version>
    +            </dependency>
    +            <dependency>
    +                <groupId>org.apache.kafka</groupId>
    +                <artifactId>${kafka.artifact.id}</artifactId>
    +                <version>${kafka.version}</version>
    +                <exclusions>
    +                    <exclusion>
    +                        <groupId>org.apache.zookeeper</groupId>
    +                        <artifactId>zookeeper</artifactId>
    +                    </exclusion>
    +                    <exclusion>
    +                        <groupId>log4j</groupId>
    +                        <artifactId>log4j</artifactId>
    +                    </exclusion>
    +                    <exclusion>
    +                        <groupId>org.slf4j</groupId>
    +                        <artifactId>slf4j-log4j12</artifactId>
    +                    </exclusion>
    +                </exclusions>
                 </dependency>
                 <dependency>
    -              <groupId>com.fasterxml.jackson.core</groupId>
    -              <artifactId>jackson-databind</artifactId>
    -              <version>${jackson.version}</version>
    +                <groupId>org.apache.kafka</groupId>
    +                <artifactId>kafka-clients</artifactId>
    +                <version>${kafka.version}</version>
    +                <scope>provided</scope>
    --- End diff --
    
    Actually I looked a bit more at this and I don't think we want kafka-clients or kafka itself to be marked as provided.
    
    The `provided` scope tells the shade plugin or the assembly plugin when it is creating the uber topology jar to not include this dependency in the uber jar, because it will already be on the classpath.  If we do this for the kafka libraries any topology pom.xml will either have to explicitly include their own version of kafka that is not `provided` or the topology jar will just not run.  Looking at storm-starter because of this it does not have kafka in the uber jar so running any of the kafka examples will fail.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by ptgoetz <gi...@git.apache.org>.
Github user ptgoetz commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-204619936
  
    This has been merged into the 1.x branch and will be included in the 1.0 release.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by knusbaum <gi...@git.apache.org>.
Github user knusbaum commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-190890638
  
    @hmcl I'm copying the jar out of storm-kafka-new-consumer/target/ and trying to run `storm jar` on it.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r57803066
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutRetryService.java ---
    @@ -0,0 +1,69 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.TopicPartition;
    +
    +import java.io.Serializable;
    +import java.util.Collection;
    +import java.util.Set;
    +
    +public interface KafkaSpoutRetryService extends Serializable {
    +    /**
    +     * Schedules this {@link KafkaSpoutMessageId} if not yet scheduled, or updates retry time if it has already been scheduled.
    +     * @param msgId message to schedule for retrial
    +     */
    +    void schedule(KafkaSpoutMessageId msgId);
    +
    +    /**
    +     * Removes a message from the list of messages scheduled for retrial
    +     * @param msgId message to remove from retrial
    +     */
    +    boolean remove(KafkaSpoutMessageId msgId);
    +
    +    /**
    +     * Removes all the messages whose {@link TopicPartition} does NOT belong to the specified {@code Collection<TopicPartition>}.
    +     * All messages that come from a {@link TopicPartition} existing in the collection will be kept.
    +     * This method is useful to cleanup state following partition rebalance.
    +     * @param topicPartitions Collection of {@link TopicPartition} for which to keep messages
    +     * @return true if at least one message was removed, false otherwise
    +     */
    +    boolean remove(Collection<TopicPartition> topicPartitions);
    +
    +    /**
    +     * @return set of topic partitions that have offsets that are ready to be retried, i.e.,
    +     * for which a tuple has failed and has retry time less than current time
    +     */
    +    Set<TopicPartition> retriableTopicPartitions();
    +
    +    /**
    +     * Checks if a specific failed {@link KafkaSpoutMessageId} is is ready to be retried,
    +     * i.e is scheduled and has retry time that is less than current time.
    +     * @return true if message is ready to be retried, false otherwise
    +     */
    +    boolean retry(KafkaSpoutMessageId msgId);
    +
    +    /**
    +     * Checks if a specific failed {@link KafkaSpoutMessageId} is scheduled to be retried.
    +     * The message may or may not be ready to be retried yet.
    +     * @return true if the message is scheduled to be retried, regardless of being or not ready to be retried.
    +     * Returns false is this message is not scheduled for retrial
    +     */
    +    boolean scheduled(KafkaSpoutMessageId msgId);
    --- End diff --
    
    DONE


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-222308004
  
    @hmcl, 1, work load is not distributed well is not because of the spout, that is a kafka cluster setup issue and now is resolved 2, for the other two, I dig into the log(sent to your via email) - seems everytime when a re-balance happens, the spout seek to a bigger offset than the committed offset in this partition, per my understanding, this will cause some message not be able to consumed/emitted, so all the log show "Non continuous offset found"
    user spout setting is: firstPollOffsetStrategy=UNCOMMITTED_LATEST, pollTimeoutMs=2000, offsetCommitPeriodMs=10000, maxRetries=2147483647
    I know firstPollOffsetStrategy cannot be EARLIEST or LATEST, but seems to me UNCOMMITTED_LATEST should not cause this issue.
    I asked user to try UNCOMMITTED_EARLIEST and now seems the issue does NOT happen again as per the log, though it may happen later...
    From the code perspective, i cannot understand why the weird behavior happened, could you help?
    
    Also, per our previous testing - we find once - a worker died and re balance happened, we find one spout(not in the died worker) have some message not acked or failed back. That also caused the "Non continuous offset found" show many times in the log,  which will cause no message will be committed to kafka. The only solution will be restart the storm topology.
    
    We emit message in this way - kafkaSpoutStreams.emit(collector, tuple, msgId); Could you please help confirm - **storm would ensure  all the messages that emitted by the spout will be acked/failed back without exception?** Because if this is not the case, the spout will not be able to find the continuous offset to commit, then we must fix this issue urgently as we plan to release the change early next month. Please help advise. thanks!


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r57770615
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,292 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    // Kafka consumer configuration
    +    private final Map<String, Object> kafkaProps;
    +    private final Deserializer<K> keyDeserializer;
    +    private final Deserializer<V> valueDeserializer;
    +    private final long pollTimeoutMs;
    +
    +    // Kafka spout configuration
    +    private final long offsetCommitPeriodMs;
    +    private final int maxRetries;
    +    private final int maxUncommittedOffsets;
    +    private final FirstPollOffsetStrategy firstPollOffsetStrategy;
    +    private final KafkaSpoutStreams kafkaSpoutStreams;
    +    private final KafkaSpoutTuplesBuilder<K, V> tuplesBuilder;
    +    private final KafkaSpoutRetryService retryService;
    +
    +    private KafkaSpoutConfig(Builder<K,V> builder) {
    +        this.kafkaProps = setDefaultsAndGetKafkaProps(builder.kafkaProps);
    +        this.keyDeserializer = builder.keyDeserializer;
    +        this.valueDeserializer = builder.valueDeserializer;
    +        this.pollTimeoutMs = builder.pollTimeoutMs;
    +        this.offsetCommitPeriodMs = builder.offsetCommitPeriodMs;
    +        this.maxRetries = builder.maxRetries;
    +        this.firstPollOffsetStrategy = builder.firstPollOffsetStrategy;
    +        this.kafkaSpoutStreams = builder.kafkaSpoutStreams;
    +        this.maxUncommittedOffsets = builder.maxUncommittedOffsets;
    +        this.tuplesBuilder = builder.tuplesBuilder;
    +        this.retryService = builder.retryService;
    +    }
    +
    +    private Map<String, Object> setDefaultsAndGetKafkaProps(Map<String, Object> kafkaProps) {
    +        // set defaults for properties not specified
    +        if (!kafkaProps.containsKey(Consumer.ENABLE_AUTO_COMMIT)) {
    +            kafkaProps.put(Consumer.ENABLE_AUTO_COMMIT, "false");
    +        }
    +        return kafkaProps;
    +    }
    +
    +    public static class Builder<K,V> {
    +        private final Map<String, Object> kafkaProps;
    +        private Deserializer<K> keyDeserializer;
    +        private Deserializer<V> valueDeserializer;
    +        private long pollTimeoutMs = DEFAULT_POLL_TIMEOUT_MS;
    +        private long offsetCommitPeriodMs = DEFAULT_OFFSET_COMMIT_PERIOD_MS;
    +        private int maxRetries = DEFAULT_MAX_RETRIES;
    +        private FirstPollOffsetStrategy firstPollOffsetStrategy = FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +        private final KafkaSpoutStreams kafkaSpoutStreams;
    +        private int maxUncommittedOffsets = DEFAULT_MAX_UNCOMMITTED_OFFSETS;
    +        private final KafkaSpoutTuplesBuilder<K, V> tuplesBuilder;
    +        private final KafkaSpoutRetryService retryService;
    +
    +        /***
    +         * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    +         * The optional configuration can be specified using the set methods of this builder
    +         * @param kafkaProps    properties defining consumer connection to Kafka broker as specified in @see <a href="http://kafka.apache.org/090/javadoc/index.html?org/apache/kafka/clients/consumer/KafkaConsumer.html">KafkaConsumer</a>
    +         * @param kafkaSpoutStreams    streams to where the tuples are emitted for each tuple. Multiple topics can emit in the same stream.
    +         */
    +        public Builder(Map<String, Object> kafkaProps, KafkaSpoutStreams kafkaSpoutStreams,
    +                       KafkaSpoutTuplesBuilder<K,V> tuplesBuilder, KafkaSpoutRetryService retryService) {
    --- End diff --
    
    Would it be possible to have retryService be optional with a default instead of forceing everyone to pass in an argument.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-217894464
  
    @jianbzhou thanks for your feedback. Let me take a look at this and I will get back to you shortly.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55328743
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,458 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K, V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed periodically when the timer expires, on consumer rebalance, or on close/deactivate
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig, KafkaTupleBuilder<K, V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitFreqMs(), TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            if(!consumerAutoCommitMode) {
    +                acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            }
    +
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // do nothing - by default polling starts at the last committed offset
    +                    fetchOffset = committedOffset.offset();
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.isExpiredResetOnTrue();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());
    +
    +            for (final ConsumerRecord<K, V> record : records) {
    +                if (record.offset() == 0 || consumerAutoCommitMode || record.offset() > acked.get(tp).committedOffset) {      // The first poll includes the last committed offset. This if avoids duplication
    +                    final List<Object> tuple = tupleBuilder.buildTuple(record);
    +                    final MessageId messageId = new MessageId(record, tuple);
    +
    +                    kafkaSpoutStreams.emit(collector, messageId);           // emits one tuple per record
    +                    LOG.debug("Emitted tuple [{}] for record [{}]", tuple, record);
    +                }
    +            }
    +        }
    +    }
    +
    +    private void commitOffsetsForAckedTuples() {
    +        // Find offsets that are ready to be committed for every topic partition
    +        final Map<TopicPartition, OffsetAndMetadata> nextCommitOffsets = new HashMap<>();
    +        for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +            final OffsetAndMetadata nextCommitOffset = tpOffset.getValue().findNextCommitOffset();
    +            if (nextCommitOffset != null) {
    +                nextCommitOffsets.put(tpOffset.getKey(), nextCommitOffset);
    +            }
    +        }
    +
    +        // Commit offsets that are ready to be committed for every topic partition
    +        if (!nextCommitOffsets.isEmpty()) {
    +            kafkaConsumer.commitSync(nextCommitOffsets);
    +            LOG.debug("Offsets successfully committed to Kafka [{}]", nextCommitOffsets);
    +            // Instead of iterating again, it would be possible to commit and update the state for each TopicPartition
    +            // in the prior loop, but the multiple network calls should be more expensive than iterating twice over a small loop
    +            for (Map.Entry<TopicPartition, OffsetEntry> tpOffset : acked.entrySet()) {
    +                final OffsetEntry offsetEntry = tpOffset.getValue();
    +                offsetEntry.commit(nextCommitOffsets.get(tpOffset.getKey()));
    +            }
    +        } else {
    +            LOG.trace("No offsets to commit. {}", toString());
    +        }
    +    }
    +
    +    // ======== Ack =======
    +    @Override
    +    public void ack(Object messageId) {
    +        if (!consumerAutoCommitMode) {  // Only need to keep track of acked tuples if commits are not done automatically
    --- End diff --
    
    so if consumerAutoCommitMode is enabled, there is no at least once or at most once processing guarantee? It can be documented if so. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-194880255
  
    @kishorvpatil I think you are confusing our internal code with open source code.  We have been using an early patch of kafka with security, that supported security on the older kafka consumer API, and added a corresponding patch to the kafka-spout to support it.  When security finally was officially merged into kafka only the new consumer API was supported, and it can be configured completely through the properties/the broker URI.
    
    http://docs.confluent.io/2.0.0/kafka/ssl.html#configuring-kafka-clients
    
    as such `securityProtocol` is not needed any more, and was actually never in open source.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-191002473
  
    
    @knusbaum you must either create a uber jar or put kafka-clients/0.9.0.0/kafka-clients-0.9.0.0.jar under extlib


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56466552
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaRecordTupleBuilder.java ---
    @@ -0,0 +1,44 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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,
    + *   See the License for the specific language governing permissions and
    + *   limitations under the License.
    + */
    +package org.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.tuple.Values;
    +
    +import java.util.List;
    +
    +public class KafkaRecordTupleBuilder<K, V> implements KafkaSpoutTupleBuilder<K, V> {
    +    @Override
    +    public List<Object> buildTuple(final ConsumerRecord<K, V> consumerRecord, KafkaSpoutStreams kafkaSpoutStreams) {
    --- End diff --
    
    @abhishekagarwal87 I disagree. The schema is a subset of the output fields, which are  "in a sense"  part of the stream definition. To build a tuple, one may or may not need access to the output fields information (in the example I provide this info is only used the count the num of output fields). 
    
    This information is available in the `KafkaSpoutStreams` "stream definition" object, and makes no sense to be passing it around in a different object for several reasons. 
    1. Redundancy. 
    2. It is easier to make mistakes that cannot be found at compile time (e.g. using a  field in the schema that does not exist in the output fields)


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-189923836
  
    @tgravescs @revans2 I have pushed the latest changes. Please let me know of any feedback or further requirements you may have. Thanks.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by satishd <gi...@git.apache.org>.
Github user satishd commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-193353011
  
    @revans2 @hmcl putIfAbsent issue on JDK 7 can be avoided if it is changed from Map to ConcurrentMap.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r57791718
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,547 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Collections;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Iterator;
    +import java.util.LinkedList;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<KafkaSpoutMessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private transient KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +
    +
    +    // Bookkeeping
    +    private transient int maxRetries;                                   // Max number of times a tuple is retried
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;  // Strategy to determine the fetch offset of the first realized by the spout upon activation
    +    private transient KafkaSpoutRetryService retryService;              // Class that has the logic to handle tuple failure
    +    private transient Timer commitTimer;                                // timer == null for auto commit mode
    +    private transient boolean initialized;                              // Flag indicating that the spout is still undergoing initialization process.
    +    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +    private KafkaSpoutStreams kafkaSpoutStreams;                        // Object that wraps all the logic to declare output fields and emit tuples
    +    private transient KafkaSpoutTuplesBuilder<K, V> tuplesBuilder;      // Object that contains the logic to build tuples for each ConsumerRecord
    +
    +    private transient Map<TopicPartition, OffsetEntry> acked;           // Tuples that were successfully acked. These tuples will be committed periodically when the commit timer expires, after consumer rebalance, or on close/deactivate
    +    private transient Set<KafkaSpoutMessageId> emitted;                 // Tuples that have been emitted but that are "on the wire", i.e. pending being acked or failed
    +    private transient Iterator<ConsumerRecord<K, V>> waitingToEmit;         // Records that have been polled and are queued to be emitted in the nextTuple() call. One record is emitted per nextTuple()
    +    private transient long numUncommittedOffsets;                       // Number of offsets that have been polled and emitted but not yet been committed
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +        numUncommittedOffsets = 0;
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        // Retries management
    +        retryService = kafkaSpoutConfig.getRetryService();
    +
    +        // Tuples builder delegate
    +        tuplesBuilder = kafkaSpoutConfig.getTuplesBuilder();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            commitTimer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitPeriodMs(), TimeUnit.MILLISECONDS);
    +        }
    +
    +        acked = new HashMap<>();
    +        emitted = new HashSet<>();
    +        waitingToEmit = Collections.emptyListIterator();
    +
    +        LOG.info("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            if (!consumerAutoCommitMode) {
    +                acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            }
    +
    +            retryService.remove(partitions);
    +
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // By default polling starts at the last committed offset. +1 to point fetch to the first uncommitted offset.
    +                    fetchOffset = committedOffset.offset() + 1;
    +                    kafkaConsumer.seek(tp, fetchOffset);
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            }
    +
    +            if (poll()) {
    +                setWaitingToEmit(pollKafkaBroker());
    +            }
    +
    +            if (waitingToEmit()) {
    +                emit();
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && commitTimer.isExpiredResetOnTrue();    // timer != null for non auto commit mode
    +    }
    +
    +    private boolean poll() {
    +        return !waitingToEmit() && numUncommittedOffsets < kafkaSpoutConfig.getMaxUncommittedOffsets();
    +    }
    +
    +    private boolean waitingToEmit() {
    +        return waitingToEmit != null && waitingToEmit.hasNext();
    +    }
    +
    +    public void setWaitingToEmit(ConsumerRecords<K,V> consumerRecords) {
    +        List<ConsumerRecord<K,V>> waitingToEmitList = new LinkedList<>();
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            waitingToEmitList.addAll(consumerRecords.records(tp));
    +        }
    +        waitingToEmit = waitingToEmitList.iterator();
    +        LOG.trace("Records waiting to be emitted {}", waitingToEmitList);
    +    }
    +
    +    // ======== poll =========
    +    private ConsumerRecords<K, V> pollKafkaBroker() {
    +        doSeekRetriableTopicPartitions();
    +
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        final int numPolledRecords = consumerRecords.count();
    +        LOG.debug("Polled [{}] records from Kafka. NumUncommittedOffsets=[{}]", numPolledRecords, numUncommittedOffsets);
    +        return consumerRecords;
    +    }
    +
    +    private void doSeekRetriableTopicPartitions() {
    +        final Set<TopicPartition> retriableTopicPartitions = retryService.retriableTopicPartitions();
    +
    +        for (TopicPartition rtp : retriableTopicPartitions) {
    +            final OffsetAndMetadata offsetAndMeta = acked.get(rtp).findNextCommitOffset();
    +            if (offsetAndMeta != null) {
    +                kafkaConsumer.seek(rtp, offsetAndMeta.offset() + 1);  // seek to the next offset that is ready to commit in next commit cycle
    +            } else {
    +                kafkaConsumer.seekToEnd(rtp);    // Seek to last committed offset
    +            }
    +        }
    +    }
    +
    +    // ======== emit  =========
    +    private void emit() {
    +        emitTupleIfNotEmitted(waitingToEmit.next());
    +        waitingToEmit.remove();
    --- End diff --
    
    @revans2 can you please detail what you mean by too slow? In the previous implementation we had a `for` loop iterating over all the records fetched (polled) and emitting all of those records during a single call to `nextTuple()`. You suggested that we send one tuple only per call to `nextTuple()`, which is what it's done in the current implementation. It has to wait at most the amount of time between two consecutive calls to `nextTuple()` before either fetching (polling) or emitting the next record. What do you mean by very slow?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-222278660
  
    @connieyang I am finishing addressing some issues brought up by the initial users of this kafka spout, as well as unit test coverage, and will push the trident API right after.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56408115
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    @revans2 I too would prefer to trade off memory footprint with more kafka fetch calls since it will happen only in case of failures. I was suggesting a method to have the option to do so without losing in-memory replay. If we are doing away with in-memory replay completely, that is even better and cleaner. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54773759
  
    --- Diff: external/storm-kafka-new-consumer-api/pom.xml ---
    @@ -0,0 +1,91 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!--/**
    +        * Licensed to the Apache Software Foundation (ASF) under one
    +        * or more contributor license agreements.  See the NOTICE file
    +        * distributed with this work for additional information
    +        * regarding copyright ownership.  The ASF licenses this file
    +        * to you 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.
    +        */-->
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    +    <modelVersion>4.0.0</modelVersion>
    +
    +    <parent>
    +        <artifactId>storm</artifactId>
    +        <groupId>org.apache.storm</groupId>
    +        <version>2.0.0-SNAPSHOT</version>
    +        <relativePath>../../pom.xml</relativePath>
    +    </parent>
    +
    +    <artifactId>storm-kafka-new-consumer-api</artifactId>
    --- End diff --
    
    I don't really like the name of this artifact.  I realize that it describes what it is, but kafka's new consumer api will become old eventually.  I'm not sure of a better name, but perhaps storm-kafka-clients might be better, because we are going through kafka-clients instead of kafka itself.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by ptgoetz <gi...@git.apache.org>.
Github user ptgoetz commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-203633842
  
    +1 for merging.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by abhishekagarwal87 <gi...@git.apache.org>.
Github user abhishekagarwal87 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55942561
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutStreams.java ---
    @@ -0,0 +1,142 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.tuple.Fields;
    +import org.apache.storm.utils.Utils;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * Represents the output streams associated with each topic, and provides a public API to
    + * declare output streams and emmit tuples, on the appropriate stream, for all the topics specified.
    + */
    +public class KafkaSpoutStreams implements Serializable {
    +    private final Map<String, KafkaSpoutStream> topicToStream;
    +
    +    private KafkaSpoutStreams(Builder builder) {
    +        this.topicToStream = builder.topicToStream;
    +    }
    +
    +    /**
    +     * @param topic the topic for which to get output fields
    +     * @return the output fields declared
    +     */
    +    public Fields getOutputFields(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getOutputFields();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @param topic the topic to for which to get the stream id
    +     * @return the id of the stream to where the tuples are emitted
    +     */
    +    public String getStreamId(String topic) {
    +        if (topicToStream.containsKey(topic)) {
    +            return topicToStream.get(topic).getStreamId();
    +        }
    +        throw new IllegalStateException(this.getClass().getName() + " not configured for topic: " + topic);
    +    }
    +
    +    /**
    +     * @return list of topics subscribed and emitting tuples to a stream as configured by {@link KafkaSpoutStream}
    +     */
    +    public List<String> getTopics() {
    +        return new ArrayList<>(topicToStream.keySet());
    +    }
    +
    +    void declareOutputFields(OutputFieldsDeclarer declarer) {
    +        for (KafkaSpoutStream stream : topicToStream.values()) {
    +            declarer.declareStream(stream.getStreamId(), stream.getOutputFields());
    +        }
    +    }
    +
    +    void emit(SpoutOutputCollector collector, MessageId messageId) {
    +        collector.emit(getStreamId(messageId.topic()), messageId.getTuple(), messageId);
    --- End diff --
    
    if my understanding is correct, that too is bounded by KafkaConfig.fetchSizeBytes (1 MB by default). And they are not stored in buffer for long, but emitted at the rate, nextTuple is called. In this code, message ids are not cleared from the state after the ack. They are removed only when the commit timer expires. so now message ids will be stored for longer in memory. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56414932
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    > BATCH can go away as well once we fix the in-memory replay.
    
    OK So long as we can do it before we officially release this code.  If you want to hand off the part that will move from in-memory to replay I am happy to take it over.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-193283763
  
    +1 on what I see, but we do need to fix an issue with JDK7.
    
    ```
    [ERROR] /home/travis/build/apache/storm/external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java:[90,19] cannot find symbol
      symbol:   method putIfAbsent(java.lang.String,java.lang.String)
      location: variable kafkaProps of type java.util.Map<java.lang.String,java.lang.Object>
    ```


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by knusbaum <gi...@git.apache.org>.
Github user knusbaum commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r53658796
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaRecordTupleBuilder.java ---
    @@ -0,0 +1,34 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.storm.tuple.Values;
    +
    +public class KafkaRecordTupleBuilder<K,V> implements org.apache.storm.kafka.spout.KafkaTupleBuilder<K, V> {
    --- End diff --
    
    Don't need to qualify `KafkaTupleBuilder` with full package.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-213211034
  
    Hi, i am not sure if I should post a small issue here or anyone can tell me what is the formal process to do so? thanks!
    During the testing we found a small bug in org/apache/storm/kafka/spout/KafkaSpoutRetryExponentialBackoff.java(TopicPartition doesn't implement the comparable interface), below is the fix we applied: 
         public Set<TopicPartition> retriableTopicPartitions() {
    -        final Set<TopicPartition> tps = new TreeSet<>();
    +        final Set<TopicPartition> tps = new TreeSet<>(new Comparator<TopicPartition>() {
    +            @Override
    +            public int compare(TopicPartition o1, TopicPartition o2) {
    +                return o1.toString().compareTo(o2.toString());
    +            }
    +        });


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by jianbzhou <gi...@git.apache.org>.
Github user jianbzhou commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-221922582
  
    @hmcl, currently if user give firstPollOffsetStrategy=UNCOMMITTED_LATEST or LATEST, the spout will not work, because if a kafka consumer re-balance happened, the offset will be seeked to the end, and there will be lots of messages not consumed/emitted/acked&failed, so will never find the next continuous offset to commit, so the log will keep showing that "Non continuous offset found"......
    
    I have a questions here - if a spout read and emit one message, I assume storm will ensure the message will be acked or failed without exception, right? because if it is possible that one emitted message failed to get acked or failed message under some strange situations, it means we cannot find the continuous message to commit, which will directly break the spout. Could you please help confirm if my assumption is correct?
    
    If my assumption is not correct - which means one emitted message may not be able to get acked or failed message back, then we must change the spout(need a timeout setting if failed to find next continuous message to commit) - currently the spout will always find the next continuous message to commit, it will try forever...
    
    due to the spout will always find the next continuous message to commit, we need to be cautious for below method:
    private boolean poll() {
            return !waitingToEmit() && emitted.size() < kafkaSpoutConfig.getMaxUncommittedOffsets();
        }
    if the MaxUncommittedOffsets is too small, the spout will frequently stop polling from kafka, if a rebalance happened and seek back to the failed message, at this moment if the spout stop polling, will also cause the spout failed to find the next committed message. Currently we set this value to 200000 and seems working fine for now.
    Looking forward to hearing from you! thanks!


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-220347788
  
    @jianbzhou thanks once again for your feedback. Can you please share the results of your tests? I am working on adding more tests to this, as well as fix some of these corner cases, possibly using some of your suggested solutions, if they are the most suitable.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r57972451
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,547 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Collections;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.Iterator;
    +import java.util.LinkedList;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.Set;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K, V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<KafkaSpoutMessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private transient KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +
    +
    +    // Bookkeeping
    +    private transient int maxRetries;                                   // Max number of times a tuple is retried
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;  // Strategy to determine the fetch offset of the first realized by the spout upon activation
    +    private transient KafkaSpoutRetryService retryService;              // Class that has the logic to handle tuple failure
    +    private transient Timer commitTimer;                                // timer == null for auto commit mode
    +    private transient boolean initialized;                              // Flag indicating that the spout is still undergoing initialization process.
    +    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +    private KafkaSpoutStreams kafkaSpoutStreams;                        // Object that wraps all the logic to declare output fields and emit tuples
    +    private transient KafkaSpoutTuplesBuilder<K, V> tuplesBuilder;      // Object that contains the logic to build tuples for each ConsumerRecord
    +
    +    private transient Map<TopicPartition, OffsetEntry> acked;           // Tuples that were successfully acked. These tuples will be committed periodically when the commit timer expires, after consumer rebalance, or on close/deactivate
    +    private transient Set<KafkaSpoutMessageId> emitted;                 // Tuples that have been emitted but that are "on the wire", i.e. pending being acked or failed
    +    private transient Iterator<ConsumerRecord<K, V>> waitingToEmit;         // Records that have been polled and are queued to be emitted in the nextTuple() call. One record is emitted per nextTuple()
    +    private transient long numUncommittedOffsets;                       // Number of offsets that have been polled and emitted but not yet been committed
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K, V> kafkaSpoutConfig) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutConfig.getKafkaSpoutStreams();
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +        numUncommittedOffsets = 0;
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        // Retries management
    +        retryService = kafkaSpoutConfig.getRetryService();
    +
    +        // Tuples builder delegate
    +        tuplesBuilder = kafkaSpoutConfig.getTuplesBuilder();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            commitTimer = new Timer(500, kafkaSpoutConfig.getOffsetsCommitPeriodMs(), TimeUnit.MILLISECONDS);
    +        }
    +
    +        acked = new HashMap<>();
    +        emitted = new HashSet<>();
    +        waitingToEmit = Collections.emptyListIterator();
    +
    +        LOG.info("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig);
    +    }
    +
    +    // =========== Consumer Rebalance Listener - On the same thread as the caller ===========
    +
    +    private class KafkaSpoutConsumerRebalanceListener implements ConsumerRebalanceListener {
    +        @Override
    +        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions revoked. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +            if (!consumerAutoCommitMode && initialized) {
    +                initialized = false;
    +                commitOffsetsForAckedTuples();
    +            }
    +        }
    +
    +        @Override
    +        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    +            LOG.debug("Partitions reassignment. [consumer-group={}, consumer={}, topic-partitions={}]",
    +                    kafkaSpoutConfig.getConsumerGroupId(), kafkaConsumer, partitions);
    +
    +            initialize(partitions);
    +        }
    +
    +        private void initialize(Collection<TopicPartition> partitions) {
    +            if (!consumerAutoCommitMode) {
    +                acked.keySet().retainAll(partitions);   // remove from acked all partitions that are no longer assigned to this spout
    +            }
    +
    +            retryService.remove(partitions);
    +
    +            for (TopicPartition tp : partitions) {
    +                final OffsetAndMetadata committedOffset = kafkaConsumer.committed(tp);
    +                final long fetchOffset = doSeek(tp, committedOffset);
    +                setAcked(tp, fetchOffset);
    +            }
    +            initialized = true;
    +            LOG.debug("Initialization complete");
    +        }
    +
    +        /**
    +         * sets the cursor to the location dictated by the first poll strategy and returns the fetch offset
    +         */
    +        private long doSeek(TopicPartition tp, OffsetAndMetadata committedOffset) {
    +            long fetchOffset;
    +            if (committedOffset != null) {             // offset was committed for this TopicPartition
    +                if (firstPollOffsetStrategy.equals(EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                    fetchOffset = kafkaConsumer.position(tp);
    +                } else {
    +                    // By default polling starts at the last committed offset. +1 to point fetch to the first uncommitted offset.
    +                    fetchOffset = committedOffset.offset() + 1;
    +                    kafkaConsumer.seek(tp, fetchOffset);
    +                }
    +            } else {    // no commits have ever been done, so start at the beginning or end depending on the strategy
    +                if (firstPollOffsetStrategy.equals(EARLIEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_EARLIEST)) {
    +                    kafkaConsumer.seekToBeginning(tp);
    +                } else if (firstPollOffsetStrategy.equals(LATEST) || firstPollOffsetStrategy.equals(UNCOMMITTED_LATEST)) {
    +                    kafkaConsumer.seekToEnd(tp);
    +                }
    +                fetchOffset = kafkaConsumer.position(tp);
    +            }
    +            return fetchOffset;
    +        }
    +    }
    +
    +    private void setAcked(TopicPartition tp, long fetchOffset) {
    +        // If this partition was previously assigned to this spout, leave the acked offsets as they were to resume where it left off
    +        if (!consumerAutoCommitMode && !acked.containsKey(tp)) {
    +            acked.put(tp, new OffsetEntry(tp, fetchOffset));
    +        }
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if (commit()) {
    +                commitOffsetsForAckedTuples();
    +            }
    +
    +            if (poll()) {
    +                setWaitingToEmit(pollKafkaBroker());
    +            }
    +
    +            if (waitingToEmit()) {
    +                emit();
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && commitTimer.isExpiredResetOnTrue();    // timer != null for non auto commit mode
    +    }
    +
    +    private boolean poll() {
    +        return !waitingToEmit() && numUncommittedOffsets < kafkaSpoutConfig.getMaxUncommittedOffsets();
    +    }
    +
    +    private boolean waitingToEmit() {
    +        return waitingToEmit != null && waitingToEmit.hasNext();
    +    }
    +
    +    public void setWaitingToEmit(ConsumerRecords<K,V> consumerRecords) {
    +        List<ConsumerRecord<K,V>> waitingToEmitList = new LinkedList<>();
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            waitingToEmitList.addAll(consumerRecords.records(tp));
    +        }
    +        waitingToEmit = waitingToEmitList.iterator();
    +        LOG.trace("Records waiting to be emitted {}", waitingToEmitList);
    +    }
    +
    +    // ======== poll =========
    +    private ConsumerRecords<K, V> pollKafkaBroker() {
    +        doSeekRetriableTopicPartitions();
    +
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        final int numPolledRecords = consumerRecords.count();
    +        LOG.debug("Polled [{}] records from Kafka. NumUncommittedOffsets=[{}]", numPolledRecords, numUncommittedOffsets);
    +        return consumerRecords;
    +    }
    +
    +    private void doSeekRetriableTopicPartitions() {
    +        final Set<TopicPartition> retriableTopicPartitions = retryService.retriableTopicPartitions();
    +
    +        for (TopicPartition rtp : retriableTopicPartitions) {
    +            final OffsetAndMetadata offsetAndMeta = acked.get(rtp).findNextCommitOffset();
    +            if (offsetAndMeta != null) {
    +                kafkaConsumer.seek(rtp, offsetAndMeta.offset() + 1);  // seek to the next offset that is ready to commit in next commit cycle
    +            } else {
    +                kafkaConsumer.seekToEnd(rtp);    // Seek to last committed offset
    +            }
    +        }
    +    }
    +
    +    // ======== emit  =========
    +    private void emit() {
    +        emitTupleIfNotEmitted(waitingToEmit.next());
    +        waitingToEmit.remove();
    --- End diff --
    
    @revans2 I apologize but I am really not following your thought. I am totally willing to accommodate what you have in mind, but I am not quite understanding what to do. I believe this should be a simple change. Can you please paste a code snippet, or some pseudo code, on how to do what you have in mind. I would be happy to integrate that. Thanks a lot.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm issue #1131: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the issue:

    https://github.com/apache/storm/pull/1131
  
    @jianbzhou thanks for your suggested fix and for the summary of changes. I think that the best way to go about incorporating your changes is to create a JIRA with summary along the lines "Kafka Spout Improvements/Fixes", and in the description put the contents you pasted above. We can then create a pull request with the suggested changes, and after it is reviewed, merge it into master. 
    
    It may not be a non trivial merge because other users have already found and fixed some bugs on the code, so likely your suggested fix has a different base commit. Furthermore, one has to evaluate how to safely add your suggested fixes on top of other fixes, which apparently are currently working. There is also a [refactoring](https://github.com/apache/storm/pull/1832) of the spout going on, which will also add more diffs, so we have to take everything into consideration.
    
    @jianbzhou I suggest the following: Can you please create the JIRA, or if you don't have the permissions to do so, please let me know, and I can do it. Then, you can either we can attach your patch to the JIRA, or create a pull request with commit header that matches the JIRA summary. That will link the pull request with the JIRA. If you decide to attach the patch, or even the file with the java source to the JIRA, I will create the pull request.
    
    @jianbzhou do you consider adopting a community version which may potentially include only part of your fixes, and have other fixes committed by other contributors. Or would you rather run in your production environment the version that you currently have ?
    
    @erikdw "you" would be "me" :).
    
    
    



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r54458893
  
    --- Diff: external/storm-kafka-new-consumer-api/src/main/java/org/apache/storm/kafka/spout/KafkaSpout.java ---
    @@ -0,0 +1,445 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.clients.consumer.ConsumerRecords;
    +import org.apache.kafka.clients.consumer.KafkaConsumer;
    +import org.apache.kafka.clients.consumer.OffsetAndMetadata;
    +import org.apache.kafka.common.TopicPartition;
    +import org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy;
    +import org.apache.storm.spout.SpoutOutputCollector;
    +import org.apache.storm.task.TopologyContext;
    +import org.apache.storm.topology.OutputFieldsDeclarer;
    +import org.apache.storm.topology.base.BaseRichSpout;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.util.Collection;
    +import java.util.Comparator;
    +import java.util.HashMap;
    +import java.util.Iterator;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.NavigableSet;
    +import java.util.TreeSet;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.LATEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_EARLIEST;
    +import static org.apache.storm.kafka.spout.KafkaSpoutConfig.FirstPollOffsetStrategy.UNCOMMITTED_LATEST;
    +
    +public class KafkaSpout<K,V> extends BaseRichSpout {
    +    private static final Logger LOG = LoggerFactory.getLogger(KafkaSpout.class);
    +    private static final Comparator<MessageId> OFFSET_COMPARATOR = new OffsetComparator();
    +
    +    // Storm
    +    protected SpoutOutputCollector collector;
    +
    +    // Kafka
    +    private final KafkaSpoutConfig<K, V> kafkaSpoutConfig;
    +    private KafkaConsumer<K, V> kafkaConsumer;
    +    private transient boolean consumerAutoCommitMode;
    +    private transient FirstPollOffsetStrategy firstPollOffsetStrategy;
    +
    +    // Bookkeeping
    +    private KafkaSpoutStreams kafkaSpoutStreams;
    +    private KafkaTupleBuilder<K,V> tupleBuilder;
    +    private transient Timer timer;                                    // timer == null for auto commit mode
    +    private transient Map<TopicPartition, OffsetEntry> acked;         // emitted tuples that were successfully acked. These tuples will be committed by the commitOffsetsTask or on consumer rebalance
    +    private transient int maxRetries;                                 // Max number of times a tuple is retried
    +    private transient boolean initialized;          // Flag indicating that the spout is still undergoing initialization process.
    +                                                    // Initialization is only complete after the first call to  KafkaSpoutConsumerRebalanceListener.onPartitionsAssigned()
    +
    +
    +    public KafkaSpout(KafkaSpoutConfig<K,V> kafkaSpoutConfig, KafkaSpoutStreams kafkaSpoutStreams, KafkaTupleBuilder<K,V> tupleBuilder) {
    +        this.kafkaSpoutConfig = kafkaSpoutConfig;                 // Pass in configuration
    +        this.kafkaSpoutStreams = kafkaSpoutStreams;
    +        this.tupleBuilder = tupleBuilder;
    +    }
    +
    +    @Override
    +    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
    +        initialized = false;
    +
    +        // Spout internals
    +        this.collector = collector;
    +        maxRetries = kafkaSpoutConfig.getMaxTupleRetries();
    +
    +        // Offset management
    +        firstPollOffsetStrategy = kafkaSpoutConfig.getFirstPollOffsetStrategy();
    +        consumerAutoCommitMode = kafkaSpoutConfig.isConsumerAutoCommitMode();
    +
    +        if (!consumerAutoCommitMode) {     // If it is auto commit, no need to commit offsets manually
    +            timer = new Timer(kafkaSpoutConfig.getOffsetsCommitFreqMs(), 500, TimeUnit.MILLISECONDS);
    +            acked = new HashMap<>();
    +        }
    +
    +        // Kafka consumer
    +        kafkaConsumer = new KafkaConsumer<>(kafkaSpoutConfig.getKafkaProps(),
    +                kafkaSpoutConfig.getKeyDeserializer(), kafkaSpoutConfig.getValueDeserializer());
    +        kafkaConsumer.subscribe(kafkaSpoutConfig.getSubscribedTopics(), new KafkaSpoutConsumerRebalanceListener());
    +        // Initial poll to get the consumer registration process going.
    +        // KafkaSpoutConsumerRebalanceListener will be called foloowing this poll upon partition registration
    +        kafkaConsumer.poll(0);
    +
    +        LOG.debug("Kafka Spout opened with the following configuration: {}", kafkaSpoutConfig.toString());
    +    }
    +
    +    // ======== Next Tuple =======
    +
    +    @Override
    +    public void nextTuple() {
    +        if (initialized) {
    +            if(commit()) {
    +                commitOffsetsForAckedTuples();
    +            } else {
    +                emitTuples(poll());
    +            }
    +        } else {
    +            LOG.debug("Spout not initialized. Not sending tuples until initialization completes");
    +        }
    +    }
    +
    +    private boolean commit() {
    +        return !consumerAutoCommitMode && timer.expired();    // timer != null for non auto commit mode
    +    }
    +
    +    private ConsumerRecords<K, V> poll() {
    +        final ConsumerRecords<K, V> consumerRecords = kafkaConsumer.poll(kafkaSpoutConfig.getPollTimeoutMs());
    +        LOG.debug("Polled [{}] records from Kafka", consumerRecords.count());
    +        return consumerRecords;
    +    }
    +
    +    private void emitTuples(ConsumerRecords<K, V> consumerRecords) {
    +        for (TopicPartition tp : consumerRecords.partitions()) {
    +            final Iterable<ConsumerRecord<K, V>> records = consumerRecords.records(tp.topic());     // TODO Decide if want to give flexibility to emmit/poll either per topic or per partition
    --- End diff --
    
    TODOs should not be left in the code.  We should either do this, skip it, or file a follow on JIRA for it.  Either way we should delete the comment. I personally am fine either way, but releasing a spout sooner rather then later seems better to me.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on the pull request:

    https://github.com/apache/storm/pull/1131#issuecomment-187284409
  
    @tgravescs trident will be appended to this patch. In the meantime it would be helpful if you could let me know of any other requirements that  you may have, and if this initial commit is covering most things that you may need. I am pushing another patch soon with better exception handling, logging, testing, etc. Thanks.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by hmcl <gi...@git.apache.org>.
Github user hmcl commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r55886553
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaRecordTupleBuilder.java ---
    @@ -0,0 +1,34 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.storm.tuple.Values;
    +
    +import java.util.List;
    +
    +public class KafkaRecordTupleBuilder<K,V> implements org.apache.storm.kafka.spout.KafkaTupleBuilder<K, V> {
    --- End diff --
    
    Done


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] storm pull request: STORM-822: Kafka Spout New Consumer API

Posted by revans2 <gi...@git.apache.org>.
Github user revans2 commented on a diff in the pull request:

    https://github.com/apache/storm/pull/1131#discussion_r56546945
  
    --- Diff: external/storm-kafka-client/src/main/java/org/apache/storm/kafka/spout/KafkaSpoutConfig.java ---
    @@ -0,0 +1,298 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one
    + *   or more contributor license agreements.  See the NOTICE file
    + *   distributed with this work for additional information
    + *   regarding copyright ownership.  The ASF licenses this file
    + *   to you 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.apache.storm.kafka.spout;
    +
    +import org.apache.kafka.common.serialization.Deserializer;
    +
    +import java.io.Serializable;
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +
    +/**
    + * KafkaSpoutConfig defines the required configuration to connect a consumer to a consumer group, as well as the subscribing topics
    + */
    +public class KafkaSpoutConfig<K, V> implements Serializable {
    +    public static final long DEFAULT_POLL_TIMEOUT_MS = 2_000;            // 2s
    +    public static final long DEFAULT_OFFSET_COMMIT_PERIOD_MS = 15_000;   // 15s
    +    public static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;     // Retry forever
    +    public static final int DEFAULT_MAX_UNCOMMITTED_OFFSETS = 10_000;    // 10,000 records
    +
    +    // Kafka property names
    +    public interface Consumer {
    +        String GROUP_ID = "group.id";
    +        String BOOTSTRAP_SERVERS = "bootstrap.servers";
    +        String ENABLE_AUTO_COMMIT = "enable.auto.commit";
    +        String AUTO_COMMIT_INTERVAL_MS = "auto.commit.interval.ms";
    +        String KEY_DESERIALIZER = "key.deserializer";
    +        String VALUE_DESERIALIZER = "value.deserializer";
    +    }
    +
    +    /**
    +     * The offset used by the Kafka spout in the first poll to Kafka broker. The choice of this parameter will
    +     * affect the number of consumer records returned in the first poll. By default this parameter is set to UNCOMMITTED_EARLIEST. <br/><br/>
    +     * The allowed values are EARLIEST, LATEST, UNCOMMITTED_EARLIEST, UNCOMMITTED_LATEST. <br/>
    +     * <ul>
    +     * <li>EARLIEST means that the kafka spout polls records starting in the first offset of the partition, regardless of previous commits</li>
    +     * <li>LATEST means that the kafka spout polls records with offsets greater than the last offset in the partition, regardless of previous commits</li>
    +     * <li>UNCOMMITTED_EARLIEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as EARLIEST.</li>
    +     * <li>UNCOMMITTED_LATEST means that the kafka spout polls records from the last committed offset, if any.
    +     * If no offset has been committed, it behaves as LATEST.</li>
    +     * </ul>
    +     * */
    +    public enum FirstPollOffsetStrategy {
    +        EARLIEST,
    +        LATEST,
    +        UNCOMMITTED_EARLIEST,
    +        UNCOMMITTED_LATEST }
    +
    +    /**
    +     * Defines when to poll the next batch of records from Kafka. The choice of this parameter will affect throughput and the memory
    +     * footprint of the Kafka spout. The allowed values are STREAM and BATCH. STREAM will likely have higher throughput and use more memory
    +     * (it stores in memory the entire KafkaRecord,including data). BATCH will likely have less throughput but also use less memory.
    +     * The BATCH behavior is similar to the behavior of the previous Kafka Spout. De default value is STREAM.
    +     * <ul>
    +     *     <li>STREAM Every periodic call to nextTuple polls a new batch of records from Kafka as long as the maxUncommittedOffsets
    +     *     threshold has not yet been reached. When the threshold his reached, no more records are polled until enough offsets have been
    +     *     committed, such that the number of pending offsets is less than maxUncommittedOffsets. See {@link Builder#setMaxUncommittedOffsets(int)}
    +     *     </li>
    +     *     <li>BATCH Only polls a new batch of records from kafka once all the records that came in the previous poll have been acked.</li>
    +     * </ul>
    +     */
    +    public enum PollStrategy {
    +        STREAM,
    +        BATCH
    --- End diff --
    
    https://github.com/apache/storm/blob/master/external/storm-kafka/src/jvm/org/apache/storm/kafka/PartitionManager.java#L163
    
    As soon as we emit anything, we break.  It is only one per nextTuple call.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---