-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KIP-229: DeleteGroups API #4479
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 1 commit
b282a21
10aab1a
6d3a0b7
bcad73a
a602d3c
8fb935f
eabc8b3
ca60b65
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 |
|---|---|---|
|
|
@@ -370,23 +370,38 @@ class AdminClient(val time: Time, | |
| } | ||
|
|
||
| def deleteConsumerGroups(groups: List[String]): Map[String, Errors] = { | ||
| var errors: Map[String, Errors] = Map() | ||
| val groupsPerCoordinator = groups.map { group => | ||
|
|
||
| def coordinatorLookup(group: String): Either[Node, Errors] = { | ||
| try { | ||
| (group, findCoordinator(group)) | ||
| Left(findCoordinator(group)) | ||
| } catch { | ||
| case e: Throwable => | ||
| errors += (group -> { | ||
| if (e.isInstanceOf[TimeoutException]) | ||
| Errors.COORDINATOR_NOT_AVAILABLE | ||
| else | ||
| Errors.forException(e) | ||
| }) | ||
| (group, null) | ||
| if (e.isInstanceOf[TimeoutException]) | ||
| Right(Errors.COORDINATOR_NOT_AVAILABLE) | ||
| else | ||
| Right(Errors.forException(e)) | ||
| } | ||
| }.groupBy(_._2).map { | ||
| case (coordinator, groupCoordinator) => (coordinator, groupCoordinator.unzip._1.toArray) | ||
| }.filter(_._1 != null) | ||
| } | ||
|
|
||
| val groupCoordinator = groups.map(group => (group -> coordinatorLookup(group))) | ||
|
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. Seems this is unused
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. unused? |
||
|
|
||
| var errors: Map[String, Errors] = Map() | ||
| var groupsPerCoordinator: Map[Node, List[String]] = Map() | ||
|
|
||
| groups.foreach { group => | ||
| coordinatorLookup(group) match { | ||
| case Right(error) => | ||
| errors += (group -> error) | ||
| case Left(coordinator) => | ||
| groupsPerCoordinator.get(coordinator) match { | ||
| case Some(gList: List[String]) => | ||
|
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. nit: I don't think you need the type. |
||
| val gListNew = group :: gList | ||
| groupsPerCoordinator += (coordinator -> gListNew) | ||
| case None => | ||
| groupsPerCoordinator += (coordinator -> List(group)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| groupsPerCoordinator.foreach { case (coordinator, groups) => | ||
| val responseBody = send(coordinator, ApiKeys.DELETE_GROUPS, new DeleteGroupsRequest.Builder(groups.toSet.asJava)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -362,11 +362,11 @@ object ConsumerGroupCommand extends Logging { | |
|
|
||
| def deleteGroups(): Map[String, Errors] = { | ||
| if (opts.options.has(opts.groupOpt) && opts.options.has(opts.topicOpt)) | ||
| deleteForTopic() | ||
| deleteGroupsInfoForTopic() | ||
| else if (opts.options.has(opts.groupOpt)) | ||
| deleteForGroup() | ||
| deleteGroupsInfo() | ||
| else if (opts.options.has(opts.topicOpt)) | ||
| deleteAllForTopic() | ||
| deleteAllGroupsInfoForTopic() | ||
|
|
||
| Map() | ||
|
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. This is a little odd. Maybe to follow the interface, we should try to catch exceptions and return an error?
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. Sounds good. I updated this in the new commit.
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. Seems like you were intending to use the results of the
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. Sure, I'll submit a separate PR with proper test(s) after this is merged. |
||
| } | ||
|
|
@@ -474,45 +474,57 @@ object ConsumerGroupCommand extends Logging { | |
| }.toMap | ||
| } | ||
|
|
||
| private def deleteForGroup() { | ||
| private def deleteGroupsInfo(): Map[String, Errors] = { | ||
| val groups = opts.options.valuesOf(opts.groupOpt) | ||
| groups.asScala.foreach { group => | ||
| groups.asScala.map { group => | ||
| try { | ||
| if (AdminUtils.deleteConsumerGroupInZK(zkUtils, group)) | ||
| if (AdminUtils.deleteConsumerGroupInZK(zkUtils, group)) { | ||
| println(s"Deleted all consumer group information for group '$group' in zookeeper.") | ||
| else | ||
| (group -> Errors.NONE) | ||
| } | ||
| else { | ||
| printError(s"Delete for group '$group' failed because its consumers are still active.") | ||
| (group -> Errors.NON_EMPTY_GROUP) | ||
| } | ||
| } | ||
| catch { | ||
| case e: ZkNoNodeException => | ||
| printError(s"Delete for group '$group' failed because group does not exist.", Some(e)) | ||
| (group -> Errors.forException(e)) | ||
| } | ||
| } | ||
| }.toMap | ||
| } | ||
|
|
||
| private def deleteForTopic() { | ||
| private def deleteGroupsInfoForTopic(): Map[String, Errors] = { | ||
| val groups = opts.options.valuesOf(opts.groupOpt) | ||
| val topic = opts.options.valueOf(opts.topicOpt) | ||
| Topic.validate(topic) | ||
| groups.asScala.foreach { group => | ||
| groups.asScala.map { group => | ||
| try { | ||
| if (AdminUtils.deleteConsumerGroupInfoForTopicInZK(zkUtils, group, topic)) | ||
| if (AdminUtils.deleteConsumerGroupInfoForTopicInZK(zkUtils, group, topic)) { | ||
| println(s"Deleted consumer group information for group '$group' topic '$topic' in zookeeper.") | ||
| else | ||
| (group -> Errors.NONE) | ||
|
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. nit: unneeded parenthesis. A few more like this. |
||
| } | ||
| else { | ||
| printError(s"Delete for group '$group' topic '$topic' failed because its consumers are still active.") | ||
| (group -> Errors.NON_EMPTY_GROUP) | ||
| } | ||
| } | ||
| catch { | ||
| case e: ZkNoNodeException => | ||
| printError(s"Delete for group '$group' topic '$topic' failed because group does not exist.", Some(e)) | ||
| (group -> Errors.forException(e)) | ||
| } | ||
| } | ||
| }.toMap | ||
| } | ||
|
|
||
| private def deleteAllForTopic() { | ||
| private def deleteAllGroupsInfoForTopic(): Map[String, Errors] = { | ||
| val topic = opts.options.valueOf(opts.topicOpt) | ||
| Topic.validate(topic) | ||
| AdminUtils.deleteAllConsumerGroupInfoForTopicInZK(zkUtils, topic) | ||
| val deletedGroups = AdminUtils.deleteAllConsumerGroupInfoForTopicInZK(zkUtils, topic) | ||
| println(s"Deleted consumer group information for all inactive consumer groups for topic '$topic' in zookeeper.") | ||
| deletedGroups.map((_, Errors.NONE)).toMap | ||
|
|
||
| } | ||
|
|
||
| private def getZkConsumer(brokerId: Int): Option[SimpleConsumer] = { | ||
|
|
@@ -807,7 +819,7 @@ object ConsumerGroupCommand extends Logging { | |
| val groupsToDelete = opts.options.valuesOf(opts.groupOpt).asScala.toList | ||
|
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.. It's a little weird that we allow multiple groups to be passed when using the new consumer, but we expect a single group for the old consumer. If we're to stay consistent, do you think it would be restrictive in practice to only support deletion of a single group at a time?
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. In the meantime I'll look at your other feedback (thanks btw) regarding this one, it seems the old consumer also supports deleting multiple groups, i.e. I originally wanted to support single group deletion only, but after considering the existing behavior for old consumer decided otherwise.
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. Ah, you are right. The name
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. Sure, I gave this a quick try. Let me know if you have better suggestions. |
||
| val result = adminClient.deleteConsumerGroups(groupsToDelete) | ||
| val successfullyDeleted = result.filter { | ||
| case (group, error) => error == Errors.NONE | ||
| case (_, error) => error == Errors.NONE | ||
| }.keySet | ||
|
|
||
| if (successfullyDeleted.size == result.size) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1224,17 +1224,14 @@ class KafkaApis(val requestChannel: RequestChannel, | |
| def handleDeleteGroupsRequest(request: RequestChannel.Request): Unit = { | ||
| val deleteGroupsRequest = request.body[DeleteGroupsRequest] | ||
| var groups = deleteGroupsRequest.groups.asScala.toSet | ||
| var unauthorizedGroupsDeletionResult: Map[String, Errors] = Map() | ||
|
|
||
| groups.foreach { group => | ||
| if (!authorize(request.session, Delete, new Resource(Group, group))) { | ||
| unauthorizedGroupsDeletionResult += (group -> Errors.GROUP_AUTHORIZATION_FAILED) | ||
| groups -= group | ||
| } | ||
| val (authorizedGroups, unauthorizedGroups) = groups.partition { group => | ||
| authorize(request.session, Delete, new Resource(Group, group)) | ||
| } | ||
|
|
||
| val authorizedGroupsDeletionResult = groupCoordinator.handleDeleteGroups(groups) | ||
| val groupDeletionResult = authorizedGroupsDeletionResult._2 ++ unauthorizedGroupsDeletionResult | ||
| val groupDeletionResult = groupCoordinator.handleDeleteGroups(authorizedGroups)._2 ++ | ||
|
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. looks like we are ignoring groupCoordinator.handleDeleteGroups(authorizedGroups)._1 error here.
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. Thanks for catching this. I missed updating this with the recent change to the protocol. Will update it in the next commit. |
||
| unauthorizedGroups.map((_ -> Errors.GROUP_AUTHORIZATION_FAILED)) | ||
|
|
||
| sendResponseMaybeThrottle(request, requestThrottleMs => | ||
| new DeleteGroupsResponse(requestThrottleMs, groupDeletionResult.asJava)) | ||
| } | ||
|
|
||
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 think
NOT_COORDINATORshould also be possible?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.
Correct. Also
COORDINATOR_LOAD_IN_PROGRESSif I'm not mistaken?