4848 * <p>
4949 * This class is thread-safe.
5050 */
51- public record ClientQuotasImage ( Map < ClientQuotaEntity , ClientQuotaImage > entities ) {
51+ public final class ClientQuotasImage {
5252 public static final ClientQuotasImage EMPTY = new ClientQuotasImage (Map .of ());
5353
54+ private final Map <ClientQuotaEntity , ClientQuotaImage > entities ;
55+
56+ // Map from entity type to entity name to set of entries. The entity type could be "user", "client-id", and "ip".
57+ // {
58+ // "user": { "user1": {entity1: image1}, "user2": {entity2: image2} },
59+ // "client-id": { "client-id1": {entity3: image3}, "client-id2": {entity4: image4} },
60+ // "ip": { "ip1": {entity5: image5}, "ip2": {entity6: image6} }
61+ // }
62+ private final Map <String , Map <String , Map <ClientQuotaEntity , ClientQuotaImage >>> entitiesByTypeAndName ;
63+
64+ // Map from entity type to set of entries. The entity type could be "user", "client-id", and "ip".
65+ // {
66+ // "user": { entity1: image1, entity2: image2 },
67+ // "client-id": { entity3: image3, entity4: image4 },
68+ // "ip": { entity5: image5, entity6: image6 }
69+ // }
70+ private final Map <String , Map <ClientQuotaEntity , ClientQuotaImage >> entitiesByType ;
71+
5472 public ClientQuotasImage (Map <ClientQuotaEntity , ClientQuotaImage > entities ) {
5573 this .entities = Collections .unmodifiableMap (entities );
74+ var entitiesByTypeAndName = new HashMap <String , Map <String , Map <ClientQuotaEntity , ClientQuotaImage >>>();
75+ var entitiesByType = new HashMap <String , Map <ClientQuotaEntity , ClientQuotaImage >>();
76+ for (var entry : entities .entrySet ()) {
77+ ClientQuotaEntity entity = entry .getKey ();
78+ for (var entityEntry : entity .entries ().entrySet ()) {
79+ entitiesByTypeAndName
80+ .computeIfAbsent (entityEntry .getKey (), k -> new HashMap <>())
81+ .computeIfAbsent (entityEntry .getValue (), k -> new HashMap <>())
82+ .put (entity , entry .getValue ());
83+
84+ entitiesByType
85+ .computeIfAbsent (entityEntry .getKey (), k -> new HashMap <>())
86+ .put (entity , entry .getValue ());
87+ }
88+ }
89+ this .entitiesByTypeAndName = Collections .unmodifiableMap (entitiesByTypeAndName );
90+ this .entitiesByType = Collections .unmodifiableMap (entitiesByType );
91+ }
92+
93+ public Map <ClientQuotaEntity , ClientQuotaImage > entities () {
94+ return entities ;
5695 }
5796
5897 public boolean isEmpty () {
@@ -68,7 +107,6 @@ public void write(ImageWriter writer) {
68107 }
69108
70109 public DescribeClientQuotasResponseData describe (DescribeClientQuotasRequestData request ) {
71- DescribeClientQuotasResponseData response = new DescribeClientQuotasResponseData ();
72110 Map <String , String > exactMatch = new HashMap <>();
73111 Set <String > typeMatch = new HashSet <>();
74112 for (DescribeClientQuotasRequestData .ComponentData component : request .components ()) {
@@ -118,40 +156,61 @@ public DescribeClientQuotasResponseData describe(DescribeClientQuotasRequestData
118156 "user or clientId filter component." );
119157 }
120158 }
121- // TODO: this is O(N). We should add indexing here to speed it up. See KAFKA-13022.
122- for (Entry <ClientQuotaEntity , ClientQuotaImage > entry : entities .entrySet ()) {
123- ClientQuotaEntity entity = entry .getKey ();
124- ClientQuotaImage quotaImage = entry .getValue ();
125- if (matches (entity , exactMatch , typeMatch , request .strict ())) {
126- response .entries ().add (toDescribeEntry (entity , quotaImage ));
127- }
128- }
129- return response ;
159+
160+ return matches (exactMatch , typeMatch , request .strict ());
130161 }
131162
132- private static boolean matches (ClientQuotaEntity entity ,
133- Map <String , String > exactMatch ,
134- Set <String > typeMatch ,
135- boolean strict ) {
136- if (strict ) {
137- if (entity .entries ().size () != exactMatch .size () + typeMatch .size ()) {
138- return false ;
163+ private DescribeClientQuotasResponseData matches (
164+ Map <String , String > exactMatch ,
165+ Set <String > typeMatch ,
166+ boolean strict
167+ ) {
168+ DescribeClientQuotasResponseData response = new DescribeClientQuotasResponseData ();
169+ Map <ClientQuotaEntity , ClientQuotaImage > candidates = null ;
170+ // Case 1: exact match exists. Filter candidates based on exact match first and then type match
171+ if (!exactMatch .isEmpty ()) {
172+ for (Entry <String , String > exactMatchEntry : exactMatch .entrySet ()) {
173+ String entityType = exactMatchEntry .getKey ();
174+ String entityName = exactMatchEntry .getValue ();
175+ var nameMap = entitiesByTypeAndName .get (entityType );
176+ var matches = Map .<ClientQuotaEntity , ClientQuotaImage >of ();
177+ if (nameMap != null ) matches = nameMap .getOrDefault (entityName , Map .of ());
178+ if (candidates == null ) {
179+ candidates = new HashMap <>(matches );
180+ } else {
181+ candidates .keySet ().retainAll (matches .keySet ());
182+ }
139183 }
140- }
141- for (Entry <String , String > entry : exactMatch .entrySet ()) {
142- if (!entity .entries ().containsKey (entry .getKey ())) {
143- return false ;
184+
185+ for (String type : typeMatch ) {
186+ candidates .keySet ().retainAll (entitiesByType .getOrDefault (type , Map .of ()).keySet ());
187+ }
188+ } else if (!typeMatch .isEmpty ()) {
189+ // Case 2: no exact match, only type match exists
190+ for (String type : typeMatch ) {
191+ Map <ClientQuotaEntity , ClientQuotaImage > matches = entitiesByType .getOrDefault (type , Map .of ());
192+ if (candidates == null ) {
193+ candidates = new HashMap <>(matches );
194+ } else {
195+ candidates .keySet ().retainAll (matches .keySet ());
196+ }
144197 }
145- if (!Objects .equals (entity .entries ().get (entry .getKey ()), entry .getValue ())) {
146- return false ;
198+ } else if (!strict ) {
199+ // Case 3: no exact match, no type match, no strict, return all entries
200+ for (Entry <ClientQuotaEntity , ClientQuotaImage > entry : entities .entrySet ()) {
201+ response .entries ().add (toDescribeEntry (entry .getKey (), entry .getValue ()));
147202 }
203+ return response ;
148204 }
149- for (String type : typeMatch ) {
150- if (!entity .entries ().containsKey (type )) {
151- return false ;
205+
206+ if (candidates != null ) {
207+ for (Entry <ClientQuotaEntity , ClientQuotaImage > entry : candidates .entrySet ()) {
208+ if (!strict || entry .getKey ().entries ().size () == exactMatch .size () + typeMatch .size ()) {
209+ response .entries ().add (toDescribeEntry (entry .getKey (), entry .getValue ()));
210+ }
152211 }
153212 }
154- return true ;
213+ return response ;
155214 }
156215
157216 private static EntryData toDescribeEntry (ClientQuotaEntity entity ,
@@ -166,6 +225,18 @@ private static EntryData toDescribeEntry(ClientQuotaEntity entity,
166225 return data ;
167226 }
168227
228+ @ Override
229+ public boolean equals (Object o ) {
230+ if (this == o ) return true ;
231+ if (!(o instanceof ClientQuotasImage other )) return false ;
232+ return entities .equals (other .entities );
233+ }
234+
235+ @ Override
236+ public int hashCode () {
237+ return Objects .hash (entities );
238+ }
239+
169240 @ Override
170241 public String toString () {
171242 return new ClientQuotasImageNode (this ).stringify ();
0 commit comments