Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhancement: RateLimitFilter - Provides an exact rate limiting mechanism #794

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions java/org/apache/catalina/filters/RateLimitFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@
* so it converts some configured values to more efficient values. For example, a configuration of a 60 seconds time
* bucket is converted to 65.536 seconds. That allows for very fast bucket calculation using bit shift arithmetic. In
* order to remain true to the user intent, the configured number of requests is then multiplied by the same ratio, so a
* configuration of 100 Requests per 60 seconds, has the real values of 109 Requests per 65 seconds. You can specify a
* different class as long as it implements the <code>org.apache.catalina.util.RateLimiter</code> interface.
* configuration of 100 Requests per 60 seconds, has the real values of 109 Requests per 65 seconds. An alternative
* implementation, <code>org.apache.catalina.util.ExactRateLimiter</code>, is intended to provide a less efficient but
* more accurate control, whose effective duration in seconds and number of requests configuration are consist with the
* user declared. You can specify a different class as long as it implements the
* <code>org.apache.catalina.util.RateLimiter</code> interface.
* </p>
* <p>
* It is common to set up different restrictions for different URIs. For example, a login page or authentication script
Expand Down
66 changes: 66 additions & 0 deletions java/org/apache/catalina/util/ExactRateLimiter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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.catalina.util;

import java.util.concurrent.ScheduledExecutorService;

/**
* A RateLimiter that compromises efficiency for accurate in order to provide exact rate limiting.
*/
public class ExactRateLimiter extends RateLimiterBase {

@Override
protected String getDefaultPolicyName() {
return "exact";
}

@Override
protected TimeBucketCounterBase newCounterInstance(ScheduledExecutorService executorService, int duration) {
return new ExactTimeBucketCounter(executorService, duration);
}

/**
* An accurate counter with exact bucket index, but slightly less efficient than another fast counter provided with
* the {@link FastRateLimiter}.
*/
class ExactTimeBucketCounter extends TimeBucketCounterBase {

ExactTimeBucketCounter(ScheduledExecutorService executorService, int bucketDuration) {
super(executorService, bucketDuration);
}

@Override
public long getBucketIndex(long timestamp) {
return (timestamp / 1000) / getBucketDuration();
}

@Override
public double getRatio() {
// actual value is exactly same with declared.
return 1.0d;
}

@Override
public long getMillisUntilNextBucket() {
long millis = System.currentTimeMillis();

long nextTimeBucketMillis = (getBucketIndex(millis) + 1) * getBucketDuration() * 1000;
long delta = nextTimeBucketMillis - millis;
return delta;
}
}
}
76 changes: 6 additions & 70 deletions java/org/apache/catalina/util/FastRateLimiter.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,88 +18,24 @@
package org.apache.catalina.util;

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicInteger;

import jakarta.servlet.FilterConfig;

import org.apache.tomcat.util.threads.ScheduledThreadPoolExecutor;

/**
* A RateLimiter that compromises accuracy for speed in order to provide maximum throughput.
*/
public class FastRateLimiter implements RateLimiter {

private static AtomicInteger index = new AtomicInteger();

TimeBucketCounter bucketCounter;

int duration;

int requests;

int actualRequests;

int actualDuration;

// Initial policy name can be rewritten by setPolicyName()
private String policyName = "fast-" + index.incrementAndGet();

@Override
public String getPolicyName() {
return policyName;
}

@Override
public void setPolicyName(String name) {
this.policyName = name;
}

@Override
public int getDuration() {
return actualDuration;
}

@Override
public void setDuration(int duration) {
this.duration = duration;
}

@Override
public int getRequests() {
return actualRequests;
}
public class FastRateLimiter extends RateLimiterBase implements RateLimiter {

@Override
public void setRequests(int requests) {
this.requests = requests;
protected String getDefaultPolicyName() {
return "fast";
}

@Override
public int increment(String ipAddress) {
return bucketCounter.increment(ipAddress);
protected TimeBucketCounterBase newCounterInstance(ScheduledExecutorService executorService, int duration) {
return new TimeBucketCounter(executorService, duration);
}

@Override
public void destroy() {
bucketCounter.destroy();
}

@Override
public void setFilterConfig(FilterConfig filterConfig) {

ScheduledExecutorService executorService = (ScheduledExecutorService) filterConfig.getServletContext()
.getAttribute(ScheduledThreadPoolExecutor.class.getName());

if (executorService == null) {
executorService = new java.util.concurrent.ScheduledThreadPoolExecutor(1);
}

bucketCounter = new TimeBucketCounter(duration, executorService);
actualRequests = (int) Math.round(bucketCounter.getRatio() * requests);
actualDuration = bucketCounter.getActualDuration() / 1000;
}

public TimeBucketCounter getBucketCounter() {
return bucketCounter;
return (TimeBucketCounter)bucketCounter;
}
}
6 changes: 3 additions & 3 deletions java/org/apache/catalina/util/RateLimiter.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ public interface RateLimiter {
void setRequests(int requests);

/**
* Increments the number of requests by the given ipAddress in the current time window.
* Increments the number of requests by the given identifier in the current time window.
*
* @param ipAddress the ip address
* @param identifier of target request
*
* @return the new value after incrementing
*/
int increment(String ipAddress);
int increment(String identifier);

/**
* Cleanup no longer needed resources.
Expand Down
143 changes: 143 additions & 0 deletions java/org/apache/catalina/util/RateLimiterBase.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* 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.catalina.util;

import java.util.Objects;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;

import jakarta.servlet.FilterConfig;

/**
* Basic implementation of {@link RateLimiter}, provides runtime data maintenance mechanism monitor.
*/
public abstract class RateLimiterBase implements RateLimiter {

private static final AtomicInteger index = new AtomicInteger();

TimeBucketCounterBase bucketCounter;

int requests;
int actualRequests;

int duration;
int actualDuration;

// Initial policy name can be rewritten by setPolicyName()
private String policyName = null;

/**
* If policy name has not been specified, the first call of {@link #getPolicyName()} returns a auto-generated policy
* name using the default policy name as prefix and followed by auto-increase index.
*
* @return default policy name, as a prefix of auto-generated policy name.
*/
protected abstract String getDefaultPolicyName();

@Override
public String getPolicyName() {
if (policyName == null) {
policyName = getDefaultPolicyName() + "-" + index.incrementAndGet();
}
return policyName;
}

@Override
public void setPolicyName(String name) {
Objects.requireNonNull(name);
this.policyName = name;
}

@Override
public int getDuration() {
return actualDuration;
}

@Override
public void setDuration(int duration) {
this.duration=duration;
}

@Override
public int getRequests() {
return actualRequests;
}

@Override
public void setRequests(int requests) {
this.requests=requests;
}

@Override
public int increment(String identifier) {
return bucketCounter.increment(identifier);
}

@Override
public void destroy() {
bucketCounter.destroy();
if (newExecutorService != null) {
try {
newExecutorService.shutdown();
} catch (SecurityException e) {
// ignore
}
}
}

/**
* Instantiate an instance of {@link TimeBucketCounterBase} for specific time bucket size. Concrete classes determine
* its counter policy by returning different implementation instance.
*
* @param utilityExecutor the executor
* @param duration size of each time bucket in seconds
*
* @return counter instance of {@link TimeBucketCounterBase}
*/
protected abstract TimeBucketCounterBase newCounterInstance(ScheduledExecutorService utilityExecutor, int duration);

@Override
public void setFilterConfig(FilterConfig filterConfig) {

ScheduledExecutorService executorService = (ScheduledExecutorService) filterConfig.getServletContext()
.getAttribute(ScheduledThreadPoolExecutor.class.getName());

if (executorService == null) {
newExecutorService = new java.util.concurrent.ScheduledThreadPoolExecutor(1);
executorService = newExecutorService;
}

bucketCounter = newCounterInstance(executorService, duration);
actualDuration = bucketCounter.getBucketDuration();
actualRequests = (int) Math.round(bucketCounter.getRatio() * requests);
}
/**
* Returns the internal instance of {@link TimeBucketCounterBase}
*
* @return instance of {@link TimeBucketCounterBase}
*/
public TimeBucketCounterBase getBucketCounter() {
return bucketCounter;
}

/**
* The self-owned utility executor, will be instantiated only when ScheduledThreadPoolExecutor is absent during
* filter configure phase.
*/
private ScheduledThreadPoolExecutor newExecutorService = null;
}
Loading