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());
}