/* * 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.common.internals; import org.apache.kafka.common.TopicPartition; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; /** * This class is a useful building block for doing fetch requests where topic partitions have to be rotated via * round-robin to ensure fairness and some level of determinism given the existence of a limit on the fetch response * size. Because the serialization of fetch requests is more efficient if all partitions for the same topic are grouped * together, we do such grouping in the method `set`. * * As partitions are moved to the end, the same topic may be repeated more than once. In the optimal case, a single * topic would "wrap around" and appear twice. However, as partitions are fetched in different orders and partition * leadership changes, we will deviate from the optimal. If this turns out to be an issue in practice, we can improve * it by tracking the partitions per node or calling `set` every so often. */ public class PartitionStates<S> { private final LinkedHashMap<TopicPartition, S> map = new LinkedHashMap<>(); public PartitionStates() {} public void moveToEnd(TopicPartition topicPartition) { S state = map.remove(topicPartition); if (state != null) map.put(topicPartition, state); } public void updateAndMoveToEnd(TopicPartition topicPartition, S state) { map.remove(topicPartition); map.put(topicPartition, state); } public void remove(TopicPartition topicPartition) { map.remove(topicPartition); } /** * Returns the partitions in random order. */ public Set<TopicPartition> partitionSet() { return new HashSet<>(map.keySet()); } public void clear() { map.clear(); } public boolean contains(TopicPartition topicPartition) { return map.containsKey(topicPartition); } /** * Returns the partition states in order. */ public List<PartitionState<S>> partitionStates() { List<PartitionState<S>> result = new ArrayList<>(); for (Map.Entry<TopicPartition, S> entry : map.entrySet()) { result.add(new PartitionState<>(entry.getKey(), entry.getValue())); } return result; } /** * Returns the partition state values in order. */ public List<S> partitionStateValues() { return new ArrayList<>(map.values()); } public S stateValue(TopicPartition topicPartition) { return map.get(topicPartition); } public int size() { return map.size(); } /** * Update the builder to have the received map as its state (i.e. the previous state is cleared). The builder will * "batch by topic", so if we have a, b and c, each with two partitions, we may end up with something like the * following (the order of topics and partitions within topics is dependent on the iteration order of the received * map): a0, a1, b1, b0, c0, c1. */ public void set(Map<TopicPartition, S> partitionToState) { map.clear(); update(partitionToState); } private void update(Map<TopicPartition, S> partitionToState) { LinkedHashMap<String, List<TopicPartition>> topicToPartitions = new LinkedHashMap<>(); for (TopicPartition tp : partitionToState.keySet()) { List<TopicPartition> partitions = topicToPartitions.get(tp.topic()); if (partitions == null) { partitions = new ArrayList<>(); topicToPartitions.put(tp.topic(), partitions); } partitions.add(tp); } for (Map.Entry<String, List<TopicPartition>> entry : topicToPartitions.entrySet()) { for (TopicPartition tp : entry.getValue()) { S state = partitionToState.get(tp); map.put(tp, state); } } } public static class PartitionState<S> { private final TopicPartition topicPartition; private final S value; public PartitionState(TopicPartition topicPartition, S state) { this.topicPartition = Objects.requireNonNull(topicPartition); this.value = Objects.requireNonNull(state); } public S value() { return value; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PartitionState<?> that = (PartitionState<?>) o; return topicPartition.equals(that.topicPartition) && value.equals(that.value); } @Override public int hashCode() { int result = topicPartition.hashCode(); result = 31 * result + value.hashCode(); return result; } public TopicPartition topicPartition() { return topicPartition; } @Override public String toString() { return "PartitionState(" + topicPartition + "=" + value + ')'; } } }