You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by ja...@apache.org on 2020/02/21 23:44:24 UTC

[incubator-pinot] 03/07: split out main logic of kc to different classes, added unit tests

This is an automated email from the ASF dual-hosted git repository.

jamesshao pushed a commit to branch upsert
in repository https://gitbox.apache.org/repos/asf/incubator-pinot.git

commit 2ed3218a80cd0d7766955f3adf58fee3156d03b3
Author: james Shao <sj...@uber.com>
AuthorDate: Tue Dec 17 14:55:42 2019 -0800

    split out main logic of kc to different classes, added unit tests
    
    Summary:
    split out the main logic of key coordinator core to 3 different classes to make testing/future changes easier
    1. MessageFetcher: handle logics of fetching messages from kafka and storing internally
    2. SegmentEventProcessor: handle logics of processing update events from coming in to sending updates out, storing on disk, etc
    3. VersionMessageManager: handle emitting version message and storing current version messages
    
    Reviewers: bzzhang, tingchen, #streaming_pinot
    
    Reviewed By: bzzhang, tingchen, #streaming_pinot
    
    Differential Revision: https://code.uberinternal.com/D3649709
---
 .../immutable/ImmutableUpsertSegmentImplTest.java  |   3 +-
 .../common/messages/LogCoordinatorMessage.java     |  18 +
 .../pinot/grigio/common/rpcQueue/ProduceTask.java  |  27 ++
 .../SegmentUpdateLogStorageProviderTest.java       |   4 +-
 .../org/apache/pinot/grigio/common/OffsetInfo.java |  19 +
 .../internal/DistributedKeyCoordinatorCore.java    | 509 ++-------------------
 .../keyCoordinator/internal/MessageFetcher.java    | 186 ++++++++
 ...dinatorCore.java => SegmentEventProcessor.java} | 321 +++----------
 .../internal/VersionMessageManager.java            | 160 +++++++
 .../starter/KeyCoordinatorStarter.java             |  12 +-
 .../internal/MessageFetcherTest.java               | 127 +++++
 .../internal/SegmentEventProcessorTest.java        | 229 +++++++++
 .../internal/VersionMessageManagerTest.java        |  94 ++++
 13 files changed, 972 insertions(+), 737 deletions(-)

diff --git a/pinot-core/src/test/java/org/apache/pinot/core/indexsegment/immutable/ImmutableUpsertSegmentImplTest.java b/pinot-core/src/test/java/org/apache/pinot/core/indexsegment/immutable/ImmutableUpsertSegmentImplTest.java
index 19bef14..817131c 100644
--- a/pinot-core/src/test/java/org/apache/pinot/core/indexsegment/immutable/ImmutableUpsertSegmentImplTest.java
+++ b/pinot-core/src/test/java/org/apache/pinot/core/indexsegment/immutable/ImmutableUpsertSegmentImplTest.java
@@ -111,7 +111,8 @@ public class ImmutableUpsertSegmentImplTest {
     immutableUpsertSegment.initVirtualColumn();
     long runtime = System.currentTimeMillis() - start;
     System.out.println("run time is " + runtime);
-    Assert.assertTrue(runtime < 1_000L, "run time should be less than 1 second");
+    // on regular developer laptop this should take less 1 second, but on integration server this might be longer
+    Assert.assertTrue(runtime < 10_000L, "run time should be less than 10 second");
 
     VirtualColumnLongValueReaderWriter insertReaderWrite = _readerWriters.get(0);
     VirtualColumnLongValueReaderWriter deleteReaderWrite = _readerWriters.get(1);
diff --git a/pinot-grigio/pinot-grigio-common/src/main/java/org/apache/pinot/grigio/common/messages/LogCoordinatorMessage.java b/pinot-grigio/pinot-grigio-common/src/main/java/org/apache/pinot/grigio/common/messages/LogCoordinatorMessage.java
index 521694c..581dc07 100644
--- a/pinot-grigio/pinot-grigio-common/src/main/java/org/apache/pinot/grigio/common/messages/LogCoordinatorMessage.java
+++ b/pinot-grigio/pinot-grigio-common/src/main/java/org/apache/pinot/grigio/common/messages/LogCoordinatorMessage.java
@@ -18,6 +18,8 @@
  */
 package org.apache.pinot.grigio.common.messages;
 
+import java.util.Objects;
+
 /**
  * this message contains the following 4 attributes:
  * 1. segmentName: the name of the segment for the pinot record we are going to update
@@ -58,6 +60,22 @@ public class LogCoordinatorMessage {
     this._kafkaOffset = kafkaOffset;
   }
 
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    LogCoordinatorMessage that = (LogCoordinatorMessage) o;
+    return _value == that._value &&
+        _kafkaOffset == that._kafkaOffset &&
+        Objects.equals(_segmentName, that._segmentName) &&
+        _updateEventType == that._updateEventType;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(_segmentName, _value, _updateEventType, _kafkaOffset);
+  }
+
   public String toString() {
     return _segmentName + "|"  + _updateEventType + "|" + _value + "|" + _kafkaOffset;
   }
diff --git a/pinot-grigio/pinot-grigio-common/src/main/java/org/apache/pinot/grigio/common/rpcQueue/ProduceTask.java b/pinot-grigio/pinot-grigio-common/src/main/java/org/apache/pinot/grigio/common/rpcQueue/ProduceTask.java
index 6398f0c..35bcbe9 100644
--- a/pinot-grigio/pinot-grigio-common/src/main/java/org/apache/pinot/grigio/common/rpcQueue/ProduceTask.java
+++ b/pinot-grigio/pinot-grigio-common/src/main/java/org/apache/pinot/grigio/common/rpcQueue/ProduceTask.java
@@ -18,11 +18,14 @@
  */
 package org.apache.pinot.grigio.common.rpcQueue;
 
+import java.util.Objects;
+
 /**
  * Class created to wrap around the kafka produce task object, so we can make the upsert (grigio) package stream independent
  * by using this implementation, we don't need to hard code kafka dependency in related package (pinot-server, pinot-core) etc
  */
 public class ProduceTask<K, V> {
+
   private volatile boolean _completed = false;
   private Exception _exception = null;
   private Callback _callback = null;
@@ -89,6 +92,30 @@ public class ProduceTask<K, V> {
     }
   }
 
+  @Override
+  public String toString() {
+    return "ProduceTask{" +
+        "_topic='" + _topic + '\'' +
+        ", _key=" + _key +
+        ", _value=" + _value.toString() +
+        '}';
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    ProduceTask<?, ?> that = (ProduceTask<?, ?>) o;
+    return Objects.equals(_topic, that._topic) &&
+        Objects.equals(_key, that._key) &&
+        Objects.equals(_value, that._value);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(_topic, _key, _value);
+  }
+
   public interface Callback {
     void onSuccess();
 
diff --git a/pinot-grigio/pinot-grigio-common/src/test/java/org/apache/pinot/grigio/common/storageProvider/SegmentUpdateLogStorageProviderTest.java b/pinot-grigio/pinot-grigio-common/src/test/java/org/apache/pinot/grigio/common/storageProvider/SegmentUpdateLogStorageProviderTest.java
index 219fbc8..1eba023 100644
--- a/pinot-grigio/pinot-grigio-common/src/test/java/org/apache/pinot/grigio/common/storageProvider/SegmentUpdateLogStorageProviderTest.java
+++ b/pinot-grigio/pinot-grigio-common/src/test/java/org/apache/pinot/grigio/common/storageProvider/SegmentUpdateLogStorageProviderTest.java
@@ -155,7 +155,9 @@ public class SegmentUpdateLogStorageProviderTest {
       entryHolder = entry;
     }
     long readTime = System.currentTimeMillis() - start;
-    Assert.assertTrue(readTime < 1_000L); // this should be relatively fast
     System.out.println("read data takes ms: " + readTime);
+    // this should be relatively fast (under 1 seconds), but on integration server it might take longer
+    // TODO: make sure this runs fine on integration server as well
+    Assert.assertTrue(readTime < 10_000L);
   }
 }
\ No newline at end of file
diff --git a/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/common/OffsetInfo.java b/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/common/OffsetInfo.java
index ac20c32..84b213f 100644
--- a/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/common/OffsetInfo.java
+++ b/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/common/OffsetInfo.java
@@ -18,13 +18,19 @@
  */
 package org.apache.pinot.grigio.common;
 
+import com.google.common.annotations.VisibleForTesting;
 import org.apache.kafka.clients.consumer.OffsetAndMetadata;
 import org.apache.kafka.common.TopicPartition;
 import org.apache.pinot.grigio.common.rpcQueue.QueueConsumerRecord;
 
+import javax.annotation.concurrent.NotThreadSafe;
 import java.util.HashMap;
 import java.util.Map;
 
+/**
+ * class wrap around the queue offset information for pinot internal services to use without hardcoding kafka dep
+ */
+@NotThreadSafe
 public class OffsetInfo {
   private Map<TopicPartition, Long> _offsetMap;
 
@@ -32,6 +38,16 @@ public class OffsetInfo {
     _offsetMap = new HashMap<>();
   }
 
+  @VisibleForTesting
+  public OffsetInfo(Map<TopicPartition, Long> offsets) {
+    _offsetMap = new HashMap<>(offsets);
+  }
+
+  /**
+   * update the largest offset for the given partition with the current record,
+   * if it is already larger than current highest record
+   * @param record the current consumption record
+   */
   public void updateOffsetIfNecessary(QueueConsumerRecord record) {
     TopicPartition tp = getTopicPartitionFromRecord(record);
     long offset = record.getOffset() + 1;
@@ -40,6 +56,9 @@ public class OffsetInfo {
     }
   }
 
+  /**
+   * return the mapping of partition to the largest offset
+   */
   public Map<TopicPartition, OffsetAndMetadata> getOffsetMap() {
     Map<TopicPartition, OffsetAndMetadata> offsetAndMetadataMap = new HashMap<>();
     for (Map.Entry<TopicPartition, Long> entry: _offsetMap.entrySet()) {
diff --git a/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/DistributedKeyCoordinatorCore.java b/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/DistributedKeyCoordinatorCore.java
index 044673f..5dbe342 100644
--- a/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/DistributedKeyCoordinatorCore.java
+++ b/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/DistributedKeyCoordinatorCore.java
@@ -20,59 +20,21 @@ package org.apache.pinot.grigio.keyCoordinator.internal;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
-import com.google.common.util.concurrent.Uninterruptibles;
-import org.apache.commons.configuration.Configuration;
-import org.apache.pinot.common.config.TableNameBuilder;
-import org.apache.pinot.common.utils.CommonConstants;
-import org.apache.pinot.grigio.common.DistributedCommonUtils;
-import org.apache.pinot.grigio.common.OffsetInfo;
-import org.apache.pinot.grigio.common.keyValueStore.ByteArrayWrapper;
-import org.apache.pinot.grigio.common.keyValueStore.KeyValueStoreDB;
-import org.apache.pinot.grigio.common.keyValueStore.KeyValueStoreTable;
-import org.apache.pinot.grigio.common.keyValueStore.RocksDBKeyValueStoreDB;
-import org.apache.pinot.grigio.common.messages.KeyCoordinatorMessageContext;
 import org.apache.pinot.grigio.common.messages.KeyCoordinatorQueueMsg;
-import org.apache.pinot.grigio.common.messages.LogCoordinatorMessage;
-import org.apache.pinot.grigio.common.messages.LogEventType;
-import org.apache.pinot.grigio.common.metrics.GrigioGauge;
 import org.apache.pinot.grigio.common.metrics.GrigioMeter;
 import org.apache.pinot.grigio.common.metrics.GrigioTimer;
-import org.apache.pinot.grigio.common.rpcQueue.KeyCoordinatorQueueConsumer;
-import org.apache.pinot.grigio.common.rpcQueue.LogCoordinatorQueueProducer;
-import org.apache.pinot.grigio.common.rpcQueue.ProduceTask;
 import org.apache.pinot.grigio.common.rpcQueue.QueueConsumerRecord;
-import org.apache.pinot.grigio.common.rpcQueue.VersionMsgQueueProducer;
-import org.apache.pinot.grigio.common.storageProvider.UpdateLogEntry;
-import org.apache.pinot.grigio.common.storageProvider.UpdateLogStorageProvider;
-import org.apache.pinot.grigio.common.storageProvider.retentionManager.UpdateLogRetentionManager;
-import org.apache.pinot.grigio.common.storageProvider.retentionManager.UpdateLogTableRetentionManager;
-import org.apache.pinot.grigio.common.updateStrategy.MessageResolveStrategy;
 import org.apache.pinot.grigio.common.utils.CommonUtils;
 import org.apache.pinot.grigio.keyCoordinator.GrigioKeyCoordinatorMetrics;
-import org.apache.pinot.grigio.keyCoordinator.helix.KeyCoordinatorClusterHelixManager;
-import org.apache.pinot.grigio.keyCoordinator.helix.KeyCoordinatorLeadershipManager;
-import org.apache.pinot.grigio.keyCoordinator.helix.KeyCoordinatorVersionManager;
 import org.apache.pinot.grigio.keyCoordinator.helix.State;
 import org.apache.pinot.grigio.keyCoordinator.starter.KeyCoordinatorConf;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.Timer;
-import java.util.TimerTask;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
 
 
 public class DistributedKeyCoordinatorCore {
@@ -80,94 +42,40 @@ public class DistributedKeyCoordinatorCore {
   private static final long TERMINATION_WAIT_MS = 10000;
 
   protected KeyCoordinatorConf _conf;
-  protected LogCoordinatorQueueProducer _outputKafkaProducer;
-  protected KeyCoordinatorQueueConsumer _inputKafkaConsumer;
-  protected VersionMsgQueueProducer _versionMessageKafkaProducer;
-  protected MessageResolveStrategy _messageResolveStrategy;
-  protected KeyValueStoreDB<ByteArrayWrapper, KeyCoordinatorMessageContext> _keyValueStoreDB;
   protected ExecutorService _messageProcessThread;
-  protected ExecutorService _consumerThread;
-  protected BlockingQueue<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> _consumerRecordBlockingQueue;
-  protected UpdateLogStorageProvider _storageProvider;
-  protected KeyCoordinatorClusterHelixManager _keyCoordinatorClusterHelixManager;
-  protected KeyCoordinatorLeadershipManager _keyCoordinatorLeadershipManager;
-  protected KeyCoordinatorVersionManager _keyCoordinatorVersionManager;
-  protected UpdateLogRetentionManager _retentionManager;
   protected GrigioKeyCoordinatorMetrics _metrics;
-  protected String _topicPrefix;
-  protected int _fetchMsgDelayMs;
   protected int _fetchMsgMaxDelayMs;
-  protected int _fetchMsgMaxCount;
-  protected long _versionMessageIntervalMs;
-  protected Timer _versionMessageTimer;
-  protected TimerTask _versionMessageProduceTask;
-  protected Map<Integer, Long> _currentVersionConsumed;
+
+  // sub manager for other stuff
+  protected SegmentEventProcessor _segmentEventProcessor;
+  protected MessageFetcher _messageFetcher;
+  protected VersionMessageManager _versionMessageManager;
 
   protected volatile State _state = State.SHUTDOWN;
 
   public DistributedKeyCoordinatorCore() {}
 
-  public void init(KeyCoordinatorConf conf, LogCoordinatorQueueProducer keyCoordinatorProducer,
-                   KeyCoordinatorQueueConsumer keyCoordinatorConsumer, VersionMsgQueueProducer versionMessageKafkaProducer,
-                   MessageResolveStrategy messageResolveStrategy,
-                   KeyCoordinatorClusterHelixManager keyCoordinatorClusterHelixManager,
-                   UpdateLogRetentionManager retentionManager, GrigioKeyCoordinatorMetrics metrics) {
-    init(conf, keyCoordinatorProducer, keyCoordinatorConsumer, versionMessageKafkaProducer, messageResolveStrategy,
-        getKeyValueStore(conf.subset(KeyCoordinatorConf.KEY_COORDINATOR_KV_STORE)), Executors.newSingleThreadExecutor(),
-        Executors.newSingleThreadExecutor(), UpdateLogStorageProvider.getInstance(), keyCoordinatorClusterHelixManager,
-        retentionManager, metrics);
+  public void init(KeyCoordinatorConf conf, SegmentEventProcessor segmentEventProcessor,
+                   MessageFetcher fetcher, VersionMessageManager versionMessageManager,
+                   GrigioKeyCoordinatorMetrics metrics) {
+    init(conf, Executors.newSingleThreadExecutor(), segmentEventProcessor, fetcher, versionMessageManager, metrics);
   }
 
   @VisibleForTesting
-  public void init(KeyCoordinatorConf conf, LogCoordinatorQueueProducer keyCoordinatorProducer,
-                   KeyCoordinatorQueueConsumer keyCoordinatorConsumer,
-                   VersionMsgQueueProducer versionMessageKafkaProducer,
-                   MessageResolveStrategy messageResolveStrategy,
-                   KeyValueStoreDB<ByteArrayWrapper, KeyCoordinatorMessageContext> keyValueStoreDB,
-                   ExecutorService coreThread, ExecutorService consumerThread, UpdateLogStorageProvider storageProvider,
-                   KeyCoordinatorClusterHelixManager keyCoordinatorClusterHelixManager,
-                   UpdateLogRetentionManager updateLogRetentionManager, GrigioKeyCoordinatorMetrics metrics) {
+  public void init(KeyCoordinatorConf conf, ExecutorService coreThread, SegmentEventProcessor segmentEventProcessor,
+                   MessageFetcher fetcher, VersionMessageManager versionMessageManager,
+                   GrigioKeyCoordinatorMetrics metrics) {
     CommonUtils.printConfiguration(conf, "distributed key coordinator core");
     Preconditions.checkState(_state == State.SHUTDOWN, "can only init if it is not running yet");
     _conf = conf;
-    _outputKafkaProducer = keyCoordinatorProducer;
-    _inputKafkaConsumer = keyCoordinatorConsumer;
-    _versionMessageKafkaProducer = versionMessageKafkaProducer;
-    _messageResolveStrategy = messageResolveStrategy;
-    _keyValueStoreDB = keyValueStoreDB;
     _messageProcessThread = coreThread;
-    _consumerThread = consumerThread;
-    _consumerRecordBlockingQueue = new ArrayBlockingQueue<>(_conf.getConsumerBlockingQueueSize());
-    _storageProvider = storageProvider;
-    _keyCoordinatorClusterHelixManager = keyCoordinatorClusterHelixManager;
-    _retentionManager = updateLogRetentionManager;
+    _versionMessageManager = versionMessageManager;
+    _messageFetcher = fetcher;
     _metrics = metrics;
 
-    _fetchMsgDelayMs = conf.getInt(KeyCoordinatorConf.FETCH_MSG_DELAY_MS,
-        KeyCoordinatorConf.FETCH_MSG_DELAY_MS_DEFAULT);
     _fetchMsgMaxDelayMs = conf.getInt(KeyCoordinatorConf.FETCH_MSG_MAX_DELAY_MS,
         KeyCoordinatorConf.FETCH_MSG_MAX_DELAY_MS_DEFAULT);
-    _fetchMsgMaxCount = conf.getInt(KeyCoordinatorConf.FETCH_MSG_MAX_BATCH_SIZE,
-        KeyCoordinatorConf.FETCH_MSG_MAX_BATCH_SIZE_DEFAULT);
-    _topicPrefix = conf.getTopicPrefix();
-    LOGGER.info("starting with fetch delay: {} max delay: {}, fetch max count: {}", _fetchMsgDelayMs, _fetchMsgMaxDelayMs,
-        _fetchMsgMaxCount);
-
-    _versionMessageIntervalMs = conf.getLong(KeyCoordinatorConf.VERSION_MESSAGE_INTERVAL_MS,
-        KeyCoordinatorConf.VERSION_MESSAGE_INTERVAL_MS_DEFAULT);
-    _versionMessageTimer = new Timer();
-    _versionMessageProduceTask = new TimerTask() {
-      @Override
-      public void run() {
-        produceVersionMessage();
-      }
-    };
-    _keyCoordinatorLeadershipManager =
-        new KeyCoordinatorLeadershipManager(_keyCoordinatorClusterHelixManager.getControllerHelixManager());
-    _keyCoordinatorVersionManager =
-        new KeyCoordinatorVersionManager(_keyCoordinatorClusterHelixManager.getControllerHelixManager());
 
-    _currentVersionConsumed = _keyCoordinatorVersionManager.getVersionConsumedFromPropertyStore();
     _state = State.INIT;
   }
 
@@ -175,69 +83,12 @@ public class DistributedKeyCoordinatorCore {
     Preconditions.checkState(_state == State.INIT, "key coordinate is not in correct state");
     LOGGER.info("starting key coordinator message process loop");
     _state = State.RUNNING;
-    _versionMessageTimer.schedule(_versionMessageProduceTask, 0L, _versionMessageIntervalMs);
-    _consumerThread.submit(this::consumerIngestLoop);
+    _messageFetcher.start();
+    _versionMessageManager.start();
+    _segmentEventProcessor.start();
     _messageProcessThread.submit(this::messageProcessLoop);
   }
 
-  private void produceVersionMessage() {
-    long start = System.currentTimeMillis();
-    if (_state != State.RUNNING) {
-      LOGGER.info("Key coordinator not running, skip producing version messages");
-      return;
-    }
-    if (!_keyCoordinatorLeadershipManager.isLeader()) {
-      LOGGER.debug("Not controller leader, skip producing version messages");
-      return;
-    }
-    try {
-      long versionProduced = _keyCoordinatorVersionManager.getVersionProducedFromPropertyStore();
-      long versionToProduce = versionProduced + 1;
-      // produce to all partitions
-      for (int partition = 0; partition < _conf.getKeyCoordinatorMessagePartitionCount(); partition++) {
-        ProduceTask<Integer, KeyCoordinatorQueueMsg> produceTask =
-            new ProduceTask<>(_conf.getKeyCoordinatorMessageTopic(), partition,
-                new KeyCoordinatorQueueMsg(versionToProduce));
-        _versionMessageKafkaProducer.produce(produceTask);
-      }
-      // todo: make producing version messages and setting versions to property store as one transaction
-      _keyCoordinatorVersionManager.setVersionProducedToPropertyStore(versionToProduce);
-      long duration = System.currentTimeMillis() - start;
-      _metrics.addTimedValueMs(GrigioTimer.PRODUCE_VERSION_MESSAGE, duration);
-      _metrics.setValueOfGlobalGauge(GrigioGauge.VERSION_PRODUCED, versionToProduce);
-      LOGGER.info("Produced version messages to all partitions with version {} in {} ms", versionToProduce, duration);
-    } catch (Exception ex) {
-      LOGGER.error("Failed to produce version message", ex);
-    }
-  }
-
-  private void consumerIngestLoop() {
-    while (_state == State.RUNNING) {
-      try {
-        _metrics.setValueOfGlobalGauge(GrigioGauge.MESSAGE_PROCESS_QUEUE_SIZE, _consumerRecordBlockingQueue.size());
-        List<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> records = _inputKafkaConsumer.getRequests(_fetchMsgMaxDelayMs,
-            TimeUnit.MILLISECONDS);
-        if (records.size() == 0) {
-          LOGGER.info("no message found in kafka consumer, sleep and wait for next batch");
-          Uninterruptibles.sleepUninterruptibly(_fetchMsgMaxDelayMs, TimeUnit.MILLISECONDS);
-        } else {
-          records.forEach(c -> {
-            try {
-              _consumerRecordBlockingQueue.put(c);
-            } catch (InterruptedException e) {
-              LOGGER.warn("exception while trying to put message to queue", e);
-            }
-          });
-          _metrics.setValueOfGlobalGauge(GrigioGauge.KC_INPUT_MESSAGE_LAG_MS,
-              System.currentTimeMillis() - records.get(records.size() - 1).getTimestamp());
-        }
-      } catch (Exception ex) {
-        LOGGER.error("encountered exception in consumer ingest loop, will retry", ex);
-      }
-    }
-    LOGGER.info("exiting consumer ingest loop");
-  }
-
   private void messageProcessLoop() {
     try {
       long deadline = System.currentTimeMillis() + _fetchMsgMaxDelayMs;
@@ -245,7 +96,8 @@ public class DistributedKeyCoordinatorCore {
         LOGGER.info("starting new loop");
         long start = System.currentTimeMillis();
         // process message when we got max message count or reach max delay ms
-        MessageAndOffset<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> messageAndOffset = getMessagesFromQueue(_consumerRecordBlockingQueue, deadline);
+        MessageFetcher.MessageAndOffset<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> messageAndOffset =
+            _messageFetcher.getMessages(deadline);
         List<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> messages = messageAndOffset.getMessages();
         deadline = System.currentTimeMillis() + _fetchMsgMaxDelayMs;
 
@@ -253,11 +105,11 @@ public class DistributedKeyCoordinatorCore {
         _metrics.addTimedValueMs(GrigioTimer.MESSAGE_PROCESS_THREAD_FETCH_DELAY, System.currentTimeMillis() - start);
 
         if (messages.size() > 0) {
-          processMessages(messages);
+          _segmentEventProcessor.processMessages(messages);
           // todo: make ackOffset and setVersionConsumed as one transaction
-          _inputKafkaConsumer.ackOffset(messageAndOffset.getOffsetInfo());
-          _keyCoordinatorVersionManager.setVersionConsumedToPropertyStore(_currentVersionConsumed);
-          LOGGER.info("kc message processed {} messages in this loop for {} ms", messages.size(),
+          _messageFetcher.ackOffset(messageAndOffset);
+          _versionMessageManager.setVersionConsumedToPropertyStore();
+          LOGGER.info("kc processed {} messages in this loop for {} ms", messages.size(),
               System.currentTimeMillis() - start);
         } else {
           LOGGER.info("no message received in the current loop");
@@ -272,40 +124,17 @@ public class DistributedKeyCoordinatorCore {
     LOGGER.info("existing key coordinator core /procthread");
   }
 
-  /**
-   * get a list of messages read by the consumer ingestion thread
-   * @param queue the blocking queue shared with the ingestion thread
-   * @param deadline at which time we should stop the ingestion and return it to caller with the data we have
-   * @return list of messages to be processed
-   */
-  private MessageAndOffset<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> getMessagesFromQueue(
-      BlockingQueue<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> queue, long deadline) {
-    List<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> buffer = new ArrayList<>(_fetchMsgMaxCount * 2);
-    while(System.currentTimeMillis() < deadline && buffer.size() < _fetchMsgMaxCount) {
-      queue.drainTo(buffer, _fetchMsgMaxCount - buffer.size());
-      if (buffer.size() < _fetchMsgMaxCount) {
-        Uninterruptibles.sleepUninterruptibly(_fetchMsgDelayMs, TimeUnit.MILLISECONDS);
-      }
-    }
-    OffsetInfo offsetInfo = new OffsetInfo();
-    for (QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg> record: buffer) {
-      offsetInfo.updateOffsetIfNecessary(record);
-    }
-    return new MessageAndOffset<>(buffer, offsetInfo);
-  }
-
   public void stop() {
     _state = State.SHUTTING_DOWN;
+    _messageFetcher.stop();
+    _versionMessageManager.stop();
+    _segmentEventProcessor.stop();
     _messageProcessThread.shutdown();
-    _consumerThread.shutdown();
-    _versionMessageTimer.cancel();
     try {
-      _consumerThread.awaitTermination(TERMINATION_WAIT_MS, TimeUnit.MILLISECONDS);
       _messageProcessThread.awaitTermination(TERMINATION_WAIT_MS, TimeUnit.MILLISECONDS);
     } catch (InterruptedException ex) {
       LOGGER.error("failed to wait for key coordinator thread to shutdown", ex);
     }
-    _consumerThread.shutdownNow();
     _messageProcessThread.shutdownNow();
     _state = State.SHUTDOWN;
   }
@@ -314,291 +143,5 @@ public class DistributedKeyCoordinatorCore {
     return _state;
   }
 
-  /**
-   * process a list of update logs (update kv, send to downstream kafka, etc)
-   * @param messages list of the segment ingestion event for us to use in updates
-   */
-  private void processMessages(List<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> messages) {
-    long start = System.currentTimeMillis();
-    Map<String, List<MessageWithPartitionAndVersion>> tableMsgMap = new HashMap<>();
-    for (QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg> msg: messages) {
-      if (msg.getRecord().isVersionMessage()) {
-        // update _currentVersionConsumed for each version message
-        _currentVersionConsumed.put(msg.getPartition(), msg.getRecord().getVersion());
-        _metrics.setValueOfTableGauge(String.valueOf(msg.getPartition()), GrigioGauge.KC_VERSION_CONSUMED, msg.getRecord().getVersion());
-      } else {
-        // filter out version messages, attach the current version (and partition) to each regular messages
-        tableMsgMap.computeIfAbsent(msg.getRecord().getPinotTableName(), t -> new ArrayList<>()).add(
-            new MessageWithPartitionAndVersion(
-                msg.getRecord().getKey(),
-                msg.getRecord().getSegmentName(),
-                msg.getRecord().getKafkaOffset(),
-                msg.getRecord().getTimestamp(),
-                msg.getPartition(),
-                _currentVersionConsumed.getOrDefault(msg.getPartition(), 0L)
-            )
-        );
-      }
-    }
-    for (Map.Entry<String, List<MessageWithPartitionAndVersion>> perTableUpdates: tableMsgMap.entrySet()) {
-      processMessagesForTable(perTableUpdates.getKey(), perTableUpdates.getValue());
-    }
-    _metrics.addTimedValueMs(GrigioTimer.MESSAGE_PROCESS_THREAD_PROCESS_DELAY, System.currentTimeMillis() - start);
-  }
 
-  /**
-   * process update for a specific table
-   * @param tableName the name of the table we are processing (with type)
-   * @param msgList the list of message associated with it
-   */
-  private void processMessagesForTable(String tableName, List<MessageWithPartitionAndVersion> msgList) {
-    try {
-      long start = System.currentTimeMillis();
-      if (msgList == null || msgList.size() == 0) {
-        LOGGER.warn("trying to process topic message with empty list {}", tableName);
-        return;
-      }
-      List<ProduceTask<Integer, LogCoordinatorMessage>> tasks = new ArrayList<>();
-      Map<ByteArrayWrapper, KeyCoordinatorMessageContext> primaryKeyToValueMap = fetchDataFromKVStore(tableName, msgList);
-      processMessageUpdates(tableName, msgList, primaryKeyToValueMap, tasks);
-      List<ProduceTask<Integer, LogCoordinatorMessage>> failedTasks = sendMessagesToLogCoordinator(tasks, 10, TimeUnit.SECONDS);
-      if (failedTasks.size() > 0) {
-        LOGGER.error("send to producer failed: {}", failedTasks.size());
-      }
-      storeMessageToLocal(tableName, tasks);
-      storeMessageToKVStore(tableName, primaryKeyToValueMap);
-      _metrics.addTimedTableValueMs(tableName, GrigioTimer.MESSAGE_PROCESS_THREAD_PROCESS_DELAY, System.currentTimeMillis() - start);
-    } catch (IOException e) {
-      throw new RuntimeException("failed to interact with rocksdb", e);
-    } catch (RuntimeException e) {
-      throw new RuntimeException("failed to interact with key value store", e);
-    }
-  }
-
-  /**
-   * create a message to be sent to output kafka topic based on a list of metadata
-   * partition is the partition number of the ingestion upsert event. Note that the partition of records with primary
-   * key will be the same across ingestion upsert event and segment update event topics.
-   */
-  private ProduceTask<Integer, LogCoordinatorMessage> createMessageToLogCoordinator(String tableName, String segmentName,
-                                                                                    long oldKafkaOffset, long value,
-                                                                                    LogEventType eventType, int partition) {
-    return new ProduceTask<>(DistributedCommonUtils.getKafkaTopicFromTableName(tableName, _topicPrefix),
-            partition, new LogCoordinatorMessage(segmentName, oldKafkaOffset, value, eventType));
-  }
-
-  /**
-   * fetch all available data from kv from the primary key associated with the messages in the given message list
-   * @param tableName the name of table we are processing
-   * @param msgList list of ingestion update messages
-   * @return map of primary key and their associated state in key-value store
-   * @throws IOException
-   */
-  private Map<ByteArrayWrapper, KeyCoordinatorMessageContext> fetchDataFromKVStore(String tableName,
-      List<MessageWithPartitionAndVersion> msgList)
-      throws IOException {
-    long start = System.currentTimeMillis();
-    KeyValueStoreTable<ByteArrayWrapper, KeyCoordinatorMessageContext> table = _keyValueStoreDB.getTable(tableName);
-    // get a set of unique primary key and retrieve its corresponding value in kv store
-    Set<ByteArrayWrapper> primaryKeys = msgList.stream()
-        .filter(msg -> msg.getKey() != null && msg.getKey().length > 0)
-        .map(msg -> new ByteArrayWrapper(msg.getKey()))
-        .collect(Collectors.toCollection(HashSet::new));
-    Map<ByteArrayWrapper, KeyCoordinatorMessageContext> primaryKeyToValueMap = new HashMap<>(table.multiGet(new ArrayList<>(primaryKeys)));
-    _metrics.setValueOfGlobalGauge(GrigioGauge.FETCH_MSG_FROM_KV_COUNT, primaryKeyToValueMap.size());
-    _metrics.addTimedValueMs(GrigioTimer.FETCH_MSG_FROM_KV_DELAY, System.currentTimeMillis() - start);
-    _metrics.addTimedTableValueMs(tableName, GrigioTimer.FETCH_MSG_FROM_KV_DELAY, System.currentTimeMillis() - start);
-    LOGGER.info("input keys got {} results from kv store in {} ms", primaryKeyToValueMap.size(), System.currentTimeMillis() - start);
-    return primaryKeyToValueMap;
-  }
-
-  /**
-   * process whether a list of messages should be treated as update based on the existing data from kv store
-   * @param tableName the name of the table
-   * @param msgList a list of ingestion update messages to be processed
-   * @param primaryKeyToValueMap the current kv store state associated with this list of messages, will be updated to
-   *                             reflect the new state after these updates are done
-   * @param tasks the generated producer task to be sent to segment update entry queue (kafka topic)
-   */
-  private void processMessageUpdates(String tableName, List<MessageWithPartitionAndVersion> msgList,
-                                     Map<ByteArrayWrapper, KeyCoordinatorMessageContext> primaryKeyToValueMap,
-                                     List<ProduceTask<Integer, LogCoordinatorMessage>> tasks) {
-    // TODO: add a unit test
-    long start = System.currentTimeMillis();
-    UpdateLogTableRetentionManager tableRetentionManager = _retentionManager.getRetentionManagerForTable(
-        TableNameBuilder.ensureTableNameWithType(tableName, CommonConstants.Helix.TableType.REALTIME));
-    for (MessageWithPartitionAndVersion msg: msgList) {
-      KeyCoordinatorMessageContext currentContext = msg.getContext();
-      ByteArrayWrapper key = new ByteArrayWrapper(msg.getKey());
-      if (primaryKeyToValueMap.containsKey(key)) {
-        // key conflicts, should resolve which one to delete
-        KeyCoordinatorMessageContext oldContext = primaryKeyToValueMap.get(key);
-        if (oldContext.equals(currentContext)) {
-          // message are equals, it is from another replica and should skip
-          continue;
-        }
-        if (_messageResolveStrategy.shouldDeleteFirstMessage(oldContext, currentContext)) {
-          // the existing message we have is older than the message we just processed, create delete for it
-          if (tableRetentionManager.shouldIngestForSegment(oldContext.getSegmentName())) {
-            // only generate delete event if the segment is still valid
-            tasks.add(createMessageToLogCoordinator(tableName, oldContext.getSegmentName(), oldContext.getKafkaOffset(),
-                msg.getVersion(), LogEventType.DELETE, msg.getPartition()));
-          }
-          // update the local cache to the latest message, so message within the same batch can override each other
-          primaryKeyToValueMap.put(key, currentContext);
-          tasks.add(createMessageToLogCoordinator(tableName, currentContext.getSegmentName(), currentContext.getKafkaOffset(),
-              msg.getVersion(), LogEventType.INSERT, msg.getPartition()));
-        }
-      } else {
-        // no key in the existing map, adding this key to the running set
-        primaryKeyToValueMap.put(key, currentContext);
-        tasks.add(createMessageToLogCoordinator(tableName, currentContext.getSegmentName(), currentContext.getKafkaOffset(),
-            msg.getVersion(), LogEventType.INSERT, msg.getPartition()));
-      }
-    }
-    _metrics.addTimedValueMs(GrigioTimer.PROCESS_MSG_UPDATE, System.currentTimeMillis() - start);
-    LOGGER.info("processed all messages in {} ms", System.currentTimeMillis() - start);
-  }
-
-
-  /**
-   * send the list of the message to downstream kafka topic
-   * @param tasks the list of the tasks we are going to produce to downstream kafka
-   * @param timeout how much time we wait for producer to send the messages
-   * @param timeUnit the timeunit for waiting for the producers
-   * @return a list of the tasks we failed to produce to downstream
-   */
-  private List<ProduceTask<Integer, LogCoordinatorMessage>> sendMessagesToLogCoordinator(
-      List<ProduceTask<Integer, LogCoordinatorMessage>> tasks, long timeout, TimeUnit timeUnit) {
-    long startTime = System.currentTimeMillis();
-    // send all and wait for result, batch for better perf
-    CountDownLatch countDownLatch = new CountDownLatch(tasks.size());
-    tasks.forEach(t -> t.setCallback(new ProduceTask.Callback() {
-      @Override
-      public void onSuccess() {
-        countDownLatch.countDown();
-      }
-      @Override
-      public void onFailure(Exception ex) {
-        countDownLatch.countDown();
-      }
-    }));
-    _outputKafkaProducer.batchProduce(tasks);
-    _outputKafkaProducer.flush();
-    try {
-      countDownLatch.await(timeout, timeUnit);
-      // right now we only set up a metrics for recording produce fails
-      // TODO: design a better way to handle kafka failure
-      List<ProduceTask<Integer, LogCoordinatorMessage>> failedOrTimeoutTasks = tasks.stream().filter(t -> !t.isSucceed()).collect(Collectors.toList());
-      _metrics.addMeteredGlobalValue(GrigioMeter.MESSAGE_PRODUCE_FAILED_COUNT, failedOrTimeoutTasks.size());
-      return failedOrTimeoutTasks;
-    } catch (InterruptedException e) {
-      throw new RuntimeException("encountered run time exception while waiting for the loop to finish");
-    } finally {
-      _metrics.addTimedValueMs(GrigioTimer.SEND_MSG_TO_KAFKA, System.currentTimeMillis() - startTime);
-      LOGGER.info("send to producer take {} ms", System.currentTimeMillis() - startTime);
-    }
-  }
-
-  protected KeyValueStoreDB<ByteArrayWrapper, KeyCoordinatorMessageContext> getKeyValueStore(Configuration conf) {
-    RocksDBKeyValueStoreDB keyValueStore = new RocksDBKeyValueStoreDB();
-    keyValueStore.init(conf);
-    return keyValueStore;
-  }
-
-  /**
-   * store updates to local update log file, organized by segments
-   * @param tableName
-   * @param tasks
-   * @throws IOException
-   */
-  private void storeMessageToLocal(String tableName, List<ProduceTask<Integer, LogCoordinatorMessage>> tasks) throws IOException {
-    String tableNameWithType = TableNameBuilder.ensureTableNameWithType(tableName, CommonConstants.Helix.TableType.REALTIME);
-    long start = System.currentTimeMillis();
-    Map<String, List<UpdateLogEntry>> segmentUpdateLogs = new HashMap<>();
-    for (ProduceTask<Integer, LogCoordinatorMessage> task: tasks) {
-      String segmentName = task.getValue().getSegmentName();
-      UpdateLogEntry entry = new UpdateLogEntry(task.getValue(), task.getKey());
-      segmentUpdateLogs.computeIfAbsent(segmentName, s -> new ArrayList<>()).add(entry);
-    }
-    for (Map.Entry<String, List<UpdateLogEntry>> segmentEntry: segmentUpdateLogs.entrySet()) {
-      _storageProvider.addDataToFile(tableNameWithType, segmentEntry.getKey(), segmentEntry.getValue());
-    }
-    long duration = System.currentTimeMillis() - start;
-    _metrics.addTimedValueMs(GrigioTimer.STORE_UPDATE_ON_DISK, duration);
-    _metrics.addTimedTableValueMs(tableName, GrigioTimer.STORE_UPDATE_ON_DISK, duration);
-    LOGGER.info("stored all data to files in {} ms", System.currentTimeMillis() - start);
-  }
-
-  /**
-   * store updates to local kv store
-   * @param tableName the name of the table
-   * @param primaryKeyToValueMap the mapping between the primary-key and the their associated state
-   * @throws IOException
-   */
-  private void storeMessageToKVStore(String tableName, Map<ByteArrayWrapper, KeyCoordinatorMessageContext> primaryKeyToValueMap)
-      throws IOException {
-    long start = System.currentTimeMillis();
-    // update kv store
-    KeyValueStoreTable<ByteArrayWrapper, KeyCoordinatorMessageContext> table = _keyValueStoreDB.getTable(tableName);
-    table.multiPut(primaryKeyToValueMap);
-    _metrics.addTimedValueMs(GrigioTimer.STORE_UPDATE_ON_KV, System.currentTimeMillis() - start);
-    LOGGER.info("updated {} message to key value store in {} ms", primaryKeyToValueMap.size(), System.currentTimeMillis() - start);
-  }
-
-  private static class MessageAndOffset<K> {
-    private List<K> _messages;
-    private OffsetInfo _offsetInfo;
-
-    public MessageAndOffset(List<K> messages, OffsetInfo offsetInfo) {
-      _messages = messages;
-      _offsetInfo  = offsetInfo;
-    }
-
-    public List<K> getMessages() {
-      return _messages;
-    }
-
-    public OffsetInfo getOffsetInfo() {
-      return _offsetInfo;
-    }
-  }
-
-  /**
-   * Partially processed ingestion upsert messages, with partition and version added
-   */
-  private static class MessageWithPartitionAndVersion {
-    private final byte[] _key;
-    private final String _segmentName;
-    private final long _kafkaOffset;
-    private final long _timestamp;
-    private final int _partition;
-    private final long _version;
-
-    public MessageWithPartitionAndVersion(byte[] key, String segmentName, long kafkaOffset, long timestamp,
-        int partition, long version) {
-      _key = key;
-      _segmentName = segmentName;
-      _kafkaOffset = kafkaOffset;
-      _timestamp = timestamp;
-      _partition = partition;
-      _version = version;
-    }
-
-    public byte[] getKey() {
-      return _key;
-    }
-
-    public int getPartition() {
-      return _partition;
-    }
-
-    public long getVersion() {
-      return _version;
-    }
-
-    public KeyCoordinatorMessageContext getContext() {
-      return new KeyCoordinatorMessageContext(_segmentName, _timestamp, _kafkaOffset);
-    }
-  }
 }
diff --git a/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/MessageFetcher.java b/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/MessageFetcher.java
new file mode 100644
index 0000000..4d19a18
--- /dev/null
+++ b/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/MessageFetcher.java
@@ -0,0 +1,186 @@
+/**
+ * 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.pinot.grigio.keyCoordinator.internal;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.Uninterruptibles;
+import org.apache.pinot.grigio.common.OffsetInfo;
+import org.apache.pinot.grigio.common.messages.KeyCoordinatorQueueMsg;
+import org.apache.pinot.grigio.common.metrics.GrigioGauge;
+import org.apache.pinot.grigio.common.rpcQueue.KeyCoordinatorQueueConsumer;
+import org.apache.pinot.grigio.common.rpcQueue.QueueConsumerRecord;
+import org.apache.pinot.grigio.keyCoordinator.GrigioKeyCoordinatorMetrics;
+import org.apache.pinot.grigio.keyCoordinator.helix.State;
+import org.apache.pinot.grigio.keyCoordinator.starter.KeyCoordinatorConf;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * class to handle fetching messages from the given input message queue and allow other components to get list of
+ * messages from its internal buffer
+ */
+public class MessageFetcher {
+  private static final Logger LOGGER = LoggerFactory.getLogger(MessageFetcher.class);
+  private static final long TERMINATION_WAIT_MS = 10000;
+
+  protected int _fetchMsgDelayMs;
+  protected int _fetchMsgMaxDelayMs;
+  protected int _fetchMsgMaxCount;
+
+  protected BlockingQueue<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> _consumerRecordBlockingQueue;
+  protected KeyCoordinatorQueueConsumer _inputKafkaConsumer;
+  protected GrigioKeyCoordinatorMetrics _metrics;
+  protected ExecutorService _consumerThread;
+
+  protected volatile State _state;
+
+  public MessageFetcher(KeyCoordinatorConf conf, KeyCoordinatorQueueConsumer consumer,
+                        GrigioKeyCoordinatorMetrics metrics) {
+    this(conf, consumer, Executors.newSingleThreadExecutor(), metrics);
+  }
+
+  @VisibleForTesting
+  protected MessageFetcher(KeyCoordinatorConf conf, KeyCoordinatorQueueConsumer consumer,
+                        ExecutorService service, GrigioKeyCoordinatorMetrics metrics) {
+    _inputKafkaConsumer = consumer;
+    _metrics = metrics;
+    _consumerThread = service;
+    _consumerRecordBlockingQueue = new ArrayBlockingQueue<>(conf.getConsumerBlockingQueueSize());
+
+    _fetchMsgDelayMs = conf.getInt(KeyCoordinatorConf.FETCH_MSG_DELAY_MS,
+        KeyCoordinatorConf.FETCH_MSG_DELAY_MS_DEFAULT);
+    _fetchMsgMaxDelayMs = conf.getInt(KeyCoordinatorConf.FETCH_MSG_MAX_DELAY_MS,
+        KeyCoordinatorConf.FETCH_MSG_MAX_DELAY_MS_DEFAULT);
+    _fetchMsgMaxCount = conf.getInt(KeyCoordinatorConf.FETCH_MSG_MAX_BATCH_SIZE,
+        KeyCoordinatorConf.FETCH_MSG_MAX_BATCH_SIZE_DEFAULT);
+
+    _state = State.INIT;
+    LOGGER.info("starting with fetch delay: {} max delay: {}, fetch max count: {}", _fetchMsgDelayMs, _fetchMsgMaxDelayMs,
+        _fetchMsgMaxCount);
+  }
+
+  public void start() {
+    Preconditions.checkState(_state == State.INIT, "key coordinate is not in correct state");
+    _state = State.RUNNING;
+    _consumerThread.submit(this::consumerIngestLoop);
+
+  }
+
+  /**
+   * get a list of messages read by the consumer ingestion thread
+   * @param deadlineInMs linux epoch time we should stop the ingestion and return it to caller with the data we have so far
+   * @return list of messages to be processed
+  */
+  public MessageAndOffset<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> getMessages(long deadlineInMs) {
+    List<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> buffer = new ArrayList<>(_fetchMsgMaxCount);
+    while(System.currentTimeMillis() < deadlineInMs && buffer.size() < _fetchMsgMaxCount) {
+      _consumerRecordBlockingQueue.drainTo(buffer, _fetchMsgMaxCount - buffer.size());
+      if (buffer.size() < _fetchMsgMaxCount) {
+        Uninterruptibles.sleepUninterruptibly(_fetchMsgDelayMs, TimeUnit.MILLISECONDS);
+      }
+    }
+    OffsetInfo offsetInfo = new OffsetInfo();
+    for (QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg> record: buffer) {
+      offsetInfo.updateOffsetIfNecessary(record);
+    }
+    return new MessageAndOffset<>(buffer, offsetInfo);
+  }
+
+  /**
+   * commit the current ingestion progress to internal offset storage
+   * @param messageAndOffset
+   */
+  public void ackOffset(MessageAndOffset messageAndOffset) {
+    _inputKafkaConsumer.ackOffset(messageAndOffset.getOffsetInfo());
+  }
+
+  public void stop() {
+    _state = State.SHUTTING_DOWN;
+    _consumerThread.shutdown();
+    try {
+      _consumerThread.awaitTermination(TERMINATION_WAIT_MS, TimeUnit.MILLISECONDS);
+    } catch (InterruptedException ex) {
+      LOGGER.error("failed to wait for key coordinator thread to shutdown", ex);
+    }
+    _consumerThread.shutdownNow();
+  }
+
+  private void consumerIngestLoop() {
+    while (_state == State.RUNNING) {
+      try {
+        _metrics.setValueOfGlobalGauge(GrigioGauge.MESSAGE_PROCESS_QUEUE_SIZE, _consumerRecordBlockingQueue.size());
+        List<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> records =
+            _inputKafkaConsumer.getRequests(_fetchMsgMaxDelayMs, TimeUnit.MILLISECONDS);
+        if (records.size() == 0) {
+          LOGGER.info("no message found in kafka consumer, sleep and wait for next batch");
+          Uninterruptibles.sleepUninterruptibly(_fetchMsgDelayMs, TimeUnit.MILLISECONDS);
+        } else {
+          records.forEach(c -> {
+            try {
+              _consumerRecordBlockingQueue.put(c);
+            } catch (InterruptedException e) {
+              LOGGER.warn("exception while trying to put message to queue", e);
+            }
+          });
+          _metrics.setValueOfGlobalGauge(GrigioGauge.KC_INPUT_MESSAGE_LAG_MS,
+              System.currentTimeMillis() - records.get(records.size() - 1).getTimestamp());
+        }
+      } catch (Exception ex) {
+        LOGGER.error("encountered exception in consumer ingest loop, will retry", ex);
+      }
+    }
+    LOGGER.info("exiting consumer ingest loop");
+  }
+
+  /**
+   * class wrap around the message and offset associated information
+   * @param <K>
+   */
+  public static class MessageAndOffset<K> {
+    private List<K> _messages;
+    private OffsetInfo _offsetInfo;
+
+    /**
+     * @param messages list of messages for the current batch
+     * @param offsetInfo the largest offset for each partition in current set of messages
+     */
+    public MessageAndOffset(List<K> messages, OffsetInfo offsetInfo) {
+      _messages = messages;
+      _offsetInfo  = offsetInfo;
+    }
+
+    public List<K> getMessages() {
+      return _messages;
+    }
+
+    public OffsetInfo getOffsetInfo() {
+      return _offsetInfo;
+    }
+  }
+
+}
diff --git a/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/DistributedKeyCoordinatorCore.java b/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/SegmentEventProcessor.java
similarity index 55%
copy from pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/DistributedKeyCoordinatorCore.java
copy to pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/SegmentEventProcessor.java
index 044673f..cf6f762 100644
--- a/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/DistributedKeyCoordinatorCore.java
+++ b/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/SegmentEventProcessor.java
@@ -20,12 +20,10 @@ package org.apache.pinot.grigio.keyCoordinator.internal;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
-import com.google.common.util.concurrent.Uninterruptibles;
 import org.apache.commons.configuration.Configuration;
 import org.apache.pinot.common.config.TableNameBuilder;
 import org.apache.pinot.common.utils.CommonConstants;
 import org.apache.pinot.grigio.common.DistributedCommonUtils;
-import org.apache.pinot.grigio.common.OffsetInfo;
 import org.apache.pinot.grigio.common.keyValueStore.ByteArrayWrapper;
 import org.apache.pinot.grigio.common.keyValueStore.KeyValueStoreDB;
 import org.apache.pinot.grigio.common.keyValueStore.KeyValueStoreTable;
@@ -37,21 +35,15 @@ import org.apache.pinot.grigio.common.messages.LogEventType;
 import org.apache.pinot.grigio.common.metrics.GrigioGauge;
 import org.apache.pinot.grigio.common.metrics.GrigioMeter;
 import org.apache.pinot.grigio.common.metrics.GrigioTimer;
-import org.apache.pinot.grigio.common.rpcQueue.KeyCoordinatorQueueConsumer;
 import org.apache.pinot.grigio.common.rpcQueue.LogCoordinatorQueueProducer;
 import org.apache.pinot.grigio.common.rpcQueue.ProduceTask;
 import org.apache.pinot.grigio.common.rpcQueue.QueueConsumerRecord;
-import org.apache.pinot.grigio.common.rpcQueue.VersionMsgQueueProducer;
 import org.apache.pinot.grigio.common.storageProvider.UpdateLogEntry;
 import org.apache.pinot.grigio.common.storageProvider.UpdateLogStorageProvider;
 import org.apache.pinot.grigio.common.storageProvider.retentionManager.UpdateLogRetentionManager;
 import org.apache.pinot.grigio.common.storageProvider.retentionManager.UpdateLogTableRetentionManager;
 import org.apache.pinot.grigio.common.updateStrategy.MessageResolveStrategy;
-import org.apache.pinot.grigio.common.utils.CommonUtils;
 import org.apache.pinot.grigio.keyCoordinator.GrigioKeyCoordinatorMetrics;
-import org.apache.pinot.grigio.keyCoordinator.helix.KeyCoordinatorClusterHelixManager;
-import org.apache.pinot.grigio.keyCoordinator.helix.KeyCoordinatorLeadershipManager;
-import org.apache.pinot.grigio.keyCoordinator.helix.KeyCoordinatorVersionManager;
 import org.apache.pinot.grigio.keyCoordinator.helix.State;
 import org.apache.pinot.grigio.keyCoordinator.starter.KeyCoordinatorConf;
 import org.slf4j.Logger;
@@ -59,272 +51,107 @@ import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.Timer;
-import java.util.TimerTask;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
+/**
+ * process a list of input messages for a key coordinator, sending out updates events and update internal data storage
+ */
+public class SegmentEventProcessor {
+  private static final Logger LOGGER = LoggerFactory.getLogger(SegmentEventProcessor.class);
 
-public class DistributedKeyCoordinatorCore {
-  private static final Logger LOGGER = LoggerFactory.getLogger(DistributedKeyCoordinatorCore.class);
-  private static final long TERMINATION_WAIT_MS = 10000;
+  private static final long TERMINATION_WAIT_MS = 10_000L;
+  public static final String PROCESS_THREAD_COUNT = "kc.processor.threadCount";
+  public static final int PROCESS_THREAD_COUNT_DEFAULT = 5;
 
+  // constructor provided param
   protected KeyCoordinatorConf _conf;
   protected LogCoordinatorQueueProducer _outputKafkaProducer;
-  protected KeyCoordinatorQueueConsumer _inputKafkaConsumer;
-  protected VersionMsgQueueProducer _versionMessageKafkaProducer;
   protected MessageResolveStrategy _messageResolveStrategy;
   protected KeyValueStoreDB<ByteArrayWrapper, KeyCoordinatorMessageContext> _keyValueStoreDB;
-  protected ExecutorService _messageProcessThread;
-  protected ExecutorService _consumerThread;
-  protected BlockingQueue<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> _consumerRecordBlockingQueue;
   protected UpdateLogStorageProvider _storageProvider;
-  protected KeyCoordinatorClusterHelixManager _keyCoordinatorClusterHelixManager;
-  protected KeyCoordinatorLeadershipManager _keyCoordinatorLeadershipManager;
-  protected KeyCoordinatorVersionManager _keyCoordinatorVersionManager;
   protected UpdateLogRetentionManager _retentionManager;
   protected GrigioKeyCoordinatorMetrics _metrics;
-  protected String _topicPrefix;
-  protected int _fetchMsgDelayMs;
-  protected int _fetchMsgMaxDelayMs;
-  protected int _fetchMsgMaxCount;
-  protected long _versionMessageIntervalMs;
-  protected Timer _versionMessageTimer;
-  protected TimerTask _versionMessageProduceTask;
-  protected Map<Integer, Long> _currentVersionConsumed;
-
-  protected volatile State _state = State.SHUTDOWN;
+  protected VersionMessageManager _versionMessageManager;
 
-  public DistributedKeyCoordinatorCore() {}
-
-  public void init(KeyCoordinatorConf conf, LogCoordinatorQueueProducer keyCoordinatorProducer,
-                   KeyCoordinatorQueueConsumer keyCoordinatorConsumer, VersionMsgQueueProducer versionMessageKafkaProducer,
-                   MessageResolveStrategy messageResolveStrategy,
-                   KeyCoordinatorClusterHelixManager keyCoordinatorClusterHelixManager,
-                   UpdateLogRetentionManager retentionManager, GrigioKeyCoordinatorMetrics metrics) {
-    init(conf, keyCoordinatorProducer, keyCoordinatorConsumer, versionMessageKafkaProducer, messageResolveStrategy,
-        getKeyValueStore(conf.subset(KeyCoordinatorConf.KEY_COORDINATOR_KV_STORE)), Executors.newSingleThreadExecutor(),
-        Executors.newSingleThreadExecutor(), UpdateLogStorageProvider.getInstance(), keyCoordinatorClusterHelixManager,
-        retentionManager, metrics);
+  // config provided param
+  protected String _topicPrefix;
+  protected ExecutorService _service;
+
+  protected volatile State _state;
+
+  public SegmentEventProcessor(KeyCoordinatorConf conf, LogCoordinatorQueueProducer keyCoordinatorProducer,
+                               MessageResolveStrategy messageResolveStrategy,
+                               UpdateLogRetentionManager updateLogRetentionManager,
+                               VersionMessageManager versionMessageManager,
+                               GrigioKeyCoordinatorMetrics metrics) {
+    this(conf, keyCoordinatorProducer, messageResolveStrategy,
+        getKeyValueStore(conf.subset(KeyCoordinatorConf.KEY_COORDINATOR_KV_STORE)),
+        UpdateLogStorageProvider.getInstance(),
+        updateLogRetentionManager,
+        versionMessageManager,
+        metrics);
   }
 
   @VisibleForTesting
-  public void init(KeyCoordinatorConf conf, LogCoordinatorQueueProducer keyCoordinatorProducer,
-                   KeyCoordinatorQueueConsumer keyCoordinatorConsumer,
-                   VersionMsgQueueProducer versionMessageKafkaProducer,
-                   MessageResolveStrategy messageResolveStrategy,
-                   KeyValueStoreDB<ByteArrayWrapper, KeyCoordinatorMessageContext> keyValueStoreDB,
-                   ExecutorService coreThread, ExecutorService consumerThread, UpdateLogStorageProvider storageProvider,
-                   KeyCoordinatorClusterHelixManager keyCoordinatorClusterHelixManager,
-                   UpdateLogRetentionManager updateLogRetentionManager, GrigioKeyCoordinatorMetrics metrics) {
-    CommonUtils.printConfiguration(conf, "distributed key coordinator core");
-    Preconditions.checkState(_state == State.SHUTDOWN, "can only init if it is not running yet");
+  public SegmentEventProcessor(KeyCoordinatorConf conf, LogCoordinatorQueueProducer keyCoordinatorProducer,
+                               MessageResolveStrategy messageResolveStrategy,
+                               KeyValueStoreDB<ByteArrayWrapper, KeyCoordinatorMessageContext> keyValueStoreDB,
+                               UpdateLogStorageProvider storageProvider,
+                               UpdateLogRetentionManager updateLogRetentionManager,
+                               VersionMessageManager versionMessageManager,
+                               GrigioKeyCoordinatorMetrics metrics) {
     _conf = conf;
     _outputKafkaProducer = keyCoordinatorProducer;
-    _inputKafkaConsumer = keyCoordinatorConsumer;
-    _versionMessageKafkaProducer = versionMessageKafkaProducer;
     _messageResolveStrategy = messageResolveStrategy;
     _keyValueStoreDB = keyValueStoreDB;
-    _messageProcessThread = coreThread;
-    _consumerThread = consumerThread;
-    _consumerRecordBlockingQueue = new ArrayBlockingQueue<>(_conf.getConsumerBlockingQueueSize());
     _storageProvider = storageProvider;
-    _keyCoordinatorClusterHelixManager = keyCoordinatorClusterHelixManager;
     _retentionManager = updateLogRetentionManager;
+    _versionMessageManager = versionMessageManager;
     _metrics = metrics;
-
-    _fetchMsgDelayMs = conf.getInt(KeyCoordinatorConf.FETCH_MSG_DELAY_MS,
-        KeyCoordinatorConf.FETCH_MSG_DELAY_MS_DEFAULT);
-    _fetchMsgMaxDelayMs = conf.getInt(KeyCoordinatorConf.FETCH_MSG_MAX_DELAY_MS,
-        KeyCoordinatorConf.FETCH_MSG_MAX_DELAY_MS_DEFAULT);
-    _fetchMsgMaxCount = conf.getInt(KeyCoordinatorConf.FETCH_MSG_MAX_BATCH_SIZE,
-        KeyCoordinatorConf.FETCH_MSG_MAX_BATCH_SIZE_DEFAULT);
+    // get config
     _topicPrefix = conf.getTopicPrefix();
-    LOGGER.info("starting with fetch delay: {} max delay: {}, fetch max count: {}", _fetchMsgDelayMs, _fetchMsgMaxDelayMs,
-        _fetchMsgMaxCount);
+    _service = Executors.newFixedThreadPool(conf.getInt(PROCESS_THREAD_COUNT, PROCESS_THREAD_COUNT_DEFAULT));
 
-    _versionMessageIntervalMs = conf.getLong(KeyCoordinatorConf.VERSION_MESSAGE_INTERVAL_MS,
-        KeyCoordinatorConf.VERSION_MESSAGE_INTERVAL_MS_DEFAULT);
-    _versionMessageTimer = new Timer();
-    _versionMessageProduceTask = new TimerTask() {
-      @Override
-      public void run() {
-        produceVersionMessage();
-      }
-    };
-    _keyCoordinatorLeadershipManager =
-        new KeyCoordinatorLeadershipManager(_keyCoordinatorClusterHelixManager.getControllerHelixManager());
-    _keyCoordinatorVersionManager =
-        new KeyCoordinatorVersionManager(_keyCoordinatorClusterHelixManager.getControllerHelixManager());
-
-    _currentVersionConsumed = _keyCoordinatorVersionManager.getVersionConsumedFromPropertyStore();
     _state = State.INIT;
   }
 
   public void start() {
-    Preconditions.checkState(_state == State.INIT, "key coordinate is not in correct state");
-    LOGGER.info("starting key coordinator message process loop");
     _state = State.RUNNING;
-    _versionMessageTimer.schedule(_versionMessageProduceTask, 0L, _versionMessageIntervalMs);
-    _consumerThread.submit(this::consumerIngestLoop);
-    _messageProcessThread.submit(this::messageProcessLoop);
-  }
-
-  private void produceVersionMessage() {
-    long start = System.currentTimeMillis();
-    if (_state != State.RUNNING) {
-      LOGGER.info("Key coordinator not running, skip producing version messages");
-      return;
-    }
-    if (!_keyCoordinatorLeadershipManager.isLeader()) {
-      LOGGER.debug("Not controller leader, skip producing version messages");
-      return;
-    }
-    try {
-      long versionProduced = _keyCoordinatorVersionManager.getVersionProducedFromPropertyStore();
-      long versionToProduce = versionProduced + 1;
-      // produce to all partitions
-      for (int partition = 0; partition < _conf.getKeyCoordinatorMessagePartitionCount(); partition++) {
-        ProduceTask<Integer, KeyCoordinatorQueueMsg> produceTask =
-            new ProduceTask<>(_conf.getKeyCoordinatorMessageTopic(), partition,
-                new KeyCoordinatorQueueMsg(versionToProduce));
-        _versionMessageKafkaProducer.produce(produceTask);
-      }
-      // todo: make producing version messages and setting versions to property store as one transaction
-      _keyCoordinatorVersionManager.setVersionProducedToPropertyStore(versionToProduce);
-      long duration = System.currentTimeMillis() - start;
-      _metrics.addTimedValueMs(GrigioTimer.PRODUCE_VERSION_MESSAGE, duration);
-      _metrics.setValueOfGlobalGauge(GrigioGauge.VERSION_PRODUCED, versionToProduce);
-      LOGGER.info("Produced version messages to all partitions with version {} in {} ms", versionToProduce, duration);
-    } catch (Exception ex) {
-      LOGGER.error("Failed to produce version message", ex);
-    }
-  }
-
-  private void consumerIngestLoop() {
-    while (_state == State.RUNNING) {
-      try {
-        _metrics.setValueOfGlobalGauge(GrigioGauge.MESSAGE_PROCESS_QUEUE_SIZE, _consumerRecordBlockingQueue.size());
-        List<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> records = _inputKafkaConsumer.getRequests(_fetchMsgMaxDelayMs,
-            TimeUnit.MILLISECONDS);
-        if (records.size() == 0) {
-          LOGGER.info("no message found in kafka consumer, sleep and wait for next batch");
-          Uninterruptibles.sleepUninterruptibly(_fetchMsgMaxDelayMs, TimeUnit.MILLISECONDS);
-        } else {
-          records.forEach(c -> {
-            try {
-              _consumerRecordBlockingQueue.put(c);
-            } catch (InterruptedException e) {
-              LOGGER.warn("exception while trying to put message to queue", e);
-            }
-          });
-          _metrics.setValueOfGlobalGauge(GrigioGauge.KC_INPUT_MESSAGE_LAG_MS,
-              System.currentTimeMillis() - records.get(records.size() - 1).getTimestamp());
-        }
-      } catch (Exception ex) {
-        LOGGER.error("encountered exception in consumer ingest loop, will retry", ex);
-      }
-    }
-    LOGGER.info("exiting consumer ingest loop");
-  }
-
-  private void messageProcessLoop() {
-    try {
-      long deadline = System.currentTimeMillis() + _fetchMsgMaxDelayMs;
-      while (_state == State.RUNNING) {
-        LOGGER.info("starting new loop");
-        long start = System.currentTimeMillis();
-        // process message when we got max message count or reach max delay ms
-        MessageAndOffset<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> messageAndOffset = getMessagesFromQueue(_consumerRecordBlockingQueue, deadline);
-        List<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> messages = messageAndOffset.getMessages();
-        deadline = System.currentTimeMillis() + _fetchMsgMaxDelayMs;
-
-        _metrics.addMeteredGlobalValue(GrigioMeter.MESSAGE_PROCESS_THREAD_FETCH_COUNT, messages.size());
-        _metrics.addTimedValueMs(GrigioTimer.MESSAGE_PROCESS_THREAD_FETCH_DELAY, System.currentTimeMillis() - start);
-
-        if (messages.size() > 0) {
-          processMessages(messages);
-          // todo: make ackOffset and setVersionConsumed as one transaction
-          _inputKafkaConsumer.ackOffset(messageAndOffset.getOffsetInfo());
-          _keyCoordinatorVersionManager.setVersionConsumedToPropertyStore(_currentVersionConsumed);
-          LOGGER.info("kc message processed {} messages in this loop for {} ms", messages.size(),
-              System.currentTimeMillis() - start);
-        } else {
-          LOGGER.info("no message received in the current loop");
-        }
-      }
-    } catch (Exception ex) {
-      LOGGER.warn("key coordinator is exiting due to exception", ex);
-    } finally {
-      _state = State.SHUTTING_DOWN;
-      LOGGER.info("exiting key coordinator loop");
-    }
-    LOGGER.info("existing key coordinator core /procthread");
-  }
-
-  /**
-   * get a list of messages read by the consumer ingestion thread
-   * @param queue the blocking queue shared with the ingestion thread
-   * @param deadline at which time we should stop the ingestion and return it to caller with the data we have
-   * @return list of messages to be processed
-   */
-  private MessageAndOffset<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> getMessagesFromQueue(
-      BlockingQueue<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> queue, long deadline) {
-    List<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> buffer = new ArrayList<>(_fetchMsgMaxCount * 2);
-    while(System.currentTimeMillis() < deadline && buffer.size() < _fetchMsgMaxCount) {
-      queue.drainTo(buffer, _fetchMsgMaxCount - buffer.size());
-      if (buffer.size() < _fetchMsgMaxCount) {
-        Uninterruptibles.sleepUninterruptibly(_fetchMsgDelayMs, TimeUnit.MILLISECONDS);
-      }
-    }
-    OffsetInfo offsetInfo = new OffsetInfo();
-    for (QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg> record: buffer) {
-      offsetInfo.updateOffsetIfNecessary(record);
-    }
-    return new MessageAndOffset<>(buffer, offsetInfo);
   }
 
   public void stop() {
     _state = State.SHUTTING_DOWN;
-    _messageProcessThread.shutdown();
-    _consumerThread.shutdown();
-    _versionMessageTimer.cancel();
-    try {
-      _consumerThread.awaitTermination(TERMINATION_WAIT_MS, TimeUnit.MILLISECONDS);
-      _messageProcessThread.awaitTermination(TERMINATION_WAIT_MS, TimeUnit.MILLISECONDS);
+    _service.shutdown();
+    try  {
+      _service.awaitTermination(TERMINATION_WAIT_MS, TimeUnit.MILLISECONDS);
     } catch (InterruptedException ex) {
-      LOGGER.error("failed to wait for key coordinator thread to shutdown", ex);
+      LOGGER.error("failed to wait for message processing thread to shutdown", ex);
     }
-    _consumerThread.shutdownNow();
-    _messageProcessThread.shutdownNow();
+    _service.shutdownNow();
     _state = State.SHUTDOWN;
   }
 
-  public State getState() {
-    return _state;
-  }
-
   /**
    * process a list of update logs (update kv, send to downstream kafka, etc)
    * @param messages list of the segment ingestion event for us to use in updates
    */
-  private void processMessages(List<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> messages) {
+  public void processMessages(List<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> messages) {
     long start = System.currentTimeMillis();
     Map<String, List<MessageWithPartitionAndVersion>> tableMsgMap = new HashMap<>();
     for (QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg> msg: messages) {
       if (msg.getRecord().isVersionMessage()) {
         // update _currentVersionConsumed for each version message
-        _currentVersionConsumed.put(msg.getPartition(), msg.getRecord().getVersion());
+        _versionMessageManager.maybeUpdateVersionConsumed(msg.getPartition(), msg.getRecord().getVersion());
         _metrics.setValueOfTableGauge(String.valueOf(msg.getPartition()), GrigioGauge.KC_VERSION_CONSUMED, msg.getRecord().getVersion());
       } else {
         // filter out version messages, attach the current version (and partition) to each regular messages
@@ -335,7 +162,7 @@ public class DistributedKeyCoordinatorCore {
                 msg.getRecord().getKafkaOffset(),
                 msg.getRecord().getTimestamp(),
                 msg.getPartition(),
-                _currentVersionConsumed.getOrDefault(msg.getPartition(), 0L)
+                _versionMessageManager.getVersionConsumed(msg.getPartition())
             )
         );
       }
@@ -351,7 +178,8 @@ public class DistributedKeyCoordinatorCore {
    * @param tableName the name of the table we are processing (with type)
    * @param msgList the list of message associated with it
    */
-  private void processMessagesForTable(String tableName, List<MessageWithPartitionAndVersion> msgList) {
+  protected void processMessagesForTable(String tableName, List<MessageWithPartitionAndVersion> msgList) {
+    Preconditions.checkState(_state == State.RUNNING, "segment update processor is not running");
     try {
       long start = System.currentTimeMillis();
       if (msgList == null || msgList.size() == 0) {
@@ -379,12 +207,13 @@ public class DistributedKeyCoordinatorCore {
    * create a message to be sent to output kafka topic based on a list of metadata
    * partition is the partition number of the ingestion upsert event. Note that the partition of records with primary
    * key will be the same across ingestion upsert event and segment update event topics.
+   * TODO: remove name referring log coordinator
    */
-  private ProduceTask<Integer, LogCoordinatorMessage> createMessageToLogCoordinator(String tableName, String segmentName,
+  protected ProduceTask<Integer, LogCoordinatorMessage> createMessageToLogCoordinator(String tableName, String segmentName,
                                                                                     long oldKafkaOffset, long value,
                                                                                     LogEventType eventType, int partition) {
     return new ProduceTask<>(DistributedCommonUtils.getKafkaTopicFromTableName(tableName, _topicPrefix),
-            partition, new LogCoordinatorMessage(segmentName, oldKafkaOffset, value, eventType));
+        partition, new LogCoordinatorMessage(segmentName, oldKafkaOffset, value, eventType));
   }
 
   /**
@@ -394,8 +223,8 @@ public class DistributedKeyCoordinatorCore {
    * @return map of primary key and their associated state in key-value store
    * @throws IOException
    */
-  private Map<ByteArrayWrapper, KeyCoordinatorMessageContext> fetchDataFromKVStore(String tableName,
-      List<MessageWithPartitionAndVersion> msgList)
+  protected Map<ByteArrayWrapper, KeyCoordinatorMessageContext> fetchDataFromKVStore(String tableName,
+                                                                                   List<MessageWithPartitionAndVersion> msgList)
       throws IOException {
     long start = System.currentTimeMillis();
     KeyValueStoreTable<ByteArrayWrapper, KeyCoordinatorMessageContext> table = _keyValueStoreDB.getTable(tableName);
@@ -420,7 +249,7 @@ public class DistributedKeyCoordinatorCore {
    *                             reflect the new state after these updates are done
    * @param tasks the generated producer task to be sent to segment update entry queue (kafka topic)
    */
-  private void processMessageUpdates(String tableName, List<MessageWithPartitionAndVersion> msgList,
+  protected void processMessageUpdates(String tableName, List<MessageWithPartitionAndVersion> msgList,
                                      Map<ByteArrayWrapper, KeyCoordinatorMessageContext> primaryKeyToValueMap,
                                      List<ProduceTask<Integer, LogCoordinatorMessage>> tasks) {
     // TODO: add a unit test
@@ -468,7 +297,7 @@ public class DistributedKeyCoordinatorCore {
    * @param timeUnit the timeunit for waiting for the producers
    * @return a list of the tasks we failed to produce to downstream
    */
-  private List<ProduceTask<Integer, LogCoordinatorMessage>> sendMessagesToLogCoordinator(
+  protected List<ProduceTask<Integer, LogCoordinatorMessage>> sendMessagesToLogCoordinator(
       List<ProduceTask<Integer, LogCoordinatorMessage>> tasks, long timeout, TimeUnit timeUnit) {
     long startTime = System.currentTimeMillis();
     // send all and wait for result, batch for better perf
@@ -500,7 +329,7 @@ public class DistributedKeyCoordinatorCore {
     }
   }
 
-  protected KeyValueStoreDB<ByteArrayWrapper, KeyCoordinatorMessageContext> getKeyValueStore(Configuration conf) {
+  protected static KeyValueStoreDB<ByteArrayWrapper, KeyCoordinatorMessageContext> getKeyValueStore(Configuration conf) {
     RocksDBKeyValueStoreDB keyValueStore = new RocksDBKeyValueStoreDB();
     keyValueStore.init(conf);
     return keyValueStore;
@@ -512,7 +341,7 @@ public class DistributedKeyCoordinatorCore {
    * @param tasks
    * @throws IOException
    */
-  private void storeMessageToLocal(String tableName, List<ProduceTask<Integer, LogCoordinatorMessage>> tasks) throws IOException {
+  protected void storeMessageToLocal(String tableName, List<ProduceTask<Integer, LogCoordinatorMessage>> tasks) throws IOException {
     String tableNameWithType = TableNameBuilder.ensureTableNameWithType(tableName, CommonConstants.Helix.TableType.REALTIME);
     long start = System.currentTimeMillis();
     Map<String, List<UpdateLogEntry>> segmentUpdateLogs = new HashMap<>();
@@ -536,7 +365,7 @@ public class DistributedKeyCoordinatorCore {
    * @param primaryKeyToValueMap the mapping between the primary-key and the their associated state
    * @throws IOException
    */
-  private void storeMessageToKVStore(String tableName, Map<ByteArrayWrapper, KeyCoordinatorMessageContext> primaryKeyToValueMap)
+  protected void storeMessageToKVStore(String tableName, Map<ByteArrayWrapper, KeyCoordinatorMessageContext> primaryKeyToValueMap)
       throws IOException {
     long start = System.currentTimeMillis();
     // update kv store
@@ -546,28 +375,10 @@ public class DistributedKeyCoordinatorCore {
     LOGGER.info("updated {} message to key value store in {} ms", primaryKeyToValueMap.size(), System.currentTimeMillis() - start);
   }
 
-  private static class MessageAndOffset<K> {
-    private List<K> _messages;
-    private OffsetInfo _offsetInfo;
-
-    public MessageAndOffset(List<K> messages, OffsetInfo offsetInfo) {
-      _messages = messages;
-      _offsetInfo  = offsetInfo;
-    }
-
-    public List<K> getMessages() {
-      return _messages;
-    }
-
-    public OffsetInfo getOffsetInfo() {
-      return _offsetInfo;
-    }
-  }
-
   /**
    * Partially processed ingestion upsert messages, with partition and version added
    */
-  private static class MessageWithPartitionAndVersion {
+  protected static class MessageWithPartitionAndVersion {
     private final byte[] _key;
     private final String _segmentName;
     private final long _kafkaOffset;
@@ -576,7 +387,7 @@ public class DistributedKeyCoordinatorCore {
     private final long _version;
 
     public MessageWithPartitionAndVersion(byte[] key, String segmentName, long kafkaOffset, long timestamp,
-        int partition, long version) {
+                                          int partition, long version) {
       _key = key;
       _segmentName = segmentName;
       _kafkaOffset = kafkaOffset;
@@ -600,5 +411,17 @@ public class DistributedKeyCoordinatorCore {
     public KeyCoordinatorMessageContext getContext() {
       return new KeyCoordinatorMessageContext(_segmentName, _timestamp, _kafkaOffset);
     }
+
+    @Override
+    public String toString() {
+      return "MessageWithPartitionAndVersion{" +
+          "_key=" + Arrays.toString(_key) +
+          ", _segmentName='" + _segmentName + '\'' +
+          ", _kafkaOffset=" + _kafkaOffset +
+          ", _timestamp=" + _timestamp +
+          ", _partition=" + _partition +
+          ", _version=" + _version +
+          '}';
+    }
   }
 }
diff --git a/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/VersionMessageManager.java b/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/VersionMessageManager.java
new file mode 100644
index 0000000..859bc0b
--- /dev/null
+++ b/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/internal/VersionMessageManager.java
@@ -0,0 +1,160 @@
+/**
+ * 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.pinot.grigio.keyCoordinator.internal;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import org.apache.helix.HelixManager;
+import org.apache.pinot.grigio.common.messages.KeyCoordinatorQueueMsg;
+import org.apache.pinot.grigio.common.metrics.GrigioGauge;
+import org.apache.pinot.grigio.common.metrics.GrigioTimer;
+import org.apache.pinot.grigio.common.rpcQueue.ProduceTask;
+import org.apache.pinot.grigio.common.rpcQueue.VersionMsgQueueProducer;
+import org.apache.pinot.grigio.keyCoordinator.GrigioKeyCoordinatorMetrics;
+import org.apache.pinot.grigio.keyCoordinator.helix.KeyCoordinatorLeadershipManager;
+import org.apache.pinot.grigio.keyCoordinator.helix.KeyCoordinatorVersionManager;
+import org.apache.pinot.grigio.keyCoordinator.helix.State;
+import org.apache.pinot.grigio.keyCoordinator.starter.KeyCoordinatorConf;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * class to handle version information in key coordinator, it does the following two tasks:
+ * 1. send out version message to kc input queue if it is leader at the moment
+ * 2. store and manage the current version for each input partition.
+ */
+public class VersionMessageManager {
+  private static final Logger LOGGER = LoggerFactory.getLogger(VersionMessageManager.class);
+
+  protected KeyCoordinatorConf _conf;
+  protected Timer _versionMessageTimer;
+  protected TimerTask _versionMessageProduceTask;
+  protected VersionMsgQueueProducer _versionMessageKafkaProducer;
+  protected KeyCoordinatorVersionManager _keyCoordinatorVersionManager;
+  protected KeyCoordinatorLeadershipManager _keyCoordinatorLeadershipManager;
+  protected GrigioKeyCoordinatorMetrics _metrics;
+  protected long _versionMessageIntervalMs;
+  protected Map<Integer, Long> _currentVersionConsumed;
+
+  protected volatile State _state;
+
+  public VersionMessageManager(KeyCoordinatorConf conf, VersionMsgQueueProducer versionMsgQueueProducer,
+                               HelixManager controllerHelixManager, GrigioKeyCoordinatorMetrics metrics) {
+    this(conf,
+        versionMsgQueueProducer,
+        new Timer(),
+        new KeyCoordinatorVersionManager(controllerHelixManager),
+        new KeyCoordinatorLeadershipManager(controllerHelixManager),
+        metrics);
+  }
+
+  @VisibleForTesting
+  public VersionMessageManager(KeyCoordinatorConf conf, VersionMsgQueueProducer versionProducer,
+                               Timer versionMessageTimer, KeyCoordinatorVersionManager versionManager,
+                               KeyCoordinatorLeadershipManager leadershipManager, GrigioKeyCoordinatorMetrics metrics) {
+    _conf = conf;
+    _versionMessageTimer = versionMessageTimer;
+    _versionMessageKafkaProducer = versionProducer;
+    _keyCoordinatorVersionManager = versionManager;
+    _keyCoordinatorLeadershipManager = leadershipManager;
+    _metrics = metrics;
+    _versionMessageProduceTask = new TimerTask() {
+      @Override
+      public void run() {
+        produceVersionMessage();
+      }
+    };
+    _versionMessageIntervalMs = conf.getLong(KeyCoordinatorConf.VERSION_MESSAGE_INTERVAL_MS,
+        KeyCoordinatorConf.VERSION_MESSAGE_INTERVAL_MS_DEFAULT);
+    _currentVersionConsumed = _keyCoordinatorVersionManager.getVersionConsumedFromPropertyStore();
+
+    _state = State.INIT;
+  }
+
+  public void start() {
+    _state = State.RUNNING;
+    _versionMessageTimer.schedule(_versionMessageProduceTask, 0L, _versionMessageIntervalMs);
+  }
+
+  public void stop() {
+    _versionMessageTimer.cancel();
+  }
+
+  private void produceVersionMessage() {
+    long start = System.currentTimeMillis();
+    if (_state != State.RUNNING) {
+      LOGGER.info("Key coordinator not running, skip producing version messages");
+      return;
+    }
+    if (!_keyCoordinatorLeadershipManager.isLeader()) {
+      LOGGER.debug("Not controller leader, skip producing version messages");
+      return;
+    }
+    try {
+      long versionProduced = _keyCoordinatorVersionManager.getVersionProducedFromPropertyStore();
+      long versionToProduce = versionProduced + 1;
+      // produce to all partitions
+      for (int partition = 0; partition < _conf.getKeyCoordinatorMessagePartitionCount(); partition++) {
+        ProduceTask<Integer, KeyCoordinatorQueueMsg> produceTask =
+            new ProduceTask<>(_conf.getKeyCoordinatorMessageTopic(), partition,
+                new KeyCoordinatorQueueMsg(versionToProduce));
+        _versionMessageKafkaProducer.produce(produceTask);
+      }
+      // todo: make producing version messages and setting versions to property store as one transaction
+      _keyCoordinatorVersionManager.setVersionProducedToPropertyStore(versionToProduce);
+      long duration = System.currentTimeMillis() - start;
+      _metrics.addTimedValueMs(GrigioTimer.PRODUCE_VERSION_MESSAGE, duration);
+      _metrics.setValueOfGlobalGauge(GrigioGauge.VERSION_PRODUCED, versionToProduce);
+      LOGGER.info("Produced version messages to all partitions with version {} in {} ms", versionToProduce, duration);
+    } catch (Exception ex) {
+      LOGGER.error("Failed to produce version message", ex);
+    }
+  }
+
+  /**
+   * store the current version for all known partition to property store
+   */
+  public synchronized void setVersionConsumedToPropertyStore() {
+    _keyCoordinatorVersionManager.setVersionConsumedToPropertyStore(ImmutableMap.copyOf(_currentVersionConsumed));
+  }
+
+  /**
+   * update the version for a given partition if it is larger than current version value
+   * @param partition the partition we are updating
+   * @param version the version we are updating to
+   */
+  public synchronized void maybeUpdateVersionConsumed(int partition, long version) {
+    if (!_currentVersionConsumed.containsKey(partition) || _currentVersionConsumed.get(partition) < version) {
+      _currentVersionConsumed.put(partition, version);
+    }
+  }
+
+  /**
+   * get the current version for a given partition
+   * @param partition the partition we are trying to fetch the version from
+   * @return the version associated with the current partition
+   */
+  public synchronized long getVersionConsumed(int partition) {
+    return _currentVersionConsumed.getOrDefault(partition, 0L);
+  }
+}
diff --git a/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/starter/KeyCoordinatorStarter.java b/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/starter/KeyCoordinatorStarter.java
index c7d9628..6db84d9 100644
--- a/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/starter/KeyCoordinatorStarter.java
+++ b/pinot-grigio/pinot-grigio-coordinator/src/main/java/org/apache/pinot/grigio/keyCoordinator/starter/KeyCoordinatorStarter.java
@@ -39,6 +39,9 @@ import org.apache.pinot.grigio.keyCoordinator.helix.KeyCoordinatorClusterHelixMa
 import org.apache.pinot.grigio.keyCoordinator.helix.KeyCoordinatorPinotHelixSpectator;
 import org.apache.pinot.grigio.keyCoordinator.helix.State;
 import org.apache.pinot.grigio.keyCoordinator.internal.DistributedKeyCoordinatorCore;
+import org.apache.pinot.grigio.keyCoordinator.internal.MessageFetcher;
+import org.apache.pinot.grigio.keyCoordinator.internal.SegmentEventProcessor;
+import org.apache.pinot.grigio.keyCoordinator.internal.VersionMessageManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -132,9 +135,12 @@ public class KeyCoordinatorStarter {
 
   public void start() {
     LOGGER.info("starting key coordinator instance");
-    _keyCoordinatorCore
-        .init(_keyCoordinatorConf, _producer, _consumer, _versionMessageProducer, _messageResolveStrategy,
-            _keyCoordinatorClusterHelixManager, _retentionManager, _metrics);
+    final MessageFetcher fetcher = new MessageFetcher(_keyCoordinatorConf, _consumer, _metrics);
+    final VersionMessageManager versionMessageManager = new VersionMessageManager(_keyCoordinatorConf,
+        _versionMessageProducer, _keyCoordinatorClusterHelixManager.getControllerHelixManager(), _metrics);
+    final SegmentEventProcessor segmentEventProcessor = new SegmentEventProcessor(_keyCoordinatorConf, _producer,
+        _messageResolveStrategy, _retentionManager, versionMessageManager, _metrics);
+    _keyCoordinatorCore.init(_keyCoordinatorConf, segmentEventProcessor, fetcher, versionMessageManager, _metrics);
     LOGGER.info("finished init key coordinator instance, starting loop");
     _keyCoordinatorCore.start();
     LOGGER.info("starting web service");
diff --git a/pinot-grigio/pinot-grigio-coordinator/src/test/java/org/apache/pinot/grigio/keyCoordinator/internal/MessageFetcherTest.java b/pinot-grigio/pinot-grigio-coordinator/src/test/java/org/apache/pinot/grigio/keyCoordinator/internal/MessageFetcherTest.java
new file mode 100644
index 0000000..dfacd3b
--- /dev/null
+++ b/pinot-grigio/pinot-grigio-coordinator/src/test/java/org/apache/pinot/grigio/keyCoordinator/internal/MessageFetcherTest.java
@@ -0,0 +1,127 @@
+/**
+ * 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.pinot.grigio.keyCoordinator.internal;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.apache.kafka.clients.consumer.OffsetAndMetadata;
+import org.apache.kafka.common.TopicPartition;
+import org.apache.pinot.grigio.common.OffsetInfo;
+import org.apache.pinot.grigio.common.messages.KeyCoordinatorQueueMsg;
+import org.apache.pinot.grigio.common.rpcQueue.KeyCoordinatorQueueConsumer;
+import org.apache.pinot.grigio.common.rpcQueue.QueueConsumerRecord;
+import org.apache.pinot.grigio.keyCoordinator.GrigioKeyCoordinatorMetrics;
+import org.apache.pinot.grigio.keyCoordinator.starter.KeyCoordinatorConf;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.internal.verification.VerificationModeFactory.times;
+
+public class MessageFetcherTest {
+
+  private MessageFetcher messageFetcher;
+  private KeyCoordinatorQueueConsumer mockConsumer;
+  private GrigioKeyCoordinatorMetrics mockMetrics;
+  private BlockingQueue<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> consumerRecords;
+  private int invocationCount;
+
+  @BeforeMethod
+  public void init() {
+    KeyCoordinatorConf conf = new KeyCoordinatorConf();
+
+    mockConsumer = mock(KeyCoordinatorQueueConsumer.class);
+    mockMetrics = mock(GrigioKeyCoordinatorMetrics.class);
+    consumerRecords = new ArrayBlockingQueue<>(100);
+    invocationCount = 0;
+    when(mockConsumer.getRequests(anyLong(), any())).thenAnswer((invocationOnMock) -> {
+      invocationCount++;
+      List<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> result = new ArrayList<>();
+      consumerRecords.drainTo(result);
+      return result;
+    });
+    messageFetcher = new MessageFetcher(conf, mockConsumer, Executors.newFixedThreadPool(1), mockMetrics);
+  }
+
+  @Test
+  public void testGetMessages() throws InterruptedException {
+    List<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> msgList = ImmutableList.of(
+        new QueueConsumerRecord<>("topic1", 1, 123, new byte[]{123},
+          new KeyCoordinatorQueueMsg(new byte[]{123}, "segment1", 456, 900), 123),
+        new QueueConsumerRecord<>("topic1", 2, 156, new byte[]{123},
+          new KeyCoordinatorQueueMsg(new byte[]{123}, "segment2", 456, 901),123),
+        new QueueConsumerRecord<>("topic1", 1, 140, new byte[]{123},
+          new KeyCoordinatorQueueMsg(new byte[]{123}, "segment1", 470, 901), 123));
+    msgList.forEach(consumerRecords::offer);
+    messageFetcher.start();
+    // wait necessary time for ingestion loop to start and processing the data
+    // TODO: make the wait smarter so we can ensure the messages are fetched after a certain time
+    TimeUnit.MILLISECONDS.sleep(100);
+    MessageFetcher.MessageAndOffset<QueueConsumerRecord<byte[], KeyCoordinatorQueueMsg>> result = messageFetcher.getMessages(System.currentTimeMillis() + 500);
+
+    // ensure the invocation is not too much:
+    Assert.assertTrue(invocationCount < 10);
+
+    // ensure the offset are handled properly
+    Map<TopicPartition, OffsetAndMetadata> offsetMap = result.getOffsetInfo().getOffsetMap();
+    Assert.assertEquals(offsetMap.size(), 2);
+    Assert.assertEquals(offsetMap.get(new TopicPartition("topic1", 1)).offset(), 141);
+    Assert.assertEquals(offsetMap.get(new TopicPartition("topic1", 2)).offset(), 157);
+
+    // ensure the data fetched are correct
+    Assert.assertEquals(result.getMessages().size(), 3);
+    Assert.assertEquals(result.getMessages().get(0), msgList.get(0));
+    Assert.assertEquals(result.getMessages().get(1), msgList.get(1));
+    Assert.assertEquals(result.getMessages().get(2), msgList.get(2));
+
+    // test if we fetch message again
+    msgList.forEach(consumerRecords::offer);
+    TimeUnit.MILLISECONDS.sleep(100);
+    result = messageFetcher.getMessages(System.currentTimeMillis() + 100);
+    Assert.assertEquals(result.getMessages().size(), 3);
+
+    Assert.assertEquals(result.getMessages().get(0), msgList.get(0));
+    Assert.assertEquals(result.getMessages().get(1), msgList.get(1));
+    Assert.assertEquals(result.getMessages().get(2), msgList.get(2));
+  }
+
+  @Test
+  public void testAckOffset() {
+    OffsetInfo offsetInfo = new OffsetInfo(
+      (Map) ImmutableMap.of(
+          new TopicPartition("topic1", 1), 141,
+          new TopicPartition("topic1", 2), 150)
+    );
+    messageFetcher.ackOffset(new MessageFetcher.MessageAndOffset(ImmutableList.of(), offsetInfo));
+    verify(mockConsumer, times(1)).ackOffset(offsetInfo);
+  }
+}
\ No newline at end of file
diff --git a/pinot-grigio/pinot-grigio-coordinator/src/test/java/org/apache/pinot/grigio/keyCoordinator/internal/SegmentEventProcessorTest.java b/pinot-grigio/pinot-grigio-coordinator/src/test/java/org/apache/pinot/grigio/keyCoordinator/internal/SegmentEventProcessorTest.java
new file mode 100644
index 0000000..9482617
--- /dev/null
+++ b/pinot-grigio/pinot-grigio-coordinator/src/test/java/org/apache/pinot/grigio/keyCoordinator/internal/SegmentEventProcessorTest.java
@@ -0,0 +1,229 @@
+/**
+ * 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.pinot.grigio.keyCoordinator.internal;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.apache.pinot.grigio.common.keyValueStore.ByteArrayWrapper;
+import org.apache.pinot.grigio.common.keyValueStore.KeyValueStoreDB;
+import org.apache.pinot.grigio.common.keyValueStore.KeyValueStoreTable;
+import org.apache.pinot.grigio.common.messages.KeyCoordinatorMessageContext;
+import org.apache.pinot.grigio.common.messages.KeyCoordinatorQueueMsg;
+import org.apache.pinot.grigio.common.messages.LogCoordinatorMessage;
+import org.apache.pinot.grigio.common.messages.LogEventType;
+import org.apache.pinot.grigio.common.rpcQueue.LogCoordinatorQueueProducer;
+import org.apache.pinot.grigio.common.rpcQueue.ProduceTask;
+import org.apache.pinot.grigio.common.rpcQueue.QueueConsumerRecord;
+import org.apache.pinot.grigio.common.storageProvider.UpdateLogEntry;
+import org.apache.pinot.grigio.common.storageProvider.UpdateLogStorageProvider;
+import org.apache.pinot.grigio.common.storageProvider.retentionManager.UpdateLogRetentionManager;
+import org.apache.pinot.grigio.common.storageProvider.retentionManager.UpdateLogTableRetentionManager;
+import org.apache.pinot.grigio.common.updateStrategy.MessageResolveStrategy;
+import org.apache.pinot.grigio.common.updateStrategy.MessageTimeResolveStrategy;
+import org.apache.pinot.grigio.keyCoordinator.GrigioKeyCoordinatorMetrics;
+import org.apache.pinot.grigio.keyCoordinator.starter.KeyCoordinatorConf;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.pinot.grigio.keyCoordinator.starter.KeyCoordinatorConf.KC_OUTPUT_TOPIC_PREFIX_KEY;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class SegmentEventProcessorTest {
+
+  private KeyValueStoreTable mockTable1;
+  private KeyValueStoreTable mockTable2;
+  private LogCoordinatorQueueProducer mockProducer;
+  private MessageResolveStrategy messageResolveStrategy;
+  private KeyValueStoreDB<ByteArrayWrapper, KeyCoordinatorMessageContext> mockDB;
+  private UpdateLogStorageProvider mockStorageProvider;
+  private UpdateLogRetentionManager mockRetentionManager;
+  private VersionMessageManager mockVersionManager;
+  private GrigioKeyCoordinatorMetrics mockMetrics;
+
+  private Map<String, List<ProduceTask>> capturedTasks;
+
+  private SegmentEventProcessor processor;
+
+  @BeforeMethod
+  public void init() throws IOException {
+    KeyCoordinatorConf conf = new KeyCoordinatorConf();
+    conf.addProperty(KC_OUTPUT_TOPIC_PREFIX_KEY, "prefix_");
+
+    // all mocks
+    mockProducer = mock(LogCoordinatorQueueProducer.class);
+    messageResolveStrategy = new MessageTimeResolveStrategy();
+    mockDB = mock(KeyValueStoreDB.class);
+    mockStorageProvider = mock(UpdateLogStorageProvider.class);
+    mockRetentionManager = mock(UpdateLogRetentionManager.class);
+    mockVersionManager = mock(VersionMessageManager.class);
+    mockMetrics = mock(GrigioKeyCoordinatorMetrics.class);
+
+    // inner mock for retentionManager
+    UpdateLogTableRetentionManager mockTableRetentionManager = mock(UpdateLogTableRetentionManager.class);
+    when(mockRetentionManager.getRetentionManagerForTable(anyString())).thenReturn(mockTableRetentionManager);
+    when(mockTableRetentionManager.shouldIngestForSegment(anyString())).thenReturn(true);
+
+    // inner mock for db
+    mockTable1 = mock(KeyValueStoreTable.class);
+    mockTable2 = mock(KeyValueStoreTable.class);
+    when(mockDB.getTable("table1")).thenReturn(mockTable1);
+    when(mockDB.getTable("table2")).thenReturn(mockTable2);
+    when(mockTable1.multiGet(anyList())).thenReturn(ImmutableMap.of());
+    when(mockTable2.multiGet(anyList())).thenReturn(
+        ImmutableMap.of(new ByteArrayWrapper(new byte[]{13}),
+            new KeyCoordinatorMessageContext("table2__0__9__20191027T2041Z", 90, 300)));
+
+    // mock version
+    Map<Integer, Long> versionMapping = new HashMap<>();
+    when(mockVersionManager.getVersionConsumed(anyInt())).thenAnswer(invocationOnMock -> {
+      int partition = invocationOnMock.getArgument(0);
+      return versionMapping.getOrDefault(partition, 9l);
+    });
+    doAnswer(invocationOnMock -> {
+      int partition = invocationOnMock.getArgument(0);
+      long version = invocationOnMock.getArgument(1);
+      versionMapping.put(partition, version);
+      return null;
+    }).when(mockVersionManager).maybeUpdateVersionConsumed(anyInt(), anyLong());
+
+
+    capturedTasks = new HashMap<>();
+    // mock producer
+    doAnswer(invocationOnMock -> {
+      List<ProduceTask> produceTasks = invocationOnMock.getArgument(0);
+      for (ProduceTask produceTask : produceTasks) {
+        produceTask.markComplete(null, null);
+        capturedTasks.computeIfAbsent(produceTask.getTopic(), t -> new ArrayList<>()).add(produceTask);
+      }
+      return null;
+    }).when(mockProducer).batchProduce(anyList());
+
+    processor = new SegmentEventProcessor(conf, mockProducer, messageResolveStrategy, mockDB, mockStorageProvider,
+        mockRetentionManager, mockVersionManager, mockMetrics);
+  }
+
+  @Test
+  public void testProcessMessages() throws IOException {
+    ImmutableList msgList = ImmutableList.copyOf(new QueueConsumerRecord[]{
+        // side effect: generate 1 insert for key 13
+        new QueueConsumerRecord<>("topic", 1, 45, new byte[]{13},
+            new KeyCoordinatorQueueMsg(new byte[]{13}, "table1__0__10__20191027T2041Z", 100, 500), 123),
+        // side effect: generate 1 insert & 1 delete for key 13
+        new QueueConsumerRecord<>("topic", 1, 45, new byte[]{13},
+            new KeyCoordinatorQueueMsg(new byte[]{13}, "table1__0__10__20191027T2041Z", 120, 600), 123),
+        // side effect: generate 1 insert for key 13 at table 2, verify different tables works
+        // also 1 delete message on existing message in kv store
+        new QueueConsumerRecord<>("topic", 1, 45, new byte[]{13},
+            new KeyCoordinatorQueueMsg(new byte[]{13}, "table2__0__10__20191027T2041Z", 120, 600), 123),
+        // side effect: version message should update versions
+        new QueueConsumerRecord<>("topic", 1, 45, new byte[]{12},
+            new KeyCoordinatorQueueMsg(10), 123),
+        // side effect: generate 1 insert for new key 14
+        new QueueConsumerRecord<>("topic", 1, 45, new byte[]{14},
+            new KeyCoordinatorQueueMsg(new byte[]{14}, "table1__0__10__20191027T2041Z", 120, 700), 123),
+        // side effect: generate no insert/delete message due to older timestamp
+        new QueueConsumerRecord<>("topic", 1, 45, new byte[]{13},
+            new KeyCoordinatorQueueMsg(new byte[]{13}, "table1__0__10__20191027T2041Z", 90, 800), 123),
+        // side effect: generate no update message as we reprocess same message
+        new QueueConsumerRecord<>("topic", 1, 45, new byte[]{13},
+            new KeyCoordinatorQueueMsg(new byte[]{13}, "table2__0__10__20191027T2041Z", 120, 600), 123),
+        // side effect: generate 1 insert & 1 delete for key 13 at table 2
+        new QueueConsumerRecord<>("topic", 1, 45, new byte[]{45},
+            new KeyCoordinatorQueueMsg(new byte[]{13}, "table2__0__11__20191027T2041Z", 140, 800), 123),
+        }
+    );
+    processor.start();
+    processor.processMessages(msgList);
+
+    // verify kafka output
+    List<ProduceTask> table1Tasks = capturedTasks.get("prefix_table1");
+    Assert.assertEquals(table1Tasks.size(), 4);
+    Assert.assertEquals(table1Tasks.get(0), new ProduceTask<>("prefix_table1", 1,
+        new LogCoordinatorMessage("table1__0__10__20191027T2041Z", 500, 9, LogEventType.INSERT)));
+    Assert.assertEquals(table1Tasks.get(1), new ProduceTask<>("prefix_table1", 1,
+        new LogCoordinatorMessage("table1__0__10__20191027T2041Z", 500, 9, LogEventType.DELETE)));
+    Assert.assertEquals(table1Tasks.get(2), new ProduceTask<>("prefix_table1", 1,
+        new LogCoordinatorMessage("table1__0__10__20191027T2041Z", 600, 9, LogEventType.INSERT)));
+    Assert.assertEquals(table1Tasks.get(3), new ProduceTask<>("prefix_table1", 1,
+        new LogCoordinatorMessage("table1__0__10__20191027T2041Z", 700, 10, LogEventType.INSERT)));
+
+    List<ProduceTask> table2Tasks = capturedTasks.get("prefix_table2");
+    Assert.assertEquals(table2Tasks.size(), 4);
+    Assert.assertEquals(table2Tasks.get(0), new ProduceTask<>("prefix_table2", 1,
+        new LogCoordinatorMessage("table2__0__9__20191027T2041Z", 300, 9, LogEventType.DELETE)));
+    Assert.assertEquals(table2Tasks.get(1), new ProduceTask<>("prefix_table2", 1,
+        new LogCoordinatorMessage("table2__0__10__20191027T2041Z", 600, 9, LogEventType.INSERT)));
+    Assert.assertEquals(table2Tasks.get(2), new ProduceTask<>("prefix_table2", 1,
+        new LogCoordinatorMessage("table2__0__10__20191027T2041Z", 600, 10, LogEventType.DELETE)));
+    Assert.assertEquals(table2Tasks.get(3), new ProduceTask<>("prefix_table2", 1,
+        new LogCoordinatorMessage("table2__0__11__20191027T2041Z", 800, 10, LogEventType.INSERT)));
+
+    // verify kv storage
+    verify(mockTable1).multiPut(ImmutableMap.of(
+        new ByteArrayWrapper(new byte[]{13}),
+        new KeyCoordinatorMessageContext("table1__0__10__20191027T2041Z", 120, 600),
+        new ByteArrayWrapper(new byte[]{14}),
+        new KeyCoordinatorMessageContext("table1__0__10__20191027T2041Z", 120, 700)
+    ));
+    verify(mockTable2).multiPut(ImmutableMap.of(
+        new ByteArrayWrapper(new byte[]{13}),
+        new KeyCoordinatorMessageContext("table2__0__11__20191027T2041Z", 140, 800)
+    ));
+
+    // verify local log storage
+    verify(mockStorageProvider).addDataToFile("table1_REALTIME", "table1__0__10__20191027T2041Z",
+        ImmutableList.of(
+            new UpdateLogEntry(500, 9, LogEventType.INSERT, 1),
+            new UpdateLogEntry(500, 9, LogEventType.DELETE, 1),
+            new UpdateLogEntry(600, 9, LogEventType.INSERT, 1),
+            new UpdateLogEntry(700, 10, LogEventType.INSERT, 1)
+        )
+    );
+    verify(mockStorageProvider).addDataToFile("table2_REALTIME", "table2__0__9__20191027T2041Z",
+        ImmutableList.of(
+            new UpdateLogEntry(300, 9, LogEventType.DELETE, 1)
+        )
+    );
+    verify(mockStorageProvider).addDataToFile("table2_REALTIME", "table2__0__10__20191027T2041Z",
+        ImmutableList.of(
+            new UpdateLogEntry(600, 9, LogEventType.INSERT, 1),
+            new UpdateLogEntry(600, 10, LogEventType.DELETE, 1)
+        )
+    );
+    verify(mockStorageProvider).addDataToFile("table2_REALTIME", "table2__0__11__20191027T2041Z",
+        ImmutableList.of(
+            new UpdateLogEntry(800, 10, LogEventType.INSERT, 1)
+        )
+    );
+  }
+}
\ No newline at end of file
diff --git a/pinot-grigio/pinot-grigio-coordinator/src/test/java/org/apache/pinot/grigio/keyCoordinator/internal/VersionMessageManagerTest.java b/pinot-grigio/pinot-grigio-coordinator/src/test/java/org/apache/pinot/grigio/keyCoordinator/internal/VersionMessageManagerTest.java
new file mode 100644
index 0000000..2e653d0
--- /dev/null
+++ b/pinot-grigio/pinot-grigio-coordinator/src/test/java/org/apache/pinot/grigio/keyCoordinator/internal/VersionMessageManagerTest.java
@@ -0,0 +1,94 @@
+/**
+ * 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.pinot.grigio.keyCoordinator.internal;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.pinot.grigio.common.rpcQueue.VersionMsgQueueProducer;
+import org.apache.pinot.grigio.keyCoordinator.GrigioKeyCoordinatorMetrics;
+import org.apache.pinot.grigio.keyCoordinator.helix.KeyCoordinatorLeadershipManager;
+import org.apache.pinot.grigio.keyCoordinator.helix.KeyCoordinatorVersionManager;
+import org.apache.pinot.grigio.keyCoordinator.starter.KeyCoordinatorConf;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.util.Timer;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+
+public class VersionMessageManagerTest {
+
+  private VersionMsgQueueProducer mockProducer;
+  private KeyCoordinatorVersionManager mockVersionManager;
+  private KeyCoordinatorLeadershipManager mockLeadershipManager;
+  private GrigioKeyCoordinatorMetrics mockMetrics;
+
+  private VersionMessageManager versionMessageManager;
+
+  @BeforeMethod
+  public void init() {
+    mockProducer = mock(VersionMsgQueueProducer.class);
+    mockVersionManager = mock(KeyCoordinatorVersionManager.class);
+    mockLeadershipManager = mock(KeyCoordinatorLeadershipManager.class);
+    mockMetrics = mock(GrigioKeyCoordinatorMetrics.class);
+
+    KeyCoordinatorConf conf = new KeyCoordinatorConf();
+    Timer timer = new Timer();
+
+    versionMessageManager = new VersionMessageManager(conf, mockProducer, timer, mockVersionManager,
+        mockLeadershipManager, mockMetrics);
+  }
+
+  @Test
+  public void testSetVersionConsumedToPropertyStore() {
+
+    versionMessageManager.setVersionConsumedToPropertyStore();
+    verify(mockVersionManager).setVersionConsumedToPropertyStore(ImmutableMap.of());
+
+    versionMessageManager.maybeUpdateVersionConsumed(1, 2l);
+    versionMessageManager.maybeUpdateVersionConsumed(2, 3l);
+    versionMessageManager.maybeUpdateVersionConsumed(2, 4l);
+    versionMessageManager.setVersionConsumedToPropertyStore();
+
+    verify(mockVersionManager).setVersionConsumedToPropertyStore(ImmutableMap.of(1, 2l, 2, 4l));
+  }
+
+  @Test
+  public void testGetAndUpdateVersionConsumed() {
+    Assert.assertEquals(versionMessageManager.getVersionConsumed(0), 0);
+
+    versionMessageManager.maybeUpdateVersionConsumed(1, 2);
+    Assert.assertEquals(versionMessageManager.getVersionConsumed(0), 0);
+    Assert.assertEquals(versionMessageManager.getVersionConsumed(1), 2);
+
+    versionMessageManager.maybeUpdateVersionConsumed(0, 1);
+    Assert.assertEquals(versionMessageManager.getVersionConsumed(0), 1);
+    Assert.assertEquals(versionMessageManager.getVersionConsumed(1), 2);
+
+    versionMessageManager.maybeUpdateVersionConsumed(0, 4);
+    Assert.assertEquals(versionMessageManager.getVersionConsumed(0), 4);
+    Assert.assertEquals(versionMessageManager.getVersionConsumed(1), 2);
+
+    versionMessageManager.maybeUpdateVersionConsumed(0, 3);
+    Assert.assertEquals(versionMessageManager.getVersionConsumed(0), 4);
+    Assert.assertEquals(versionMessageManager.getVersionConsumed(1), 2);
+  }
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org