diff --git a/clients/src/main/java/org/apache/kafka/clients/consumer/MockConsumer.java b/clients/src/main/java/org/apache/kafka/clients/consumer/MockConsumer.java index e43c292e6a31e..f877f9d13a451 100644 --- a/clients/src/main/java/org/apache/kafka/clients/consumer/MockConsumer.java +++ b/clients/src/main/java/org/apache/kafka/clients/consumer/MockConsumer.java @@ -188,6 +188,10 @@ public synchronized ConsumerRecords poll(final Duration timeout) { if (!subscriptions.isPaused(entry.getKey())) { final List> recs = entry.getValue(); for (final ConsumerRecord rec : recs) { + if (beginningOffsets.get(entry.getKey()) != null && beginningOffsets.get(entry.getKey()) > subscriptions.position(entry.getKey())) { + throw new OffsetOutOfRangeException(Collections.singletonMap(entry.getKey(), subscriptions.position(entry.getKey()))); + } + if (assignment().contains(entry.getKey()) && rec.offset() >= subscriptions.position(entry.getKey())) { results.computeIfAbsent(entry.getKey(), partition -> new ArrayList<>()).add(rec); subscriptions.position(entry.getKey(), rec.offset() + 1); diff --git a/streams/src/main/java/org/apache/kafka/streams/processor/internals/StoreChangelogReader.java b/streams/src/main/java/org/apache/kafka/streams/processor/internals/StoreChangelogReader.java index 34e6e5cdb6f76..fdd9d6c303cad 100644 --- a/streams/src/main/java/org/apache/kafka/streams/processor/internals/StoreChangelogReader.java +++ b/streams/src/main/java/org/apache/kafka/streams/processor/internals/StoreChangelogReader.java @@ -107,6 +107,8 @@ public Collection restore(final RestoringTasks active) { needsInitializing.remove(partition); needsRestoring.remove(partition); + final StateRestorer restorer = stateRestorers.get(partition); + restorer.setCheckpointOffset(StateRestorer.NO_CHECKPOINT); task.reinitializeStateStoresForPartitions(recoverableException.partitions()); } restoreConsumer.seekToBeginning(partitions); diff --git a/streams/src/test/java/org/apache/kafka/streams/processor/internals/StoreChangelogReaderTest.java b/streams/src/test/java/org/apache/kafka/streams/processor/internals/StoreChangelogReaderTest.java index 34f0a32b88c36..d08f0d7360d18 100644 --- a/streams/src/test/java/org/apache/kafka/streams/processor/internals/StoreChangelogReaderTest.java +++ b/streams/src/test/java/org/apache/kafka/streams/processor/internals/StoreChangelogReaderTest.java @@ -157,6 +157,42 @@ public Set partitions() { assertThat(callback.restored.size(), equalTo(messages)); } + @Test + public void shouldRecoverFromOffsetOutOfRangeExceptionAndRestoreFromStart() { + final int messages = 10; + final int startOffset = 5; + final long expiredCheckpoint = 1L; + assignPartition(messages, topicPartition); + consumer.updateBeginningOffsets(Collections.singletonMap(topicPartition, (long) startOffset)); + consumer.updateEndOffsets(Collections.singletonMap(topicPartition, (long) (messages + startOffset))); + + addRecords(messages, topicPartition, startOffset); + consumer.assign(Collections.emptyList()); + + final StateRestorer stateRestorer = new StateRestorer( + topicPartition, + restoreListener, + expiredCheckpoint, + Long.MAX_VALUE, + true, + "storeName"); + changelogReader.register(stateRestorer); + + EasyMock.expect(active.restoringTaskFor(topicPartition)).andStubReturn(task); + EasyMock.replay(active, task); + + // first restore call "fails" since OffsetOutOfRangeException but we should not die with an exception + assertEquals(0, changelogReader.restore(active).size()); + //the starting offset for stateRestorer is set to NO_CHECKPOINT + assertThat(stateRestorer.checkpoint(), equalTo(-1L)); + + //restore the active task again + changelogReader.register(stateRestorer); + //the restored task should return completed partition without Exception. + assertEquals(1, changelogReader.restore(active).size()); + //the restored size should be equal to message length. + assertThat(callback.restored.size(), equalTo(messages)); + } @Test public void shouldRestoreMessagesFromCheckpoint() {