-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-19440: Handle top-level errors in AlterShareGroupOffsets RPC #20049
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 2 commits
caed144
e13607b
b9316dd
d1f7d0d
8d91d2a
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 |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
| import org.apache.kafka.common.KafkaFuture; | ||
| import org.apache.kafka.common.TopicPartition; | ||
| import org.apache.kafka.common.annotation.InterfaceStability; | ||
| import org.apache.kafka.common.errors.ApiException; | ||
| import org.apache.kafka.common.internals.KafkaFutureImpl; | ||
| import org.apache.kafka.common.protocol.Errors; | ||
|
|
||
|
|
@@ -35,9 +36,9 @@ | |
| @InterfaceStability.Evolving | ||
| public class AlterShareGroupOffsetsResult { | ||
|
|
||
| private final KafkaFuture<Map<TopicPartition, Errors>> future; | ||
| private final KafkaFuture<Map<TopicPartition, ApiException>> future; | ||
|
|
||
| AlterShareGroupOffsetsResult(KafkaFuture<Map<TopicPartition, Errors>> future) { | ||
| AlterShareGroupOffsetsResult(KafkaFuture<Map<TopicPartition, ApiException>> future) { | ||
| this.future = future; | ||
| } | ||
|
|
||
|
|
@@ -54,11 +55,11 @@ public KafkaFuture<Void> partitionResult(final TopicPartition partition) { | |
| result.completeExceptionally(new IllegalArgumentException( | ||
| "Alter offset for partition \"" + partition + "\" was not attempted")); | ||
| } else { | ||
| final Errors error = topicPartitions.get(partition); | ||
| if (error == Errors.NONE) { | ||
| final ApiException exception = topicPartitions.get(partition); | ||
| if (exception == null) { | ||
| result.complete(null); | ||
| } else { | ||
| result.completeExceptionally(error.exception()); | ||
| result.completeExceptionally(exception); | ||
| } | ||
| } | ||
| }); | ||
|
|
@@ -68,22 +69,22 @@ public KafkaFuture<Void> partitionResult(final TopicPartition partition) { | |
|
|
||
| /** | ||
| * Return a future which succeeds if all the alter offsets succeed. | ||
| * If not, the first topic error shall be returned. | ||
| */ | ||
| public KafkaFuture<Void> all() { | ||
| return this.future.thenApply(topicPartitionErrorsMap -> { | ||
| List<TopicPartition> partitionsFailed = topicPartitionErrorsMap.entrySet() | ||
| .stream() | ||
| .filter(e -> e.getValue() != Errors.NONE) | ||
| .filter(e -> e.getValue() != null) | ||
| .map(Map.Entry::getKey) | ||
| .collect(Collectors.toList()); | ||
|
Collaborator
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. Should this change to immutable?
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. I'm not sure it matters here. The list in internal to this method, and it is just converted to a string and appended to the exception message. |
||
| for (Errors error : topicPartitionErrorsMap.values()) { | ||
| if (error != Errors.NONE) { | ||
| throw error.exception( | ||
| "Failed altering share group offsets for the following partitions: " + partitionsFailed); | ||
| for (ApiException exception : topicPartitionErrorsMap.values()) { | ||
| if (exception != null) { | ||
| throw Errors.forException(exception).exception( | ||
| "Failed altering group offsets for the following partitions: " + partitionsFailed); | ||
| } | ||
| } | ||
| return null; | ||
| }); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,8 +21,8 @@ | |
| import org.apache.kafka.clients.admin.KafkaAdminClient; | ||
| import org.apache.kafka.common.Node; | ||
| import org.apache.kafka.common.TopicPartition; | ||
| import org.apache.kafka.common.errors.ApiException; | ||
| import org.apache.kafka.common.message.AlterShareGroupOffsetsRequestData; | ||
| import org.apache.kafka.common.message.AlterShareGroupOffsetsResponseData; | ||
| import org.apache.kafka.common.protocol.Errors; | ||
| import org.apache.kafka.common.requests.AbstractResponse; | ||
| import org.apache.kafka.common.requests.AlterShareGroupOffsetsRequest; | ||
|
|
@@ -33,7 +33,6 @@ | |
| import org.slf4j.Logger; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.HashSet; | ||
| import java.util.Map; | ||
|
|
@@ -42,7 +41,7 @@ | |
| /** | ||
| * This class is the handler for {@link KafkaAdminClient#alterShareGroupOffsets(String, Map, AlterShareGroupOffsetsOptions)} call | ||
| */ | ||
| public class AlterShareGroupOffsetsHandler extends AdminApiHandler.Batched<CoordinatorKey, Map<TopicPartition, Errors>> { | ||
| public class AlterShareGroupOffsetsHandler extends AdminApiHandler.Batched<CoordinatorKey, Map<TopicPartition, ApiException>> { | ||
|
|
||
| private final CoordinatorKey groupId; | ||
|
|
||
|
|
@@ -52,16 +51,22 @@ public class AlterShareGroupOffsetsHandler extends AdminApiHandler.Batched<Coord | |
|
|
||
| private final CoordinatorStrategy lookupStrategy; | ||
|
|
||
|
|
||
| public AlterShareGroupOffsetsHandler(String groupId, Map<TopicPartition, Long> offsets, LogContext logContext) { | ||
| this.groupId = CoordinatorKey.byGroupId(groupId); | ||
| this.offsets = offsets; | ||
| this.log = logContext.logger(AlterShareGroupOffsetsHandler.class); | ||
| this.lookupStrategy = new CoordinatorStrategy(FindCoordinatorRequest.CoordinatorType.GROUP, logContext); | ||
| } | ||
|
|
||
| public static AdminApiFuture.SimpleAdminApiFuture<CoordinatorKey, Map<TopicPartition, Errors>> newFuture(String groupId) { | ||
| return AdminApiFuture.forKeys(Collections.singleton(CoordinatorKey.byGroupId(groupId))); | ||
| public static AdminApiFuture.SimpleAdminApiFuture<CoordinatorKey, Map<TopicPartition, ApiException>> newFuture(String groupId) { | ||
| return AdminApiFuture.forKeys(Set.of(CoordinatorKey.byGroupId(groupId))); | ||
| } | ||
|
|
||
| private void validateKeys(Set<CoordinatorKey> groupIds) { | ||
| if (!groupIds.equals(Set.of(groupId))) { | ||
| throw new IllegalArgumentException("Received unexpected group ids " + groupIds + | ||
| " (expected only " + Set.of(groupId) + ")"); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -87,30 +92,38 @@ public String apiName() { | |
| } | ||
|
|
||
| @Override | ||
| public ApiResult<CoordinatorKey, Map<TopicPartition, Errors>> handleResponse(Node broker, Set<CoordinatorKey> keys, AbstractResponse abstractResponse) { | ||
| public ApiResult<CoordinatorKey, Map<TopicPartition, ApiException>> handleResponse(Node broker, Set<CoordinatorKey> keys, AbstractResponse abstractResponse) { | ||
| validateKeys(keys); | ||
|
|
||
| AlterShareGroupOffsetsResponse response = (AlterShareGroupOffsetsResponse) abstractResponse; | ||
| final Map<TopicPartition, Errors> partitionResults = new HashMap<>(); | ||
| final Set<CoordinatorKey> groupsToUnmap = new HashSet<>(); | ||
| final Set<CoordinatorKey> groupsToRetry = new HashSet<>(); | ||
|
|
||
| for (AlterShareGroupOffsetsResponseData.AlterShareGroupOffsetsResponseTopic topic : response.data().responses()) { | ||
| for (AlterShareGroupOffsetsResponseData.AlterShareGroupOffsetsResponsePartition partition : topic.partitions()) { | ||
| TopicPartition topicPartition = new TopicPartition(topic.topicName(), partition.partitionIndex()); | ||
| Errors error = Errors.forCode(partition.errorCode()); | ||
|
|
||
| if (error != Errors.NONE) { | ||
| handleError( | ||
| groupId, | ||
| topicPartition, | ||
| error, | ||
| partitionResults, | ||
| groupsToUnmap, | ||
| groupsToRetry | ||
| ); | ||
| } else { | ||
| partitionResults.put(topicPartition, error); | ||
| final Map<TopicPartition, ApiException> partitionResults = new HashMap<>(); | ||
|
|
||
| if (response.data().errorCode() != Errors.NONE.code()) { | ||
| final Errors topLevelError = Errors.forCode(response.data().errorCode()); | ||
| final String topLevelErrorMessage = response.data().errorMessage(); | ||
|
|
||
| offsets.forEach((topicPartition, offset) -> | ||
| handleError( | ||
| groupId, | ||
| topicPartition, | ||
| topLevelError, | ||
| topLevelErrorMessage, | ||
| partitionResults, | ||
| groupsToUnmap, | ||
| groupsToRetry | ||
| )); | ||
| } else { | ||
| response.data().responses().forEach(topic -> topic.partitions().forEach(partition -> { | ||
| if (partition.errorCode() != Errors.NONE.code()) { | ||
| final Errors partitionError = Errors.forCode(partition.errorCode()); | ||
|
Member
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. nit: we can reuse the
Member
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. nit: the debug message should use placeholder for
Member
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. for example: var partitionError = Errors.forCode(partition.errorCode());
var partitionErrorMessage = partition.errorMessage();
if (partitionError != Errors.NONE) {
log.debug("AlterShareGroupOffsets request for group id {} and topic-partition {}-{} failed and returned error {}. {}",
groupId.idValue, topic.topicName(), partition.partitionIndex(), partitionError, partitionErrorMessage);
}
partitionResults.put(new TopicPartition(topic.topicName(), partition.partitionIndex()), partitionError.exception(partitionErrorMessage));
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. Thanks @chia7712 . I'll put in a minor PR today. |
||
| final String partitionErrorMessage = partition.errorMessage(); | ||
| log.debug("AlterShareGroupOffsets request for group id {} and topic-partition {}-{} failed and returned error {}." + partitionErrorMessage, | ||
| groupId.idValue, topic.topicName(), partition.partitionIndex(), partitionError); | ||
| } | ||
| } | ||
| partitionResults.put(new TopicPartition(topic.topicName(), partition.partitionIndex()), Errors.forCode(partition.errorCode()).exception(partition.errorMessage())); | ||
| })); | ||
| } | ||
|
|
||
| if (groupsToUnmap.isEmpty() && groupsToRetry.isEmpty()) { | ||
|
|
@@ -121,23 +134,23 @@ public ApiResult<CoordinatorKey, Map<TopicPartition, Errors>> handleResponse(Nod | |
| } | ||
|
|
||
| private void handleError( | ||
| CoordinatorKey groupId, | ||
| TopicPartition topicPartition, | ||
| Errors error, | ||
| Map<TopicPartition, Errors> partitionResults, | ||
| Set<CoordinatorKey> groupsToUnmap, | ||
| Set<CoordinatorKey> groupsToRetry | ||
| CoordinatorKey groupId, | ||
| TopicPartition topicPartition, | ||
| Errors error, | ||
| String errorMessage, | ||
| Map<TopicPartition, ApiException> partitionResults, | ||
| Set<CoordinatorKey> groupsToUnmap, | ||
| Set<CoordinatorKey> groupsToRetry | ||
| ) { | ||
| switch (error) { | ||
| case COORDINATOR_LOAD_IN_PROGRESS: | ||
| case REBALANCE_IN_PROGRESS: | ||
| log.debug("AlterShareGroupOffsets request for group id {} returned error {}. Will retry.", | ||
| groupId.idValue, error); | ||
| log.debug("AlterShareGroupOffsets request for group id {} returned error {}. Will retry." + errorMessage, groupId.idValue, error); | ||
| groupsToRetry.add(groupId); | ||
| break; | ||
| case COORDINATOR_NOT_AVAILABLE: | ||
| case NOT_COORDINATOR: | ||
| log.debug("AlterShareGroupOffsets request for group id {} returned error {}. Will rediscover the coordinator and retry.", | ||
| log.debug("AlterShareGroupOffsets request for group id {} returned error {}. Will rediscover the coordinator and retry." + errorMessage, | ||
| groupId.idValue, error); | ||
| groupsToUnmap.add(groupId); | ||
| break; | ||
|
|
@@ -147,14 +160,12 @@ private void handleError( | |
| case UNKNOWN_SERVER_ERROR: | ||
| case KAFKA_STORAGE_ERROR: | ||
| case GROUP_AUTHORIZATION_FAILED: | ||
| log.debug("AlterShareGroupOffsets request for group id {} and partition {} failed due" + | ||
| " to error {}.", groupId.idValue, topicPartition, error); | ||
| partitionResults.put(topicPartition, error); | ||
| log.debug("AlterShareGroupOffsets request for group id {} failed due to error {}." + errorMessage, groupId.idValue, error); | ||
| partitionResults.put(topicPartition, error.exception(errorMessage)); | ||
| break; | ||
| default: | ||
| log.error("AlterShareGroupOffsets request for group id {} and partition {} failed due" + | ||
| " to unexpected error {}.", groupId.idValue, topicPartition, error); | ||
| partitionResults.put(topicPartition, error); | ||
| log.error("AlterShareGroupOffsets request for group id {} failed due to unexpected error {}." + errorMessage, groupId.idValue, error); | ||
| partitionResults.put(topicPartition, error.exception(errorMessage)); | ||
| } | ||
| } | ||
|
|
||
|
|
||
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 for the patch, just out of curiosity, would the AlterConsumerGroupOffsets RPC have the same issue?
kafka/clients/src/main/java/org/apache/kafka/clients/admin/AlterConsumerGroupOffsetsResult.java
Lines 68 to 79 in 05b6e81
Uh oh!
There was an error while loading. Please reload this page.
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.
I'll check it out before I merge, but the important difference here is in
KafkaApis.scala. For DeleteSGO, it already handled a non-zero error code. For AlterSGO, that code was missing.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.
Ah, sorry. I didn't read your comment accurately. The difference is that AlterShareGroupOffsets can successfully pass back an error code, which is why this is in terms of
ApiExceptionrather thanErrors. ForAlterConsumerGroupOffsets, the RPC is actuallyOffsetCommitand this does not have anErrorMessagefield at all. So, it cannot be fixed for consumer groups until we have a version bump on theOffsetCommitRPC.