-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-8421: Still return data during rebalance #7312
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 37 commits
37b5c63
4a34116
d964cea
f5b8f96
b07bac2
29f8942
3000d6f
8464fa7
f804b3d
3c62029
284ef77
9825063
929048a
58adc09
c79a93d
75c4e88
95ef3fb
3b844ff
781514d
8b08a3e
8fa5d74
d75fcb9
b176e4e
7a826f9
31e372e
366739b
faf787e
c71af82
ffe6146
518049c
30d5e53
fa3456d
0220d96
27d013a
94611cc
b529847
33fd785
1df9808
3057405
21f531d
effb29d
6fbd976
f4e7111
91b6606
0ad52ec
27f76b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,21 +16,17 @@ | |
| */ | ||
| package org.apache.kafka.clients.consumer; | ||
|
|
||
| import org.apache.kafka.common.errors.RetriableException; | ||
| import org.apache.kafka.common.KafkaException; | ||
|
|
||
| public class RetriableCommitFailedException extends RetriableException { | ||
| public class RetriableCommitFailedException extends KafkaException { | ||
|
|
||
| private static final long serialVersionUID = 1L; | ||
|
|
||
| public static RetriableCommitFailedException withUnderlyingMessage(String additionalMessage) { | ||
| return new RetriableCommitFailedException("Offset commit failed with a retriable exception. " + | ||
| "You should retry committing the latest consumed offsets. " + | ||
| "The underlying error was: " + additionalMessage); | ||
| } | ||
|
|
||
| public RetriableCommitFailedException(Throwable t) { | ||
| super("Offset commit failed with a retriable exception. You should retry committing " + | ||
| "the latest consumed offsets.", t); | ||
| super("Offset commit failed with a retriable exception. This is usually because the consumer is not yet " + | ||
| "part of a group with auto partition assignment or is participating in a rebalance to reassign partitions " + | ||
| "right now. You should first call poll to complete the rebalance and then " + | ||
| "retry committing the consumed offsets for those assigned partitions.", t); | ||
| } | ||
|
|
||
| public RetriableCommitFailedException(String message) { | ||
|
Contributor
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. Hmm.. Are we safe to remove these given that this is a public API. It's probably unlikely anyone is using them, but still..
Contributor
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. Oh I was not sure we allow public classes to be used in our API "contract" :P Anyhow, I don't feel strong about it and I can revert. |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -433,7 +433,7 @@ public boolean poll(Timer timer) { | |
|
|
||
| invokeCompletedOffsetCommitCallbacks(); | ||
|
|
||
| if (subscriptions.partitionsAutoAssigned()) { | ||
| if (subscriptions.hasAutoAssignedPartitions()) { | ||
| if (protocol == null) { | ||
| throw new IllegalStateException("User configured " + ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG + | ||
| " to empty while trying to subscribe for group protocol to auto assign partitions"); | ||
|
|
@@ -701,7 +701,7 @@ public void onLeavePrepare() { | |
| // we should reset assignment and trigger the callback before leaving group | ||
| Set<TopicPartition> droppedPartitions = new HashSet<>(subscriptions.assignedPartitions()); | ||
|
|
||
| if (subscriptions.partitionsAutoAssigned() && !droppedPartitions.isEmpty()) { | ||
| if (subscriptions.hasAutoAssignedPartitions() && !droppedPartitions.isEmpty()) { | ||
| final Exception e; | ||
| if (generation() != Generation.NO_GENERATION) { | ||
| e = invokePartitionsRevoked(droppedPartitions); | ||
|
|
@@ -722,7 +722,7 @@ public void onLeavePrepare() { | |
| */ | ||
| @Override | ||
| public boolean rejoinNeededOrPending() { | ||
| if (!subscriptions.partitionsAutoAssigned()) | ||
| if (!subscriptions.hasAutoAssignedPartitions()) | ||
| return false; | ||
|
|
||
| // we need to rejoin if we performed the assignment and metadata has changed; | ||
|
|
@@ -761,7 +761,11 @@ public boolean refreshCommittedOffsetsIfNeeded(Timer timer) { | |
|
|
||
| log.info("Setting offset for partition {} to the committed offset {}", tp, position); | ||
| entry.getValue().leaderEpoch().ifPresent(epoch -> this.metadata.updateLastSeenEpochIfNewer(entry.getKey(), epoch)); | ||
| this.subscriptions.seekUnvalidated(tp, position); | ||
|
|
||
| // it's possible that the partition is no longer assigned when the response is received, | ||
|
Contributor
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. Do you think it's worth adding a debug message saying that we're ignoring the fetched offset? |
||
| // so we need to ignore seeking if that's the case | ||
| if (this.subscriptions.isAssigned(tp)) | ||
|
Contributor
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. @hachikuji This is the change I make for auto offset reset. I thought about adding a unit test but since the logic is wrapped in a single
Contributor
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. Does it make sense to move this check above a little bit? if (offsetAndMetadata != null && subscriptions.isAssigned(tp)) {That will make the log message less confusing.
Contributor
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. Hmm, but I thought even if the partition is no longer assigned, we may still want to update its epoch; on the other hand your concern is valid. Will tweak it a bit more. |
||
| this.subscriptions.seekUnvalidated(tp, position); | ||
| } | ||
| } | ||
| return true; | ||
|
|
@@ -972,7 +976,7 @@ private void doAutoCommitOffsetsAsync() { | |
|
|
||
| commitOffsetsAsync(allConsumedOffsets, (offsets, exception) -> { | ||
| if (exception != null) { | ||
| if (exception instanceof RetriableException) { | ||
| if (exception instanceof RetriableCommitFailedException) { | ||
| log.debug("Asynchronous auto-commit of offsets {} failed due to retriable error: {}", offsets, | ||
| exception); | ||
| nextAutoCommitTimer.updateAndReset(rebalanceConfig.retryBackoffMs); | ||
|
|
@@ -1052,13 +1056,17 @@ private RequestFuture<Void> sendOffsetCommitRequest(final Map<TopicPartition, Of | |
| } | ||
|
|
||
| final Generation generation; | ||
| if (subscriptions.partitionsAutoAssigned()) { | ||
| if (subscriptions.hasAutoAssignedPartitions()) { | ||
| generation = generationIfStable(); | ||
| // if the generation is null, we are not part of an active group (and we expect to be). | ||
| // the only thing we can do is fail the commit and let the user rejoin the group in poll() | ||
| // the only thing we can do is fail the commit and let the user rejoin the group in poll(); | ||
| // we use RetriableCommitFailedException to indicate this may not be a fatal error, and leave | ||
| // it to users whether they want to handle differently than CommitFailed | ||
| if (generation == null) { | ||
| log.info("Failing OffsetCommit request since the consumer is not part of an active group"); | ||
| return RequestFuture.failure(new CommitFailedException()); | ||
| return RequestFuture.failure(new RetriableCommitFailedException("Offset commit cannot be completed since the " + | ||
| "consumer is not part of an active group for auto partition assignment yet. You can try completing the rebalance " + | ||
| "by calling poll() and then retry the operation")); | ||
| } | ||
| } else | ||
| generation = Generation.NO_GENERATION; | ||
|
|
@@ -1075,14 +1083,16 @@ private RequestFuture<Void> sendOffsetCommitRequest(final Map<TopicPartition, Of | |
| log.trace("Sending OffsetCommit request with {} to coordinator {}", offsets, coordinator); | ||
|
|
||
| return client.send(coordinator, builder) | ||
| .compose(new OffsetCommitResponseHandler(offsets)); | ||
| .compose(new OffsetCommitResponseHandler(offsets, generation)); | ||
| } | ||
|
|
||
| private class OffsetCommitResponseHandler extends CoordinatorResponseHandler<OffsetCommitResponse, Void> { | ||
|
|
||
| private final Generation generation; | ||
| private final Map<TopicPartition, OffsetAndMetadata> offsets; | ||
|
|
||
| private OffsetCommitResponseHandler(Map<TopicPartition, OffsetAndMetadata> offsets) { | ||
| private OffsetCommitResponseHandler(Map<TopicPartition, OffsetAndMetadata> offsets, Generation generation) { | ||
| this.generation = generation; | ||
| this.offsets = offsets; | ||
| } | ||
|
|
||
|
|
@@ -1134,22 +1144,34 @@ public void handle(OffsetCommitResponse commitResponse, RequestFuture<Void> futu | |
| future.raise(error); | ||
| return; | ||
| } else if (error == Errors.REBALANCE_IN_PROGRESS) { | ||
| /* Consumer never tries to commit offset in between join-group and sync-group, | ||
| /* Consumer should not try to commit offset in between join-group and sync-group, | ||
| * and hence on broker-side it is not expected to see a commit offset request | ||
| * during CompletingRebalance phase; if it ever happens then broker would return | ||
| * this error. In this case we should just treat as a fatal CommitFailed exception. | ||
| * However, we do not need to reset generations and just request re-join, such that | ||
| * if the caller decides to proceed and poll, it would still try to proceed and re-join normally. | ||
| * this error to indicate that we are still in the middle of a rebalance. | ||
| * In this case we would throw a RetriableCommitFailedException, | ||
| * request re-join but do not reset generations. If the callers decide to retry they | ||
| * can go ahead and call poll to finish up the rebalance first, and then try commit again. | ||
| */ | ||
| requestRejoin(); | ||
| future.raise(new CommitFailedException()); | ||
| future.raise(new RetriableCommitFailedException("Offset commit cannot be completed since the " + | ||
| "consumer group is executing a rebalance at the moment. You can try completing the rebalance " + | ||
| "by calling poll() and then retry commit again")); | ||
| return; | ||
| } else if (error == Errors.UNKNOWN_MEMBER_ID | ||
|
guozhangwang marked this conversation as resolved.
|
||
| || error == Errors.ILLEGAL_GENERATION) { | ||
| } else if (error == Errors.UNKNOWN_MEMBER_ID) { | ||
| // need to reset generation and re-join group | ||
| resetGenerationOnResponseError(ApiKeys.OFFSET_COMMIT, error); | ||
| future.raise(new CommitFailedException()); | ||
| return; | ||
| } else if (error == Errors.ILLEGAL_GENERATION) { | ||
| if (this.generation.equals(ConsumerCoordinator.this.generation())) { | ||
|
Contributor
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. How much effort would it be to write a test case which hits this path?
Contributor
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. I tried to add the test but found it may not be a valid case actually: we think this can happen when a join request is sent, and then a commit request is sent, and then a join response is received and then a commit response is received all from the same socket. However when a join-request is sent we already transit to the we would immediately fail with a RetriableCommitFailure exception: if it is called with an async commit call, it would mark it as failure, if it is called with a sync commit, that exception would be thrown.
Contributor
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. So it sounds to me that we do not need this specific handling since we should actually never hit this scenario? |
||
| // need to reset generation and re-join group if the generation has not changed, meaning | ||
| // this is indeed a illegal generation; otherwise if the generation has changed the current | ||
| // generation may actually be valid so we do not reset it -- if it is not, the next request | ||
| // would still fail and then we would reset generation and re-join at that time. | ||
| resetGenerationOnResponseError(ApiKeys.OFFSET_COMMIT, error); | ||
| } | ||
| future.raise(new CommitFailedException()); | ||
| return; | ||
| } else { | ||
| future.raise(new KafkaException("Unexpected error in commit: " + error.message())); | ||
| return; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,17 +26,16 @@ | |
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| public class KafkaConsumerMetrics implements AutoCloseable { | ||
| private final Metrics metrics; | ||
|
Contributor
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 a minor cleanup: do not need local variable. |
||
| private final MetricName lastPollMetricName; | ||
| private final Sensor timeBetweenPollSensor; | ||
| private final Sensor pollIdleSensor; | ||
| private final Metrics metrics; | ||
| private long lastPollMs; | ||
| private long pollStartMs; | ||
| private long timeSinceLastPollMs; | ||
|
|
||
| public KafkaConsumerMetrics(Metrics metrics, String metricGrpPrefix) { | ||
| this.metrics = metrics; | ||
|
|
||
| String metricGroupName = metricGrpPrefix + "-metrics"; | ||
| Measurable lastPoll = (mConfig, now) -> { | ||
| if (lastPollMs == 0L) | ||
|
|
||
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.
This function is not used anywhere so remove.