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 4fc9c58ba4a KAFKA-17840 Move ClientRequestQuotaManager to server 
module (#22403)
4fc9c58ba4a is described below

commit 4fc9c58ba4a40e647f9acc0cb9744749e4d2dff8
Author: Mickael Maison <[email protected]>
AuthorDate: Wed Jun 24 22:27:45 2026 +0200

    KAFKA-17840 Move ClientRequestQuotaManager to server module (#22403)
    
    Also move the quota manager tests classes
    
    Reviewers: Chia-Ping Tsai <[email protected]>
---
 core/src/main/java/kafka/server/QuotaFactory.java  |   1 +
 .../kafka/server/BaseClientQuotaManagerTest.scala  |  84 ---
 .../unit/kafka/server/ClientQuotaManagerTest.scala | 626 ---------------------
 .../server/ClientRequestQuotaManagerTest.scala     |  99 ----
 .../unit/kafka/server/ControllerApisTest.scala     |   2 +-
 .../ControllerMutationQuotaManagerTest.scala       | 243 --------
 .../scala/unit/kafka/server/KafkaApisTest.scala    |   2 +-
 .../metadata/KRaftMetadataRequestBenchmark.java    |   2 +-
 .../server/quota}/ClientRequestQuotaManager.java   |   6 +-
 .../server/quota/BaseClientQuotaManagerTest.java   |  97 ++++
 .../kafka/server/quota/ClientQuotaManagerTest.java | 620 ++++++++++++++++++++
 .../quota/ClientRequestQuotaManagerTest.java       | 104 ++++
 .../quota/ControllerMutationQuotaManagerTest.java  | 243 ++++++++
 13 files changed, 1069 insertions(+), 1060 deletions(-)

diff --git a/core/src/main/java/kafka/server/QuotaFactory.java 
b/core/src/main/java/kafka/server/QuotaFactory.java
index aef041dff46..aa28856cdbb 100644
--- a/core/src/main/java/kafka/server/QuotaFactory.java
+++ b/core/src/main/java/kafka/server/QuotaFactory.java
@@ -27,6 +27,7 @@ import org.apache.kafka.server.config.QuotaConfig;
 import org.apache.kafka.server.config.ReplicationQuotaManagerConfig;
 import org.apache.kafka.server.quota.ClientQuotaCallback;
 import org.apache.kafka.server.quota.ClientQuotaManager;
+import org.apache.kafka.server.quota.ClientRequestQuotaManager;
 import org.apache.kafka.server.quota.ControllerMutationQuotaManager;
 import org.apache.kafka.server.quota.QuotaType;
 import org.apache.kafka.server.quota.ReplicaQuota;
diff --git 
a/core/src/test/scala/unit/kafka/server/BaseClientQuotaManagerTest.scala 
b/core/src/test/scala/unit/kafka/server/BaseClientQuotaManagerTest.scala
deleted file mode 100644
index 294d5f27988..00000000000
--- a/core/src/test/scala/unit/kafka/server/BaseClientQuotaManagerTest.scala
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * 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 kafka.server
-
-import java.net.InetAddress
-import java.util
-import java.util.Collections
-import org.apache.kafka.common.TopicPartition
-import org.apache.kafka.common.memory.MemoryPool
-import org.apache.kafka.common.metrics.{MetricConfig, Metrics}
-import org.apache.kafka.common.network.{ClientInformation, ListenerName}
-import org.apache.kafka.common.protocol.ApiKeys
-import org.apache.kafka.common.requests.FetchRequest.PartitionData
-import org.apache.kafka.common.requests.{AbstractRequest, FetchRequest, 
RequestContext, RequestHeader}
-import org.apache.kafka.common.security.auth.{KafkaPrincipal, SecurityProtocol}
-import org.apache.kafka.common.utils.MockTime
-import org.apache.kafka.network.{Request, Session}
-import org.apache.kafka.network.metrics.RequestChannelMetrics
-import org.apache.kafka.server.quota.{ClientQuotaManager, ThrottleCallback}
-import org.junit.jupiter.api.AfterEach
-import org.mockito.Mockito.mock
-
-class BaseClientQuotaManagerTest {
-  protected val time = new MockTime
-  protected var numCallbacks: Int = 0
-  protected val metrics = new Metrics(new MetricConfig(), 
Collections.emptyList(), time)
-
-  @AfterEach
-  def tearDown(): Unit = {
-    metrics.close()
-  }
-
-  protected def callback: ThrottleCallback = new ThrottleCallback {
-    override def startThrottling(): Unit = {}
-    override def endThrottling(): Unit = {
-      // Count how many times this callback is called for 
notifyThrottlingDone().
-      numCallbacks += 1
-    }
-  }
-
-  protected def buildRequest[T <: AbstractRequest](builder: 
AbstractRequest.Builder[T],
-                                                   listenerName: ListenerName 
= ListenerName.forSecurityProtocol(SecurityProtocol.PLAINTEXT)): (T, Request) = 
{
-
-    val request = builder.build()
-    val buffer = request.serializeWithHeader(new RequestHeader(builder.apiKey, 
request.version, "", 0))
-    val requestChannelMetrics: RequestChannelMetrics = 
mock(classOf[RequestChannelMetrics])
-
-    // read the header from the buffer first so that the body can be read next 
from the Request constructor
-    val header = RequestHeader.parse(buffer)
-    val context = new RequestContext(header, "1", InetAddress.getLocalHost, 
KafkaPrincipal.ANONYMOUS,
-      listenerName, SecurityProtocol.PLAINTEXT, ClientInformation.EMPTY, false)
-    (request, new Request(1, context, 0, MemoryPool.NONE, buffer, 
requestChannelMetrics))
-  }
-
-  protected def buildSession(user: String): Session = {
-    val principal = new KafkaPrincipal(KafkaPrincipal.USER_TYPE, user)
-    new Session(principal, null)
-  }
-
-  protected def maybeRecord(quotaManager: ClientQuotaManager, user: String, 
clientId: String, value: Double): Int = {
-
-    quotaManager.maybeRecordAndGetThrottleTimeMs(buildSession(user), clientId, 
value, time.milliseconds)
-  }
-
-  protected def throttle(quotaManager: ClientQuotaManager, user: String, 
clientId: String, throttleTimeMs: Int,
-                         channelThrottlingCallback: ThrottleCallback): Unit = {
-    val (_, request) = 
buildRequest(FetchRequest.Builder.forConsumer(ApiKeys.FETCH.latestVersion, 0, 
1000, new util.HashMap[TopicPartition, PartitionData]))
-    quotaManager.throttle(request.header.clientId(), request.session, 
channelThrottlingCallback, throttleTimeMs)
-  }
-}
diff --git a/core/src/test/scala/unit/kafka/server/ClientQuotaManagerTest.scala 
b/core/src/test/scala/unit/kafka/server/ClientQuotaManagerTest.scala
deleted file mode 100644
index f0d86ba9fdf..00000000000
--- a/core/src/test/scala/unit/kafka/server/ClientQuotaManagerTest.scala
+++ /dev/null
@@ -1,626 +0,0 @@
-/**
- * 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 kafka.server
-
-import java.net.InetAddress
-import org.apache.kafka.common.internals.Plugin
-import org.apache.kafka.common.metrics.Quota
-import org.apache.kafka.common.security.auth.KafkaPrincipal
-import org.apache.kafka.network.Session
-import org.apache.kafka.server.config.ClientQuotaManagerConfig
-import org.apache.kafka.server.quota.{ClientQuotaCallback, ClientQuotaEntity, 
ClientQuotaManager, ClientQuotaType, QuotaType}
-import org.junit.jupiter.api.Assertions._
-import org.junit.jupiter.api.Test
-
-import java.util.{Collections, Map, HashMap, Optional}
-
-class ClientQuotaManagerTest extends BaseClientQuotaManagerTest {
-  private val config = new ClientQuotaManagerConfig()
-
-  private def testQuotaParsing(config: ClientQuotaManagerConfig, client1: 
UserClient, client2: UserClient, randomClient: UserClient, defaultConfigClient: 
UserClient): Unit = {
-    val clientQuotaManager = new ClientQuotaManager(config, metrics, 
QuotaType.PRODUCE, time, "")
-
-    try {
-      // Case 1: Update the quota. Assert that the new quota value is returned
-      clientQuotaManager.updateQuota(
-        client1.configUser,
-        client1.configClientEntity,
-        Optional.of(new Quota(2000, true))
-      )
-      clientQuotaManager.updateQuota(
-        client2.configUser,
-        client2.configClientEntity,
-        Optional.of(new Quota(4000, true))
-      )
-
-      assertEquals(Long.MaxValue.toDouble, 
clientQuotaManager.quota(randomClient.user, randomClient.clientId).bound, 0.0,
-        "Default producer quota should be " + Long.MaxValue.toDouble)
-      assertEquals(2000, clientQuotaManager.quota(client1.user, 
client1.clientId).bound, 0.0,
-        "Should return the overridden value (2000)")
-      assertEquals(4000, clientQuotaManager.quota(client2.user, 
client2.clientId).bound, 0.0,
-        "Should return the overridden value (4000)")
-
-      // p1 should be throttled using the overridden quota
-      var throttleTimeMs = maybeRecord(clientQuotaManager, client1.user, 
client1.clientId, 2500 * config.numQuotaSamples)
-      assertTrue(throttleTimeMs > 0, s"throttleTimeMs should be > 0. was 
$throttleTimeMs")
-
-      // Case 2: Change quota again. The quota should be updated within 
KafkaMetrics as well since the sensor was created.
-      // p1 should not longer be throttled after the quota change
-      clientQuotaManager.updateQuota(
-        client1.configUser,
-        client1.configClientEntity,
-        Optional.of(new Quota(3000, true))
-      )
-      assertEquals(3000, clientQuotaManager.quota(client1.user, 
client1.clientId).bound, 0.0, "Should return the newly overridden value (3000)")
-
-      throttleTimeMs = maybeRecord(clientQuotaManager, client1.user, 
client1.clientId, 0)
-      assertEquals(0, throttleTimeMs, s"throttleTimeMs should be 0. was 
$throttleTimeMs")
-
-      // Case 3: Change quota back to default. Should be throttled again
-      clientQuotaManager.updateQuota(
-        client1.configUser,
-        client1.configClientEntity,
-        Optional.of(new Quota(500, true))
-      )
-      assertEquals(500, clientQuotaManager.quota(client1.user, 
client1.clientId).bound, 0.0, "Should return the default value (500)")
-
-      throttleTimeMs = maybeRecord(clientQuotaManager, client1.user, 
client1.clientId, 0)
-      assertTrue(throttleTimeMs > 0, s"throttleTimeMs should be > 0. was 
$throttleTimeMs")
-
-      // Case 4: Set high default quota, remove p1 quota. p1 should no longer 
be throttled
-      clientQuotaManager.updateQuota(
-        client1.configUser,
-        client1.configClientEntity,
-        Optional.empty
-      )
-      clientQuotaManager.updateQuota(
-        defaultConfigClient.configUser,
-        defaultConfigClient.configClientEntity,
-        Optional.of(new Quota(4000, true))
-      )
-      assertEquals(4000, clientQuotaManager.quota(client1.user, 
client1.clientId).bound, 0.0, "Should return the newly overridden value (4000)")
-
-      throttleTimeMs = maybeRecord(clientQuotaManager, client1.user, 
client1.clientId, 1000 * config.numQuotaSamples)
-      assertEquals(0, throttleTimeMs, s"throttleTimeMs should be 0. was 
$throttleTimeMs")
-
-    } finally {
-      clientQuotaManager.shutdown()
-    }
-  }
-
-  /**
-   * Tests parsing for <user> quotas when client-id default quota properties 
are set.
-   */
-  @Test
-  def testUserQuotaParsingWithDefaultClientIdQuota(): Unit = {
-    val client1 = UserClient("User1", "p1", Optional.of(new 
ClientQuotaManager.UserEntity("User1")), Optional.empty)
-    val client2 = UserClient("User2", "p2", Optional.of(new 
ClientQuotaManager.UserEntity("User2")), Optional.empty)
-    val randomClient = UserClient("RandomUser", "random-client-id", 
Optional.empty, Optional.empty)
-    val defaultConfigClient = UserClient("", "", 
Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY), Optional.empty)
-    testQuotaParsing(config, client1, client2, randomClient, 
defaultConfigClient)
-  }
-
-  private def checkQuota(quotaManager: ClientQuotaManager, user: String, 
clientId: String, expectedBound: Long, value: Int, expectThrottle: Boolean): 
Unit = {
-    assertEquals(expectedBound.toDouble, quotaManager.quota(user, 
clientId).bound, 0.0)
-    val session = new Session(new KafkaPrincipal(KafkaPrincipal.USER_TYPE, 
user), InetAddress.getLocalHost)
-    val expectedMaxValueInQuotaWindow =
-      if (expectedBound < Long.MaxValue) config.quotaWindowSizeSeconds * 
(config.numQuotaSamples - 1) * expectedBound.toDouble
-      else Double.MaxValue
-    assertEquals(expectedMaxValueInQuotaWindow, 
quotaManager.maxValueInQuotaWindow(session, clientId), 0.01)
-
-    val throttleTimeMs = maybeRecord(quotaManager, user, clientId, value * 
config.numQuotaSamples)
-    if (expectThrottle)
-      assertTrue(throttleTimeMs > 0, s"throttleTimeMs should be > 0. was 
$throttleTimeMs")
-    else
-      assertEquals(0, throttleTimeMs, s"throttleTimeMs should be 0. was 
$throttleTimeMs")
-  }
-
-  @Test
-  def testMaxValueInQuotaWindowWithNonDefaultQuotaWindow(): Unit = {
-    val numFullQuotaWindows = 3   // 3 seconds window (vs. 10 seconds default)
-    val nonDefaultConfig = new ClientQuotaManagerConfig(numFullQuotaWindows + 
1)
-    val clientQuotaManager = new ClientQuotaManager(nonDefaultConfig, metrics, 
QuotaType.FETCH, time, "")
-    val userSession = new Session(new KafkaPrincipal(KafkaPrincipal.USER_TYPE, 
"userA"), InetAddress.getLocalHost)
-
-    try {
-      // no quota set
-      assertEquals(Double.MaxValue, 
clientQuotaManager.maxValueInQuotaWindow(userSession, "client1"), 0.01)
-
-      // Set default <user> quota config
-      clientQuotaManager.updateQuota(
-        Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY),
-        Optional.empty,
-        Optional.of(new Quota(10, true))
-      )
-      assertEquals(10 * numFullQuotaWindows, 
clientQuotaManager.maxValueInQuotaWindow(userSession, "client1"), 0.01)
-    } finally {
-      clientQuotaManager.shutdown()
-    }
-  }
-
-  @Test
-  def testSetAndRemoveDefaultUserQuota(): Unit = {
-    // quotaTypesEnabled will be QuotaTypes.NoQuotas initially
-    val clientQuotaManager = new ClientQuotaManager(new 
ClientQuotaManagerConfig(),
-      metrics, QuotaType.PRODUCE, time, "")
-
-    try {
-      // no quota set yet, should not throttle
-      checkQuota(clientQuotaManager, "userA", "client1", Long.MaxValue, 1000, 
expectThrottle = false)
-
-      // Set default <user> quota config
-      clientQuotaManager.updateQuota(
-        Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY),
-        Optional.empty,
-        Optional.of(new Quota(10, true))
-      )
-      checkQuota(clientQuotaManager, "userA", "client1", 10, 1000, 
expectThrottle = true)
-
-      // Remove default <user> quota config, back to no quotas
-      clientQuotaManager.updateQuota(
-        Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY),
-        Optional.empty,
-        Optional.empty
-      )
-      checkQuota(clientQuotaManager, "userA", "client1", Long.MaxValue, 1000, 
expectThrottle = false)
-    } finally {
-      clientQuotaManager.shutdown()
-    }
-  }
-
-  @Test
-  def testSetAndRemoveUserQuota(): Unit = {
-
-    val clientQuotaManager = new ClientQuotaManager(new 
ClientQuotaManagerConfig(),
-      metrics, QuotaType.PRODUCE, time, "")
-
-    try {
-      // Set <user> quota config
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userA")),
-        Optional.empty,
-        Optional.of(new Quota(10, true))
-      )
-      checkQuota(clientQuotaManager, "userA", "client1", 10, 1000, 
expectThrottle = true)
-
-      // Remove <user> quota config, back to no quotas
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userA")),
-        Optional.empty,
-        Optional.empty
-      )
-      checkQuota(clientQuotaManager, "userA", "client1", Long.MaxValue, 1000, 
expectThrottle = false)
-    } finally {
-      clientQuotaManager.shutdown()
-    }
-  }
-
-  @Test
-  def testSetAndRemoveUserClientQuota(): Unit = {
-    // quotaTypesEnabled will be QuotaTypes.NoQuotas initially
-    val clientQuotaManager = new ClientQuotaManager(new 
ClientQuotaManagerConfig(),
-      metrics, QuotaType.PRODUCE, time, "")
-
-    try {
-      // Set <user, client-id> quota config
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userA")),
-        Optional.of(new ClientQuotaManager.ClientIdEntity("client1")),
-        Optional.of(new Quota(10, true))
-      )
-      checkQuota(clientQuotaManager, "userA", "client1", 10, 1000, 
expectThrottle = true)
-
-      // Remove <user, client-id> quota config, back to no quotas
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userA")),
-        Optional.of(new ClientQuotaManager.ClientIdEntity("client1")),
-        Optional.empty
-      )
-      checkQuota(clientQuotaManager, "userA", "client1", Long.MaxValue, 1000, 
expectThrottle = false)
-    } finally {
-      clientQuotaManager.shutdown()
-    }
-  }
-
-  @Test
-  def testQuotaConfigPrecedence(): Unit = {
-    val clientQuotaManager = new ClientQuotaManager(new 
ClientQuotaManagerConfig(),
-      metrics, QuotaType.PRODUCE, time, "")
-
-    try {
-      clientQuotaManager.updateQuota(
-        Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY),
-        Optional.empty,
-        Optional.of(new Quota(1000, true))
-      )
-      clientQuotaManager.updateQuota(
-        Optional.empty,
-        Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
-        Optional.of(new Quota(2000, true))
-      )
-      clientQuotaManager.updateQuota(
-        Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY),
-        Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
-        Optional.of(new Quota(3000, true))
-      )
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userA")),
-        Optional.empty,
-        Optional.of(new Quota(4000, true))
-      )
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userA")),
-        Optional.of(new ClientQuotaManager.ClientIdEntity("client1")),
-        Optional.of(new Quota(5000, true))
-      )
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userB")),
-        Optional.empty,
-        Optional.of(new Quota(6000, true))
-      )
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userB")),
-        Optional.of(new ClientQuotaManager.ClientIdEntity("client1")),
-        Optional.of(new Quota(7000, true))
-      )
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userB")),
-        Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
-        Optional.of(new Quota(8000, true))
-      )
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userC")),
-        Optional.empty,
-        Optional.of(new Quota(10000, true))
-      )
-      clientQuotaManager.updateQuota(
-        Optional.empty,
-        Optional.of(new ClientQuotaManager.ClientIdEntity("client1")),
-        Optional.of(new Quota(9000, true))
-      )
-
-      checkQuota(clientQuotaManager, "userA", "client1", 5000, 4500, 
expectThrottle = false) // <user, client> quota takes precedence over <user>
-      checkQuota(clientQuotaManager, "userA", "client2", 4000, 4500, 
expectThrottle = true)  // <user> quota takes precedence over <client> and 
defaults
-      checkQuota(clientQuotaManager, "userA", "client3", 4000, 0, 
expectThrottle = true)     // <user> quota is shared across clients of user
-      checkQuota(clientQuotaManager, "userA", "client1", 5000, 0, 
expectThrottle = false)    // <user, client> is exclusive use, unaffected by 
other clients
-
-      checkQuota(clientQuotaManager, "userB", "client1", 7000, 8000, 
expectThrottle = true)
-      checkQuota(clientQuotaManager, "userB", "client2", 8000, 7000, 
expectThrottle = false) // Default per-client quota for exclusive use of <user, 
client>
-      checkQuota(clientQuotaManager, "userB", "client3", 8000, 7000, 
expectThrottle = false)
-
-      checkQuota(clientQuotaManager, "userD", "client1", 3000, 3500, 
expectThrottle = true)  // Default <user, client> quota
-      checkQuota(clientQuotaManager, "userD", "client2", 3000, 2500, 
expectThrottle = false)
-      checkQuota(clientQuotaManager, "userE", "client1", 3000, 2500, 
expectThrottle = false)
-
-      // Remove default <user, client> quota config, revert to <user> default
-      clientQuotaManager.updateQuota(
-        Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY),
-        Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
-        Optional.empty
-      )
-      checkQuota(clientQuotaManager, "userD", "client1", 1000, 0, 
expectThrottle = false)    // Metrics tags changed, restart counter
-      checkQuota(clientQuotaManager, "userE", "client4", 1000, 1500, 
expectThrottle = true)
-      checkQuota(clientQuotaManager, "userF", "client4", 1000, 800, 
expectThrottle = false)  // Default <user> quota shared across clients of user
-      checkQuota(clientQuotaManager, "userF", "client5", 1000, 800, 
expectThrottle = true)
-
-      // Remove default <user> quota config, revert to <client-id> default
-      clientQuotaManager.updateQuota(
-        Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY),
-        Optional.empty,
-        Optional.empty
-      )
-      checkQuota(clientQuotaManager, "userF", "client4", 2000, 0, 
expectThrottle = false)  // Default <client-id> quota shared across client-id 
of all users
-      checkQuota(clientQuotaManager, "userF", "client5", 2000, 0, 
expectThrottle = false)
-      checkQuota(clientQuotaManager, "userF", "client5", 2000, 2500, 
expectThrottle = true)
-      checkQuota(clientQuotaManager, "userG", "client5", 2000, 0, 
expectThrottle = true)
-
-      // Update quotas
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userA")),
-        Optional.empty,
-        Optional.of(new Quota(8000, true))
-      )
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userA")),
-        Optional.of(new ClientQuotaManager.ClientIdEntity("client1")),
-        Optional.of(new Quota(10000, true))
-      )
-      checkQuota(clientQuotaManager, "userA", "client2", 8000, 0, 
expectThrottle = false)
-      checkQuota(clientQuotaManager, "userA", "client2", 8000, 4500, 
expectThrottle = true) // Throttled due to sum of new and earlier values
-      checkQuota(clientQuotaManager, "userA", "client1", 10000, 0, 
expectThrottle = false)
-      checkQuota(clientQuotaManager, "userA", "client1", 10000, 6000, 
expectThrottle = true)
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userA")),
-        Optional.of(new ClientQuotaManager.ClientIdEntity("client1")),
-        Optional.empty
-      )
-      checkQuota(clientQuotaManager, "userA", "client6", 8000, 0, 
expectThrottle = true)    // Throttled due to shared user quota
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userA")),
-        Optional.of(new ClientQuotaManager.ClientIdEntity("client6")),
-        Optional.of(new Quota(11000, true))
-      )
-      checkQuota(clientQuotaManager, "userA", "client6", 11000, 8500, 
expectThrottle = false)
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userA")),
-        Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
-        Optional.of(new Quota(12000, true))
-      )
-      clientQuotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity("userA")),
-        Optional.of(new ClientQuotaManager.ClientIdEntity("client6")),
-        Optional.empty
-      )
-      checkQuota(clientQuotaManager, "userA", "client6", 12000, 4000, 
expectThrottle = true) // Throttled due to sum of new and earlier values
-
-    } finally {
-      clientQuotaManager.shutdown()
-    }
-  }
-
-  @Test
-  def testQuotaViolation(): Unit = {
-    val clientQuotaManager = new ClientQuotaManager(config, metrics, 
QuotaType.PRODUCE, time, "")
-    val queueSizeMetric = 
metrics.metrics().get(metrics.metricName("queue-size", "Produce", ""))
-    try {
-      clientQuotaManager.updateQuota(
-        Optional.empty,
-        Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
-        Optional.of(new Quota(500, true))
-      )
-
-      // We have 10 seconds windows. Make sure that there is no quota violation
-      // if we produce under the quota
-      for (_ <- 0 until 10) {
-        assertEquals(0, maybeRecord(clientQuotaManager, "ANONYMOUS", 
"unknown", 400))
-        time.sleep(1000)
-      }
-      assertEquals(0, queueSizeMetric.metricValue.asInstanceOf[Double].toInt)
-
-      // Create a spike.
-      // 400*10 + 2000 + 300 = 6300/10.5 = 600 bytes per second.
-      // (600 - quota)/quota*window-size = (600-500)/500*10.5 seconds = 2100
-      // 10.5 seconds because the last window is half complete
-      time.sleep(500)
-      val throttleTime = maybeRecord(clientQuotaManager, "ANONYMOUS", 
"unknown", 2300)
-
-      assertEquals(2100, throttleTime, "Should be throttled")
-      throttle(clientQuotaManager, "ANONYMOUS", "unknown", throttleTime, 
callback)
-      assertEquals(1, queueSizeMetric.metricValue.asInstanceOf[Double].toInt)
-      // After a request is delayed, the callback cannot be triggered 
immediately
-      clientQuotaManager.processThrottledChannelReaperDoWork
-      assertEquals(0, numCallbacks)
-      time.sleep(throttleTime)
-
-      // Callback can only be triggered after the delay time passes
-      clientQuotaManager.processThrottledChannelReaperDoWork()
-      assertEquals(0, queueSizeMetric.metricValue.asInstanceOf[Double].toInt)
-      assertEquals(1, numCallbacks)
-
-      // Could continue to see delays until the bursty sample disappears
-      for (_ <- 0 until 10) {
-        maybeRecord(clientQuotaManager, "ANONYMOUS", "unknown", 400)
-        time.sleep(1000)
-      }
-
-      assertEquals(0, maybeRecord(clientQuotaManager, "ANONYMOUS", "unknown", 
0),
-        "Should be unthrottled since bursty sample has rolled over")
-    } finally {
-      clientQuotaManager.shutdown()
-    }
-  }
-
-  @Test
-  def testExpireThrottleTimeSensor(): Unit = {
-    val clientQuotaManager = new ClientQuotaManager(config, metrics, 
QuotaType.PRODUCE, time, "")
-    try {
-      clientQuotaManager.updateQuota(
-        Optional.empty,
-        Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
-        Optional.of(new Quota(500, true))
-      )
-
-      maybeRecord(clientQuotaManager, "ANONYMOUS", "client1", 100)
-      // remove the throttle time sensor
-      metrics.removeSensor("ProduceThrottleTime-:client1")
-      // should not throw an exception even if the throttle time sensor does 
not exist.
-      val throttleTime = maybeRecord(clientQuotaManager, "ANONYMOUS", 
"client1", 10000)
-      assertTrue(throttleTime > 0, "Should be throttled")
-      // the sensor should get recreated
-      val throttleTimeSensor = 
metrics.getSensor("ProduceThrottleTime-:client1")
-      assertNotNull(throttleTimeSensor, "Throttle time sensor should exist")
-      assertNotNull(throttleTimeSensor, "Throttle time sensor should exist")
-    } finally {
-      clientQuotaManager.shutdown()
-    }
-  }
-
-  @Test
-  def testExpireQuotaSensors(): Unit = {
-    val clientQuotaManager = new ClientQuotaManager(config, metrics, 
QuotaType.PRODUCE, time, "")
-    try {
-      clientQuotaManager.updateQuota(
-        Optional.empty,
-        Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
-        Optional.of(new Quota(500, true))
-      )
-
-      maybeRecord(clientQuotaManager, "ANONYMOUS", "client1", 100)
-      // remove all the sensors
-      metrics.removeSensor("ProduceThrottleTime-:client1")
-      metrics.removeSensor("Produce-ANONYMOUS:client1")
-      // should not throw an exception
-      val throttleTime = maybeRecord(clientQuotaManager, "ANONYMOUS", 
"client1", 10000)
-      assertTrue(throttleTime > 0, "Should be throttled")
-
-      // all the sensors should get recreated
-      val throttleTimeSensor = 
metrics.getSensor("ProduceThrottleTime-:client1")
-      assertNotNull(throttleTimeSensor, "Throttle time sensor should exist")
-
-      val byteRateSensor = metrics.getSensor("Produce-:client1")
-      assertNotNull(byteRateSensor, "Byte rate sensor should exist")
-    } finally {
-      clientQuotaManager.shutdown()
-    }
-  }
-
-  @Test
-  def testClientIdNotSanitized(): Unit = {
-    val clientQuotaManager = new ClientQuotaManager(config, metrics, 
QuotaType.PRODUCE, time, "")
-    val clientId = "client@#$%"
-    try {
-      clientQuotaManager.updateQuota(
-        Optional.empty,
-        Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
-        Optional.of(new Quota(500, true))
-      )
-
-      maybeRecord(clientQuotaManager, "ANONYMOUS", clientId, 100)
-
-      // The metrics should use the raw client ID, even if the reporters 
internally sanitize them
-      val throttleTimeSensor = metrics.getSensor("ProduceThrottleTime-:" + 
clientId)
-      assertNotNull(throttleTimeSensor, "Throttle time sensor should exist")
-
-      val byteRateSensor = metrics.getSensor("Produce-:"  + clientId)
-      assertNotNull(byteRateSensor, "Byte rate sensor should exist")
-    } finally {
-      clientQuotaManager.shutdown()
-    }
-  }
-
-  @Test
-  def testQuotaTypesEnabledUpdatesWithDefaultCallback(): Unit = {
-    val clientQuotaManager = new ClientQuotaManager(config, metrics, 
QuotaType.CONTROLLER_MUTATION, time, "")
-    try {
-      assertEquals(ClientQuotaManager.NO_QUOTAS, 
clientQuotaManager.quotaTypesEnabled())
-      assertFalse(clientQuotaManager.quotasEnabled)
-
-      clientQuotaManager.updateQuota(Optional.empty(), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.of(new Quota(5, true)))
-      assertEquals(ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED, 
clientQuotaManager.quotaTypesEnabled())
-      assertTrue(clientQuotaManager.quotasEnabled)
-
-      clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.empty(), Optional.of(new 
Quota(5, true)))
-      assertEquals(ClientQuotaManager.USER_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED, 
clientQuotaManager.quotaTypesEnabled())
-      assertTrue(clientQuotaManager.quotasEnabled)
-
-      clientQuotaManager.updateQuota(Optional.empty(), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client2")), Optional.of(new Quota(5, true)))
-      assertEquals(ClientQuotaManager.USER_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED, 
clientQuotaManager.quotaTypesEnabled())
-      assertTrue(clientQuotaManager.quotasEnabled)
-
-      clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userB")), Optional.empty(), Optional.of(new 
Quota(5, true)))
-      assertEquals(ClientQuotaManager.USER_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED, 
clientQuotaManager.quotaTypesEnabled())
-      assertTrue(clientQuotaManager.quotasEnabled)
-
-      clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.of(new Quota(10, true)))
-      assertEquals(ClientQuotaManager.USER_CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.USER_QUOTA_ENABLED, clientQuotaManager.quotaTypesEnabled())
-      assertTrue(clientQuotaManager.quotasEnabled)
-
-      clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.of(new Quota(12, true)))
-      assertEquals(ClientQuotaManager.USER_CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.USER_QUOTA_ENABLED, clientQuotaManager.quotaTypesEnabled)
-      assertTrue(clientQuotaManager.quotasEnabled)
-
-      clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.empty(), Optional.empty())
-      assertEquals(ClientQuotaManager.USER_CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.USER_QUOTA_ENABLED, clientQuotaManager.quotaTypesEnabled)
-      assertTrue(clientQuotaManager.quotasEnabled)
-
-      clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userB")), Optional.empty(), Optional.empty())
-      assertEquals(ClientQuotaManager.USER_CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED, 
clientQuotaManager.quotaTypesEnabled)
-      assertTrue(clientQuotaManager.quotasEnabled)
-
-      clientQuotaManager.updateQuota(Optional.empty(), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.empty())
-      assertEquals(ClientQuotaManager.USER_CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED, 
clientQuotaManager.quotaTypesEnabled)
-      assertTrue(clientQuotaManager.quotasEnabled)
-
-      clientQuotaManager.updateQuota(Optional.empty(), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client2")), Optional.empty())
-      assertEquals(ClientQuotaManager.USER_CLIENT_ID_QUOTA_ENABLED, 
clientQuotaManager.quotaTypesEnabled)
-      assertTrue(clientQuotaManager.quotasEnabled)
-
-      clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.empty())
-      assertEquals(ClientQuotaManager.NO_QUOTAS, 
clientQuotaManager.quotaTypesEnabled)
-      assertFalse(clientQuotaManager.quotasEnabled)
-
-      clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.empty())
-      assertEquals(ClientQuotaManager.NO_QUOTAS, 
clientQuotaManager.quotaTypesEnabled)
-      assertFalse(clientQuotaManager.quotasEnabled)
-    } finally {
-      clientQuotaManager.shutdown()
-    }
-  }
-
-  @Test
-  def testQuotaTypesEnabledUpdatesWithCustomCallback(): Unit = {
-    val customQuotaCallback = new ClientQuotaCallback {
-      val quotas = new HashMap[ClientQuotaEntity, Quota]()
-      override def configure(configs: Map[String, _]): Unit = {}
-
-      override def quotaMetricTags(quotaType: ClientQuotaType, principal: 
KafkaPrincipal, clientId: String): Map[String, String] = Collections.emptyMap()
-
-      override def quotaLimit(quotaType: ClientQuotaType, metricTags: 
Map[String, String]): java.lang.Double = 1
-
-      override def updateQuota(quotaType: ClientQuotaType, entity: 
ClientQuotaEntity, newValue: Double): Unit = {
-        quotas.put(entity.asInstanceOf[ClientQuotaManager.KafkaQuotaEntity], 
new Quota(newValue.toLong, true))
-      }
-
-      override def removeQuota(quotaType: ClientQuotaType, entity: 
ClientQuotaEntity): Unit = {
-        quotas.remove(entity.asInstanceOf[ClientQuotaManager.KafkaQuotaEntity])
-      }
-
-      override def quotaResetRequired(quotaType: ClientQuotaType): Boolean = 
false
-
-      override def close(): Unit = {}
-    }
-    val clientQuotaManager = new ClientQuotaManager(
-      new ClientQuotaManagerConfig(),
-      metrics,
-      QuotaType.CONTROLLER_MUTATION,
-      time,
-      "",
-      Optional.of(Plugin.wrapInstance(customQuotaCallback, metrics, ""))
-    )
-
-    try {
-      assertEquals(ClientQuotaManager.CUSTOM_QUOTAS, 
clientQuotaManager.quotaTypesEnabled)
-      assertTrue(clientQuotaManager.quotasEnabled, "quotasEnabled should be 
true with custom callback")
-
-      clientQuotaManager.updateQuota(Optional.empty(), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.of(new Quota(12, true)))
-      assertEquals(ClientQuotaManager.CUSTOM_QUOTAS, 
clientQuotaManager.quotaTypesEnabled)
-
-      clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.empty(), Optional.of(new 
Quota(12, true)))
-      assertEquals(ClientQuotaManager.CUSTOM_QUOTAS, 
clientQuotaManager.quotaTypesEnabled)
-      assertTrue(clientQuotaManager.quotasEnabled, "quotasEnabled should 
remain true")
-
-      clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.of(new Quota(12, true)))
-      assertEquals(ClientQuotaManager.CUSTOM_QUOTAS, 
clientQuotaManager.quotaTypesEnabled())
-      assertTrue(clientQuotaManager.quotasEnabled, "quotasEnabled should 
remain true")
-
-      clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.empty())
-      clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.empty(), Optional.empty())
-      clientQuotaManager.updateQuota(Optional.empty(), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.empty())
-      assertEquals(ClientQuotaManager.CUSTOM_QUOTAS, 
clientQuotaManager.quotaTypesEnabled())
-      assertTrue(clientQuotaManager.quotasEnabled, "quotasEnabled should 
remain true")
-    } finally {
-      clientQuotaManager.shutdown()
-    }
-  }
-
-  private case class UserClient(
-    user: String,
-    clientId: String,
-    configUser: Optional[ClientQuotaEntity.ConfigEntity] = Optional.empty,
-    configClientEntity: Optional[ClientQuotaEntity.ConfigEntity] = 
Optional.empty
-  )
-}
diff --git 
a/core/src/test/scala/unit/kafka/server/ClientRequestQuotaManagerTest.scala 
b/core/src/test/scala/unit/kafka/server/ClientRequestQuotaManagerTest.scala
deleted file mode 100644
index 9b8e85c44e7..00000000000
--- a/core/src/test/scala/unit/kafka/server/ClientRequestQuotaManagerTest.scala
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * 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 kafka.server
-
-import org.apache.kafka.common.metrics.Quota
-import org.apache.kafka.server.config.ClientQuotaManagerConfig
-import org.apache.kafka.server.quota.{ClientQuotaManager, QuotaType}
-import org.apache.kafka.server.quota.ClientQuotaEntity
-import org.junit.jupiter.api.Assertions._
-import org.junit.jupiter.api.Test
-
-import java.util.Optional
-
-class ClientRequestQuotaManagerTest extends BaseClientQuotaManagerTest {
-  private val config = new ClientQuotaManagerConfig()
-
-  @Test
-  def testRequestPercentageQuotaViolation(): Unit = {
-    val clientRequestQuotaManager = new ClientRequestQuotaManager(config, 
metrics, time, "", Optional.empty())
-    val userEntity: ClientQuotaEntity.ConfigEntity = new 
ClientQuotaManager.UserEntity("ANONYMOUS")
-    val clientEntity: ClientQuotaEntity.ConfigEntity = new 
ClientQuotaManager.ClientIdEntity("test-client")
-
-    clientRequestQuotaManager.updateQuota(
-      Optional.of(userEntity),
-      Optional.of(clientEntity),
-      Optional.of(Quota.upperBound(1))
-    )
-    val queueSizeMetric = 
metrics.metrics().get(metrics.metricName("queue-size", 
QuotaType.REQUEST.toString, ""))
-    def millisToPercent(millis: Double) = millis * 1000 * 1000 * 
ClientRequestQuotaManager.NANOS_TO_PERCENTAGE_PER_SECOND
-    try {
-      // We have 10 seconds windows. Make sure that there is no quota violation
-      // if we are under the quota
-      for (_ <- 0 until 10) {
-        assertEquals(0, maybeRecord(clientRequestQuotaManager, "ANONYMOUS", 
"test-client", millisToPercent(4)))
-        time.sleep(1000)
-      }
-      assertEquals(0, queueSizeMetric.metricValue.asInstanceOf[Double].toInt)
-
-      // Create a spike.
-      // quota = 1% (10ms per second)
-      // 4*10 + 67.1 = 107.1/10.5 = 10.2ms per second.
-      // (10.2 - quota)/quota*window-size = (10.2-10)/10*10.5 seconds = 210ms
-      // 10.5 seconds interval because the last window is half complete
-      time.sleep(500)
-      val throttleTime = maybeRecord(clientRequestQuotaManager, "ANONYMOUS", 
"test-client", millisToPercent(67.1))
-
-      assertEquals(210, throttleTime, "Should be throttled")
-
-      throttle(clientRequestQuotaManager, "ANONYMOUS", "test-client", 
throttleTime, callback)
-      assertEquals(1, queueSizeMetric.metricValue.asInstanceOf[Double].toInt)
-      // After a request is delayed, the callback cannot be triggered 
immediately
-      clientRequestQuotaManager.processThrottledChannelReaperDoWork()
-      assertEquals(0, numCallbacks)
-      time.sleep(throttleTime)
-
-      // Callback can only be triggered after the delay time passes
-      clientRequestQuotaManager.processThrottledChannelReaperDoWork()
-      assertEquals(0, queueSizeMetric.metricValue.asInstanceOf[Double].toInt)
-      assertEquals(1, numCallbacks)
-
-      // Could continue to see delays until the bursty sample disappears
-      for (_ <- 0 until 11) {
-        maybeRecord(clientRequestQuotaManager, "ANONYMOUS", "test-client", 
millisToPercent(4))
-        time.sleep(1000)
-      }
-
-      assertEquals(0,
-        maybeRecord(clientRequestQuotaManager, "ANONYMOUS", "test-client", 0), 
"Should be unthrottled since bursty sample has rolled over")
-
-      // Create a very large spike which requires > one quota window to bring 
within quota
-      assertEquals(1000, maybeRecord(clientRequestQuotaManager, "ANONYMOUS", 
"test-client", millisToPercent(500)))
-      for (_ <- 0 until 10) {
-        time.sleep(1000)
-        assertEquals(1000, maybeRecord(clientRequestQuotaManager, "ANONYMOUS", 
"test-client", 0))
-      }
-      time.sleep(1000)
-      assertEquals(0,
-        maybeRecord(clientRequestQuotaManager, "ANONYMOUS", "test-client", 0), 
"Should be unthrottled since bursty sample has rolled over")
-
-    } finally {
-      clientRequestQuotaManager.shutdown()
-    }
-  }
-}
-
diff --git a/core/src/test/scala/unit/kafka/server/ControllerApisTest.scala 
b/core/src/test/scala/unit/kafka/server/ControllerApisTest.scala
index fd189d1c95e..da1d4d2112a 100644
--- a/core/src/test/scala/unit/kafka/server/ControllerApisTest.scala
+++ b/core/src/test/scala/unit/kafka/server/ControllerApisTest.scala
@@ -59,7 +59,7 @@ import org.apache.kafka.server.SimpleApiVersionManager
 import org.apache.kafka.server.authorizer.{Action, AuthorizableRequestContext, 
AuthorizationResult, Authorizer}
 import org.apache.kafka.server.common.{ApiMessageAndVersion, 
FinalizedFeatures, KRaftVersion, MetadataVersion, ProducerIdsBlock, 
RequestLocal}
 import org.apache.kafka.server.config.ServerConfigs
-import org.apache.kafka.server.quota.{ClientQuotaManager, 
ControllerMutationQuota, ControllerMutationQuotaManager, 
ReplicationQuotaManager}
+import org.apache.kafka.server.quota.{ClientQuotaManager, 
ClientRequestQuotaManager, ControllerMutationQuota, 
ControllerMutationQuotaManager, ReplicationQuotaManager}
 import org.apache.kafka.storage.internals.log.CleanerConfig
 import org.apache.kafka.test.TestUtils
 import org.junit.jupiter.api.Assertions._
diff --git 
a/core/src/test/scala/unit/kafka/server/ControllerMutationQuotaManagerTest.scala
 
b/core/src/test/scala/unit/kafka/server/ControllerMutationQuotaManagerTest.scala
deleted file mode 100644
index a40087a5973..00000000000
--- 
a/core/src/test/scala/unit/kafka/server/ControllerMutationQuotaManagerTest.scala
+++ /dev/null
@@ -1,243 +0,0 @@
-/**
- * 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 kafka.server
-
-import java.util.concurrent.TimeUnit
-import org.apache.kafka.common.errors.ThrottlingQuotaExceededException
-import org.apache.kafka.common.metrics.MetricConfig
-import org.apache.kafka.common.metrics.Metrics
-import org.apache.kafka.common.metrics.Quota
-import org.apache.kafka.common.metrics.QuotaViolationException
-import org.apache.kafka.common.metrics.stats.TokenBucket
-import org.apache.kafka.common.utils.MockTime
-import org.apache.kafka.server.config.ClientQuotaManagerConfig
-import org.apache.kafka.server.quota.{ClientQuotaManager, 
ControllerMutationQuota, ControllerMutationQuotaManager, 
PermissiveControllerMutationQuota, QuotaType, StrictControllerMutationQuota}
-import org.junit.jupiter.api.Assertions._
-import org.junit.jupiter.api.Assertions.assertEquals
-import org.junit.jupiter.api.Assertions.assertFalse
-import org.junit.jupiter.api.Test
-
-import java.util.Optional
-
-class StrictControllerMutationQuotaTest {
-  @Test
-  def testControllerMutationQuotaViolation(): Unit = {
-    val time = new MockTime(0, System.currentTimeMillis, 0)
-    val metrics = new Metrics(time)
-    val sensor = metrics.sensor("sensor", new MetricConfig()
-      .quota(Quota.upperBound(10))
-      .timeWindow(1, TimeUnit.SECONDS)
-      .samples(10))
-    val metricName = metrics.metricName("rate", "test-group")
-    assertTrue(sensor.add(metricName, new TokenBucket))
-
-    val quota = new StrictControllerMutationQuota(time, sensor)
-    assertFalse(quota.isExceeded)
-
-    // Recording a first value at T to bring the tokens to 10. Value is 
accepted
-    // because the quota is not exhausted yet.
-    quota.record(90)
-    assertFalse(quota.isExceeded)
-    assertEquals(0, quota.throttleTime)
-
-    // Recording a second value at T to bring the tokens to -80. Value is 
accepted
-    quota.record(90)
-    assertFalse(quota.isExceeded)
-    assertEquals(0, quota.throttleTime)
-
-    // Recording a third value at T is rejected immediately because there are 
not
-    // tokens available in the bucket.
-    assertThrows(classOf[ThrottlingQuotaExceededException], () => 
quota.record(90))
-    assertTrue(quota.isExceeded)
-    assertEquals(8000, quota.throttleTime)
-
-    // Throttle time is adjusted with time
-    time.sleep(5000)
-    assertEquals(3000, quota.throttleTime)
-
-    metrics.close()
-  }
-}
-
-class PermissiveControllerMutationQuotaTest {
-  @Test
-  def testControllerMutationQuotaViolation(): Unit = {
-    val time = new MockTime(0, System.currentTimeMillis, 0)
-    val metrics = new Metrics(time)
-    val sensor = metrics.sensor("sensor", new MetricConfig()
-      .quota(Quota.upperBound(10))
-      .timeWindow(1, TimeUnit.SECONDS)
-      .samples(10))
-    val metricName = metrics.metricName("rate", "test-group")
-    assertTrue(sensor.add(metricName, new TokenBucket))
-
-    val quota = new PermissiveControllerMutationQuota(time, sensor)
-    assertFalse(quota.isExceeded)
-
-    // Recording a first value at T to bring the tokens 10. Value is accepted
-    // because the quota is not exhausted yet.
-    quota.record(90)
-    assertFalse(quota.isExceeded)
-    assertEquals(0, quota.throttleTime)
-
-    // Recording a second value at T to bring the tokens to -80. Value is 
accepted
-    quota.record(90)
-    assertFalse(quota.isExceeded)
-    assertEquals(8000, quota.throttleTime)
-
-    // Recording a second value at T to bring the tokens to -170. Value is 
accepted
-    // even though the quota is exhausted.
-    quota.record(90)
-    assertFalse(quota.isExceeded) // quota is never exceeded
-    assertEquals(17000, quota.throttleTime)
-
-    // Throttle time is adjusted with time
-    time.sleep(5000)
-    assertEquals(12000, quota.throttleTime)
-
-    metrics.close()
-  }
-}
-
-class ControllerMutationQuotaManagerTest extends BaseClientQuotaManagerTest {
-  private val User = "ANONYMOUS"
-  private val ClientId = "test-client"
-
-  private val config = new ClientQuotaManagerConfig(10, 1)
-
-  private def withQuotaManager(f: ControllerMutationQuotaManager => Unit): 
Unit = {
-    val quotaManager = new ControllerMutationQuotaManager(config, metrics, 
time,"", Optional.empty())
-    try {
-      f(quotaManager)
-    } finally {
-      quotaManager.shutdown()
-    }
-  }
-
-  @Test
-  def testThrottleTime(): Unit = {
-    import ControllerMutationQuotaManager._
-
-    val time = new MockTime(0, System.currentTimeMillis, 0)
-    val metrics = new Metrics(time)
-    val sensor = metrics.sensor("sensor")
-    val metricName = metrics.metricName("tokens", "test-group")
-    sensor.add(metricName, new TokenBucket)
-    val metric = metrics.metric(metricName)
-
-    assertEquals(0, throttleTimeMs(new QuotaViolationException(metric, 0, 10)))
-    assertEquals(500, throttleTimeMs(new QuotaViolationException(metric, -5, 
10)))
-    assertEquals(1000, throttleTimeMs(new QuotaViolationException(metric, -10, 
10)))
-  }
-
-  @Test
-  def testControllerMutationQuotaViolation(): Unit = {
-    withQuotaManager { quotaManager =>
-      quotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity(User)),
-        Optional.of(new ClientQuotaManager.ClientIdEntity(ClientId)),
-        Optional.of(Quota.upperBound(10))
-      )
-      val queueSizeMetric = metrics.metrics().get(
-        metrics.metricName("queue-size", 
QuotaType.CONTROLLER_MUTATION.toString, ""))
-
-      // Verify that there is no quota violation if we remain under the quota.
-      for (_ <- 0 until 10) {
-        assertEquals(0, maybeRecord(quotaManager, User, ClientId, 10))
-        time.sleep(1000)
-      }
-      assertEquals(0, queueSizeMetric.metricValue.asInstanceOf[Double].toInt)
-
-      // Create a spike worth of 110 mutations.
-      // Current tokens in the bucket = 100
-      // As we use the Strict enforcement, the quota is checked before 
updating the rate. Hence,
-      // the spike is accepted and no quota violation error is raised.
-      var throttleTime = maybeRecord(quotaManager, User, ClientId, 110)
-      assertEquals(0, throttleTime, "Should not be throttled")
-
-      // Create a spike worth of 110 mutations.
-      // Current tokens in the bucket = 100 - 110 = -10
-      // As the quota is already violated, the spike is rejected immediately 
without updating the
-      // rate. The client must wait:
-      // 10 / 10 = 1s
-      throttleTime = maybeRecord(quotaManager, User, ClientId, 110)
-      assertEquals(1000, throttleTime, "Should be throttled")
-
-      // Throttle
-      throttle(quotaManager, User, ClientId, throttleTime, callback)
-      assertEquals(1, queueSizeMetric.metricValue.asInstanceOf[Double].toInt)
-
-      // After a request is delayed, the callback cannot be triggered 
immediately
-      quotaManager.processThrottledChannelReaperDoWork()
-      assertEquals(0, numCallbacks)
-
-      // Callback can only be triggered after the delay time passes
-      time.sleep(throttleTime)
-      quotaManager.processThrottledChannelReaperDoWork()
-      assertEquals(0, queueSizeMetric.metricValue.asInstanceOf[Double].toInt)
-      assertEquals(1, numCallbacks)
-
-      // Retry to spike worth of 110 mutations after having waited the 
required throttle time.
-      // Current tokens in the bucket = 0
-      throttleTime = maybeRecord(quotaManager, User, ClientId, 110)
-      assertEquals(0, throttleTime, "Should be throttled")
-    }
-  }
-
-  @Test
-  def testNewStrictQuotaForReturnsUnboundedQuotaWhenQuotaIsDisabled(): Unit = {
-    withQuotaManager { quotaManager =>
-      assertEquals(ControllerMutationQuota.UNBOUNDED_CONTROLLER_MUTATION_QUOTA,
-        quotaManager.newStrictQuotaFor(buildSession(User), ClientId))
-    }
-  }
-
-  @Test
-  def testNewStrictQuotaForReturnsStrictQuotaWhenQuotaIsEnabled(): Unit = {
-    withQuotaManager { quotaManager =>
-      quotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity(User)),
-        Optional.of(new ClientQuotaManager.ClientIdEntity(ClientId)),
-        Optional.of(Quota.upperBound(10))
-      )
-      val quota = quotaManager.newStrictQuotaFor(buildSession(User), ClientId)
-      assertTrue(quota.isInstanceOf[StrictControllerMutationQuota])
-
-    }
-  }
-
-  @Test
-  def testNewPermissiveQuotaForReturnsUnboundedQuotaWhenQuotaIsDisabled(): 
Unit = {
-    withQuotaManager { quotaManager =>
-      assertEquals(ControllerMutationQuota.UNBOUNDED_CONTROLLER_MUTATION_QUOTA,
-        quotaManager.newPermissiveQuotaFor(buildSession(User), ClientId))
-    }
-  }
-
-  @Test
-  def testNewPermissiveQuotaForReturnsStrictQuotaWhenQuotaIsEnabled(): Unit = {
-    withQuotaManager { quotaManager =>
-      quotaManager.updateQuota(
-        Optional.of(new ClientQuotaManager.UserEntity(User)),
-        Optional.of(new ClientQuotaManager.ClientIdEntity(ClientId)),
-        Optional.of(Quota.upperBound(10))
-      )
-      val quota = quotaManager.newPermissiveQuotaFor(buildSession(User), 
ClientId)
-      assertTrue(quota.isInstanceOf[PermissiveControllerMutationQuota])
-    }
-  }
-}
diff --git a/core/src/test/scala/unit/kafka/server/KafkaApisTest.scala 
b/core/src/test/scala/unit/kafka/server/KafkaApisTest.scala
index 6310d0a2f6c..f5d838edf83 100644
--- a/core/src/test/scala/unit/kafka/server/KafkaApisTest.scala
+++ b/core/src/test/scala/unit/kafka/server/KafkaApisTest.scala
@@ -99,7 +99,7 @@ import org.apache.kafka.server.config.{ReplicationConfigs, 
ServerConfigs, Server
 import org.apache.kafka.server.logger.LoggingController
 import org.apache.kafka.server.metrics.{ClientMetricsTestUtils, 
KafkaYammerMetrics}
 import org.apache.kafka.server.share.{CachedSharePartition, 
ErroneousAndValidPartitionData, SharePartitionKey}
-import org.apache.kafka.server.quota.{ClientQuotaManager, 
ControllerMutationQuota, ControllerMutationQuotaManager, ReplicaQuota, 
ReplicationQuotaManager, ThrottleCallback}
+import org.apache.kafka.server.quota.{ClientQuotaManager, 
ClientRequestQuotaManager, ControllerMutationQuota, 
ControllerMutationQuotaManager, ReplicaQuota, ReplicationQuotaManager, 
ThrottleCallback}
 import org.apache.kafka.server.share.acknowledge.ShareAcknowledgementBatch
 import org.apache.kafka.server.share.context.{FinalContext, 
ShareSessionContext}
 import org.apache.kafka.server.share.session.{ShareSession, ShareSessionKey}
diff --git 
a/jmh-benchmarks/src/main/java/org/apache/kafka/jmh/metadata/KRaftMetadataRequestBenchmark.java
 
b/jmh-benchmarks/src/main/java/org/apache/kafka/jmh/metadata/KRaftMetadataRequestBenchmark.java
index 227951f075c..0dad41f0f87 100644
--- 
a/jmh-benchmarks/src/main/java/org/apache/kafka/jmh/metadata/KRaftMetadataRequestBenchmark.java
+++ 
b/jmh-benchmarks/src/main/java/org/apache/kafka/jmh/metadata/KRaftMetadataRequestBenchmark.java
@@ -19,7 +19,6 @@ package org.apache.kafka.jmh.metadata;
 
 import kafka.coordinator.transaction.TransactionCoordinator;
 import kafka.network.RequestChannel;
-import kafka.server.ClientRequestQuotaManager;
 import kafka.server.KafkaApis;
 import kafka.server.KafkaConfig;
 import kafka.server.QuotaFactory;
@@ -64,6 +63,7 @@ import org.apache.kafka.server.common.FinalizedFeatures;
 import org.apache.kafka.server.common.KRaftVersion;
 import org.apache.kafka.server.common.MetadataVersion;
 import org.apache.kafka.server.quota.ClientQuotaManager;
+import org.apache.kafka.server.quota.ClientRequestQuotaManager;
 import org.apache.kafka.server.quota.ControllerMutationQuotaManager;
 import org.apache.kafka.server.quota.ReplicationQuotaManager;
 import org.apache.kafka.storage.log.metrics.BrokerTopicStats;
diff --git a/core/src/main/java/kafka/server/ClientRequestQuotaManager.java 
b/server/src/main/java/org/apache/kafka/server/quota/ClientRequestQuotaManager.java
similarity index 95%
rename from core/src/main/java/kafka/server/ClientRequestQuotaManager.java
rename to 
server/src/main/java/org/apache/kafka/server/quota/ClientRequestQuotaManager.java
index e685b6aa50a..b28ba68d9ab 100644
--- a/core/src/main/java/kafka/server/ClientRequestQuotaManager.java
+++ 
b/server/src/main/java/org/apache/kafka/server/quota/ClientRequestQuotaManager.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package kafka.server;
+package org.apache.kafka.server.quota;
 
 import org.apache.kafka.common.MetricName;
 import org.apache.kafka.common.internals.Plugin;
@@ -25,10 +25,6 @@ import org.apache.kafka.common.metrics.stats.Rate;
 import org.apache.kafka.common.utils.Time;
 import org.apache.kafka.network.Request;
 import org.apache.kafka.server.config.ClientQuotaManagerConfig;
-import org.apache.kafka.server.quota.ClientQuotaCallback;
-import org.apache.kafka.server.quota.ClientQuotaManager;
-import org.apache.kafka.server.quota.QuotaType;
-import org.apache.kafka.server.quota.QuotaUtils;
 
 import java.util.Map;
 import java.util.Optional;
diff --git 
a/server/src/test/java/org/apache/kafka/server/quota/BaseClientQuotaManagerTest.java
 
b/server/src/test/java/org/apache/kafka/server/quota/BaseClientQuotaManagerTest.java
new file mode 100644
index 00000000000..c01ed490a67
--- /dev/null
+++ 
b/server/src/test/java/org/apache/kafka/server/quota/BaseClientQuotaManagerTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.server.quota;
+
+import org.apache.kafka.common.memory.MemoryPool;
+import org.apache.kafka.common.metrics.MetricConfig;
+import org.apache.kafka.common.metrics.Metrics;
+import org.apache.kafka.common.network.ClientInformation;
+import org.apache.kafka.common.network.ListenerName;
+import org.apache.kafka.common.protocol.ApiKeys;
+import org.apache.kafka.common.requests.AbstractRequest;
+import org.apache.kafka.common.requests.FetchRequest;
+import org.apache.kafka.common.requests.RequestContext;
+import org.apache.kafka.common.requests.RequestHeader;
+import org.apache.kafka.common.security.auth.KafkaPrincipal;
+import org.apache.kafka.common.security.auth.SecurityProtocol;
+import org.apache.kafka.common.utils.MockTime;
+import org.apache.kafka.network.Request;
+import org.apache.kafka.network.Session;
+import org.apache.kafka.network.metrics.RequestChannelMetrics;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Mockito.mock;
+
+public class BaseClientQuotaManagerTest {
+
+    protected final MockTime time = new MockTime();
+    protected int numCallbacks = 0;
+    protected Metrics metrics;
+
+    @BeforeEach
+    public void setup() {
+        metrics = new Metrics(new MetricConfig(), List.of(), time);
+    }
+
+    @AfterEach
+    public void tearDown() {
+        metrics.close();
+    }
+
+    protected final ThrottleCallback callback = new ThrottleCallback() {
+        @Override
+        public void startThrottling() {}
+
+        @Override
+        public void endThrottling() {
+            // Count how many times this callback is called for 
notifyThrottlingDone().
+            numCallbacks += 1;
+        }
+    };
+
+    protected Session buildSession(String user) {
+        KafkaPrincipal principal = new 
KafkaPrincipal(KafkaPrincipal.USER_TYPE, user);
+        return new Session(principal, null);
+    }
+
+    protected int maybeRecord(ClientQuotaManager quotaManager, String user, 
String clientId, double value) {
+        return 
quotaManager.maybeRecordAndGetThrottleTimeMs(buildSession(user), clientId, 
value, time.milliseconds());
+    }
+
+    protected void throttle(ClientQuotaManager quotaManager, int 
throttleTimeMs, ThrottleCallback channelThrottlingCallback) {
+        AbstractRequest.Builder<FetchRequest> builder = 
FetchRequest.Builder.forConsumer(ApiKeys.FETCH.latestVersion(), 0, 1000, 
Map.of());
+        FetchRequest fetchRequest = builder.build();
+        ByteBuffer buffer = fetchRequest.serializeWithHeader(new 
RequestHeader(builder.apiKey(), fetchRequest.version(), "", 0));
+        RequestChannelMetrics requestChannelMetrics = 
mock(RequestChannelMetrics.class);
+
+        // read the header from the buffer first so that the body can be read 
next from the Request constructor
+        RequestHeader header = RequestHeader.parse(buffer);
+        RequestContext context = new RequestContext(header, "1", 
assertDoesNotThrow(InetAddress::getLocalHost), KafkaPrincipal.ANONYMOUS,
+                ListenerName.forSecurityProtocol(SecurityProtocol.PLAINTEXT), 
SecurityProtocol.PLAINTEXT, ClientInformation.EMPTY, false);
+        Request request = new Request(1, context, 0, MemoryPool.NONE, buffer, 
requestChannelMetrics);
+        quotaManager.throttle(request.header().clientId(), request.session(), 
channelThrottlingCallback, throttleTimeMs);
+    }
+
+}
diff --git 
a/server/src/test/java/org/apache/kafka/server/quota/ClientQuotaManagerTest.java
 
b/server/src/test/java/org/apache/kafka/server/quota/ClientQuotaManagerTest.java
new file mode 100644
index 00000000000..ac841e47ec9
--- /dev/null
+++ 
b/server/src/test/java/org/apache/kafka/server/quota/ClientQuotaManagerTest.java
@@ -0,0 +1,620 @@
+/*
+ * 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.server.quota;
+
+import org.apache.kafka.common.internals.Plugin;
+import org.apache.kafka.common.metrics.KafkaMetric;
+import org.apache.kafka.common.metrics.Quota;
+import org.apache.kafka.common.metrics.Sensor;
+import org.apache.kafka.common.security.auth.KafkaPrincipal;
+import org.apache.kafka.network.Session;
+import org.apache.kafka.server.config.ClientQuotaManagerConfig;
+
+import org.junit.jupiter.api.Test;
+
+import java.net.InetAddress;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ClientQuotaManagerTest extends BaseClientQuotaManagerTest {
+
+    private final ClientQuotaManagerConfig config = new 
ClientQuotaManagerConfig();
+
+    private void withQuotaManager(QuotaType quotaType, 
ClientQuotaManagerConfig config, Consumer<ClientQuotaManager> f) {
+        ClientQuotaManager quotaManager = new ClientQuotaManager(config, 
metrics, quotaType, time, "");
+        try {
+            f.accept(quotaManager);
+        } finally {
+            quotaManager.shutdown();
+        }
+    }
+
+    private void withQuotaManager(QuotaType quotaType, 
Consumer<ClientQuotaManager> f) {
+        withQuotaManager(quotaType, config, f);
+    }
+
+    private void testQuotaParsing(ClientQuotaManagerConfig config, UserClient 
client1, UserClient client2, UserClient randomClient, UserClient 
defaultConfigClient) {
+        withQuotaManager(QuotaType.PRODUCE, clientQuotaManager -> {
+            // Case 1: Update the quota. Assert that the new quota value is 
returned
+            clientQuotaManager.updateQuota(
+                    client1.configUser,
+                    client1.configClientEntity,
+                    Optional.of(new Quota(2000, true))
+            );
+            clientQuotaManager.updateQuota(
+                    client2.configUser,
+                    client2.configClientEntity,
+                    Optional.of(new Quota(4000, true))
+            );
+
+            assertEquals(Long.MAX_VALUE, 
clientQuotaManager.quota(randomClient.user, randomClient.clientId).bound(), 0.0,
+                    "Default producer quota should be " + Long.MAX_VALUE);
+            assertEquals(2000, clientQuotaManager.quota(client1.user, 
client1.clientId).bound(), 0.0,
+                    "Should return the overridden value (2000)");
+            assertEquals(4000, clientQuotaManager.quota(client2.user, 
client2.clientId).bound(), 0.0,
+                    "Should return the overridden value (4000)");
+
+            // p1 should be throttled using the overridden quota
+            int throttleTimeMs = maybeRecord(clientQuotaManager, client1.user, 
client1.clientId, 2500 * config.numQuotaSamples());
+            assertTrue(throttleTimeMs > 0, "throttleTimeMs should be > 0. was 
" + throttleTimeMs);
+
+            // Case 2: Change quota again. The quota should be updated within 
KafkaMetrics as well since the sensor was created.
+            // p1 should no longer be throttled after the quota change
+            clientQuotaManager.updateQuota(
+                    client1.configUser,
+                    client1.configClientEntity,
+                    Optional.of(new Quota(3000, true))
+            );
+            assertEquals(3000, clientQuotaManager.quota(client1.user, 
client1.clientId).bound(), 0.0, "Should return the newly overridden value 
(3000)");
+
+            throttleTimeMs = maybeRecord(clientQuotaManager, client1.user, 
client1.clientId, 0);
+            assertEquals(0, throttleTimeMs, "throttleTimeMs should be 0. was " 
+ throttleTimeMs);
+
+            // Case 3: Change quota back to default. Should be throttled again
+            clientQuotaManager.updateQuota(
+                    client1.configUser,
+                    client1.configClientEntity,
+                    Optional.of(new Quota(500, true))
+            );
+            assertEquals(500, clientQuotaManager.quota(client1.user, 
client1.clientId).bound(), 0.0, "Should return the default value (500)");
+
+            throttleTimeMs = maybeRecord(clientQuotaManager, client1.user, 
client1.clientId, 0);
+            assertTrue(throttleTimeMs > 0, "throttleTimeMs should be > 0. was 
" + throttleTimeMs);
+
+            // Case 4: Set high default quota, remove p1 quota. p1 should no 
longer be throttled
+            clientQuotaManager.updateQuota(
+                    client1.configUser,
+                    client1.configClientEntity,
+                    Optional.empty()
+            );
+            clientQuotaManager.updateQuota(
+                    defaultConfigClient.configUser,
+                    defaultConfigClient.configClientEntity,
+                    Optional.of(new Quota(4000, true))
+            );
+            assertEquals(4000, clientQuotaManager.quota(client1.user, 
client1.clientId).bound(), 0.0, "Should return the newly overridden value 
(4000)");
+
+            throttleTimeMs = maybeRecord(clientQuotaManager, client1.user, 
client1.clientId, 1000 * config.numQuotaSamples());
+            assertEquals(0, throttleTimeMs, "throttleTimeMs should be 0. was " 
+ throttleTimeMs);
+
+        });
+    }
+
+    /**
+     * Tests parsing for <user> quotas when client-id default quota properties 
are set.
+     */
+    @Test
+    public void testUserQuotaParsingWithDefaultClientIdQuota() {
+        UserClient client1 = new UserClient("User1", "p1", Optional.of(new 
ClientQuotaManager.UserEntity("User1")), Optional.empty());
+        UserClient client2 = new UserClient("User2", "p2", Optional.of(new 
ClientQuotaManager.UserEntity("User2")), Optional.empty());
+        UserClient randomClient = new UserClient("RandomUser", 
"random-client-id", Optional.empty(), Optional.empty());
+        UserClient defaultConfigClient = new UserClient("", "", 
Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY), Optional.empty());
+        testQuotaParsing(config, client1, client2, randomClient, 
defaultConfigClient);
+    }
+
+    private void checkQuota(ClientQuotaManager quotaManager, String user, 
String clientId, long expectedBound, int value, boolean expectThrottle) {
+        assertEquals(expectedBound, quotaManager.quota(user, 
clientId).bound(), 0.0);
+        Session session = new Session(new 
KafkaPrincipal(KafkaPrincipal.USER_TYPE, user), 
assertDoesNotThrow(InetAddress::getLocalHost));
+        double expectedMaxValueInQuotaWindow = expectedBound < Long.MAX_VALUE
+            ? config.quotaWindowSizeSeconds() * (config.numQuotaSamples() - 1) 
* expectedBound
+            : Double.MAX_VALUE;
+        assertEquals(expectedMaxValueInQuotaWindow, 
quotaManager.maxValueInQuotaWindow(session, clientId), 0.01);
+
+        int throttleTimeMs = maybeRecord(quotaManager, user, clientId, value * 
config.numQuotaSamples());
+        if (expectThrottle) {
+            assertTrue(throttleTimeMs > 0, "throttleTimeMs should be > 0. was 
" + throttleTimeMs);
+        } else {
+            assertEquals(0, throttleTimeMs, "throttleTimeMs should be 0. was " 
+ throttleTimeMs);
+        }
+    }
+
+    @Test
+    public void testMaxValueInQuotaWindowWithNonDefaultQuotaWindow() {
+        int numFullQuotaWindows = 3;   // 3 seconds window (vs. 10 seconds 
default)
+        ClientQuotaManagerConfig nonDefaultConfig = new 
ClientQuotaManagerConfig(numFullQuotaWindows + 1);
+        withQuotaManager(QuotaType.FETCH, nonDefaultConfig, clientQuotaManager 
-> {
+            Session userSession = new Session(new 
KafkaPrincipal(KafkaPrincipal.USER_TYPE, "userA"), 
assertDoesNotThrow(InetAddress::getLocalHost));
+
+            // no quota set
+            assertEquals(Double.MAX_VALUE, 
clientQuotaManager.maxValueInQuotaWindow(userSession, "client1"), 0.01);
+
+            // Set default <user> quota config
+            clientQuotaManager.updateQuota(
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY),
+                    Optional.empty(),
+                    Optional.of(new Quota(10, true))
+            );
+            assertEquals(10 * numFullQuotaWindows, 
clientQuotaManager.maxValueInQuotaWindow(userSession, "client1"), 0.01);
+        });
+    }
+
+    @Test
+    public void testSetAndRemoveDefaultUserQuota() {
+        // quotaTypesEnabled will be QuotaTypes.NoQuotas initially
+        withQuotaManager(QuotaType.PRODUCE, clientQuotaManager -> {
+            // no quota set yet, should not throttle
+            checkQuota(clientQuotaManager, "userA", "client1", Long.MAX_VALUE, 
1000, false);
+
+            // Set default <user> quota config
+            clientQuotaManager.updateQuota(
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY),
+                    Optional.empty(),
+                    Optional.of(new Quota(10, true))
+            );
+            checkQuota(clientQuotaManager, "userA", "client1", 10, 1000, true);
+
+            // Remove default <user> quota config, back to no quotas
+            clientQuotaManager.updateQuota(
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY),
+                    Optional.empty(),
+                    Optional.empty()
+            );
+            checkQuota(clientQuotaManager, "userA", "client1", Long.MAX_VALUE, 
1000, false);
+        });
+    }
+
+    @Test
+    public void testSetAndRemoveUserQuota() {
+        withQuotaManager(QuotaType.PRODUCE, clientQuotaManager -> {
+            // Set <user> quota config
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userA")),
+                    Optional.empty(),
+                    Optional.of(new Quota(10, true))
+            );
+            checkQuota(clientQuotaManager, "userA", "client1", 10, 1000, true);
+
+            // Remove <user> quota config, back to no quotas
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userA")),
+                    Optional.empty(),
+                    Optional.empty()
+            );
+            checkQuota(clientQuotaManager, "userA", "client1", Long.MAX_VALUE, 
1000, false);
+        });
+    }
+
+    @Test
+    public void testSetAndRemoveUserClientQuota() {
+        // quotaTypesEnabled will be QuotaTypes.NoQuotas initially
+        withQuotaManager(QuotaType.PRODUCE, clientQuotaManager -> {
+            // Set <user, client-id> quota config
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userA")),
+                    Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")),
+                    Optional.of(new Quota(10, true))
+            );
+            checkQuota(clientQuotaManager, "userA", "client1", 10, 1000, true);
+
+            // Remove <user, client-id> quota config, back to no quotas
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userA")),
+                    Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")),
+                    Optional.empty()
+            );
+            checkQuota(clientQuotaManager, "userA", "client1", Long.MAX_VALUE, 
1000, false);
+        });
+    }
+
+    @Test
+    public void testQuotaConfigPrecedence() {
+        withQuotaManager(QuotaType.PRODUCE, clientQuotaManager -> {
+            clientQuotaManager.updateQuota(
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY),
+                    Optional.empty(),
+                    Optional.of(new Quota(1000, true))
+            );
+            clientQuotaManager.updateQuota(
+                    Optional.empty(),
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
+                    Optional.of(new Quota(2000, true))
+            );
+            clientQuotaManager.updateQuota(
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY),
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
+                    Optional.of(new Quota(3000, true))
+            );
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userA")),
+                    Optional.empty(),
+                    Optional.of(new Quota(4000, true))
+            );
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userA")),
+                    Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")),
+                    Optional.of(new Quota(5000, true))
+            );
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userB")),
+                    Optional.empty(),
+                    Optional.of(new Quota(6000, true))
+            );
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userB")),
+                    Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")),
+                    Optional.of(new Quota(7000, true))
+            );
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userB")),
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
+                    Optional.of(new Quota(8000, true))
+            );
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userC")),
+                    Optional.empty(),
+                    Optional.of(new Quota(10000, true))
+            );
+            clientQuotaManager.updateQuota(
+                    Optional.empty(),
+                    Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")),
+                    Optional.of(new Quota(9000, true))
+            );
+
+            checkQuota(clientQuotaManager, "userA", "client1", 5000, 4500, 
false); // <user, client> quota takes precedence over <user>
+            checkQuota(clientQuotaManager, "userA", "client2", 4000, 4500, 
true);  // <user> quota takes precedence over <client> and defaults
+            checkQuota(clientQuotaManager, "userA", "client3", 4000, 0, true); 
    // <user> quota is shared across clients of user
+            checkQuota(clientQuotaManager, "userA", "client1", 5000, 0, 
false);    // <user, client> is exclusive use, unaffected by other clients
+
+            checkQuota(clientQuotaManager, "userB", "client1", 7000, 8000, 
true);
+            checkQuota(clientQuotaManager, "userB", "client2", 8000, 7000, 
false); // Default per-client quota for exclusive use of <user, client>
+            checkQuota(clientQuotaManager, "userB", "client3", 8000, 7000, 
false);
+
+            checkQuota(clientQuotaManager, "userD", "client1", 3000, 3500, 
true);  // Default <user, client> quota
+            checkQuota(clientQuotaManager, "userD", "client2", 3000, 2500, 
false);
+            checkQuota(clientQuotaManager, "userE", "client1", 3000, 2500, 
false);
+
+            // Remove default <user, client> quota config, revert to <user> 
default
+            clientQuotaManager.updateQuota(
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY),
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
+                    Optional.empty()
+            );
+            checkQuota(clientQuotaManager, "userD", "client1", 1000, 0, 
false);    // Metrics tags changed, restart counter
+            checkQuota(clientQuotaManager, "userE", "client4", 1000, 1500, 
true);
+            checkQuota(clientQuotaManager, "userF", "client4", 1000, 800, 
false);  // Default <user> quota shared across clients of user
+            checkQuota(clientQuotaManager, "userF", "client5", 1000, 800, 
true);
+
+            // Remove default <user> quota config, revert to <client-id> 
default
+            clientQuotaManager.updateQuota(
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_ENTITY),
+                    Optional.empty(),
+                    Optional.empty()
+            );
+            checkQuota(clientQuotaManager, "userF", "client4", 2000, 0, 
false); // Default <client-id> quota shared across client-id of all users
+            checkQuota(clientQuotaManager, "userF", "client5", 2000, 0, false);
+            checkQuota(clientQuotaManager, "userF", "client5", 2000, 2500, 
true);
+            checkQuota(clientQuotaManager, "userG", "client5", 2000, 0, true);
+
+            // Update quotas
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userA")),
+                    Optional.empty(),
+                    Optional.of(new Quota(8000, true))
+            );
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userA")),
+                    Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")),
+                    Optional.of(new Quota(10000, true))
+            );
+            checkQuota(clientQuotaManager, "userA", "client2", 8000, 0, false);
+            checkQuota(clientQuotaManager, "userA", "client2", 8000, 4500, 
true); // Throttled due to sum of new and earlier values
+            checkQuota(clientQuotaManager, "userA", "client1", 10000, 0, 
false);
+            checkQuota(clientQuotaManager, "userA", "client1", 10000, 6000, 
true);
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userA")),
+                    Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")),
+                    Optional.empty()
+            );
+            checkQuota(clientQuotaManager, "userA", "client6", 8000, 0, true); 
  // Throttled due to shared user quota
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userA")),
+                    Optional.of(new 
ClientQuotaManager.ClientIdEntity("client6")),
+                    Optional.of(new Quota(11000, true))
+            );
+            checkQuota(clientQuotaManager, "userA", "client6", 11000, 8500, 
false);
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userA")),
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
+                    Optional.of(new Quota(12000, true))
+            );
+            clientQuotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity("userA")),
+                    Optional.of(new 
ClientQuotaManager.ClientIdEntity("client6")),
+                    Optional.empty()
+            );
+            checkQuota(clientQuotaManager, "userA", "client6", 12000, 4000, 
true); // Throttled due to sum of new and earlier values
+
+        });
+    }
+
+    @Test
+    public void testQuotaViolation() {
+        withQuotaManager(QuotaType.PRODUCE, clientQuotaManager -> {
+            KafkaMetric queueSizeMetric = 
metrics.metrics().get(metrics.metricName("queue-size", "Produce", ""));
+            clientQuotaManager.updateQuota(
+                    Optional.empty(),
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
+                    Optional.of(new Quota(500, true))
+            );
+
+            // We have 10 seconds windows. Make sure that there is no quota 
violation
+            // if we produce under the quota
+            for (int i = 0; i < 10; i++) {
+                assertEquals(0, maybeRecord(clientQuotaManager, "ANONYMOUS", 
"unknown", 400));
+                time.sleep(1000);
+            }
+            assertEquals(0, ((Double) 
queueSizeMetric.metricValue()).intValue());
+
+            // Create a spike.
+            // 400*10 + 2000 + 300 = 6300/10.5 = 600 bytes per second.
+            // (600 - quota)/quota*window-size = (600-500)/500*10.5 seconds = 
2100
+            // 10.5 seconds because the last window is half complete
+            time.sleep(500);
+            int throttleTime = maybeRecord(clientQuotaManager, "ANONYMOUS", 
"unknown", 2300);
+
+            assertEquals(2100, throttleTime, "Should be throttled");
+            throttle(clientQuotaManager, throttleTime, callback);
+            assertEquals(1, ((Double) 
queueSizeMetric.metricValue()).intValue());
+            // After a request is delayed, the callback cannot be triggered 
immediately
+            clientQuotaManager.processThrottledChannelReaperDoWork();
+            assertEquals(0, numCallbacks);
+            time.sleep(throttleTime);
+
+            // Callback can only be triggered after the delay time passes
+            clientQuotaManager.processThrottledChannelReaperDoWork();
+            assertEquals(0, ((Double) 
queueSizeMetric.metricValue()).intValue());
+            assertEquals(1, numCallbacks);
+
+            // Could continue to see delays until the bursty sample disappears
+            for (int i = 0; i < 10; i++) {
+                maybeRecord(clientQuotaManager, "ANONYMOUS", "unknown", 400);
+                time.sleep(1000);
+            }
+
+            assertEquals(0, maybeRecord(clientQuotaManager, "ANONYMOUS", 
"unknown", 0),
+                    "Should be unthrottled since bursty sample has rolled 
over");
+        });
+    }
+
+    @Test
+    public void testExpireThrottleTimeSensor() {
+        withQuotaManager(QuotaType.PRODUCE, clientQuotaManager -> {
+            clientQuotaManager.updateQuota(
+                    Optional.empty(),
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
+                    Optional.of(new Quota(500, true))
+            );
+
+            maybeRecord(clientQuotaManager, "ANONYMOUS", "client1", 100);
+            // remove the throttle time sensor
+            metrics.removeSensor("ProduceThrottleTime-:client1");
+            // should not throw an exception even if the throttle time sensor 
does not exist.
+            int throttleTime = maybeRecord(clientQuotaManager, "ANONYMOUS", 
"client1", 10000);
+            assertTrue(throttleTime > 0, "Should be throttled");
+            // the sensor should get recreated
+            Sensor throttleTimeSensor = 
metrics.getSensor("ProduceThrottleTime-:client1");
+            assertNotNull(throttleTimeSensor, "Throttle time sensor should 
exist");
+        });
+    }
+
+    @Test
+    public void testExpireQuotaSensors() {
+        withQuotaManager(QuotaType.PRODUCE, clientQuotaManager -> {
+            clientQuotaManager.updateQuota(
+                    Optional.empty(),
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
+                    Optional.of(new Quota(500, true))
+            );
+
+            maybeRecord(clientQuotaManager, "ANONYMOUS", "client1", 100);
+            // remove all the sensors
+            metrics.removeSensor("ProduceThrottleTime-:client1");
+            metrics.removeSensor("Produce-ANONYMOUS:client1");
+            // should not throw an exception
+            int throttleTime = maybeRecord(clientQuotaManager, "ANONYMOUS", 
"client1", 10000);
+            assertTrue(throttleTime > 0, "Should be throttled");
+
+            // all the sensors should get recreated
+            Sensor throttleTimeSensor = 
metrics.getSensor("ProduceThrottleTime-:client1");
+            assertNotNull(throttleTimeSensor, "Throttle time sensor should 
exist");
+
+            Sensor byteRateSensor = metrics.getSensor("Produce-:client1");
+            assertNotNull(byteRateSensor, "Byte rate sensor should exist");
+        });
+    }
+
+    @Test
+    public void testClientIdNotSanitized() {
+        withQuotaManager(QuotaType.PRODUCE, clientQuotaManager -> {
+            String clientId = "client@#$%";
+            clientQuotaManager.updateQuota(
+                    Optional.empty(),
+                    Optional.of(ClientQuotaManager.DEFAULT_USER_CLIENT_ID),
+                    Optional.of(new Quota(500, true))
+            );
+
+            maybeRecord(clientQuotaManager, "ANONYMOUS", clientId, 100);
+
+            // The metrics should use the raw client ID, even if the reporters 
internally sanitize them
+            Sensor throttleTimeSensor = 
metrics.getSensor("ProduceThrottleTime-:" + clientId);
+            assertNotNull(throttleTimeSensor, "Throttle time sensor should 
exist");
+
+            Sensor byteRateSensor = metrics.getSensor("Produce-:" + clientId);
+            assertNotNull(byteRateSensor, "Byte rate sensor should exist");
+        });
+    }
+
+    @Test
+    public void testQuotaTypesEnabledUpdatesWithDefaultCallback() {
+        withQuotaManager(QuotaType.CONTROLLER_MUTATION, clientQuotaManager -> {
+            assertEquals(ClientQuotaManager.NO_QUOTAS, 
clientQuotaManager.quotaTypesEnabled());
+            assertFalse(clientQuotaManager.quotasEnabled());
+
+            clientQuotaManager.updateQuota(Optional.empty(), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.of(new Quota(5, true)));
+            assertEquals(ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED, 
clientQuotaManager.quotaTypesEnabled());
+            assertTrue(clientQuotaManager.quotasEnabled());
+
+            clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.empty(), Optional.of(new 
Quota(5, true)));
+            assertEquals(ClientQuotaManager.USER_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED, 
clientQuotaManager.quotaTypesEnabled());
+            assertTrue(clientQuotaManager.quotasEnabled());
+
+            clientQuotaManager.updateQuota(Optional.empty(), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client2")), Optional.of(new Quota(5, true)));
+            assertEquals(ClientQuotaManager.USER_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED, 
clientQuotaManager.quotaTypesEnabled());
+            assertTrue(clientQuotaManager.quotasEnabled());
+
+            clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userB")), Optional.empty(), Optional.of(new 
Quota(5, true)));
+            assertEquals(ClientQuotaManager.USER_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED, 
clientQuotaManager.quotaTypesEnabled());
+            assertTrue(clientQuotaManager.quotasEnabled());
+
+            clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.of(new Quota(10, 
true)));
+            assertEquals(ClientQuotaManager.USER_CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.USER_QUOTA_ENABLED, clientQuotaManager.quotaTypesEnabled());
+            assertTrue(clientQuotaManager.quotasEnabled());
+
+            clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.of(new Quota(12, 
true)));
+            assertEquals(ClientQuotaManager.USER_CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.USER_QUOTA_ENABLED, clientQuotaManager.quotaTypesEnabled());
+            assertTrue(clientQuotaManager.quotasEnabled());
+
+            clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.empty(), Optional.empty());
+            assertEquals(ClientQuotaManager.USER_CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.USER_QUOTA_ENABLED, clientQuotaManager.quotaTypesEnabled());
+            assertTrue(clientQuotaManager.quotasEnabled());
+
+            clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userB")), Optional.empty(), Optional.empty());
+            assertEquals(ClientQuotaManager.USER_CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED, 
clientQuotaManager.quotaTypesEnabled());
+            assertTrue(clientQuotaManager.quotasEnabled());
+
+            clientQuotaManager.updateQuota(Optional.empty(), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.empty());
+            assertEquals(ClientQuotaManager.USER_CLIENT_ID_QUOTA_ENABLED | 
ClientQuotaManager.CLIENT_ID_QUOTA_ENABLED, 
clientQuotaManager.quotaTypesEnabled());
+            assertTrue(clientQuotaManager.quotasEnabled());
+
+            clientQuotaManager.updateQuota(Optional.empty(), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client2")), Optional.empty());
+            assertEquals(ClientQuotaManager.USER_CLIENT_ID_QUOTA_ENABLED, 
clientQuotaManager.quotaTypesEnabled());
+            assertTrue(clientQuotaManager.quotasEnabled());
+
+            clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.empty());
+            assertEquals(ClientQuotaManager.NO_QUOTAS, 
clientQuotaManager.quotaTypesEnabled());
+            assertFalse(clientQuotaManager.quotasEnabled());
+
+            clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.empty());
+            assertEquals(ClientQuotaManager.NO_QUOTAS, 
clientQuotaManager.quotaTypesEnabled());
+            assertFalse(clientQuotaManager.quotasEnabled());
+        });
+    }
+
+    @Test
+    public void testQuotaTypesEnabledUpdatesWithCustomCallback() {
+        ClientQuotaCallback customQuotaCallback = new ClientQuotaCallback() {
+            final Map<ClientQuotaEntity, Quota> quotas = new HashMap<>();
+            @Override
+            public void configure(Map<String, ?> configs) {}
+
+            @Override
+            public Map<String, String> quotaMetricTags(ClientQuotaType 
quotaType, KafkaPrincipal principal, String clientId) {
+                return Map.of();
+            }
+
+            @Override
+            public Double quotaLimit(ClientQuotaType quotaType, Map<String, 
String> metricTags) {
+                return 1.0;
+            }
+
+            @Override
+            public void updateQuota(ClientQuotaType quotaType, 
ClientQuotaEntity entity, double newValue) {
+                quotas.put(entity, new Quota(newValue, true));
+            }
+
+            @Override
+            public void removeQuota(ClientQuotaType quotaType, 
ClientQuotaEntity entity) {
+                quotas.remove(entity);
+            }
+
+            @Override
+            public boolean quotaResetRequired(ClientQuotaType quotaType) {
+                return false;
+            }
+
+            @Override
+            public void close() {}
+        };
+        ClientQuotaManager clientQuotaManager = new ClientQuotaManager(
+                new ClientQuotaManagerConfig(),
+                metrics,
+                QuotaType.CONTROLLER_MUTATION,
+                time,
+                "",
+                Optional.of(Plugin.wrapInstance(customQuotaCallback, metrics, 
""))
+        );
+
+        try {
+            assertEquals(ClientQuotaManager.CUSTOM_QUOTAS, 
clientQuotaManager.quotaTypesEnabled());
+            assertTrue(clientQuotaManager.quotasEnabled(), "quotasEnabled 
should be true with custom callback");
+
+            clientQuotaManager.updateQuota(Optional.empty(), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.of(new Quota(12, 
true)));
+            assertEquals(ClientQuotaManager.CUSTOM_QUOTAS, 
clientQuotaManager.quotaTypesEnabled());
+
+            clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.empty(), Optional.of(new 
Quota(12, true)));
+            assertEquals(ClientQuotaManager.CUSTOM_QUOTAS, 
clientQuotaManager.quotaTypesEnabled());
+            assertTrue(clientQuotaManager.quotasEnabled(), "quotasEnabled 
should remain true");
+
+            clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.of(new Quota(12, 
true)));
+            assertEquals(ClientQuotaManager.CUSTOM_QUOTAS, 
clientQuotaManager.quotaTypesEnabled());
+            assertTrue(clientQuotaManager.quotasEnabled(), "quotasEnabled 
should remain true");
+
+            clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.empty());
+            clientQuotaManager.updateQuota(Optional.of(new 
ClientQuotaManager.UserEntity("userA")), Optional.empty(), Optional.empty());
+            clientQuotaManager.updateQuota(Optional.empty(), Optional.of(new 
ClientQuotaManager.ClientIdEntity("client1")), Optional.empty());
+            assertEquals(ClientQuotaManager.CUSTOM_QUOTAS, 
clientQuotaManager.quotaTypesEnabled());
+            assertTrue(clientQuotaManager.quotasEnabled(), "quotasEnabled 
should remain true");
+        } finally {
+            clientQuotaManager.shutdown();
+        }
+    }
+
+    record UserClient(
+            String user,
+            String clientId,
+            Optional<ClientQuotaEntity.ConfigEntity> configUser,
+            Optional<ClientQuotaEntity.ConfigEntity> configClientEntity) { }
+
+}
diff --git 
a/server/src/test/java/org/apache/kafka/server/quota/ClientRequestQuotaManagerTest.java
 
b/server/src/test/java/org/apache/kafka/server/quota/ClientRequestQuotaManagerTest.java
new file mode 100644
index 00000000000..8ce603b6b27
--- /dev/null
+++ 
b/server/src/test/java/org/apache/kafka/server/quota/ClientRequestQuotaManagerTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.server.quota;
+
+import org.apache.kafka.common.metrics.KafkaMetric;
+import org.apache.kafka.common.metrics.Quota;
+import org.apache.kafka.server.config.ClientQuotaManagerConfig;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ClientRequestQuotaManagerTest extends BaseClientQuotaManagerTest {
+    private final ClientQuotaManagerConfig config = new 
ClientQuotaManagerConfig();
+
+    private static double millisToPercent(double millis) {
+        return millis * 1000 * 1000 * 
ClientRequestQuotaManager.NANOS_TO_PERCENTAGE_PER_SECOND;
+    }
+
+    @Test
+    public void testRequestPercentageQuotaViolation() {
+        ClientRequestQuotaManager clientRequestQuotaManager = new 
ClientRequestQuotaManager(config, metrics, time, "", Optional.empty());
+        ClientQuotaEntity.ConfigEntity userEntity = new 
ClientQuotaManager.UserEntity("ANONYMOUS");
+        ClientQuotaEntity.ConfigEntity clientEntity = new 
ClientQuotaManager.ClientIdEntity("test-client");
+
+        clientRequestQuotaManager.updateQuota(
+                Optional.of(userEntity),
+                Optional.of(clientEntity),
+                Optional.of(Quota.upperBound(1))
+        );
+        KafkaMetric queueSizeMetric = 
metrics.metrics().get(metrics.metricName("queue-size", 
QuotaType.REQUEST.toString(), ""));
+
+        try {
+            // We have 10 seconds windows. Make sure that there is no quota 
violation
+            // if we are under the quota
+            for (int i = 0; i < 10; i++) {
+                assertEquals(0, maybeRecord(clientRequestQuotaManager, 
"ANONYMOUS", "test-client", millisToPercent(4)));
+                time.sleep(1000);
+            }
+            assertEquals(0, ((Double) 
queueSizeMetric.metricValue()).intValue());
+
+            // Create a spike.
+            // quota = 1% (10ms per second)
+            // 4*10 + 67.1 = 107.1/10.5 = 10.2ms per second.
+            // (10.2 - quota)/quota*window-size = (10.2-10)/10*10.5 seconds = 
210ms
+            // 10.5 seconds interval because the last window is half complete
+            time.sleep(500);
+            int throttleTime = maybeRecord(clientRequestQuotaManager, 
"ANONYMOUS", "test-client", millisToPercent(67.1));
+
+            assertEquals(210, throttleTime, "Should be throttled");
+
+            throttle(clientRequestQuotaManager, throttleTime, callback);
+            assertEquals(1, ((Double) 
queueSizeMetric.metricValue()).intValue());
+            // After a request is delayed, the callback cannot be triggered 
immediately
+            clientRequestQuotaManager.processThrottledChannelReaperDoWork();
+            assertEquals(0, numCallbacks);
+            time.sleep(throttleTime);
+
+            // Callback can only be triggered after the delay time passes
+            clientRequestQuotaManager.processThrottledChannelReaperDoWork();
+            assertEquals(0, ((Double) 
queueSizeMetric.metricValue()).intValue());
+            assertEquals(1, numCallbacks);
+
+            // Could continue to see delays until the bursty sample disappears
+            for (int i = 0; i < 11; i++) {
+                maybeRecord(clientRequestQuotaManager, "ANONYMOUS", 
"test-client", millisToPercent(4));
+                time.sleep(1000);
+            }
+
+            assertEquals(0,
+                    maybeRecord(clientRequestQuotaManager, "ANONYMOUS", 
"test-client", 0), "Should be unthrottled since bursty sample has rolled over");
+
+            // Create a very large spike which requires > one quota window to 
bring within quota
+            assertEquals(1000, maybeRecord(clientRequestQuotaManager, 
"ANONYMOUS", "test-client", millisToPercent(500)));
+            for (int i = 0; i < 10; i++) {
+                time.sleep(1000);
+                assertEquals(1000, maybeRecord(clientRequestQuotaManager, 
"ANONYMOUS", "test-client", 0));
+            }
+            time.sleep(1000);
+            assertEquals(0,
+                    maybeRecord(clientRequestQuotaManager, "ANONYMOUS", 
"test-client", 0), "Should be unthrottled since bursty sample has rolled over");
+
+        } finally {
+            clientRequestQuotaManager.shutdown();
+        }
+    }
+
+}
diff --git 
a/server/src/test/java/org/apache/kafka/server/quota/ControllerMutationQuotaManagerTest.java
 
b/server/src/test/java/org/apache/kafka/server/quota/ControllerMutationQuotaManagerTest.java
new file mode 100644
index 00000000000..b9e2c1219bb
--- /dev/null
+++ 
b/server/src/test/java/org/apache/kafka/server/quota/ControllerMutationQuotaManagerTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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.server.quota;
+
+import org.apache.kafka.common.MetricName;
+import org.apache.kafka.common.errors.ThrottlingQuotaExceededException;
+import org.apache.kafka.common.metrics.KafkaMetric;
+import org.apache.kafka.common.metrics.MetricConfig;
+import org.apache.kafka.common.metrics.Metrics;
+import org.apache.kafka.common.metrics.Quota;
+import org.apache.kafka.common.metrics.QuotaViolationException;
+import org.apache.kafka.common.metrics.Sensor;
+import org.apache.kafka.common.metrics.stats.TokenBucket;
+import org.apache.kafka.common.utils.MockTime;
+import org.apache.kafka.server.config.ClientQuotaManagerConfig;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+import static 
org.apache.kafka.server.quota.ControllerMutationQuotaManager.throttleTimeMs;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ControllerMutationQuotaManagerTest extends 
BaseClientQuotaManagerTest {
+
+    private static final String USER = "ANONYMOUS";
+    private static final String CLIENT_ID = "test-client";
+
+    private final ClientQuotaManagerConfig config = new 
ClientQuotaManagerConfig(10, 1);
+
+    @Test
+    public void testStrictControllerMutationQuotaViolation() {
+        MockTime time = new MockTime(0, System.currentTimeMillis(), 0);
+        try (Metrics metrics = new Metrics(time)) {
+            Sensor sensor = metrics.sensor("sensor", new MetricConfig()
+                    .quota(Quota.upperBound(10))
+                    .timeWindow(1, TimeUnit.SECONDS)
+                    .samples(10));
+            MetricName metricName = metrics.metricName("rate", "test-group");
+            assertTrue(sensor.add(metricName, new TokenBucket()));
+
+            StrictControllerMutationQuota quota = new 
StrictControllerMutationQuota(time, sensor);
+            assertFalse(quota.isExceeded());
+
+            // Recording a first value at T to bring the tokens to 10. Value 
is accepted
+            // because the quota is not exhausted yet.
+            quota.record(90);
+            assertFalse(quota.isExceeded());
+            assertEquals(0, quota.throttleTime());
+
+            // Recording a second value at T to bring the tokens to -80. Value 
is accepted
+            quota.record(90);
+            assertFalse(quota.isExceeded());
+            assertEquals(0, quota.throttleTime());
+
+            // Recording a third value at T is rejected immediately because 
there are no
+            // tokens available in the bucket.
+            assertThrows(ThrottlingQuotaExceededException.class, () -> 
quota.record(90));
+            assertTrue(quota.isExceeded());
+            assertEquals(8000, quota.throttleTime());
+
+            // Throttle time is adjusted with time
+            time.sleep(5000);
+            assertEquals(3000, quota.throttleTime());
+        }
+    }
+
+    @Test
+    public void testPermissiveControllerMutationQuotaViolation() {
+        MockTime time = new MockTime(0, System.currentTimeMillis(), 0);
+        try (Metrics metrics = new Metrics(time)) {
+            Sensor sensor = metrics.sensor("sensor", new MetricConfig()
+                    .quota(Quota.upperBound(10))
+                    .timeWindow(1, TimeUnit.SECONDS)
+                    .samples(10));
+            MetricName metricName = metrics.metricName("rate", "test-group");
+            assertTrue(sensor.add(metricName, new TokenBucket()));
+
+            PermissiveControllerMutationQuota quota = new 
PermissiveControllerMutationQuota(time, sensor);
+            assertFalse(quota.isExceeded());
+
+            // Recording a first value at T to bring the tokens 10. Value is 
accepted
+            // because the quota is not exhausted yet.
+            quota.record(90);
+            assertFalse(quota.isExceeded());
+            assertEquals(0, quota.throttleTime());
+
+            // Recording a second value at T to bring the tokens to -80. Value 
is accepted
+            quota.record(90);
+            assertFalse(quota.isExceeded());
+            assertEquals(8000, quota.throttleTime());
+
+            // Recording a third value at T to bring the tokens to -170. Value 
is accepted
+            // even though the quota is exhausted.
+            quota.record(90);
+            assertFalse(quota.isExceeded()); // quota is never exceeded
+            assertEquals(17000, quota.throttleTime());
+
+            // Throttle time is adjusted with time
+            time.sleep(5000);
+            assertEquals(12000, quota.throttleTime());
+        }
+    }
+
+    private void withQuotaManager(Consumer<ControllerMutationQuotaManager> f) {
+        ControllerMutationQuotaManager quotaManager = new 
ControllerMutationQuotaManager(config, metrics, time, "", Optional.empty());
+        try {
+            f.accept(quotaManager);
+        } finally {
+            quotaManager.shutdown();
+        }
+    }
+
+    @Test
+    public void testThrottleTime() {
+        MockTime time = new MockTime(0, System.currentTimeMillis(), 0);
+        try (Metrics metrics = new Metrics(time)) {
+            Sensor sensor = metrics.sensor("sensor");
+            MetricName metricName = metrics.metricName("tokens", "test-group");
+            sensor.add(metricName, new TokenBucket());
+            KafkaMetric metric = metrics.metric(metricName);
+
+            assertEquals(0, throttleTimeMs(new QuotaViolationException(metric, 
0, 10)));
+            assertEquals(500, throttleTimeMs(new 
QuotaViolationException(metric, -5, 10)));
+            assertEquals(1000, throttleTimeMs(new 
QuotaViolationException(metric, -10, 10)));
+        }
+    }
+
+    @Test
+    public void testControllerMutationQuotaViolation() {
+        withQuotaManager(quotaManager -> {
+            quotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity(USER)),
+                    Optional.of(new 
ClientQuotaManager.ClientIdEntity(CLIENT_ID)),
+                    Optional.of(Quota.upperBound(10))
+            );
+            KafkaMetric queueSizeMetric = metrics.metrics().get(
+                metrics.metricName("queue-size", 
QuotaType.CONTROLLER_MUTATION.toString(), ""));
+
+            // Verify that there is no quota violation if we remain under the 
quota.
+            for (int i = 0; i < 10; i++) {
+                assertEquals(0, maybeRecord(quotaManager, USER, CLIENT_ID, 
10));
+                time.sleep(1000);
+            }
+            assertEquals(0, ((Double) 
queueSizeMetric.metricValue()).intValue());
+
+            // Create a spike worth of 110 mutations.
+            // Current tokens in the bucket = 100
+            // As we use the Strict enforcement, the quota is checked before 
updating the rate. Hence,
+            // the spike is accepted and no quota violation error is raised.
+            int throttleTime = maybeRecord(quotaManager, USER, CLIENT_ID, 110);
+            assertEquals(0, throttleTime, "Should not be throttled");
+
+            // Create a spike worth of 110 mutations.
+            // Current tokens in the bucket = 100 - 110 = -10
+            // As the quota is already violated, the spike is rejected 
immediately without updating the
+            // rate. The client must wait:
+            // 10 / 10 = 1s
+            throttleTime = maybeRecord(quotaManager, USER, CLIENT_ID, 110);
+            assertEquals(1000, throttleTime, "Should be throttled");
+
+            // Throttle
+            throttle(quotaManager, throttleTime, callback);
+            assertEquals(1, ((Double) 
queueSizeMetric.metricValue()).intValue());
+
+            // After a request is delayed, the callback cannot be triggered 
immediately
+            quotaManager.processThrottledChannelReaperDoWork();
+            assertEquals(0, numCallbacks);
+
+            // Callback can only be triggered after the delay time passes
+            time.sleep(throttleTime);
+            quotaManager.processThrottledChannelReaperDoWork();
+            assertEquals(0, ((Double) 
queueSizeMetric.metricValue()).intValue());
+            assertEquals(1, numCallbacks);
+
+            // Retry to spike worth of 110 mutations after having waited the 
required throttle time.
+            // Current tokens in the bucket = 0
+            throttleTime = maybeRecord(quotaManager, USER, CLIENT_ID, 110);
+            assertEquals(0, throttleTime, "Should not be throttled");
+        });
+    }
+
+    @Test
+    public void 
testNewStrictQuotaForReturnsUnboundedQuotaWhenQuotaIsDisabled() {
+        withQuotaManager(quotaManager ->
+                
assertEquals(ControllerMutationQuota.UNBOUNDED_CONTROLLER_MUTATION_QUOTA,
+                        quotaManager.newStrictQuotaFor(buildSession(USER), 
CLIENT_ID)));
+    }
+
+    @Test
+    public void testNewStrictQuotaForReturnsStrictQuotaWhenQuotaIsEnabled() {
+        withQuotaManager(quotaManager -> {
+            quotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity(USER)),
+                    Optional.of(new 
ClientQuotaManager.ClientIdEntity(CLIENT_ID)),
+                    Optional.of(Quota.upperBound(10))
+            );
+            ControllerMutationQuota quota = 
quotaManager.newStrictQuotaFor(buildSession(USER), CLIENT_ID);
+            assertInstanceOf(StrictControllerMutationQuota.class, quota);
+        });
+    }
+
+    @Test
+    public void 
testNewPermissiveQuotaForReturnsUnboundedQuotaWhenQuotaIsDisabled() {
+        withQuotaManager(quotaManager ->
+                
assertEquals(ControllerMutationQuota.UNBOUNDED_CONTROLLER_MUTATION_QUOTA,
+                        quotaManager.newPermissiveQuotaFor(buildSession(USER), 
CLIENT_ID)));
+    }
+
+    @Test
+    public void 
testNewPermissiveQuotaForReturnsStrictQuotaWhenQuotaIsEnabled() {
+        withQuotaManager(quotaManager -> {
+            quotaManager.updateQuota(
+                    Optional.of(new ClientQuotaManager.UserEntity(USER)),
+                    Optional.of(new 
ClientQuotaManager.ClientIdEntity(CLIENT_ID)),
+                    Optional.of(Quota.upperBound(10))
+            );
+            ControllerMutationQuota quota = 
quotaManager.newPermissiveQuotaFor(buildSession(USER), CLIENT_ID);
+            assertInstanceOf(PermissiveControllerMutationQuota.class, quota);
+        });
+    }
+
+}

Reply via email to