From 2fe8453334b20166ee62c1601b97350a601c333d Mon Sep 17 00:00:00 2001 From: Ming-Yen Chung Date: Sat, 18 Apr 2026 19:00:08 +0800 Subject: [PATCH 1/2] Update ConfigEntity.name() Javadoc to document null for default quotas Align the Javadoc with the intended semantics where default quotas are represented by null, matching how the Admin API uses null to denote default client quota entities. --- .../java/org/apache/kafka/server/quota/ClientQuotaEntity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/clients/src/main/java/org/apache/kafka/server/quota/ClientQuotaEntity.java b/clients/src/main/java/org/apache/kafka/server/quota/ClientQuotaEntity.java index 0f0eb6253c828..626ddcce605c1 100644 --- a/clients/src/main/java/org/apache/kafka/server/quota/ClientQuotaEntity.java +++ b/clients/src/main/java/org/apache/kafka/server/quota/ClientQuotaEntity.java @@ -43,7 +43,8 @@ enum ConfigEntityType { */ interface ConfigEntity { /** - * Returns the name of this entity. For default quotas, an empty string is returned. + * Returns the name of this entity. For default quotas, {@code null} is returned, + * consistent with how the Admin API represents default client quota entities. */ String name(); From 150eb3013160301210107634c5705d1a4c39efb6 Mon Sep 17 00:00:00 2001 From: Ming-Yen Chung Date: Sat, 18 Apr 2026 19:23:25 +0800 Subject: [PATCH 2/2] Remove placeholder from quota callback SPI and internal helpers Change DEFAULT_USER_ENTITY and DEFAULT_USER_CLIENT_ID to return null from ConfigEntity.name(), matching the Admin API representation of default client quota entities. Drop the internal DEFAULT_NAME constant and simplify KafkaQuotaEntity.sanitizedUser() and clientId() so that neither method returns the legacy ZooKeeper-era "" string. The internal metric-tag lookup path in updateQuotaMetricConfigs is unaffected: default entities are routed through the iterate-all-metrics branch (updatedQuotaEntity=Optional.empty()), so sanitizedUser() and clientId() are never invoked on a default-entity KafkaQuotaEntity in practice. This is a behavior change in the ClientQuotaCallback SPI. Third party implementations that compare ConfigEntity.name() against the literal string "" must be updated to check for null instead. The prior behavior contradicted the Javadoc, which documented an empty-string return value, so such comparisons were relying on undocumented behavior. Add tests asserting that ConfigEntity.name() returns null for all default-entity paths produced by transferToClientQuotaEntity, and that explicit-name entities continue to return their configured name. --- .../ClientQuotaMetadataManagerTest.scala | 29 +++++++++++++++++-- .../server/quota/ClientQuotaManager.java | 12 ++++---- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/core/src/test/scala/kafka/server/metadata/ClientQuotaMetadataManagerTest.scala b/core/src/test/scala/kafka/server/metadata/ClientQuotaMetadataManagerTest.scala index 0b197d467213a..7db3c3a81d3da 100644 --- a/core/src/test/scala/kafka/server/metadata/ClientQuotaMetadataManagerTest.scala +++ b/core/src/test/scala/kafka/server/metadata/ClientQuotaMetadataManagerTest.scala @@ -18,7 +18,7 @@ package kafka.server.metadata import org.apache.kafka.image.ClientQuotaDelta import org.apache.kafka.server.quota.ClientQuotaManager -import org.junit.jupiter.api.Assertions.{assertDoesNotThrow, assertEquals, assertThrows} +import org.junit.jupiter.api.Assertions.{assertDoesNotThrow, assertEquals, assertNull, assertThrows} import org.junit.jupiter.api.Test import org.junit.jupiter.api.function.Executable import java.util.Optional @@ -73,4 +73,29 @@ class ClientQuotaMetadataManagerTest { ClientQuotaMetadataManager.transferToClientQuotaEntity(DefaultUserDefaultClientIdEntity) ) } -} \ No newline at end of file + + @Test + def testDefaultConfigEntityNameReturnsNull(): Unit = { + assertNull(ClientQuotaManager.DEFAULT_USER_ENTITY.name()) + assertNull(ClientQuotaManager.DEFAULT_USER_CLIENT_ID.name()) + + val (defaultUser, _) = ClientQuotaMetadataManager.transferToClientQuotaEntity(DefaultUserEntity) + assertNull(defaultUser.get().name()) + + val (_, defaultClientId) = ClientQuotaMetadataManager.transferToClientQuotaEntity(DefaultClientIdEntity) + assertNull(defaultClientId.get().name()) + + val (defaultUser2, defaultClientId2) = ClientQuotaMetadataManager.transferToClientQuotaEntity(DefaultUserDefaultClientIdEntity) + assertNull(defaultUser2.get().name()) + assertNull(defaultClientId2.get().name()) + } + + @Test + def testExplicitConfigEntityNameReturnsValue(): Unit = { + val (user, _) = ClientQuotaMetadataManager.transferToClientQuotaEntity(UserEntity("testUser")) + assertEquals("testUser", user.get().name()) + + val (_, clientId) = ClientQuotaMetadataManager.transferToClientQuotaEntity(ClientIdEntity("testClient")) + assertEquals("testClient", clientId.get().name()) + } +} diff --git a/server/src/main/java/org/apache/kafka/server/quota/ClientQuotaManager.java b/server/src/main/java/org/apache/kafka/server/quota/ClientQuotaManager.java index 010b10af91abc..c3849f4d9280b 100644 --- a/server/src/main/java/org/apache/kafka/server/quota/ClientQuotaManager.java +++ b/server/src/main/java/org/apache/kafka/server/quota/ClientQuotaManager.java @@ -60,7 +60,6 @@ public class ClientQuotaManager { // Purge sensors after 1 hour of inactivity private static final int INACTIVE_SENSOR_EXPIRATION_TIME_SECONDS = 3600; - private static final String DEFAULT_NAME = ""; public record UserEntity(String sanitizedUser) implements ClientQuotaEntity.ConfigEntity { @@ -105,7 +104,7 @@ public ClientQuotaEntity.ConfigEntityType entityType() { @Override public String name() { - return DEFAULT_NAME; + return null; } @Override @@ -121,7 +120,7 @@ public ClientQuotaEntity.ConfigEntityType entityType() { } @Override public String name() { - return DEFAULT_NAME; + return null; } @Override public String toString() { @@ -154,14 +153,15 @@ public List configEntities() { public String sanitizedUser() { if (userEntity instanceof UserEntity userRecord) { return userRecord.sanitizedUser(); - } else if (userEntity == DEFAULT_USER_ENTITY) { - return DEFAULT_NAME; } return ""; } public String clientId() { - return clientIdEntity != null ? clientIdEntity.name() : ""; + if (clientIdEntity instanceof ClientIdEntity clientIdRecord) { + return clientIdRecord.clientId(); + } + return ""; } @Override