A helper method for extracting multi-instance application configuration, * specified via property names of the form PREFIX[.LISTENER_NAME].PROPERTY.
diff --git a/core/src/main/java/io/confluent/rest/ratelimit/NetworkTrafficRateLimitBackend.java b/core/src/main/java/io/confluent/rest/ratelimit/NetworkTrafficRateLimitBackend.java new file mode 100644 index 0000000000..12ca84ba9e --- /dev/null +++ b/core/src/main/java/io/confluent/rest/ratelimit/NetworkTrafficRateLimitBackend.java @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Confluent Inc. + * + * Licensed 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 io.confluent.rest.ratelimit; + +public enum NetworkTrafficRateLimitBackend { + GUAVA, + RESILIENCE4J +} diff --git a/core/src/main/java/io/confluent/rest/ratelimit/NetworkTrafficRateLimiter.java b/core/src/main/java/io/confluent/rest/ratelimit/NetworkTrafficRateLimiter.java new file mode 100644 index 0000000000..9ad4b79db0 --- /dev/null +++ b/core/src/main/java/io/confluent/rest/ratelimit/NetworkTrafficRateLimiter.java @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Confluent Inc. + * + * Licensed 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 io.confluent.rest.ratelimit; + +public interface NetworkTrafficRateLimiter { + + void rateLimit(int cost); +} diff --git a/core/src/main/java/io/confluent/rest/ratelimit/NetworkTrafficRateLimiterFactory.java b/core/src/main/java/io/confluent/rest/ratelimit/NetworkTrafficRateLimiterFactory.java new file mode 100644 index 0000000000..1f42be81f0 --- /dev/null +++ b/core/src/main/java/io/confluent/rest/ratelimit/NetworkTrafficRateLimiterFactory.java @@ -0,0 +1,86 @@ +/* + * Copyright 2023 Confluent Inc. + * + * Licensed 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 io.confluent.rest.ratelimit; + +import com.google.common.util.concurrent.RateLimiter; +import io.confluent.rest.RestConfig; +import io.github.resilience4j.ratelimiter.RateLimiterConfig; +import java.time.Duration; + +public final class NetworkTrafficRateLimiterFactory { + + private NetworkTrafficRateLimiterFactory() { + // prevent instantiation + } + + public static NetworkTrafficRateLimiter create(RestConfig restConfig) { + int bytesPerSecond = restConfig.getNetworkTrafficRateLimitBytesPerSec(); + switch (restConfig.getNetworkTrafficRateLimitBackend()) { + case GUAVA: + return GuavaNetworkTrafficRateLimiter.create(bytesPerSecond); + case RESILIENCE4J: + return Resilience4JNetworkTrafficRateLimiter.create(bytesPerSecond); + default: + throw new AssertionError("Unknown enum constant: " + + restConfig.getNetworkTrafficRateLimitBackend()); + } + } + + static final class GuavaNetworkTrafficRateLimiter implements NetworkTrafficRateLimiter { + + private final RateLimiter delegate; + + GuavaNetworkTrafficRateLimiter(RateLimiter delegate) { + this.delegate = delegate; + } + + static GuavaNetworkTrafficRateLimiter create(int bytesPerSecond) { + return new GuavaNetworkTrafficRateLimiter(RateLimiter.create(bytesPerSecond)); + } + + @Override + public void rateLimit(final int cost) { + delegate.acquire(cost); + } + } + + static final class Resilience4JNetworkTrafficRateLimiter implements NetworkTrafficRateLimiter { + + private final io.github.resilience4j.ratelimiter.RateLimiter delegate; + + Resilience4JNetworkTrafficRateLimiter(io.github.resilience4j.ratelimiter.RateLimiter delegate) { + this.delegate = delegate; + } + + static Resilience4JNetworkTrafficRateLimiter create(int bytesPerSecond) { + RateLimiterConfig config = + RateLimiterConfig.custom() + .limitRefreshPeriod(Duration.ofSeconds(1)) + .limitForPeriod(bytesPerSecond) + .build(); + return new Resilience4JNetworkTrafficRateLimiter( + io.github.resilience4j.ratelimiter.RateLimiter.of( + "Resilience4JNetworkTrafficRateLimiter", config) + ); + } + + @Override + public void rateLimit(final int cost) { + delegate.acquirePermission(cost); + } + } +} diff --git a/core/src/test/java/io/confluent/rest/RateLimitNetworkTrafficListenerTest.java b/core/src/test/java/io/confluent/rest/RateLimitNetworkTrafficListenerTest.java new file mode 100644 index 0000000000..30a80d3aea --- /dev/null +++ b/core/src/test/java/io/confluent/rest/RateLimitNetworkTrafficListenerTest.java @@ -0,0 +1,219 @@ +/* + * Copyright 2023 Confluent Inc. + * + * Licensed 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 io.confluent.rest; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +import java.net.URI; +import java.time.Duration; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Configurable; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +public class RateLimitNetworkTrafficListenerTest { + + private static TestRestConfig testConfig; + private static ApplicationServer