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

added quota rate limit #19

Merged
merged 7 commits into from
Sep 19, 2017
Merged
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,17 @@ zuul:
repository: REDIS
behind-proxy: true
default-policy: #optional - will apply unless specific policy exists
limit: 10
limit: 10 #optional - request number limit per refresh interval window
quota: 1000 #optional - request time limit per refresh interval window (in seconds)
refresh-interval: 60 #default value (in seconds)
type: #optional
- user
- origin
- url
policies:
myServiceId:
limit: 10
limit: 10 #optional - request number limit per refresh interval window
quota: 1000 #optional - request time limit per refresh interval window (in seconds)
refresh-interval: 60 #default value (in seconds)
type: #optional
- user
Expand Down Expand Up @@ -103,6 +105,7 @@ Policy properties:
|Property name| Values |Default Value|
|-------------|:-------|:-------------:|
|limit|number of calls| - |
|quota|time of calls| - |
|refresh-interval|seconds|60|
|type| [ORIGIN, USER, URL] | [] |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit;

import static com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.PREFIX;

import com.ecwid.consul.v1.ConsulClient;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.DefaultRateLimitKeyGenerator;
Expand All @@ -27,7 +29,9 @@
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.RedisRateLimiter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.springdata.JpaRateLimiter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.springdata.RateLimiterRepository;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.RateLimitFilter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.RateLimitPostFilter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.RateLimitPreFilter;
import com.netflix.zuul.ZuulFilter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
Expand All @@ -42,8 +46,7 @@
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;

import static com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.PREFIX;
import org.springframework.web.util.UrlPathHelper;

/**
* @author Marcos Barbero
Expand All @@ -53,12 +56,22 @@
@ConditionalOnProperty(prefix = PREFIX, name = "enabled", havingValue = "true")
public class RateLimitAutoConfiguration {

private final UrlPathHelper urlPathHelper = new UrlPathHelper();

@Bean
public ZuulFilter rateLimiterPreFilter(final RateLimiter rateLimiter,
final RateLimitProperties rateLimitProperties,
final RouteLocator routeLocator,
final RateLimitKeyGenerator rateLimitKeyGenerator) {
return new RateLimitPreFilter(rateLimitProperties, routeLocator, urlPathHelper, rateLimiter, rateLimitKeyGenerator);
}

@Bean
public RateLimitFilter rateLimiterFilter(final RateLimiter rateLimiter,
final RateLimitProperties rateLimitProperties,
final RouteLocator routeLocator,
final RateLimitKeyGenerator rateLimitKeyGenerator) {
return new RateLimitFilter(rateLimiter, rateLimitProperties, routeLocator, rateLimitKeyGenerator);
public ZuulFilter rateLimiterPostFilter(final RateLimiter rateLimiter,
final RateLimitProperties rateLimitProperties,
final RouteLocator routeLocator,
final RateLimitKeyGenerator rateLimitKeyGenerator) {
return new RateLimitPostFilter(rateLimitProperties, routeLocator, urlPathHelper, rateLimiter, rateLimitKeyGenerator);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config;

import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.Id;
Expand All @@ -41,7 +42,9 @@ public class Rate {
@Id
private String key;
private Long remaining;
private Long remainingQuota;
private Long reset;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy HH:mm:ss")
private Date expiration;

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@
public interface RateLimiter {

/**
* @param policy - Template for which rates should be created in case there's no rate limit associated with the key
* @param key - Unique key that identifies a request
* @param policy - Template for which rates should be created in case there's no rate limit associated with the key
* @param key - Unique key that identifies a request
* @param requestTime - The total time it took to handle the request
* @return a view of a user's rate request limit
*/
Rate consume(Policy policy, String key);
Rate consume(Policy policy, String key, Long requestTime);
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ public static class Policy {

@NotNull
private Long refreshInterval = TimeUnit.MINUTES.toSeconds(1L);
@NotNull
private Long limit;
private Long quota;
@NotNull
private List<Type> type = Lists.newArrayList();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ public abstract class AbstractRateLimiter implements RateLimiter {
protected abstract void saveRate(Rate rate);

@Override
public synchronized Rate consume(final Policy policy, final String key) {
public synchronized Rate consume(final Policy policy, final String key, final Long requestTime) {
Rate rate = this.create(policy, key);
this.updateRate(rate);
this.updateRate(policy, rate, requestTime);
this.saveRate(rate);
return rate;
}
Expand All @@ -32,20 +32,26 @@ private Rate create(final Policy policy, final String key) {
if (isExpired(rate)) {

final Long limit = policy.getLimit();
final Long quota = policy.getQuota() != null ? SECONDS.toMillis(policy.getQuota()) : null;
final Long refreshInterval = SECONDS.toMillis(policy.getRefreshInterval());
final Date expiration = new Date(System.currentTimeMillis() + refreshInterval);

rate = new Rate(key, limit, refreshInterval, expiration);
rate = new Rate(key, limit, quota, refreshInterval, expiration);
}
return rate;
}

private void updateRate(final Rate rate) {
private void updateRate(final Policy policy, final Rate rate, final Long requestTime) {
if (rate.getReset() > 0) {
Long reset = rate.getExpiration().getTime() - System.currentTimeMillis();
rate.setReset(reset);
}
rate.setRemaining(Math.max(-1, rate.getRemaining() - 1));
if (policy.getLimit() != null && requestTime == null) {
rate.setRemaining(Math.max(-1, rate.getRemaining() - 1));
}
if (policy.getQuota() != null && requestTime != null) {
rate.setRemainingQuota(Math.max(-1, rate.getRemainingQuota() - requestTime));
}
}

private boolean isExpired(final Rate rate) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ public class ConsulRateLimiter extends AbstractRateLimiter {
protected Rate getRate(String key) {
Rate rate = null;
GetValue value = this.consulClient.getKVValue(key).getValue();
if (value != null) {
if (value != null && value.getValue() != null) {
try {
rate = this.objectMapper.readValue(value.getDecodedValue(), Rate.class);
rate = this.objectMapper.readValue(value.getValue(), Rate.class);
} catch (IOException e) {
log.error("Failed to deserialize Rate", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,42 @@
* @author Marcos Barbero
*/
@RequiredArgsConstructor
@SuppressWarnings("unchecked")
public class RedisRateLimiter implements RateLimiter {

private final RedisTemplate template;

@Override
@SuppressWarnings("unchecked")
public Rate consume(final Policy policy, final String key) {
final Long limit = policy.getLimit();
public Rate consume(final Policy policy, final String key, final Long requestTime) {
final Long refreshInterval = policy.getRefreshInterval();
final Long current = this.template.boundValueOps(key).increment(1L);
final Long quota = policy.getQuota() != null ? SECONDS.toMillis(policy.getQuota()) : null;
Rate rate = new Rate(key, policy.getLimit(), quota, null, null);

final Long limit = policy.getLimit();
if (limit != null) {
handleExpiration(key, refreshInterval, rate);
long usage = requestTime == null ? 1L : 0L;
final Long current = this.template.boundValueOps(key).increment(usage);
rate.setRemaining(Math.max(-1, limit - current));
}

if (quota != null) {
String quotaKey = key + "-quota";
handleExpiration(quotaKey, refreshInterval, rate);
Long usage = requestTime != null ? requestTime : 0L;
final Long current = this.template.boundValueOps(quotaKey).increment(usage);
rate.setRemainingQuota(Math.max(-1, quota - current));
}

return rate;
}

private void handleExpiration(String key, Long refreshInterval, Rate rate) {
Long expire = this.template.getExpire(key);
if (expire == null || expire == -1) {
this.template.expire(key, refreshInterval, SECONDS);
expire = refreshInterval;
}
return new Rate(key, Math.max(-1, limit - current), SECONDS.toMillis(expire), null);
rate.setReset(SECONDS.toMillis(expire));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* 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 com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters;

import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.web.util.UrlPathHelper;

/**
* @author Marcos Barbero
* @author Liel Chayoun
*/
@RequiredArgsConstructor
public abstract class AbstractRateLimitFilter extends ZuulFilter {

public static final String QUOTA_HEADER = "X-RateLimit-Quota";
public static final String REMAINING_QUOTA_HEADER = "X-RateLimit-Remaining-Quota";
public static final String LIMIT_HEADER = "X-RateLimit-Limit";
public static final String REMAINING_HEADER = "X-RateLimit-Remaining";
public static final String RESET_HEADER = "X-RateLimit-Reset";
public static final String REQUEST_START_TIME = "rateLimitRequestStartTime";

private final RateLimitProperties properties;
private final RouteLocator routeLocator;
private final UrlPathHelper urlPathHelper;

@Override
public boolean shouldFilter() {
return properties.isEnabled() && policy(route()).isPresent();
}

protected Route route() {
String requestURI = urlPathHelper.getPathWithinApplication(RequestContext.getCurrentContext().getRequest());
return routeLocator.getMatchingRoute(requestURI);
}

protected Optional<Policy> policy(final Route route) {
if (route != null) {
return properties.getPolicy(route.getId());
}
return Optional.ofNullable(properties.getDefaultPolicy());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* 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 com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.springframework.web.context.request.RequestAttributes.SCOPE_REQUEST;

import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitKeyGenerator;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimiter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties;
import com.netflix.zuul.context.RequestContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.util.UrlPathHelper;

/**
* @author Marcos Barbero
* @author Liel Chayoun
*/
public class RateLimitPostFilter extends AbstractRateLimitFilter {

private final RateLimiter rateLimiter;
private final RateLimitKeyGenerator rateLimitKeyGenerator;

public RateLimitPostFilter(
RateLimitProperties properties,
RouteLocator routeLocator,
UrlPathHelper urlPathHelper,
RateLimiter rateLimiter,
RateLimitKeyGenerator rateLimitKeyGenerator) {
super(properties, routeLocator, urlPathHelper);
this.rateLimiter = rateLimiter;
this.rateLimitKeyGenerator = rateLimitKeyGenerator;
}

@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}

@Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 10;
}

@Override
public boolean shouldFilter() {
return super.shouldFilter() && getRequestStartTime() != null;
}

private Long getRequestStartTime() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
return (Long) requestAttributes.getAttribute(RateLimitPreFilter.REQUEST_START_TIME, SCOPE_REQUEST);
}

public Object run() {
final RequestContext ctx = RequestContext.getCurrentContext();
final HttpServletResponse response = ctx.getResponse();
final HttpServletRequest request = ctx.getRequest();
final Route route = route();

policy(route).ifPresent(policy -> {

final Long requestTime = System.currentTimeMillis() - getRequestStartTime();
final String key = rateLimitKeyGenerator.key(request, route, policy);
final Rate rate = rateLimiter.consume(policy, key, requestTime);

final Long quota = policy.getQuota();
final Long remainingQuota = rate.getRemainingQuota();
if (quota != null) {
RequestContextHolder.getRequestAttributes().setAttribute(REQUEST_START_TIME, System.currentTimeMillis(), SCOPE_REQUEST);
response.setHeader(QUOTA_HEADER, String.valueOf(quota));
response.setHeader(REMAINING_QUOTA_HEADER, String.valueOf(MILLISECONDS.toSeconds(Math.max(remainingQuota, 0))));
}
});

return null;
}
}
Loading