Conversation
| memberId, String.join(" and ", reasons)); | ||
| } | ||
|
|
||
| consumerGroup.removeMember(memberId); |
There was a problem hiding this comment.
When this condition (line 5930: targetAssignment().containsKey(memberId)) is hit, this now warns and calls removeMember(memberId) — but ConsumerGroup#removeMember doesn't touch targetAssignment (same for ShareGroup#removeMember and StreamsGroup#removeMember), and nothing else will clean up that entry for a member that's already gone. So this leaves a permanently orphaned entry in targetAssignment(), not just a logged compaction-artifact warning.
Should this also call removeTargetAssignment(memberId) (already available on the shared ModernGroup base class) here? Same question applies to the two sibling occurrences: share group (line 6352 condition / line 6365 removeMember call) and streams group (line 6459 condition / line 6472 removeMember call).
There was a problem hiding this comment.
I've thought about this but I was not sure what's impact of clearing target assignments so just left it as it.
There was a problem hiding this comment.
updated PR with cleanup
There was a problem hiding this comment.
I wonder if we should invoke replay with synthetic tombstones when we detect missing ones, instead of duplicating the tombstone logic from other methods. It could be more robust against future changes. In normal operation we will almost never take the missing tombstone path so we don't need to worry about performance that much.
There was a problem hiding this comment.
agree. I will update so that every path that has missing sibling tombstones will replay these tombstones
squah-confluent
left a comment
There was a problem hiding this comment.
Thanks for the patch and for checking all three group types!
| if (oldMember.memberEpoch() != LEAVE_GROUP_MEMBER_EPOCH || consumerGroup.targetAssignment().containsKey(memberId)) { | ||
| List<String> reasons = new ArrayList<>(); | ||
| if (oldMember.memberEpoch() != LEAVE_GROUP_MEMBER_EPOCH) { | ||
| reasons.add("has invalid leave group epoch"); |
There was a problem hiding this comment.
The tombstone that's missing is a ConsumerGroupCurrentMemberAssignment. How about wording it like this?
| reasons.add("has invalid leave group epoch"); | |
| reasons.add("still has a current assignment"); |
The same for share and streams groups.
| reasons.add("has invalid leave group epoch"); | ||
| } | ||
| if (consumerGroup.targetAssignment().containsKey(memberId)) { | ||
| reasons.add("member exists in target assignment"); |
There was a problem hiding this comment.
typo? when formatted we will render "but member member exists in target assignment".
| reasons.add("member exists in target assignment"); | |
| reasons.add("still has a target assignment"); |
The same for share and streams groups.
There was a problem hiding this comment.
yea mb, copy-pasted the sentence
| memberId, String.join(" and ", reasons)); | ||
| } | ||
|
|
||
| consumerGroup.removeMember(memberId); |
There was a problem hiding this comment.
I wonder if we should invoke replay with synthetic tombstones when we detect missing ones, instead of duplicating the tombstone logic from other methods. It could be more robust against future changes. In normal operation we will almost never take the missing tombstone path so we don't need to worry about performance that much.
| reasons.add(consumerGroup.targetAssignment().size() + " target assignments"); | ||
| } | ||
| if (consumerGroup.assignmentEpoch() != -1) { | ||
| reasons.add("assignment epoch " + consumerGroup.assignmentEpoch()); |
There was a problem hiding this comment.
| reasons.add("assignment epoch " + consumerGroup.assignmentEpoch()); | |
| reasons.add("target assignment epoch " + consumerGroup.assignmentEpoch()); |
The same for share and streams groups.
| // The sibling tombstones arriving afterwards are no-ops. | ||
| context.replay(GroupCoordinatorRecordHelpers.newConsumerGroupCurrentAssignmentTombstoneRecord("foo", "m1")); | ||
| context.replay(GroupCoordinatorRecordHelpers.newConsumerGroupTargetAssignmentTombstoneRecord("foo", "m1")); | ||
|
|
||
| assertFalse(context.groupMetadataManager.consumerGroup("foo").hasMember("m1")); | ||
| assertFalse(context.groupMetadataManager.consumerGroup("foo").targetAssignment().containsKey("m1")); |
There was a problem hiding this comment.
Aren't these already tested by the sibling tombstone test methods?
(may apply elsewhere too)
There was a problem hiding this comment.
you are right, it's true for consumer and streams, but shares group's existing tests do not test this. I'll add missing tests for shared group
| } | ||
|
|
||
| @Test | ||
| public void testReplayConsumerGroupEpochTombstoneExisting() { |
There was a problem hiding this comment.
The record type is called ConsumerGroupMetadata, so this method should start with testReplayConsumerGroupMetadataTombstone.
(may apply elsewhere too)
| } | ||
|
|
||
| @Test | ||
| public void testReplayConsumerGroupMemberMetadataTombstoneExisting() { |
There was a problem hiding this comment.
The existing testReplay methods are grouped by record type. Can we preserve the grouping?
| } | ||
|
|
||
| @Test | ||
| public void testReplayConsumerGroupMemberMetadataTombstoneExisting() { |
There was a problem hiding this comment.
There's already a testReplayConsumerGroupMemberMetadataTombstone method that tombstones an existing group. Can we choose a suffix that means "existing, but other tombstones are missing"?
There was a problem hiding this comment.
agree, changed suffix to WithMissingSiblingTombstones
When a member, group, or target-assignment-metadata tombstone is replayed while in-memory state shows that sibling tombstones written in the same deletion were never seen (a compaction artifact), replay those missing tombstones through their own replay methods instead of duplicating their effects inline. A member tombstone replays the member's current-assignment and target-assignment tombstones; a group tombstone replays the remaining member tombstones, leftover target-assignment tombstones and the target-assignment-metadata tombstone; a target-assignment-metadata tombstone replays the leftover target-assignment tombstones. This removes the hand-rolled target-assignment and topic-subscription cleanup and keeps the repair in sync with whatever each tombstone replay does. Each replay logs a single warning per received tombstone listing the inconsistencies that were repaired.
Name the tests that replay a tombstone while sibling tombstones are missing with a WithMissingSiblingTombstones suffix, since the Existing suffix already means "the group exists" in this file. Use the record type name (ConsumerGroupMetadata rather than Epoch). Place each test next to the other tests for its record type. Drop the trailing replays of sibling tombstones, which only exercised the no-op paths already covered by the dedicated tombstone tests.
…stone The target-assignment-metadata tombstone is only ever written as part of a group deletion, after the per-member target-assignment tombstones, so any per-member entries still present when it is replayed are an artifact and are now removed by replaying their tombstones.
Mirror the existing consumer group tests that replay a tombstone for a missing member or a missing group and assert it is ignored, which share groups lacked.
Soften tombstone replay exception to log for consumer/streams/share
group. Also add missing tests for consumer/share group.
Reviewers: rishi-rana rishi-rana@hotmail.com, Sean Quah squah@confluent.io