Skip to content

KAFKA-20846:Make the streams assignor GroupSpec deeply unmodifiable - #22970

Open
gabriellefu wants to merge 4 commits into
apache:trunkfrom
gabriellefu:kip1357_followup
Open

KAFKA-20846:Make the streams assignor GroupSpec deeply unmodifiable#22970
gabriellefu wants to merge 4 commits into
apache:trunkfrom
gabriellefu:kip1357_followup

Conversation

@gabriellefu

@gabriellefu gabriellefu commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Changes

  • MemberMetadataAndStateImpl now wraps the nested collections of
    activeTasks, standbyTasks, warmupTasks, taskOffsets, and
    taskEndOffsets.
  • GroupSpecImpl now wraps both members and configs, and the
    now-redundant wrapping in TargetAssignmentBuilder is removed so
    immutability is enforced by the type rather than at each call site.
  • MockAssignor now deep-copies the active task sets it grows. It
    previously relied on input sets being mutable (new HashMap<>(...)
    followed by computeIfAbsent(...).add(...)), which now throws.
  • The read-only contract is now documented on GroupSpec#configs,
    MemberAssignmentMetadata#clientTags, and MemberAssignmentState.

Testing

TargetAssignmentBuilderTest#testCreateMemberMetadataAndStateReturnsUnmodifiableCollections
builds a member and its reported offsets from mutable collections,
asserts that every map and nested collection exposed to the assignor
rejects mutation, and verifies that the member’s own tasks remain
unchanged.

  • GroupSpecImplTest#testMembersAndConfigsAreUnmodifiable covers both
    members() and configs().
  • Both new tests fail against the previous implementation and pass with
    this change.
  • MockAssignorTest and StickyTaskAssignorTest pass unchanged,
    covering the two in-tree assignors against the tightened collections.

Reviewers: Matthias J. Sax matthias@confluent.io

@github-actions github-actions Bot added triage PRs from the community group-coordinator small Small PRs labels Jul 27, 2026
@github-actions github-actions Bot removed the small Small PRs label Jul 27, 2026
@mjsax mjsax added streams kip Requires or implements a KIP ci-approved KIP-1071 PRs related to KIP-1071 and removed triage PRs from the community labels Jul 28, 2026
void testTaskSetsAreDeeplyUnmodifiable() {
MemberMetadataAndStateImpl member = memberWith(
Map.of("subtopology-1", new HashSet<>(Set.of(0, 1))),
Map.of()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Map.of gives us already an unmodifiable Map, so the test would pass even w/o the fix. We need to ensure that the outer Map is writable here, to verify that we indeed wrap it with an unmodifiable view.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, changed

void testTaskOffsetsAreDeeplyUnmodifiable() {
MemberMetadataAndStateImpl member = memberWith(
Map.of(),
Map.of("subtopology-1", new HashMap<>(Map.of(0, 100L)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above


assertThrows(UnsupportedOperationException.class, () -> member.taskOffsets().put("subtopology-2", Map.of()));
assertThrows(UnsupportedOperationException.class, () -> member.taskOffsets().get("subtopology-1").put(0, 200L));
assertThrows(UnsupportedOperationException.class, () -> member.taskEndOffsets().get("subtopology-1").put(1, 200L));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added another put test

@gabriellefu gabriellefu left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated base on @mjsax 's review


assertThrows(UnsupportedOperationException.class, () -> member.taskOffsets().put("subtopology-2", Map.of()));
assertThrows(UnsupportedOperationException.class, () -> member.taskOffsets().get("subtopology-1").put(0, 200L));
assertThrows(UnsupportedOperationException.class, () -> member.taskEndOffsets().get("subtopology-1").put(1, 200L));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added another put test

@mjsax mjsax left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make deep copies instead of just views for the inner collections to keep it symmetric -- we also deep-copy the outer collections.


private static Map<String, Set<Integer>> unmodifiableTasks(Map<String, Set<Integer>> tasks) {
return Objects.requireNonNull(tasks).entrySet().stream()
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, entry -> Collections.unmodifiableSet(entry.getValue())));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, entry -> Collections.unmodifiableSet(entry.getValue())));
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, entry -> Set.copyOf(entry.getValue())));


private static Map<String, Map<Integer, Long>> unmodifiableOffsets(Map<String, Map<Integer, Long>> offsets) {
return Objects.requireNonNull(offsets).entrySet().stream()
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, entry -> Collections.unmodifiableMap(entry.getValue())));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, entry -> Collections.unmodifiableMap(entry.getValue())));
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, entry -> Map.copyOf(entry.getValue())));

}

@Test
public void testCreateMemberMetadataAndStateReturnsUnmodifiableCollections() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can actually remove this test? It seems to replicate what we already get from MemberMetadataAndStateImplTest so no need to duplicate test coverage. We reach the same MemberMetadataAndStateImpl code just from our more layer around it -- doesn't really add anything.

We should just move the client-tag test coverage that we only have here, to the other test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-approved group-coordinator kip Requires or implements a KIP KIP-1071 PRs related to KIP-1071 streams

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants