-
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 |
|---|---|---|
|
|
@@ -340,23 +340,48 @@ class GroupCoordinator(val brokerId: Int, | |
|
|
||
| def handleDeleteGroups(groupIds: Set[String]): Map[String, Errors] = { | ||
| if (!isActive.get) { | ||
| groupIds.map((_ -> Errors.COORDINATOR_NOT_AVAILABLE)).toMap | ||
| groupIds.map(_ -> Errors.COORDINATOR_NOT_AVAILABLE).toMap | ||
| } else { | ||
| var groupErrors: Map[String, Errors] = Map() | ||
| var eligibleGroups: Seq[String] = Seq() | ||
| var eligibleGroups: Seq[GroupMetadata] = Seq() | ||
|
|
||
| groupIds.foreach { groupId => | ||
| if (!validGroupId(groupId)) | ||
| groupErrors += (groupId -> Errors.INVALID_GROUP_ID) | ||
| else if (!isCoordinatorForGroup(groupId)) { | ||
| groupErrors += (groupId -> Errors.NOT_COORDINATOR) | ||
| } else if (isCoordinatorLoadInProgress(groupId)) | ||
| groupErrors += (groupId -> Errors.COORDINATOR_LOAD_IN_PROGRESS) | ||
| else | ||
| eligibleGroups ++= Seq(groupId) | ||
| groupErrors += groupId -> Errors.INVALID_GROUP_ID | ||
| else if (!isCoordinatorForGroup(groupId)) | ||
| groupErrors += groupId -> Errors.NOT_COORDINATOR | ||
| else if (isCoordinatorLoadInProgress(groupId)) | ||
| groupErrors += groupId -> Errors.COORDINATOR_LOAD_IN_PROGRESS | ||
| else { | ||
| groupManager.getGroup(groupId) match { | ||
| case None => | ||
| groupErrors += groupId -> Errors.GROUP_ID_NOT_FOUND | ||
|
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 case should be handled the same as if the group is dead. You can probably add a little helper to avoid the duplication.
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. On second thought, this probably doesn't solve the underlying issue. I'm trying to think how we can be sure that we're returning this error code correctly. Maybe we need to check for group existence while holding the
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. If // return true iff group is owned and the group doesn't exist
def groupNotExists(groupId: String) = inLock(partitionLock) {
isGroupLocal(groupId) && (!groupMetadataCache.contains(groupId) || groupMetadataCache.get(groupId).is(Dead))
}Then we can use this function here instead of checking the coordinator again. The name could probably be improved.
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. For both
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. The point is to check it while holding the partition lock so that it is an atomic operation. This ensures that we will not have any race conditions with partition loading/unloading.
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. Aah, right. That makes sense. |
||
| case Some(group) => | ||
| group.inLock { | ||
| group.currentState match { | ||
| case Dead => | ||
| if (!isCoordinatorForGroup(groupId)) | ||
| groupErrors += groupId -> Errors.NOT_COORDINATOR | ||
| else | ||
| groupErrors += groupId -> Errors.GROUP_ID_NOT_FOUND | ||
| case Empty => | ||
| group.transitionTo(Dead) | ||
| eligibleGroups :+= group | ||
| case _ => | ||
| groupErrors += groupId -> Errors.NON_EMPTY_GROUP | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (eligibleGroups.nonEmpty) { | ||
| groupManager.cleanupGroupMetadata(None, eligibleGroups, Long.MaxValue) | ||
|
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 still feels a bit hacky. As an alternative, maybe we can let the offset selector be provided as a function. Something like this: def cleanupGroupMetadata(
groups: Iterable[GroupMetadata],
collectOffsetsToRemove: Group => Map[TopicPartition, OffsetAndMetadata])What do you think?
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'm not sure which part you consider hacky, and am trying to understand your suggestion. For the sake of On the same assumption, we also need to factor in the concept of current time so we can determine the expired offsets for the existing functionality. On the other hand if you are proposing to create A new Or maybe I'm missing the point :)
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. It's not that big of a deal. I just thought it was a mild abuse to reuse the expiration logic to delete all offsets. Alternatively, what I was suggesting is to let the caller choose the offsets to delete. |
||
| groupErrors ++= eligibleGroups.map(_.groupId -> Errors.NONE).toMap | ||
| info(s"The following groups were deleted: ${eligibleGroups.map(_.groupId).mkString(", ")}") | ||
| } | ||
|
|
||
| groupErrors ++ groupManager.deleteGroups(eligibleGroups) | ||
| groupErrors | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -714,28 +714,6 @@ class GroupMetadataManagerTest { | |
| assertEquals(group, groupMetadataManager.addGroup(new GroupMetadata("foo", initialState = Empty))) | ||
| } | ||
|
|
||
| @Test | ||
| def testDeleteEmptyGroup() { | ||
| val group = new GroupMetadata(groupId, initialState = Empty) | ||
| assertEquals(group, groupMetadataManager.addGroup(group)) | ||
|
|
||
| EasyMock.expect(replicaManager.getMagic(EasyMock.anyObject())).andStubReturn(Some(RecordBatch.CURRENT_MAGIC_VALUE)) | ||
| mockGetPartition() | ||
| EasyMock.replay(replicaManager, partition) | ||
|
|
||
| val result = groupMetadataManager.deleteGroups(Seq(group.groupId)) | ||
| assert(result.size == 1 && result.contains(group.groupId) && result.get(group.groupId).contains(Errors.NONE)) | ||
| } | ||
|
|
||
| @Test | ||
| def testDeleteNonEmptyGroup() { | ||
|
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. Why remove these test cases?
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. They made a call to |
||
| val group = new GroupMetadata(groupId, initialState = Stable) | ||
| assertEquals(group, groupMetadataManager.addGroup(group)) | ||
|
|
||
| val result = groupMetadataManager.deleteGroups(Seq(group.groupId)) | ||
| assert(result.size == 1 && result.contains(group.groupId) && result.get(group.groupId).contains(Errors.NON_EMPTY_GROUP)) | ||
| } | ||
|
|
||
| @Test | ||
| def testStoreEmptyGroup() { | ||
| val generation = 27 | ||
|
|
||
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.
In fact, this is also a "check and act." It is possible for an eligible group to be unloaded between these checks and the call to
deleteGroups.I think we should follow a structure more similar to the other API handlers. I would suggest moving the state checking that we currently have in
GroupMetadataManager.deleteGroupsinto theelsecase below. We should do the following:Dead. Once we do so, we are ensured that no other thread will attempt to use theGroupMetadataobject. We can then collect this eligible group in a collection (as is currently done) and send it tocleanupGroupMetadataoutside of the lock.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'm working on this and have a couple of questions for now:
GroupMetadataManager.deleteGroups(). Do you see an issue with it?Dead?Thanks.
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.
Seems reasonable to me.
We are trying to address the case in which a group gets deleted or migrated in between the time that we check if the coordinator is assigned and the time we delete the group metadata. We have the
Deadstate for this purpose, so whenever we checkGroupMetadata, the first thing we should check is whether it is alreadyDead. If it is, then we know it was either already deleted or already migrated. My suggestion is to check again whether we are still the coordinator for the group to disambiguate the two cases.Note that I am not sure that this is 100% bulletproof. For example, it may not handle the case when the coordinator is migrated away and then back very quickly. A spurious NOT_COORDINATOR error is not a big deal because clients are expected to handle it, but I am not too sure about the GROUP_ID_NOT_FOUND error. Maybe clients just have to treat it with the same skepticism that they treat the UNKOWN_TOPIC_OR_PARTITION errors.
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 clarifying this. I'll try to do just that in the new patch. Will submit shortly.