Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import org.apache.kafka.common.utils.LogContext;
import org.apache.kafka.streams.KeyValue;
import org.apache.kafka.streams.errors.StreamsException;
import org.apache.kafka.streams.errors.TaskMigratedException;
import org.apache.kafka.streams.processor.StateRestoreListener;
import org.slf4j.Logger;

Expand All @@ -50,6 +49,7 @@ public class StoreChangelogReader implements ChangelogReader {
private final Map<TopicPartition, StateRestorer> stateRestorers = new HashMap<>();
private final Map<TopicPartition, StateRestorer> needsRestoring = new HashMap<>();
private final Map<TopicPartition, StateRestorer> needsInitializing = new HashMap<>();
private Map<TopicPartition, Long> updatedEndOffsets = new HashMap<>();

public StoreChangelogReader(final Consumer<byte[], byte[]> restoreConsumer,
final StateRestoreListener userStateRestoreListener,
Expand All @@ -66,9 +66,6 @@ public void register(final StateRestorer restorer) {
needsInitializing.put(restorer.partition(), restorer);
}

/**
* @throws TaskMigratedException if another thread wrote to the changelog topic that is currently restored
*/
public Collection<TopicPartition> restore(final RestoringTasks active) {
if (!needsInitializing.isEmpty()) {
initialize();
Expand All @@ -81,10 +78,25 @@ public Collection<TopicPartition> restore(final RestoringTasks active) {

final Set<TopicPartition> restoringPartitions = new HashSet<>(needsRestoring.keySet());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove this variable to simplify the code further.

try {
final ConsumerRecords<byte[], byte[]> allRecords = restoreConsumer.poll(10);
for (final TopicPartition partition : restoringPartitions) {
restorePartition(allRecords, partition, active.restoringTaskFor(partition));
final Set<TopicPartition> remainingPartitions = new HashSet<>(needsRestoring.keySet());
remainingPartitions.removeAll(updatedEndOffsets.keySet());
updatedEndOffsets.putAll(restoreConsumer.endOffsets(remainingPartitions));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be correct. Still wondering why you did not move this code into

if (!needsInitializing.isEmpty()) {
    initialize();
    // put the three lines here
}

I think that remainingPartitions can only contain data if !needsInitializing.isEmpty() is true -- thus, no need to execute the code in each iteration. Or do I miss anything?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry about my delay. It should be fine now.

@ConcurrencyPractitioner ConcurrencyPractitioner Jun 1, 2018

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mjsax Actually, needsInitializing can still be empty, but remainingPartitions would still have partitions which needs to be added from needsRestoring. I think the main reason is that needsInitializing and needsRestoring are completely independent of one another. If one does not contain partitions, that does not neccesarily effect the other. In other words, as long as needsRestoring has partitions which needs to be restored, we will probably need to check for partitions every time -- regardless of what needsInitializing contains.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot follow. Not sure what I am missing. From my understanding the flow is as follows: (1) after new partitions are assigned, they are added to needsInitializing. (2) If partitions/task are initialized, it's check if the need restoring: (2a) if they need restoring they are removed from needsInitializing and added to needsRestoring (2b) if they don't need restoring, they are only removed from needsInitializing.

We need to maintain updatedEndOffsets only for partitions that needs restoring. And we need to get the endOffset for those partitions only once. Thus, new end-offsets only need to be added, after new partitions are assigned and thus, needsInitializing.isEmpty() is false.

Actually, needsInitializing can still be empty, but remainingPartitions would still have partitions which needs to be added from needsRestoring

Why? Those partitions should have been added to updatedEndOffsets after they got assigned initially.

I think the main reason is that needsInitializing and needsRestoring are completely independent of one another.

From my understanding, they are not independent. Each partitions is first in needsInitializing and might be "moved" from there to needsRestoring.

In other words, as long as needsRestoring has partitions which needs to be restored, we will probably need to check for partitions every time -- regardless of what needsInitializing contains

Why? The end-offset should not change and thus, for each partition that is moved from needsInitializing to needsRestoring we only add the end-offset once to updatedEndOffsets

Please let me know what I miss.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see what happened. Sorry, this was my bad. What happened was that I was adding the end offsets to updatedEndOffsets before initialize() in the conditional was called. Consequently, I never detected any change in needsRestoring.

final ConsumerRecords<byte[], byte[]> records = restoreConsumer.poll(10);
final Iterator<TopicPartition> iterator = restoringPartitions.iterator();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace restoringPartitions with needsRestoring.keySet() to get rid of the unnecessary variable.

final Set<TopicPartition> completedPartitions = new HashSet<>();
while (iterator.hasNext()) {
final TopicPartition partition = iterator.next();
final StateRestorer restorer = stateRestorers.get(partition);
final long pos = processNext(records.records(partition), restorer, updatedEndOffsets.get(partition));
restorer.setRestoredOffset(pos);
if (restorer.hasCompleted(pos, updatedEndOffsets.get(partition))) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can also remove the entry in updatedEndOffsets if this is true.

restorer.restoreDone();
needsRestoring.remove(partition);
updatedEndOffsets.remove(partition);
completedPartitions.add(partition);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing -- missed this before: can't we remove completedPartitions and call iterator.remove() instead?

Maybe we need to change the iterator to iterate over the HashMap instead of the "key-set" of the HashMap though.

}
}
restoringPartitions.removeAll(completedPartitions);
} catch (final InvalidOffsetException recoverableException) {
log.warn("Restoring StreamTasks failed. Deleting StreamTasks stores to recreate from scratch.", recoverableException);
final Set<TopicPartition> partitions = recoverableException.partitions();
Expand Down Expand Up @@ -240,41 +252,6 @@ public void reset() {
needsInitializing.clear();
}

/**
* @throws TaskMigratedException if another thread wrote to the changelog topic that is currently restored
*/
private void restorePartition(final ConsumerRecords<byte[], byte[]> allRecords,
final TopicPartition topicPartition,
final Task task) {
final StateRestorer restorer = stateRestorers.get(topicPartition);
final Long endOffset = endOffsets.get(topicPartition);
final long pos = processNext(allRecords.records(topicPartition), restorer, endOffset);
restorer.setRestoredOffset(pos);
if (restorer.hasCompleted(pos, endOffset)) {
if (pos > endOffset) {
throw new TaskMigratedException(task, topicPartition, endOffset, pos);
}

// need to check for changelog topic
if (restorer.offsetLimit() == Long.MAX_VALUE) {
final Long updatedEndOffset = restoreConsumer.endOffsets(Collections.singletonList(topicPartition)).get(topicPartition);
if (!restorer.hasCompleted(pos, updatedEndOffset)) {
throw new TaskMigratedException(task, topicPartition, updatedEndOffset, pos);
}
}


log.debug("Completed restoring state from changelog {} with {} records ranging from offset {} to {}",
topicPartition,
restorer.restoredNumRecords(),
restorer.startingOffset(),
restorer.restoredOffset());

restorer.restoreDone();
needsRestoring.remove(topicPartition);
}
}

private long processNext(final List<ConsumerRecord<byte[], byte[]>> records,
final StateRestorer restorer,
final Long endOffset) {
Expand Down Expand Up @@ -326,7 +303,6 @@ private boolean hasPartition(final TopicPartition topicPartition) {
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,6 @@ void setConsumer(final Consumer<byte[], byte[]> consumer) {
/**
* @throws IllegalStateException If store gets registered after initialized is already finished
* @throws StreamsException if the store's change log does not contain the partition
* @throws TaskMigratedException if the task producer got fenced or consumer discovered changelog offset changes (EOS only)
*/
boolean updateNewAndRestoringTasks() {
active.initializeNewTasks();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import org.apache.kafka.common.utils.Utils;
import org.apache.kafka.streams.KeyValue;
import org.apache.kafka.streams.errors.StreamsException;
import org.apache.kafka.streams.errors.TaskMigratedException;
import org.apache.kafka.streams.processor.StateRestoreListener;
import org.apache.kafka.test.MockRestoreCallback;
import org.apache.kafka.test.MockStateRestoreListener;
Expand Down Expand Up @@ -377,46 +376,6 @@ public void shouldRestorePartitionsRegisteredPostInitialization() {
assertThat(callbackTwo.restored.size(), equalTo(3));
}

@Test
public void shouldThrowTaskMigratedExceptionIfEndOffsetGetsExceededDuringRestoreForChangelogTopic() {
final int messages = 10;
setupConsumer(messages, topicPartition);
consumer.updateEndOffsets(Collections.singletonMap(topicPartition, 5L));
changelogReader.register(new StateRestorer(topicPartition, restoreListener, null, Long.MAX_VALUE, true, "storeName"));

expect(active.restoringTaskFor(topicPartition)).andReturn(task);
replay(active);

try {
changelogReader.restore(active);
fail("Should have thrown TaskMigratedException");
} catch (final TaskMigratedException expected) {
/* ignore */
}
}


@Test
public void shouldThrowTaskMigratedExceptionIfChangelogTopicUpdatedDuringRestoreProcessFoundInSecondCheck() {
final int messages = 10;
setupConsumer(messages, topicPartition);
// in this case first call to endOffsets returns correct value, but a second thread has updated the changelog topic
// so a subsequent call to endOffsets returns a value exceeding the expected end value
consumer.addEndOffsets(Collections.singletonMap(topicPartition, 15L));
changelogReader.register(new StateRestorer(topicPartition, restoreListener, null, Long.MAX_VALUE, true, "storeName"));

expect(active.restoringTaskFor(topicPartition)).andReturn(task);
replay(active);

try {
changelogReader.restore(active);
fail("Should have thrown TaskMigratedException");
} catch (final TaskMigratedException expected) {
// verifies second block threw exception with updated end offset
assertTrue(expected.getMessage().contains("end offset 15, current offset 10"));
}
}


@Test
public void shouldNotThrowTaskMigratedExceptionIfSourceTopicUpdatedDuringRestoreProcess() {
Expand All @@ -434,30 +393,6 @@ public void shouldNotThrowTaskMigratedExceptionIfSourceTopicUpdatedDuringRestore
}


@Test
public void shouldThrowTaskMigratedExceptionIfEndOffsetGetsExceededDuringRestoreForChangelogTopicEOSEnabled() {
final int totalMessages = 10;
assignPartition(totalMessages, topicPartition);
// records 0..4
addRecords(5, topicPartition, 0);
//EOS enabled commit marker at offset 5 so rest of records 6..10
addRecords(5, topicPartition, 6);
consumer.assign(Collections.<TopicPartition>emptyList());

// end offsets should start after commit marker of 5 from above
consumer.updateEndOffsets(Collections.singletonMap(topicPartition, 6L));
changelogReader.register(new StateRestorer(topicPartition, restoreListener, null, Long.MAX_VALUE, true, "storeName"));

expect(active.restoringTaskFor(topicPartition)).andReturn(task);
replay(active);
try {
changelogReader.restore(active);
fail("Should have thrown task migrated exception");
} catch (final TaskMigratedException expected) {
/* ignore */
}
}

@Test
public void shouldNotThrowTaskMigratedExceptionDuringRestoreForChangelogTopicWhenEndOffsetNotExceededEOSEnabled() {
final int totalMessages = 10;
Expand Down