Status

Current state: "Under Discussion"

Discussion thread: here [Change the link from the KIP proposal email archive to your own email thread]

JIRA: KAFKA-18239

Please keep the discussion on the mailing list rather than commenting on the wiki (wiki discussions get unwieldy fast).

Motivation

In ClientQuotaCallback#updateClusterMetadata, we pass a Cluster object to this method. The Cluster is an immutable object that holds a lot of information, and a new Cluster is created every time there is a change in metadata. For large clusters with many partitions, this can result in significant memory pressure on Kafka.

Additionally, some information in the Cluster object is unnecessary or confusing. For instance, Cluster#controller refers to a random broker node in a KRaft cluster, which is retained for backward compatibility as it represents the ZK controller. Furthermore, in ZK mode, we parse the listener using the listener name from the request. However, in KRaft mode, there is no listener name in the updateClusterMetadata path. As a result, the callback may receive multiple partition info entries for each listener name. (See KAFKA-19122).

To address these issues, this KIP propose deprecating the current method ClientQuotaCallback#updateClusterMetadata(Cluster) and reimplementing it in a more efficient way, providing clearer and more relevant cluster information.

Public Interfaces

This KIP introduces a new interface, ClientQuotaCallbackHandler, which adds a new method updateClusterMetadata(ClusterMetadata). The parameter ClusterMetadata is a new interface, which will be described below. This new interface will include all existing methods from ClientQuotaCallback, except for the deprecated updateClusterMetadata(Cluster) method.

Normally, to deprecate a method in an interface, we can simply add a new method to the existing interface. However, the proposal here aims to eliminate the creation of Cluster objects, so we will use an alternative approach: introduce a new interface. This will allow us to use instanceof to determine whether we should use the old or the more efficient method to trigger updateClusterMetadata.

public interface ClientQuotaCallbackHandler extends Configurable {
    // All methods except updateClusterMetadata(Cluster) in ClientQuotaCallback are now placed here.

    /**
     * This callback is invoked whenever there are changes in the cluster metadata, such as
     * brokers being added or removed, topics being created or deleted, or partition leadership updates.
     * This is useful if quota computation takes partitions into account.
     * Topics that are being deleted will not be included in `cluster`.
     *
     * @param clusterMetadata Cluster metadata including partitions and their leaders if known
     * @return true if quotas have changed and metric configs may need to be updated
     */
    default boolean updateClusterMetadata(ClusterMetadata clusterMetadata) {
        throw new UnsupportedOperationException();
    }
}

The ClientQuotaCallback interface now extends the new interface ClientQuotaCallbackHandler. It is deprecated and expected to be removed in future major releases.

/**
 * Deprecated, please use ClientQuotaCallbackHandler instead
 */
@Deprecated
public interface ClientQuotaCallback extends ClientQuotaCallbackHandler {

    /**
     * This callback is invoked whenever there are changes in the cluster metadata, such as 
     * brokers being added or removed, topics being created or deleted, or partition leadership updates.
     * This is useful if quota computation takes partitions into account.
     * Topics that are being deleted will not be included in `cluster`.
     *
     * @deprecated please use {@link ClientQuotaCallback#updateClusterMetadata(ClusterMetadata)} instead
     * @param cluster Cluster metadata including partitions and their leaders, if known
     * @return true if quotas have changed and metric configs may need to be updated
     */
    @Deprecated
    boolean updateClusterMetadata(Cluster cluster);
}

The ClusterMetadata used in ClientQuotaCallbackHandler#updateClusterMetadata is also a new interface. The goal of ClusterMetadata is:


public interface ClusterMetadata {

    /**
     * Get a map of brokers.
     * The key is the broker node ID, and the value is the metadata
     * associated with that broker.
     */
    Map<Integer, BrokerMetadata> brokers();

    /**
     * Get specific broker information.
     */
    Optional<BrokerMetadata> broker(int nodeId);

    /**
     * Get partition information for a specific topic.
     * The key is the partition ID, and the value is the metadata
     * associated with that partition.
     */
    Map<Integer, PartitionMetadata> partitionsForTopic(String topic);

    /**
     * Get partition information for a specific node.
     * The key is the TopicPartition, and the value is the metadata
     * associated with that partition.
     */
    Map<TopicPartition, PartitionMetadata> partitionsForNode(int nodeId);

    /**
     * Get specific partition information.
     */
    Optional<PartitionMetadata> partition(TopicPartition topicPartition);

    /**
     * Get a map of topics.
     * The key is the topic ID, and the value is the topic name.
     */
    Map<Uuid, String> topics();

    /**
     * Get ClusterResource, which includes the cluster ID.
     */
    ClusterResource clusterResource();

    /**
     * Given partition metadata, return the subset of the replicas that are offline.
     * If there is no offline replica in the partition, return an empty array.
     */
    int[] offlineReplicas(PartitionMetadata partitionMetadata);
}

Unlike the Cluster#brokers using the org.apache.kafka.common.Node to present broker information.  ClusterMetadata use the new BrokerMetadata interface. The reason behind it is  ClientQuotaCallback#updateClusterMetadata does not filter by listener name like the old ZK-based path. Thus, using the existing Node is not appropriate. Additionally, DynamicTopicClusterQuotaPublisher can access BrokerRegistration, and we can add a new interface that already includes the necessary information in BrokerRegistration, making the ClusterMetadata implementation much more efficient.

public interface BrokerMetadata {
    /**
     * The broker node ID
     */
    int id();

    /**
     * The listeners of the broker node
     */
    Map<String, Endpoint> listeners();
}

PartitionMetadata is similar to BrokerMetadata. We can leverage the existing PartitionRegistration object to create a new interface that provides partition metadata efficiently, avoiding the need to convert PartitionRegistration into PartitionInfo.

The main difference is isAvailable(), as PartitionRegistration does not include this information. However, since Cluster#availablePartitionsForTopic allows users to query available partitions, providing a default method here for convenience.

public interface PartitionMetadata {
    /**
     * The node ID of the node currently acting as the leader for this partition or -1 if there is no leader.
     */
    int leader();

    /**
     * The complete set of replicas for this partition, regardless of whether they are alive or up-to-date.
     */
    int[] replicas();

    /**
     * The subset of replicas that are in sync, meaning they are up-to-date and ready to take over as the leader if
     * the current leader fails.
     */
    int[] inSyncReplicas();

    /**
     * Check if the partition is available.
     */
    default boolean isAvailable() {
        return leader() != Node.noNode().id();
    }
}

Proposed Changes

In DynamicTopicClusterQuotaPublisher#onMetadataUpdate,  determine which interface the user has implemented and decide which updateClusterMetadata method to call.

clientQuotaCallback match {
  case _: ClientQuotaCallback =>
    val cluster = MetadataCache.toCluster(clusterId, newImage)
    if (clientQuotaCallback.updateClusterMetadata(cluster)) {
      ...
    }
  case _: ClientQuotaCallbackHandler =>
    val clusterMetadata = new DefaultClusterMetadata(clusterId, newImage)
    if (clientQuotaCallback.updateClusterMetadata(clusterMetadata)) {
      ...
    }
  case _ =>
    throw new IllegalArgumentException("Unsupported client quota callback")
}

Make PartitionRegistration implement PartitionMetadata and BrokerRegistration implement BrokerMetadata. In DefaultClusterMetadata (the implementation of ClusterMetadata), we can use BrokerRegistration and PartitionRegistration to return the necessary information without duplicating data. This avoids the overhead of creating new objects.

For example, the implementation of ClusterMetadata#partitionsForTopic(String) simply wraps the map returned by TopicImage.partitions().

public Map<Integer, PartitionMetadata> partitionsForTopic(String topic) {
    return image.topics().getTopic(topic).partitions().entrySet().stream()
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

The implementation for ClusterMetadata#partition(TopicPartition topicPartition) is similarly efficient.

public PartitionMetadata partition(TopicPartition topicPartition) {
    return image.topics().getTopic(topicPartition.topic()).partitions().get(topicPartition.partition());
}

Other methods in ClusterMetadata will follow a similar pattern.

Compatibility, Deprecation, and Migration Plan

This KIP should be backward-compatible, as the deprecated interface ClientQuotaCallback is still supported.

However, users are expected to implement the new interface ClientQuotaCallbackHandler. The ClientQuotaCallback interface will be removed in a future major release.

Test Plan

We will rely on the existing CustomQuotaCallbackTest.java to ensure the deprecated interface still works.

Additionally, we will add a new test to confirm that the new interface functions as expected.

Rejected Alternatives

Add new replacement method to the existing interface - ClientQuotaCallback.java

The proposal here aims to eliminate the creation of Cluster objects, so the following pseudo code below cannot meet the requirement.

We will use an alternative approach: introduce a new interface thus we can decide which interface user is used and then trigger the corresponding updateClusterMetadata.

public interface ClientQuotaCallback extends Configurable {
    ...
     
    @Deprecated
    boolean updateClusterMetadata(Cluster cluster);

    default boolean updateClusterMetadata(ClusterMetadata clusterMetadata) {
        Cluster cluster = toCluster(clusterMetadata)
        return updateClusterMetadata(Cluster cluster);
    }
}