mirror of https://github.com/apache/kafka.git
KAFKA-18453: Add StreamsTopology class to group coordinator (#18446)
Adds a class that represent the topology of a Streams group sent by a Streams client in the Streams group heartbeat during initialization to the group coordinator. This topology representation is used together with the partition metadata on the broker to create a configured topology. Reviewer: Lucas Brutschy <lbrutschy@confluent.io>
This commit is contained in:
parent
8b72204bfd
commit
11459ae7e9
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.kafka.coordinator.group.streams;
|
||||
|
||||
import org.apache.kafka.coordinator.group.generated.StreamsGroupTopologyValue;
|
||||
import org.apache.kafka.coordinator.group.generated.StreamsGroupTopologyValue.Subtopology;
|
||||
import org.apache.kafka.coordinator.group.generated.StreamsGroupTopologyValue.TopicInfo;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Contains the topology sent by a Streams client in the Streams heartbeat during initialization.
|
||||
* <p>
|
||||
* This topology is used together with the partition metadata on the broker to create a
|
||||
* {@link org.apache.kafka.coordinator.group.streams.topics.ConfiguredTopology configured topology}.
|
||||
* This class allows to look-up subtopologies by subtopology ID in constant time by getting the subtopologies map.
|
||||
* The information in this class is fully backed by records stored in the __consumer_offsets topic.
|
||||
*
|
||||
* @param topologyEpoch The epoch of the topology (must be non-negative).
|
||||
* @param subtopologies The subtopologies of the topology containing information about source topics,
|
||||
* repartition topics, changelog topics, co-partition groups etc. (must be non-null)
|
||||
*/
|
||||
public record StreamsTopology(int topologyEpoch,
|
||||
Map<String, Subtopology> subtopologies) {
|
||||
|
||||
public StreamsTopology {
|
||||
if (topologyEpoch < 0) {
|
||||
throw new IllegalArgumentException("Topology epoch must be non-negative.");
|
||||
}
|
||||
subtopologies = Collections.unmodifiableMap(Objects.requireNonNull(subtopologies, "Subtopologies cannot be null."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of topics that are required by the topology.
|
||||
* <p>
|
||||
* The required topics are used to determine the partition metadata on the brokers needed to configure the topology.
|
||||
*
|
||||
* @return set of topics required by the topology
|
||||
*/
|
||||
public Set<String> requiredTopics() {
|
||||
return subtopologies.values().stream()
|
||||
.flatMap(x ->
|
||||
Stream.concat(
|
||||
Stream.concat(
|
||||
x.sourceTopics().stream(),
|
||||
x.repartitionSourceTopics().stream().map(TopicInfo::name)
|
||||
),
|
||||
x.stateChangelogTopics().stream().map(TopicInfo::name)
|
||||
)
|
||||
).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of StreamsTopology from a StreamsGroupTopologyValue record.
|
||||
*
|
||||
* @param record The StreamsGroupTopologyValue record.
|
||||
* @return The instance of StreamsTopology created from the record.
|
||||
*/
|
||||
public static StreamsTopology fromRecord(StreamsGroupTopologyValue record) {
|
||||
return new StreamsTopology(
|
||||
record.epoch(),
|
||||
record.subtopologies().stream().collect(Collectors.toMap(Subtopology::subtopologyId, x -> x))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.kafka.coordinator.group.streams;
|
||||
|
||||
import org.apache.kafka.coordinator.group.generated.StreamsGroupTopologyValue;
|
||||
import org.apache.kafka.coordinator.group.generated.StreamsGroupTopologyValue.Subtopology;
|
||||
import org.apache.kafka.coordinator.group.generated.StreamsGroupTopologyValue.TopicInfo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.apache.kafka.common.utils.Utils.mkEntry;
|
||||
import static org.apache.kafka.common.utils.Utils.mkMap;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class StreamsTopologyTest {
|
||||
|
||||
private static final String SUBTOPOLOGY_ID_1 = "subtopology-1";
|
||||
private static final String SUBTOPOLOGY_ID_2 = "subtopology-2";
|
||||
private static final String SOURCE_TOPIC_1 = "source-topic-1";
|
||||
private static final String SOURCE_TOPIC_2 = "source-topic-2";
|
||||
private static final String SOURCE_TOPIC_3 = "source-topic-3";
|
||||
private static final String REPARTITION_TOPIC_1 = "repartition-topic-1";
|
||||
private static final String REPARTITION_TOPIC_2 = "repartition-topic-2";
|
||||
private static final String REPARTITION_TOPIC_3 = "repartition-topic-3";
|
||||
private static final String CHANGELOG_TOPIC_1 = "changelog-1";
|
||||
private static final String CHANGELOG_TOPIC_2 = "changelog-2";
|
||||
private static final String CHANGELOG_TOPIC_3 = "changelog-3";
|
||||
|
||||
@Test
|
||||
public void subtopologiesMapShouldNotBeNull() {
|
||||
final Exception exception = assertThrows(NullPointerException.class, () -> new StreamsTopology(1, null));
|
||||
assertEquals("Subtopologies cannot be null.", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void topologyEpochShouldNotBeNegative() {
|
||||
Map<String, Subtopology> subtopologies = mkMap(
|
||||
mkEntry(SUBTOPOLOGY_ID_1, mkSubtopology1())
|
||||
);
|
||||
final Exception exception = assertThrows(IllegalArgumentException.class, () -> new StreamsTopology(-1, subtopologies));
|
||||
assertEquals("Topology epoch must be non-negative.", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subtopologiesMapShouldBeImmutable() {
|
||||
Map<String, Subtopology> subtopologies = mkMap(
|
||||
mkEntry(SUBTOPOLOGY_ID_1, mkSubtopology1())
|
||||
);
|
||||
assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> new StreamsTopology(1, subtopologies).subtopologies().put("subtopology-2", mkSubtopology2())
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requiredTopicsShouldBeCorrect() {
|
||||
Map<String, Subtopology> subtopologies = mkMap(
|
||||
mkEntry(SUBTOPOLOGY_ID_1, mkSubtopology1()),
|
||||
mkEntry(SUBTOPOLOGY_ID_2, mkSubtopology2())
|
||||
);
|
||||
StreamsTopology topology = new StreamsTopology(1, subtopologies);
|
||||
Set<String> expectedTopics = Set.of(
|
||||
SOURCE_TOPIC_1, SOURCE_TOPIC_2, SOURCE_TOPIC_3,
|
||||
REPARTITION_TOPIC_1, REPARTITION_TOPIC_2, REPARTITION_TOPIC_3,
|
||||
CHANGELOG_TOPIC_1, CHANGELOG_TOPIC_2, CHANGELOG_TOPIC_3
|
||||
);
|
||||
|
||||
assertEquals(expectedTopics, topology.requiredTopics());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromRecordShouldCreateCorrectTopology() {
|
||||
StreamsGroupTopologyValue record = new StreamsGroupTopologyValue()
|
||||
.setEpoch(1)
|
||||
.setSubtopologies(Arrays.asList(mkSubtopology1(), mkSubtopology2()));
|
||||
StreamsTopology topology = StreamsTopology.fromRecord(record);
|
||||
assertEquals(1, topology.topologyEpoch());
|
||||
assertEquals(2, topology.subtopologies().size());
|
||||
assertTrue(topology.subtopologies().containsKey(SUBTOPOLOGY_ID_1));
|
||||
assertEquals(mkSubtopology1(), topology.subtopologies().get(SUBTOPOLOGY_ID_1));
|
||||
assertTrue(topology.subtopologies().containsKey(SUBTOPOLOGY_ID_2));
|
||||
assertEquals(mkSubtopology2(), topology.subtopologies().get(SUBTOPOLOGY_ID_2));
|
||||
}
|
||||
|
||||
private Subtopology mkSubtopology1() {
|
||||
return new Subtopology()
|
||||
.setSubtopologyId(SUBTOPOLOGY_ID_1)
|
||||
.setSourceTopics(List.of(
|
||||
SOURCE_TOPIC_1,
|
||||
SOURCE_TOPIC_2,
|
||||
REPARTITION_TOPIC_1,
|
||||
REPARTITION_TOPIC_2
|
||||
))
|
||||
.setRepartitionSourceTopics(List.of(
|
||||
new TopicInfo().setName(REPARTITION_TOPIC_1),
|
||||
new TopicInfo().setName(REPARTITION_TOPIC_2)
|
||||
))
|
||||
.setRepartitionSinkTopics(List.of(
|
||||
REPARTITION_TOPIC_3
|
||||
))
|
||||
.setStateChangelogTopics(List.of(
|
||||
new TopicInfo().setName(CHANGELOG_TOPIC_1),
|
||||
new TopicInfo().setName(CHANGELOG_TOPIC_2)
|
||||
))
|
||||
.setCopartitionGroups(List.of(
|
||||
new StreamsGroupTopologyValue.CopartitionGroup()
|
||||
.setRepartitionSourceTopics(List.of((short) 0))
|
||||
.setSourceTopics(List.of((short) 0)),
|
||||
new StreamsGroupTopologyValue.CopartitionGroup()
|
||||
.setRepartitionSourceTopics(List.of((short) 1))
|
||||
.setSourceTopics(List.of((short) 1))
|
||||
));
|
||||
}
|
||||
|
||||
private Subtopology mkSubtopology2() {
|
||||
return new Subtopology()
|
||||
.setSubtopologyId(SUBTOPOLOGY_ID_2)
|
||||
.setSourceTopics(List.of(
|
||||
SOURCE_TOPIC_3,
|
||||
REPARTITION_TOPIC_3
|
||||
))
|
||||
.setRepartitionSourceTopics(List.of(
|
||||
new TopicInfo().setName(REPARTITION_TOPIC_3)
|
||||
))
|
||||
.setStateChangelogTopics(List.of(
|
||||
new TopicInfo().setName(CHANGELOG_TOPIC_3)
|
||||
));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue