This is an automated email from the ASF dual-hosted git repository.

chia7712 pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git


The following commit(s) were added to refs/heads/trunk by this push:
     new ff2ba93a5c5 KAFKA-13022 Optimize ClientQuotasImage#describe (#19079)
ff2ba93a5c5 is described below

commit ff2ba93a5c5bd4884d6ec2eecde182d1b481ca05
Author: PoAn Yang <[email protected]>
AuthorDate: Sat May 2 06:45:26 2026 +0800

    KAFKA-13022 Optimize ClientQuotasImage#describe (#19079)
    
    In previous implementation, the `ClientQuotasImage#describe` goes
    through all `ClientQuotaEntity` and checks whether entity type and
    entity name are matched the filter. In this PR, it goes through all
    `ClientQuotaEntity#entries` in the constructor and build a new data
    structure `Map<String, Map<String, Set<Entry<ClientQuotaEntity,
    ClientQuotaImage>>>>`. The first layer is entity type. There are only
    three entity types: user, client-id, and ip. The second layer is entity
    name. The last layer is a set of matched entries. With this data
    structure, we can speed up `describe` function.
    
    Correctness is covered by `ClientQuotasRequestTest`.
    
    Reviewers: Kuan-Po Tseng <[email protected]>, Mickael Maison
     <[email protected]>, Alyssa Huang <[email protected]>, TaiJuWu
     <[email protected]>, Chia-Ping Tsai <[email protected]>
---
 .../ClientQuotasImageDescribeBenchmark.java        | 100 +++++++
 .../org/apache/kafka/image/ClientQuotasImage.java  | 127 +++++++--
 .../apache/kafka/image/ClientQuotasImageTest.java  | 306 +++++++++++++++++++++
 3 files changed, 505 insertions(+), 28 deletions(-)

diff --git 
a/jmh-benchmarks/src/main/java/org/apache/kafka/jmh/metadata/ClientQuotasImageDescribeBenchmark.java
 
b/jmh-benchmarks/src/main/java/org/apache/kafka/jmh/metadata/ClientQuotasImageDescribeBenchmark.java
new file mode 100644
index 00000000000..4424522e30d
--- /dev/null
+++ 
b/jmh-benchmarks/src/main/java/org/apache/kafka/jmh/metadata/ClientQuotasImageDescribeBenchmark.java
@@ -0,0 +1,100 @@
+/*
+ * 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.jmh.metadata;
+
+import org.apache.kafka.common.message.DescribeClientQuotasRequestData;
+import org.apache.kafka.common.message.DescribeClientQuotasResponseData;
+import org.apache.kafka.common.quota.ClientQuotaEntity;
+import org.apache.kafka.common.requests.DescribeClientQuotasRequest;
+import org.apache.kafka.image.ClientQuotaImage;
+import org.apache.kafka.image.ClientQuotasImage;
+import org.apache.kafka.server.config.QuotaConfig;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@State(Scope.Benchmark)
+@Fork(value = 1)
+@Warmup(iterations = 5)
+@Measurement(iterations = 15)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+public class ClientQuotasImageDescribeBenchmark {
+
+    @Param({"10", "100", "1000"})
+    private int eachEntityCount;
+
+    private ClientQuotasImage clientQuotasImage;
+
+    @Setup(Level.Trial)
+    public void setup() {
+        clientQuotasImage = createClientQuotasImage(eachEntityCount);
+    }
+
+    static ClientQuotasImage createClientQuotasImage(int eachEntityCount) {
+        Map<ClientQuotaEntity, ClientQuotaImage> entities = new HashMap<>();
+        ClientQuotaImage defaultImage = new 
ClientQuotaImage(Map.of(QuotaConfig.REQUEST_PERCENTAGE_OVERRIDE_CONFIG, 1.0));
+        for (int i = 0; i < eachEntityCount; i++) {
+            entities.put(new ClientQuotaEntity(Map.of(ClientQuotaEntity.USER, 
"user-" + i)), defaultImage);
+            entities.put(new 
ClientQuotaEntity(Map.of(ClientQuotaEntity.CLIENT_ID, "client-id-" + i)), 
defaultImage);
+            entities.put(new ClientQuotaEntity(Map.of(ClientQuotaEntity.IP, 
"ip-" + i)), defaultImage);
+        }
+        return new ClientQuotasImage(entities);
+    }
+
+    @Benchmark
+    public DescribeClientQuotasResponseData describeSpecified() {
+        return clientQuotasImage.describe(new DescribeClientQuotasRequestData()
+            .setComponents(List.of(new 
DescribeClientQuotasRequestData.ComponentData()
+                .setEntityType(ClientQuotaEntity.USER)
+                .setMatchType(DescribeClientQuotasRequest.MATCH_TYPE_SPECIFIED)
+                .setMatch(null))));
+    }
+
+    @Benchmark
+    public DescribeClientQuotasResponseData describeDefault() {
+        return clientQuotasImage.describe(new DescribeClientQuotasRequestData()
+            .setComponents(List.of(new 
DescribeClientQuotasRequestData.ComponentData()
+                .setEntityType(ClientQuotaEntity.USER)
+                .setMatchType(DescribeClientQuotasRequest.MATCH_TYPE_DEFAULT)
+                .setMatch(null))));
+    }
+
+    @Benchmark
+    public DescribeClientQuotasResponseData describeExact() {
+        return clientQuotasImage.describe(new DescribeClientQuotasRequestData()
+            .setComponents(List.of(new 
DescribeClientQuotasRequestData.ComponentData()
+                .setEntityType(ClientQuotaEntity.USER)
+                .setMatchType(DescribeClientQuotasRequest.MATCH_TYPE_EXACT)
+                .setMatch("user-0"))));
+    }
+}
diff --git 
a/metadata/src/main/java/org/apache/kafka/image/ClientQuotasImage.java 
b/metadata/src/main/java/org/apache/kafka/image/ClientQuotasImage.java
index e5dfb0433d0..7d853980fcb 100644
--- a/metadata/src/main/java/org/apache/kafka/image/ClientQuotasImage.java
+++ b/metadata/src/main/java/org/apache/kafka/image/ClientQuotasImage.java
@@ -48,11 +48,50 @@ import static 
org.apache.kafka.common.requests.DescribeClientQuotasRequest.MATCH
  * <p>
  * This class is thread-safe.
  */
-public record ClientQuotasImage(Map<ClientQuotaEntity, ClientQuotaImage> 
entities) {
+public final class ClientQuotasImage {
     public static final ClientQuotasImage EMPTY = new 
ClientQuotasImage(Map.of());
 
+    private final Map<ClientQuotaEntity, ClientQuotaImage> entities;
+
+    // Map from entity type to entity name to set of entries. The entity type 
could be "user", "client-id", and "ip".
+    // {
+    //   "user": { "user1": {entity1: image1}, "user2": {entity2: image2} },
+    //   "client-id": { "client-id1": {entity3: image3}, "client-id2": 
{entity4: image4} },
+    //   "ip": { "ip1": {entity5: image5}, "ip2": {entity6: image6} }
+    // }
+    private final Map<String, Map<String, Map<ClientQuotaEntity, 
ClientQuotaImage>>> entitiesByTypeAndName;
+
+    // Map from entity type to set of entries. The entity type could be 
"user", "client-id", and "ip".
+    // {
+    //   "user": { entity1: image1, entity2: image2 },
+    //   "client-id": { entity3: image3, entity4: image4 },
+    //   "ip": { entity5: image5, entity6: image6 }
+    // }
+    private final Map<String, Map<ClientQuotaEntity, ClientQuotaImage>> 
entitiesByType;
+
     public ClientQuotasImage(Map<ClientQuotaEntity, ClientQuotaImage> 
entities) {
         this.entities = Collections.unmodifiableMap(entities);
+        var entitiesByTypeAndName = new HashMap<String, Map<String, 
Map<ClientQuotaEntity, ClientQuotaImage>>>();
+        var entitiesByType = new HashMap<String, Map<ClientQuotaEntity, 
ClientQuotaImage>>();
+        for (var entry : entities.entrySet()) {
+            ClientQuotaEntity entity = entry.getKey();
+            for (var entityEntry : entity.entries().entrySet()) {
+                entitiesByTypeAndName
+                    .computeIfAbsent(entityEntry.getKey(), k -> new 
HashMap<>())
+                    .computeIfAbsent(entityEntry.getValue(), k -> new 
HashMap<>())
+                    .put(entity, entry.getValue());
+
+                entitiesByType
+                    .computeIfAbsent(entityEntry.getKey(), k -> new 
HashMap<>())
+                    .put(entity, entry.getValue());
+            }
+        }
+        this.entitiesByTypeAndName = 
Collections.unmodifiableMap(entitiesByTypeAndName);
+        this.entitiesByType = Collections.unmodifiableMap(entitiesByType);
+    }
+
+    public Map<ClientQuotaEntity, ClientQuotaImage> entities() {
+        return entities;
     }
 
     public boolean isEmpty() {
@@ -68,7 +107,6 @@ public record ClientQuotasImage(Map<ClientQuotaEntity, 
ClientQuotaImage> entitie
     }
 
     public DescribeClientQuotasResponseData 
describe(DescribeClientQuotasRequestData request) {
-        DescribeClientQuotasResponseData response = new 
DescribeClientQuotasResponseData();
         Map<String, String> exactMatch = new HashMap<>();
         Set<String> typeMatch = new HashSet<>();
         for (DescribeClientQuotasRequestData.ComponentData component : 
request.components()) {
@@ -118,40 +156,61 @@ public record ClientQuotasImage(Map<ClientQuotaEntity, 
ClientQuotaImage> entitie
                     "user or clientId filter component.");
             }
         }
-        // TODO: this is O(N). We should add indexing here to speed it up. See 
KAFKA-13022.
-        for (Entry<ClientQuotaEntity, ClientQuotaImage> entry : 
entities.entrySet()) {
-            ClientQuotaEntity entity = entry.getKey();
-            ClientQuotaImage quotaImage = entry.getValue();
-            if (matches(entity, exactMatch, typeMatch, request.strict())) {
-                response.entries().add(toDescribeEntry(entity, quotaImage));
-            }
-        }
-        return response;
+
+        return matches(exactMatch, typeMatch, request.strict());
     }
 
-    private static boolean matches(ClientQuotaEntity entity,
-                                   Map<String, String> exactMatch,
-                                   Set<String> typeMatch,
-                                   boolean strict) {
-        if (strict) {
-            if (entity.entries().size() != exactMatch.size() + 
typeMatch.size()) {
-                return false;
+    private DescribeClientQuotasResponseData matches(
+        Map<String, String> exactMatch,
+        Set<String> typeMatch,
+        boolean strict
+    ) {
+        DescribeClientQuotasResponseData response = new 
DescribeClientQuotasResponseData();
+        Map<ClientQuotaEntity, ClientQuotaImage> candidates = null;
+        // Case 1: exact match exists. Filter candidates based on exact match 
first and then type match
+        if (!exactMatch.isEmpty()) {
+            for (Entry<String, String> exactMatchEntry : 
exactMatch.entrySet()) {
+                String entityType = exactMatchEntry.getKey();
+                String entityName = exactMatchEntry.getValue();
+                var nameMap = entitiesByTypeAndName.get(entityType);
+                var matches = Map.<ClientQuotaEntity, ClientQuotaImage>of();
+                if (nameMap != null) matches = 
nameMap.getOrDefault(entityName, Map.of());
+                if (candidates == null) {
+                    candidates = new HashMap<>(matches);
+                } else {
+                    candidates.keySet().retainAll(matches.keySet());
+                }
             }
-        }
-        for (Entry<String, String> entry : exactMatch.entrySet()) {
-            if (!entity.entries().containsKey(entry.getKey())) {
-                return false;
+
+            for (String type : typeMatch) {
+                
candidates.keySet().retainAll(entitiesByType.getOrDefault(type, 
Map.of()).keySet());
+            }
+        } else if (!typeMatch.isEmpty()) {
+            // Case 2: no exact match, only type match exists
+            for (String type : typeMatch) {
+                Map<ClientQuotaEntity, ClientQuotaImage> matches = 
entitiesByType.getOrDefault(type, Map.of());
+                if (candidates == null) {
+                    candidates = new HashMap<>(matches);
+                } else {
+                    candidates.keySet().retainAll(matches.keySet());
+                }
             }
-            if (!Objects.equals(entity.entries().get(entry.getKey()), 
entry.getValue())) {
-                return false;
+        } else if (!strict) {
+            // Case 3: no exact match, no type match, no strict, return all 
entries
+            for (Entry<ClientQuotaEntity, ClientQuotaImage> entry : 
entities.entrySet()) {
+                response.entries().add(toDescribeEntry(entry.getKey(), 
entry.getValue()));
             }
+            return response;
         }
-        for (String type : typeMatch) {
-            if (!entity.entries().containsKey(type)) {
-                return false;
+
+        if (candidates != null) {
+            for (Entry<ClientQuotaEntity, ClientQuotaImage> entry : 
candidates.entrySet()) {
+                if (!strict || entry.getKey().entries().size() == 
exactMatch.size() + typeMatch.size()) {
+                    response.entries().add(toDescribeEntry(entry.getKey(), 
entry.getValue()));
+                }
             }
         }
-        return true;
+        return response;
     }
 
     private static EntryData toDescribeEntry(ClientQuotaEntity entity,
@@ -166,6 +225,18 @@ public record ClientQuotasImage(Map<ClientQuotaEntity, 
ClientQuotaImage> entitie
         return data;
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof ClientQuotasImage other)) return false;
+        return entities.equals(other.entities);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(entities);
+    }
+
     @Override
     public String toString() {
         return new ClientQuotasImageNode(this).stringify();
diff --git 
a/metadata/src/test/java/org/apache/kafka/image/ClientQuotasImageTest.java 
b/metadata/src/test/java/org/apache/kafka/image/ClientQuotasImageTest.java
index 46f8e908fe2..eb93fd29cb6 100644
--- a/metadata/src/test/java/org/apache/kafka/image/ClientQuotasImageTest.java
+++ b/metadata/src/test/java/org/apache/kafka/image/ClientQuotasImageTest.java
@@ -17,6 +17,9 @@
 
 package org.apache.kafka.image;
 
+import org.apache.kafka.common.errors.InvalidRequestException;
+import org.apache.kafka.common.message.DescribeClientQuotasRequestData;
+import org.apache.kafka.common.message.DescribeClientQuotasResponseData;
 import org.apache.kafka.common.metadata.ClientQuotaRecord;
 import org.apache.kafka.common.metadata.ClientQuotaRecord.EntityData;
 import org.apache.kafka.common.quota.ClientQuotaEntity;
@@ -27,6 +30,8 @@ import org.apache.kafka.server.config.QuotaConfig;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -35,7 +40,14 @@ import java.util.Map;
 import java.util.Optional;
 
 import static 
org.apache.kafka.common.metadata.MetadataRecordType.CLIENT_QUOTA_RECORD;
+import static 
org.apache.kafka.common.requests.DescribeClientQuotasRequest.MATCH_TYPE_DEFAULT;
+import static 
org.apache.kafka.common.requests.DescribeClientQuotasRequest.MATCH_TYPE_EXACT;
+import static 
org.apache.kafka.common.requests.DescribeClientQuotasRequest.MATCH_TYPE_SPECIFIED;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 
 @Timeout(value = 40)
@@ -116,6 +128,300 @@ public class ClientQuotasImageTest {
         testToImage(IMAGE2);
     }
 
+    @ParameterizedTest(name = "{0}")
+    @ValueSource(strings = {ClientQuotaEntity.USER, 
ClientQuotaEntity.CLIENT_ID, ClientQuotaEntity.IP})
+    public void testDescribeWithNonStrictExactMatch(String entityType) {
+        Map<ClientQuotaEntity, ClientQuotaImage> entities = Map.of(
+            new ClientQuotaEntity(Map.of(ClientQuotaEntity.USER, "foo", 
ClientQuotaEntity.CLIENT_ID, "baz")), new 
ClientQuotaImage(Map.of(QuotaConfig.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, 100.0)),
+            new ClientQuotaEntity(Map.of(ClientQuotaEntity.USER, "bar", 
ClientQuotaEntity.CLIENT_ID, "baz")), new 
ClientQuotaImage(Map.of(QuotaConfig.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, 200.0)),
+            new ClientQuotaEntity(Map.of(ClientQuotaEntity.CLIENT_ID, "foo", 
ClientQuotaEntity.USER, "baz")), new 
ClientQuotaImage(Map.of(QuotaConfig.CONSUMER_BYTE_RATE_OVERRIDE_CONFIG, 100.0)),
+            new ClientQuotaEntity(Map.of(ClientQuotaEntity.CLIENT_ID, "bar", 
ClientQuotaEntity.USER, "baz")), new 
ClientQuotaImage(Map.of(QuotaConfig.CONSUMER_BYTE_RATE_OVERRIDE_CONFIG, 200.0)),
+            new ClientQuotaEntity(Map.of(ClientQuotaEntity.IP, "foo", 
ClientQuotaEntity.USER, "baz")), new 
ClientQuotaImage(Map.of(QuotaConfig.REQUEST_PERCENTAGE_OVERRIDE_CONFIG, 10.0)),
+            new ClientQuotaEntity(Map.of(ClientQuotaEntity.IP, "bar", 
ClientQuotaEntity.USER, "baz")), new 
ClientQuotaImage(Map.of(QuotaConfig.REQUEST_PERCENTAGE_OVERRIDE_CONFIG, 20.0))
+        );
+        ClientQuotasImage image = new ClientQuotasImage(entities);
+
+        DescribeClientQuotasRequestData request = new 
DescribeClientQuotasRequestData()
+            .setComponents(List.of(
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(entityType)
+                    .setMatchType(MATCH_TYPE_EXACT)
+                    .setMatch("foo")));
+
+        DescribeClientQuotasResponseData response = image.describe(request);
+        assertEquals(1, response.entries().size());
+        Optional<DescribeClientQuotasResponseData.EntityData> entity = 
response.entries().get(0).entity().stream()
+            .filter(e -> e.entityType().equals(entityType))
+            .findFirst();
+        assertTrue(entity.isPresent());
+        assertEquals("foo", entity.get().entityName());
+
+        request = new DescribeClientQuotasRequestData()
+            .setComponents(List.of(
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(entityType)
+                    .setMatchType(MATCH_TYPE_EXACT)
+                    .setMatch("nonexistent")));
+        response = image.describe(request);
+        assertEquals(0, response.entries().size());
+    }
+
+    @Test
+    public void testDescribeWithStrictMode() {
+        Map<ClientQuotaEntity, ClientQuotaImage> entities = Map.of(
+            new ClientQuotaEntity(Map.of(ClientQuotaEntity.USER, "foo", 
ClientQuotaEntity.CLIENT_ID, "id1")), new 
ClientQuotaImage(Map.of(QuotaConfig.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, 100.0)),
+            new ClientQuotaEntity(Map.of(ClientQuotaEntity.USER, "foo", 
ClientQuotaEntity.CLIENT_ID, "id1", ClientQuotaEntity.IP, "ip")), new 
ClientQuotaImage(Map.of(QuotaConfig.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, 200.0)),
+            new ClientQuotaEntity(Map.of(ClientQuotaEntity.USER, "bar", 
ClientQuotaEntity.CLIENT_ID, "id2")), new 
ClientQuotaImage(Map.of(QuotaConfig.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, 300.0))
+        );
+        ClientQuotasImage image = new ClientQuotasImage(entities);
+
+        // 1. All exact match
+        DescribeClientQuotasRequestData allExactMatchRequest = new 
DescribeClientQuotasRequestData()
+            .setStrict(true)
+            .setComponents(List.of(
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(ClientQuotaEntity.USER)
+                    .setMatchType(MATCH_TYPE_EXACT)
+                    .setMatch("foo"),
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(ClientQuotaEntity.CLIENT_ID)
+                    .setMatchType(MATCH_TYPE_EXACT)
+                    .setMatch("id1")
+            ));
+        DescribeClientQuotasResponseData allExactMatchResponse = 
image.describe(allExactMatchRequest);
+        assertEquals(1, allExactMatchResponse.entries().size());
+        List<DescribeClientQuotasResponseData.EntityData> 
allExactMatchEntities = allExactMatchResponse.entries().get(0).entity();
+        assertTrue(allExactMatchEntities.contains(new 
DescribeClientQuotasResponseData.EntityData()
+            .setEntityType(ClientQuotaEntity.CLIENT_ID)
+            .setEntityName("id1")));
+        assertTrue(allExactMatchEntities.contains(new 
DescribeClientQuotasResponseData.EntityData()
+            .setEntityType(ClientQuotaEntity.USER)
+            .setEntityName("foo")));
+
+        // 2. All match type specified
+        DescribeClientQuotasRequestData allTypeMatchRequest = new 
DescribeClientQuotasRequestData()
+            .setStrict(true)
+            .setComponents(List.of(
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(ClientQuotaEntity.USER)
+                    .setMatchType(MATCH_TYPE_SPECIFIED)
+                    .setMatch(null),
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(ClientQuotaEntity.CLIENT_ID)
+                    .setMatchType(MATCH_TYPE_SPECIFIED)
+                    .setMatch(null)
+            ));
+        DescribeClientQuotasResponseData allTypeMatchResponse = 
image.describe(allTypeMatchRequest);
+        assertEquals(2, allTypeMatchResponse.entries().size());
+        for (DescribeClientQuotasResponseData.EntryData entry : 
allTypeMatchResponse.entries()) {
+            for  (DescribeClientQuotasResponseData.EntityData entity : 
entry.entity()) {
+                assertNotEquals(ClientQuotaEntity.IP, entity.entityType());
+            }
+        }
+
+        // 3. Mixed exact and match type specified
+        DescribeClientQuotasRequestData exactAndMatchTypeRequest = new 
DescribeClientQuotasRequestData()
+            .setStrict(true)
+            .setComponents(List.of(
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(ClientQuotaEntity.USER)
+                    .setMatchType(MATCH_TYPE_EXACT)
+                    .setMatch("foo"),
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(ClientQuotaEntity.CLIENT_ID)
+                    .setMatchType(MATCH_TYPE_SPECIFIED)
+                    .setMatch(null)
+            ));
+        DescribeClientQuotasResponseData exactAndMatchTypeResponse = 
image.describe(exactAndMatchTypeRequest);
+        assertEquals(1, exactAndMatchTypeResponse.entries().size());
+        List<DescribeClientQuotasResponseData.EntityData> 
exactAndMatchEntities = allExactMatchResponse.entries().get(0).entity();
+        assertTrue(exactAndMatchEntities.contains(new 
DescribeClientQuotasResponseData.EntityData()
+            .setEntityType(ClientQuotaEntity.CLIENT_ID)
+            .setEntityName("id1")));
+        assertTrue(exactAndMatchEntities.contains(new 
DescribeClientQuotasResponseData.EntityData()
+            .setEntityType(ClientQuotaEntity.USER)
+            .setEntityName("foo")));
+    }
+
+    @Test
+    public void testDescribeWithNonStrictTypeMatch() {
+        Map<ClientQuotaEntity, ClientQuotaImage> entities = Map.of(
+            new ClientQuotaEntity(Map.of(ClientQuotaEntity.USER, "foo", 
ClientQuotaEntity.CLIENT_ID, "id")), new 
ClientQuotaImage(Map.of(QuotaConfig.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, 100.0)),
+            new ClientQuotaEntity(Map.of(ClientQuotaEntity.USER, "bar", 
ClientQuotaEntity.IP, "ip")), new 
ClientQuotaImage(Map.of(QuotaConfig.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, 200.0)),
+            new ClientQuotaEntity(Map.of(ClientQuotaEntity.IP, "ip", 
ClientQuotaEntity.CLIENT_ID, "id")), new 
ClientQuotaImage(Map.of(QuotaConfig.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, 200.0))
+        );
+        ClientQuotasImage image = new ClientQuotasImage(entities);
+        DescribeClientQuotasRequestData request = new 
DescribeClientQuotasRequestData()
+            .setComponents(List.of(
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(ClientQuotaEntity.USER)
+                    .setMatchType(MATCH_TYPE_SPECIFIED)
+                    .setMatch(null)
+            ));
+
+        DescribeClientQuotasResponseData response = image.describe(request);
+        assertEquals(2, response.entries().size());
+        for (DescribeClientQuotasResponseData.EntryData entry : 
response.entries()) {
+            assertTrue(entry.entity().stream().anyMatch(e -> 
e.entityType().equals(ClientQuotaEntity.USER)));
+        }
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @ValueSource(strings = {ClientQuotaEntity.USER, 
ClientQuotaEntity.CLIENT_ID, ClientQuotaEntity.IP})
+    public void testDescribeWithNonStrictDefaultMatch(String entityType) {
+        Map<String, String> defaultEntity = new HashMap<>();
+        defaultEntity.put(entityType, null);
+        Map<ClientQuotaEntity, ClientQuotaImage> entities = Map.of(
+            new ClientQuotaEntity(defaultEntity), new 
ClientQuotaImage(Map.of(QuotaConfig.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, 100.0)),
+            new ClientQuotaEntity(Map.of(entityType, "foo")), new 
ClientQuotaImage(Map.of(QuotaConfig.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, 200.0))
+        );
+        ClientQuotasImage image = new ClientQuotasImage(entities);
+
+        DescribeClientQuotasRequestData request = new 
DescribeClientQuotasRequestData()
+            .setComponents(List.of(
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(entityType)
+                    .setMatchType(MATCH_TYPE_DEFAULT)
+                    .setMatch(null)
+            ));
+
+        DescribeClientQuotasResponseData response = image.describe(request);
+        assertEquals(1, response.entries().size());
+        assertEquals(entityType, 
response.entries().get(0).entity().get(0).entityType());
+        assertNull(response.entries().get(0).entity().get(0).entityName());
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @ValueSource(strings = {ClientQuotaEntity.USER, 
ClientQuotaEntity.CLIENT_ID, ClientQuotaEntity.IP})
+    public void testDescribeDefaultMatchWithNoData(String entityType) {
+        ClientQuotasImage image = new ClientQuotasImage(Map.of());
+
+        DescribeClientQuotasRequestData request = new 
DescribeClientQuotasRequestData()
+            .setComponents(List.of(
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(entityType)
+                    .setMatchType(MATCH_TYPE_DEFAULT)
+                    .setMatch(null)
+            ));
+
+        DescribeClientQuotasResponseData response = image.describe(request);
+        assertEquals(0, response.entries().size());
+    }
+
+    @Test
+    public void testDescribeNonStrictEmptyRequest() {
+        Map<ClientQuotaEntity, ClientQuotaImage> entities = Map.of(
+            new ClientQuotaEntity(Map.of(ClientQuotaEntity.USER, "foo")), new 
ClientQuotaImage(Map.of(QuotaConfig.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, 100.0)),
+            new ClientQuotaEntity(Map.of(ClientQuotaEntity.CLIENT_ID, "bar")), 
new ClientQuotaImage(Map.of(QuotaConfig.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, 
200.0)),
+            new ClientQuotaEntity(Map.of(ClientQuotaEntity.IP, "baz")), new 
ClientQuotaImage(Map.of(QuotaConfig.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, 300.0))
+        );
+        ClientQuotasImage image = new ClientQuotasImage(entities);
+        DescribeClientQuotasRequestData request = new 
DescribeClientQuotasRequestData()
+            .setStrict(false)
+            .setComponents(List.of());
+
+        DescribeClientQuotasResponseData response = image.describe(request);
+        assertEquals(3, response.entries().size());
+    }
+
+    @Test
+    public void testDescribeWithEmptyEntityType() {
+        DescribeClientQuotasRequestData request = new 
DescribeClientQuotasRequestData()
+            .setComponents(List.of(
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType("")
+                    .setMatchType(MATCH_TYPE_EXACT)
+                    .setMatch("foo")));
+
+        InvalidRequestException exception = 
assertThrows(InvalidRequestException.class, () -> IMAGE1.describe(request));
+        assertEquals("Invalid empty entity type.", exception.getMessage());
+    }
+
+    @Test
+    public void testDescribeWithDuplicateEntityType() {
+        DescribeClientQuotasRequestData request = new 
DescribeClientQuotasRequestData()
+            .setComponents(List.of(
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(ClientQuotaEntity.USER)
+                    .setMatchType(MATCH_TYPE_EXACT)
+                    .setMatch("foo"),
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(ClientQuotaEntity.USER)
+                    .setMatchType(MATCH_TYPE_SPECIFIED)));
+
+        InvalidRequestException exception = 
assertThrows(InvalidRequestException.class, () -> IMAGE1.describe(request));
+        assertEquals("Entity type user cannot appear more than once in the 
filter.", exception.getMessage());
+    }
+
+    @Test
+    public void testDescribeWithExactMatchNullMatch() {
+        DescribeClientQuotasRequestData request = new 
DescribeClientQuotasRequestData()
+            .setComponents(List.of(
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(ClientQuotaEntity.USER)
+                    .setMatchType(MATCH_TYPE_EXACT)
+                    .setMatch(null)));
+
+        InvalidRequestException exception = 
assertThrows(InvalidRequestException.class, () -> IMAGE1.describe(request));
+        assertEquals("Request specified MATCH_TYPE_EXACT, but set match string 
to null.", exception.getMessage());
+    }
+
+    @Test
+    public void testDescribeWithDefaultMatchNonNullMatch() {
+        DescribeClientQuotasRequestData request = new 
DescribeClientQuotasRequestData()
+            .setComponents(List.of(
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(ClientQuotaEntity.USER)
+                    .setMatchType(MATCH_TYPE_DEFAULT)
+                    .setMatch("foo")));
+
+        InvalidRequestException exception = 
assertThrows(InvalidRequestException.class, () -> IMAGE1.describe(request));
+        assertEquals("Request specified MATCH_TYPE_DEFAULT, but also specified 
a match string.", exception.getMessage());
+    }
+
+    @Test
+    public void testDescribeWithSpecifiedMatchNonNullMatch() {
+        DescribeClientQuotasRequestData request = new 
DescribeClientQuotasRequestData()
+            .setComponents(List.of(
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(ClientQuotaEntity.USER)
+                    .setMatchType(MATCH_TYPE_SPECIFIED)
+                    .setMatch("foo")));
+
+        InvalidRequestException exception = 
assertThrows(InvalidRequestException.class, () -> IMAGE1.describe(request));
+        assertEquals("Request specified MATCH_TYPE_SPECIFIED, but also 
specified a match string.", exception.getMessage());
+    }
+
+    @Test
+    public void testDescribeWithUnknownMatchType() {
+        DescribeClientQuotasRequestData request = new 
DescribeClientQuotasRequestData()
+            .setComponents(List.of(
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(ClientQuotaEntity.USER)
+                    .setMatchType((byte) 99)));
+
+        InvalidRequestException exception = 
assertThrows(InvalidRequestException.class, () -> IMAGE1.describe(request));
+        assertEquals("Unknown match type 99", exception.getMessage());
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @ValueSource(strings = {ClientQuotaEntity.USER, 
ClientQuotaEntity.CLIENT_ID})
+    public void testDescribeWithIpAndOtherTypeCombination(String entityType) {
+        DescribeClientQuotasRequestData request = new 
DescribeClientQuotasRequestData()
+            .setComponents(List.of(
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(ClientQuotaEntity.IP)
+                    .setMatchType(MATCH_TYPE_EXACT)
+                    .setMatch("127.0.0.1"),
+                new DescribeClientQuotasRequestData.ComponentData()
+                    .setEntityType(entityType)
+                    .setMatchType(MATCH_TYPE_EXACT)
+                    .setMatch("foo")));
+
+        InvalidRequestException exception = 
assertThrows(InvalidRequestException.class, () -> IMAGE1.describe(request));
+        assertTrue(exception.getMessage().contains("IP filter component should 
not be used with user or clientId filter component"));
+    }
+
     private static void testToImage(ClientQuotasImage image) {
         testToImage(image, Optional.empty());
     }

Reply via email to