|
@@ -1,5 +1,6 @@
|
|
package com.provectus.kafka.ui.service;
|
|
package com.provectus.kafka.ui.service;
|
|
|
|
|
|
|
|
+import com.google.common.collect.Table;
|
|
import com.provectus.kafka.ui.model.ConsumerGroupOrderingDTO;
|
|
import com.provectus.kafka.ui.model.ConsumerGroupOrderingDTO;
|
|
import com.provectus.kafka.ui.model.InternalConsumerGroup;
|
|
import com.provectus.kafka.ui.model.InternalConsumerGroup;
|
|
import com.provectus.kafka.ui.model.InternalTopicConsumerGroup;
|
|
import com.provectus.kafka.ui.model.InternalTopicConsumerGroup;
|
|
@@ -7,6 +8,7 @@ import com.provectus.kafka.ui.model.KafkaCluster;
|
|
import com.provectus.kafka.ui.model.SortOrderDTO;
|
|
import com.provectus.kafka.ui.model.SortOrderDTO;
|
|
import com.provectus.kafka.ui.service.rbac.AccessControlService;
|
|
import com.provectus.kafka.ui.service.rbac.AccessControlService;
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
|
|
+import java.util.Collection;
|
|
import java.util.Comparator;
|
|
import java.util.Comparator;
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.List;
|
|
@@ -14,22 +16,21 @@ import java.util.Map;
|
|
import java.util.Properties;
|
|
import java.util.Properties;
|
|
import java.util.function.ToIntFunction;
|
|
import java.util.function.ToIntFunction;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Collectors;
|
|
|
|
+import java.util.stream.Stream;
|
|
import javax.annotation.Nullable;
|
|
import javax.annotation.Nullable;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.RequiredArgsConstructor;
|
|
-import lombok.Value;
|
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
import org.apache.commons.lang3.StringUtils;
|
|
import org.apache.kafka.clients.admin.ConsumerGroupDescription;
|
|
import org.apache.kafka.clients.admin.ConsumerGroupDescription;
|
|
|
|
+import org.apache.kafka.clients.admin.ConsumerGroupListing;
|
|
import org.apache.kafka.clients.admin.OffsetSpec;
|
|
import org.apache.kafka.clients.admin.OffsetSpec;
|
|
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
|
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
|
import org.apache.kafka.clients.consumer.KafkaConsumer;
|
|
import org.apache.kafka.clients.consumer.KafkaConsumer;
|
|
|
|
+import org.apache.kafka.common.ConsumerGroupState;
|
|
import org.apache.kafka.common.TopicPartition;
|
|
import org.apache.kafka.common.TopicPartition;
|
|
import org.apache.kafka.common.serialization.BytesDeserializer;
|
|
import org.apache.kafka.common.serialization.BytesDeserializer;
|
|
import org.apache.kafka.common.utils.Bytes;
|
|
import org.apache.kafka.common.utils.Bytes;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
-import reactor.core.publisher.Flux;
|
|
|
|
import reactor.core.publisher.Mono;
|
|
import reactor.core.publisher.Mono;
|
|
-import reactor.util.function.Tuple2;
|
|
|
|
-import reactor.util.function.Tuples;
|
|
|
|
|
|
|
|
@Service
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
@RequiredArgsConstructor
|
|
@@ -41,21 +42,16 @@ public class ConsumerGroupService {
|
|
private Mono<List<InternalConsumerGroup>> getConsumerGroups(
|
|
private Mono<List<InternalConsumerGroup>> getConsumerGroups(
|
|
ReactiveAdminClient ac,
|
|
ReactiveAdminClient ac,
|
|
List<ConsumerGroupDescription> descriptions) {
|
|
List<ConsumerGroupDescription> descriptions) {
|
|
- return Flux.fromIterable(descriptions)
|
|
|
|
- // 1. getting committed offsets for all groups
|
|
|
|
- .flatMap(desc -> ac.listConsumerGroupOffsets(desc.groupId())
|
|
|
|
- .map(offsets -> Tuples.of(desc, offsets)))
|
|
|
|
- .collectMap(Tuple2::getT1, Tuple2::getT2)
|
|
|
|
- .flatMap((Map<ConsumerGroupDescription, Map<TopicPartition, Long>> groupOffsetsMap) -> {
|
|
|
|
- var tpsFromGroupOffsets = groupOffsetsMap.values().stream()
|
|
|
|
- .flatMap(v -> v.keySet().stream())
|
|
|
|
- .collect(Collectors.toSet());
|
|
|
|
|
|
+ var groupNames = descriptions.stream().map(ConsumerGroupDescription::groupId).toList();
|
|
|
|
+ // 1. getting committed offsets for all groups
|
|
|
|
+ return ac.listConsumerGroupOffsets(groupNames, null)
|
|
|
|
+ .flatMap((Table<String, TopicPartition, Long> committedOffsets) -> {
|
|
// 2. getting end offsets for partitions with committed offsets
|
|
// 2. getting end offsets for partitions with committed offsets
|
|
- return ac.listOffsets(tpsFromGroupOffsets, OffsetSpec.latest(), false)
|
|
|
|
|
|
+ return ac.listOffsets(committedOffsets.columnKeySet(), OffsetSpec.latest(), false)
|
|
.map(endOffsets ->
|
|
.map(endOffsets ->
|
|
descriptions.stream()
|
|
descriptions.stream()
|
|
.map(desc -> {
|
|
.map(desc -> {
|
|
- var groupOffsets = groupOffsetsMap.get(desc);
|
|
|
|
|
|
+ var groupOffsets = committedOffsets.row(desc.groupId());
|
|
var endOffsetsForGroup = new HashMap<>(endOffsets);
|
|
var endOffsetsForGroup = new HashMap<>(endOffsets);
|
|
endOffsetsForGroup.keySet().retainAll(groupOffsets.keySet());
|
|
endOffsetsForGroup.keySet().retainAll(groupOffsets.keySet());
|
|
// 3. gathering description & offsets
|
|
// 3. gathering description & offsets
|
|
@@ -73,105 +69,122 @@ public class ConsumerGroupService {
|
|
.flatMap(endOffsets -> {
|
|
.flatMap(endOffsets -> {
|
|
var tps = new ArrayList<>(endOffsets.keySet());
|
|
var tps = new ArrayList<>(endOffsets.keySet());
|
|
// 2. getting all consumer groups
|
|
// 2. getting all consumer groups
|
|
- return describeConsumerGroups(ac, null)
|
|
|
|
- .flatMap((List<ConsumerGroupDescription> groups) ->
|
|
|
|
- Flux.fromIterable(groups)
|
|
|
|
- // 3. for each group trying to find committed offsets for topic
|
|
|
|
- .flatMap(g ->
|
|
|
|
- ac.listConsumerGroupOffsets(g.groupId(), tps)
|
|
|
|
- // 4. keeping only groups that relates to topic
|
|
|
|
- .filter(offsets -> isConsumerGroupRelatesToTopic(topic, g, offsets))
|
|
|
|
- // 5. constructing results
|
|
|
|
- .map(offsets -> InternalTopicConsumerGroup.create(topic, g, offsets, endOffsets))
|
|
|
|
- ).collectList());
|
|
|
|
|
|
+ return describeConsumerGroups(ac)
|
|
|
|
+ .flatMap((List<ConsumerGroupDescription> groups) -> {
|
|
|
|
+ // 3. trying to find committed offsets for topic
|
|
|
|
+ var groupNames = groups.stream().map(ConsumerGroupDescription::groupId).toList();
|
|
|
|
+ return ac.listConsumerGroupOffsets(groupNames, tps).map(offsets ->
|
|
|
|
+ groups.stream()
|
|
|
|
+ // 4. keeping only groups that relates to topic
|
|
|
|
+ .filter(g -> isConsumerGroupRelatesToTopic(topic, g, offsets.containsRow(g.groupId())))
|
|
|
|
+ .map(g ->
|
|
|
|
+ // 5. constructing results
|
|
|
|
+ InternalTopicConsumerGroup.create(topic, g, offsets.row(g.groupId()), endOffsets))
|
|
|
|
+ .toList()
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+ );
|
|
}));
|
|
}));
|
|
}
|
|
}
|
|
|
|
|
|
private boolean isConsumerGroupRelatesToTopic(String topic,
|
|
private boolean isConsumerGroupRelatesToTopic(String topic,
|
|
ConsumerGroupDescription description,
|
|
ConsumerGroupDescription description,
|
|
- Map<TopicPartition, Long> committedGroupOffsetsForTopic) {
|
|
|
|
|
|
+ boolean hasCommittedOffsets) {
|
|
boolean hasActiveMembersForTopic = description.members()
|
|
boolean hasActiveMembersForTopic = description.members()
|
|
.stream()
|
|
.stream()
|
|
.anyMatch(m -> m.assignment().topicPartitions().stream().anyMatch(tp -> tp.topic().equals(topic)));
|
|
.anyMatch(m -> m.assignment().topicPartitions().stream().anyMatch(tp -> tp.topic().equals(topic)));
|
|
- boolean hasCommittedOffsets = !committedGroupOffsetsForTopic.isEmpty();
|
|
|
|
return hasActiveMembersForTopic || hasCommittedOffsets;
|
|
return hasActiveMembersForTopic || hasCommittedOffsets;
|
|
}
|
|
}
|
|
|
|
|
|
- @Value
|
|
|
|
- public static class ConsumerGroupsPage {
|
|
|
|
- List<InternalConsumerGroup> consumerGroups;
|
|
|
|
- int totalPages;
|
|
|
|
|
|
+ public record ConsumerGroupsPage(List<InternalConsumerGroup> consumerGroups, int totalPages) {
|
|
}
|
|
}
|
|
|
|
|
|
public Mono<ConsumerGroupsPage> getConsumerGroupsPage(
|
|
public Mono<ConsumerGroupsPage> getConsumerGroupsPage(
|
|
KafkaCluster cluster,
|
|
KafkaCluster cluster,
|
|
- int page,
|
|
|
|
|
|
+ int pageNum,
|
|
int perPage,
|
|
int perPage,
|
|
@Nullable String search,
|
|
@Nullable String search,
|
|
ConsumerGroupOrderingDTO orderBy,
|
|
ConsumerGroupOrderingDTO orderBy,
|
|
SortOrderDTO sortOrderDto) {
|
|
SortOrderDTO sortOrderDto) {
|
|
- var comparator = sortOrderDto.equals(SortOrderDTO.ASC)
|
|
|
|
- ? getPaginationComparator(orderBy)
|
|
|
|
- : getPaginationComparator(orderBy).reversed();
|
|
|
|
return adminClientService.get(cluster).flatMap(ac ->
|
|
return adminClientService.get(cluster).flatMap(ac ->
|
|
- describeConsumerGroups(ac, search).flatMap(descriptions ->
|
|
|
|
- getConsumerGroups(
|
|
|
|
- ac,
|
|
|
|
- descriptions.stream()
|
|
|
|
- .sorted(comparator)
|
|
|
|
- .skip((long) (page - 1) * perPage)
|
|
|
|
- .limit(perPage)
|
|
|
|
- .collect(Collectors.toList())
|
|
|
|
|
|
+ ac.listConsumerGroups()
|
|
|
|
+ .map(listing -> search == null
|
|
|
|
+ ? listing
|
|
|
|
+ : listing.stream()
|
|
|
|
+ .filter(g -> StringUtils.containsIgnoreCase(g.groupId(), search))
|
|
|
|
+ .toList()
|
|
)
|
|
)
|
|
- .flatMapMany(Flux::fromIterable)
|
|
|
|
- .filterWhen(
|
|
|
|
- cg -> accessControlService.isConsumerGroupAccessible(cg.getGroupId(), cluster.getName()))
|
|
|
|
- .collect(Collectors.toList())
|
|
|
|
- .map(cgs -> new ConsumerGroupsPage(
|
|
|
|
- cgs,
|
|
|
|
- (descriptions.size() / perPage) + (descriptions.size() % perPage == 0 ? 0 : 1))))
|
|
|
|
- );
|
|
|
|
|
|
+ .flatMapIterable(lst -> lst)
|
|
|
|
+ .filterWhen(cg -> accessControlService.isConsumerGroupAccessible(cg.groupId(), cluster.getName()))
|
|
|
|
+ .collectList()
|
|
|
|
+ .flatMap(allGroups ->
|
|
|
|
+ loadSortedDescriptions(ac, allGroups, pageNum, perPage, orderBy, sortOrderDto)
|
|
|
|
+ .flatMap(descriptions -> getConsumerGroups(ac, descriptions)
|
|
|
|
+ .map(page -> new ConsumerGroupsPage(
|
|
|
|
+ page,
|
|
|
|
+ (allGroups.size() / perPage) + (allGroups.size() % perPage == 0 ? 0 : 1))))));
|
|
}
|
|
}
|
|
|
|
|
|
- private Comparator<ConsumerGroupDescription> getPaginationComparator(ConsumerGroupOrderingDTO
|
|
|
|
- orderBy) {
|
|
|
|
- switch (orderBy) {
|
|
|
|
- case NAME:
|
|
|
|
- return Comparator.comparing(ConsumerGroupDescription::groupId);
|
|
|
|
- case STATE:
|
|
|
|
- ToIntFunction<ConsumerGroupDescription> statesPriorities = cg -> {
|
|
|
|
- switch (cg.state()) {
|
|
|
|
- case STABLE:
|
|
|
|
- return 0;
|
|
|
|
- case COMPLETING_REBALANCE:
|
|
|
|
- return 1;
|
|
|
|
- case PREPARING_REBALANCE:
|
|
|
|
- return 2;
|
|
|
|
- case EMPTY:
|
|
|
|
- return 3;
|
|
|
|
- case DEAD:
|
|
|
|
- return 4;
|
|
|
|
- case UNKNOWN:
|
|
|
|
- return 5;
|
|
|
|
- default:
|
|
|
|
- return 100;
|
|
|
|
- }
|
|
|
|
- };
|
|
|
|
- return Comparator.comparingInt(statesPriorities);
|
|
|
|
- case MEMBERS:
|
|
|
|
- return Comparator.comparingInt(cg -> cg.members().size());
|
|
|
|
- default:
|
|
|
|
- throw new IllegalStateException("Unsupported order by: " + orderBy);
|
|
|
|
- }
|
|
|
|
|
|
+ private Mono<List<ConsumerGroupDescription>> loadSortedDescriptions(ReactiveAdminClient ac,
|
|
|
|
+ List<ConsumerGroupListing> groups,
|
|
|
|
+ int pageNum,
|
|
|
|
+ int perPage,
|
|
|
|
+ ConsumerGroupOrderingDTO orderBy,
|
|
|
|
+ SortOrderDTO sortOrderDto) {
|
|
|
|
+ return switch (orderBy) {
|
|
|
|
+ case NAME -> {
|
|
|
|
+ Comparator<ConsumerGroupListing> comparator = Comparator.comparing(ConsumerGroupListing::groupId);
|
|
|
|
+ yield loadDescriptionsByListings(ac, groups, comparator, pageNum, perPage, sortOrderDto);
|
|
|
|
+ }
|
|
|
|
+ case STATE -> {
|
|
|
|
+ ToIntFunction<ConsumerGroupListing> statesPriorities =
|
|
|
|
+ cg -> switch (cg.state().orElse(ConsumerGroupState.UNKNOWN)) {
|
|
|
|
+ case STABLE -> 0;
|
|
|
|
+ case COMPLETING_REBALANCE -> 1;
|
|
|
|
+ case PREPARING_REBALANCE -> 2;
|
|
|
|
+ case EMPTY -> 3;
|
|
|
|
+ case DEAD -> 4;
|
|
|
|
+ case UNKNOWN -> 5;
|
|
|
|
+ };
|
|
|
|
+ var comparator = Comparator.comparingInt(statesPriorities);
|
|
|
|
+ yield loadDescriptionsByListings(ac, groups, comparator, pageNum, perPage, sortOrderDto);
|
|
|
|
+ }
|
|
|
|
+ case MEMBERS -> {
|
|
|
|
+ var comparator = Comparator.<ConsumerGroupDescription>comparingInt(cg -> cg.members().size());
|
|
|
|
+ var groupNames = groups.stream().map(ConsumerGroupListing::groupId).toList();
|
|
|
|
+ yield ac.describeConsumerGroups(groupNames)
|
|
|
|
+ .map(descriptions ->
|
|
|
|
+ sortAndPaginate(descriptions.values(), comparator, pageNum, perPage, sortOrderDto).toList());
|
|
|
|
+ }
|
|
|
|
+ };
|
|
}
|
|
}
|
|
|
|
|
|
- private Mono<List<ConsumerGroupDescription>> describeConsumerGroups(ReactiveAdminClient ac,
|
|
|
|
- @Nullable String search) {
|
|
|
|
- return ac.listConsumerGroups()
|
|
|
|
- .map(groupIds -> groupIds
|
|
|
|
- .stream()
|
|
|
|
- .filter(groupId -> search == null || StringUtils.containsIgnoreCase(groupId, search))
|
|
|
|
- .collect(Collectors.toList()))
|
|
|
|
|
|
+ private Mono<List<ConsumerGroupDescription>> loadDescriptionsByListings(ReactiveAdminClient ac,
|
|
|
|
+ List<ConsumerGroupListing> listings,
|
|
|
|
+ Comparator<ConsumerGroupListing> comparator,
|
|
|
|
+ int pageNum,
|
|
|
|
+ int perPage,
|
|
|
|
+ SortOrderDTO sortOrderDto) {
|
|
|
|
+ List<String> sortedGroups = sortAndPaginate(listings, comparator, pageNum, perPage, sortOrderDto)
|
|
|
|
+ .map(ConsumerGroupListing::groupId)
|
|
|
|
+ .toList();
|
|
|
|
+ return ac.describeConsumerGroups(sortedGroups)
|
|
|
|
+ .map(descrMap -> sortedGroups.stream().map(descrMap::get).toList());
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private <T> Stream<T> sortAndPaginate(Collection<T> collection,
|
|
|
|
+ Comparator<T> comparator,
|
|
|
|
+ int pageNum,
|
|
|
|
+ int perPage,
|
|
|
|
+ SortOrderDTO sortOrderDto) {
|
|
|
|
+ return collection.stream()
|
|
|
|
+ .sorted(sortOrderDto == SortOrderDTO.ASC ? comparator : comparator.reversed())
|
|
|
|
+ .skip((long) (pageNum - 1) * perPage)
|
|
|
|
+ .limit(perPage);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private Mono<List<ConsumerGroupDescription>> describeConsumerGroups(ReactiveAdminClient ac) {
|
|
|
|
+ return ac.listConsumerGroupNames()
|
|
.flatMap(ac::describeConsumerGroups)
|
|
.flatMap(ac::describeConsumerGroups)
|
|
.map(cgs -> new ArrayList<>(cgs.values()));
|
|
.map(cgs -> new ArrayList<>(cgs.values()));
|
|
}
|
|
}
|