-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-10306: GlobalThread should fail on InvalidOffsetException #9075
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
edfc3f2
267b081
7a3eda4
6719589
3a24926
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,6 @@ | |
| import org.apache.kafka.clients.consumer.Consumer; | ||
| import org.apache.kafka.clients.consumer.ConsumerRecord; | ||
| import org.apache.kafka.clients.consumer.ConsumerRecords; | ||
| import org.apache.kafka.clients.consumer.InvalidOffsetException; | ||
| import org.apache.kafka.common.PartitionInfo; | ||
| import org.apache.kafka.common.TopicPartition; | ||
| import org.apache.kafka.common.errors.TimeoutException; | ||
|
|
@@ -291,27 +290,17 @@ private void restoreState(final StateRestoreCallback stateRestoreCallback, | |
| long restoreCount = 0L; | ||
|
|
||
| while (offset < highWatermark) { | ||
| try { | ||
| final ConsumerRecords<byte[], byte[]> records = globalConsumer.poll(pollTime); | ||
| final List<ConsumerRecord<byte[], byte[]>> restoreRecords = new ArrayList<>(); | ||
| for (final ConsumerRecord<byte[], byte[]> record : records.records(topicPartition)) { | ||
| if (record.key() != null) { | ||
| restoreRecords.add(recordConverter.convert(record)); | ||
| } | ||
| final ConsumerRecords<byte[], byte[]> records = globalConsumer.poll(pollTime); | ||
| final List<ConsumerRecord<byte[], byte[]>> restoreRecords = new ArrayList<>(); | ||
| for (final ConsumerRecord<byte[], byte[]> record : records.records(topicPartition)) { | ||
| if (record.key() != null) { | ||
| restoreRecords.add(recordConverter.convert(record)); | ||
| } | ||
| offset = globalConsumer.position(topicPartition); | ||
| stateRestoreAdapter.restoreBatch(restoreRecords); | ||
| stateRestoreListener.onBatchRestored(topicPartition, storeName, offset, restoreRecords.size()); | ||
| restoreCount += restoreRecords.size(); | ||
| } catch (final InvalidOffsetException recoverableException) { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the actual bug: we swallow the exception. However, because we don't do any "seek", we just hit the same exception in |
||
| log.warn("Restoring GlobalStore {} failed due to: {}. Deleting global store to recreate from scratch.", | ||
| storeName, | ||
| recoverableException.toString()); | ||
|
|
||
| // TODO K9113: we remove the re-init logic and push it to be handled by the thread directly | ||
|
|
||
| restoreCount = 0L; | ||
| } | ||
| offset = globalConsumer.position(topicPartition); | ||
| stateRestoreAdapter.restoreBatch(restoreRecords); | ||
| stateRestoreListener.onBatchRestored(topicPartition, storeName, offset, restoreRecords.size()); | ||
| restoreCount += restoreRecords.size(); | ||
| } | ||
| stateRestoreListener.onRestoreEnd(topicPartition, storeName, restoreCount); | ||
| checkpointFileCache.put(topicPartition, offset); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -234,24 +234,18 @@ void initialize() { | |
| } | ||
|
|
||
| void pollAndUpdate() { | ||
| try { | ||
| final ConsumerRecords<byte[], byte[]> received = globalConsumer.poll(pollTime); | ||
| for (final ConsumerRecord<byte[], byte[]> record : received) { | ||
| stateMaintainer.update(record); | ||
| } | ||
| final long now = time.milliseconds(); | ||
| if (now >= lastFlush + flushInterval) { | ||
| stateMaintainer.flushState(); | ||
| lastFlush = now; | ||
| } | ||
| } catch (final InvalidOffsetException recoverableException) { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We just let the original exception bubble up, to be able to wipe out the store. -- This is also just a side "improvement"; we could also just die and let users cleanup the state directory manually. However, it seems better to wipe it out directly. |
||
| log.error("Updating global state failed. You can restart KafkaStreams to recover from this error.", recoverableException); | ||
| throw new StreamsException("Updating global state failed. " + | ||
| "You can restart KafkaStreams to recover from this error.", recoverableException); | ||
| final ConsumerRecords<byte[], byte[]> received = globalConsumer.poll(pollTime); | ||
| for (final ConsumerRecord<byte[], byte[]> record : received) { | ||
| stateMaintainer.update(record); | ||
| } | ||
| final long now = time.milliseconds(); | ||
| if (now >= lastFlush + flushInterval) { | ||
| stateMaintainer.flushState(); | ||
| lastFlush = now; | ||
| } | ||
| } | ||
|
|
||
| public void close() throws IOException { | ||
| public void close(final boolean wipeStateStore) throws IOException { | ||
| try { | ||
| globalConsumer.close(); | ||
| } catch (final RuntimeException e) { | ||
|
|
@@ -260,7 +254,7 @@ public void close() throws IOException { | |
| log.error("Failed to close global consumer due to the following error:", e); | ||
| } | ||
|
|
||
| stateMaintainer.close(); | ||
| stateMaintainer.close(wipeStateStore); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -284,10 +278,21 @@ public void run() { | |
| } | ||
| setState(State.RUNNING); | ||
|
|
||
| boolean wipeStateStore = false; | ||
| try { | ||
| while (stillRunning()) { | ||
| stateConsumer.pollAndUpdate(); | ||
| } | ||
| } catch (final InvalidOffsetException recoverableException) { | ||
| wipeStateStore = true; | ||
| log.error( | ||
| "Updating global state failed due to inconsistent local state. Will attempt to clean up the local state. You can restart KafkaStreams to recover from this error.", | ||
| recoverableException | ||
| ); | ||
| throw new StreamsException( | ||
| "Updating global state failed. You can restart KafkaStreams to recover from this error.", | ||
| recoverableException | ||
| ); | ||
| } finally { | ||
| // set the state to pending shutdown first as it may be called due to error; | ||
| // its state may already be PENDING_SHUTDOWN so it will return false but we | ||
|
|
@@ -297,7 +302,7 @@ public void run() { | |
| log.info("Shutting down"); | ||
|
|
||
| try { | ||
| stateConsumer.close(); | ||
| stateConsumer.close(wipeStateStore); | ||
| } catch (final IOException e) { | ||
| log.error("Failed to close state maintainer due to the following error:", e); | ||
| } | ||
|
|
@@ -331,17 +336,36 @@ private StateConsumer initialize() { | |
| logContext, | ||
| globalConsumer, | ||
| new GlobalStateUpdateTask( | ||
| logContext, | ||
| topology, | ||
| globalProcessorContext, | ||
| stateMgr, | ||
| config.defaultDeserializationExceptionHandler(), | ||
| logContext | ||
| config.defaultDeserializationExceptionHandler() | ||
| ), | ||
| time, | ||
| Duration.ofMillis(config.getLong(StreamsConfig.POLL_MS_CONFIG)), | ||
| config.getLong(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG) | ||
| ); | ||
| stateConsumer.initialize(); | ||
|
|
||
| try { | ||
| stateConsumer.initialize(); | ||
| } catch (final InvalidOffsetException recoverableException) { | ||
| log.error( | ||
| "Bootstrapping global state failed. You can restart KafkaStreams to recover from this error.", | ||
|
mjsax marked this conversation as resolved.
Outdated
|
||
| recoverableException | ||
| ); | ||
|
|
||
| try { | ||
| stateConsumer.close(true); | ||
| } catch (final IOException e) { | ||
| log.error("Failed to close state consumer due to the following error:", e); | ||
| } | ||
|
|
||
| throw new StreamsException( | ||
| "Bootstrapping global state failed. You can restart KafkaStreams to recover from this error.", | ||
| recoverableException | ||
| ); | ||
| } | ||
|
|
||
| return stateConsumer; | ||
| } catch (final LockException fatalException) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,6 @@ | |
| package org.apache.kafka.streams.processor.internals; | ||
|
|
||
| import org.apache.kafka.clients.consumer.ConsumerRecord; | ||
| import org.apache.kafka.clients.consumer.InvalidOffsetException; | ||
| import org.apache.kafka.clients.consumer.MockConsumer; | ||
| import org.apache.kafka.clients.consumer.OffsetResetStrategy; | ||
| import org.apache.kafka.common.PartitionInfo; | ||
|
|
@@ -323,21 +322,6 @@ public void shouldRestoreRecordsUpToHighwatermark() { | |
| assertEquals(2, stateRestoreCallback.restored.size()); | ||
| } | ||
|
|
||
| @Test | ||
| public void shouldRecoverFromInvalidOffsetExceptionAndRestoreRecords() { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test was broken: it throw the |
||
| initializeConsumer(2, 0, t1); | ||
| consumer.setPollException(new InvalidOffsetException("Try Again!") { | ||
| public Set<TopicPartition> partitions() { | ||
| return Collections.singleton(t1); | ||
| } | ||
| }); | ||
|
|
||
| stateManager.initialize(); | ||
|
|
||
| stateManager.registerStore(store1, stateRestoreCallback); | ||
| assertEquals(2, stateRestoreCallback.restored.size()); | ||
| } | ||
|
|
||
| @Test | ||
| public void shouldListenForRestoreEvents() { | ||
| initializeConsumer(5, 1, t1); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also not wipe out the store and let the user do it manually. A manual cleanup is actually required nowadays, thus, it's actually a small side "improvement".
Note that we wipe out the whole global task dir here -- in contrast, users could do a manual per-store wipe out... But as we do the same coarse grained wipe out for all tasks and we have already a ticket for "per store cleanup" I though it would be ok for now.
Let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @mjsax , this sounds perfect to me.