Skip to content

Commit 4085611

Browse files
Merge pull request #46 from marcosbarbero/spring-cloud-zuul-ratelimit-45
2 parents 5eeb631 + 188e4f0 commit 4085611

19 files changed

Lines changed: 281 additions & 50 deletions

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ cache:
1818
notifications:
1919
email:
2020
- marcos.hgb@gmail.com
21+
- lchayoun@gmail.com
2122
env:
2223
global:
2324
secure: B5QVZ6SWzgQmlKmStQCHg0RcQPVbkti2Z3s5vi1gR0Oef6KYxBpkzP87yES08OvHG7Ha6XfJ28feML8URtTcdhBtkr4aKMGy9R+VvY9i7hykPLGnuejip89oTt4k8SlWkqmvzx4G6qB1YbooVdAbvaqARlS+t1RjSfOJU5kIBMyyrBshxPlavfzDp55tUcBrfbDEbQmdnRY84gXng+gz+ceJTzg/vxNPyKCcG5Ap/2qW0lH+ZRcJTUgtmNZZIpiw56YD3T/ltc3wVIHEgFzCGUiZAaRtHJW3PGComDKd9Z3JPaZO1nuJlD999Kok4I9IEZjpu7g1mXlaioYsAwholJTsugoinEZ6JChaeT/92ZWY1Ua0vT5YJjz0QCGzlhJ12I751QFk1Lam9hwlhZ7LafDi1p8vhQuN1I5NOulgnoXwZboZ9/2jbNp63o1nq773jULJiAsMXY+Pv8SLCNKzVANyEK9ECYKG3DSxuVLIpX/j1cYWf/UH5FxQ5aiDYnZpBVblWfsQxPuyVjHnIyaAvw4LsTq4DfRG8JMqjjGr0IUGV6Xa/6/hCfx5ojvWFQ17YJeGHJMGAfPwvsTPhLTNl6Wm1Iw6Lt3N9KJ6VaGnJDuza/mJuTrTAdGhZ28bcevNq03vW3bFenLh0EC8zamBBA9eNuqqDak6Q1OzV3z6dRo=

spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/RateLimitAutoConfiguration.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties;
2828
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy;
2929
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.ConsulRateLimiter;
30+
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.DefaultRateLimiterErrorHandler;
31+
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.RateLimiterErrorHandler;
3032
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.InMemoryRateLimiter;
3133
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.RedisRateLimiter;
3234
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.springdata.JpaRateLimiter;
@@ -64,6 +66,12 @@ public class RateLimitAutoConfiguration {
6466

6567
private final UrlPathHelper urlPathHelper = new UrlPathHelper();
6668

69+
@Bean
70+
@ConditionalOnMissingBean(RateLimiterErrorHandler.class)
71+
public RateLimiterErrorHandler rateLimiterErrorHandler() {
72+
return new DefaultRateLimiterErrorHandler();
73+
}
74+
6775
@Bean
6876
public ZuulFilter rateLimiterPreFilter(final RateLimiter rateLimiter,
6977
final RateLimitProperties rateLimitProperties,
@@ -99,8 +107,9 @@ public StringRedisTemplate redisTemplate(final RedisConnectionFactory connection
99107
}
100108

101109
@Bean
102-
public RateLimiter redisRateLimiter(@Qualifier("rateLimiterRedisTemplate") final RedisTemplate redisTemplate) {
103-
return new RedisRateLimiter(redisTemplate);
110+
public RateLimiter redisRateLimiter(RateLimiterErrorHandler rateLimiterErrorHandler,
111+
@Qualifier("rateLimiterRedisTemplate") final RedisTemplate redisTemplate) {
112+
return new RedisRateLimiter(rateLimiterErrorHandler, redisTemplate);
104113
}
105114
}
106115

@@ -110,8 +119,9 @@ public RateLimiter redisRateLimiter(@Qualifier("rateLimiterRedisTemplate") final
110119
public static class ConsulConfiguration {
111120

112121
@Bean
113-
public RateLimiter consultRateLimiter(final ConsulClient consulClient, final ObjectMapper objectMapper) {
114-
return new ConsulRateLimiter(consulClient, objectMapper);
122+
public RateLimiter consultRateLimiter(final RateLimiterErrorHandler rateLimiterErrorHandler,
123+
final ConsulClient consulClient, final ObjectMapper objectMapper) {
124+
return new ConsulRateLimiter(rateLimiterErrorHandler, consulClient, objectMapper);
115125
}
116126

117127
}
@@ -123,8 +133,9 @@ public RateLimiter consultRateLimiter(final ConsulClient consulClient, final Obj
123133
public static class SpringDataConfiguration {
124134

125135
@Bean
126-
public RateLimiter springDataRateLimiter(final RateLimiterRepository rateLimiterRepository) {
127-
return new JpaRateLimiter(rateLimiterRepository);
136+
public RateLimiter springDataRateLimiter(final RateLimiterErrorHandler rateLimiterErrorHandler,
137+
final RateLimiterRepository rateLimiterRepository) {
138+
return new JpaRateLimiter(rateLimiterErrorHandler, rateLimiterRepository);
128139
}
129140

130141
}
@@ -134,8 +145,8 @@ public RateLimiter springDataRateLimiter(final RateLimiterRepository rateLimiter
134145
public static class InMemoryConfiguration {
135146

136147
@Bean
137-
public RateLimiter inMemoryRateLimiter() {
138-
return new InMemoryRateLimiter();
148+
public RateLimiter inMemoryRateLimiter(final RateLimiterErrorHandler rateLimiterErrorHandler) {
149+
return new InMemoryRateLimiter(rateLimiterErrorHandler);
139150
}
140151
}
141152

spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/AbstractRateLimiter.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616

1717
package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository;
1818

19+
import static java.util.concurrent.TimeUnit.SECONDS;
20+
1921
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate;
2022
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimiter;
2123
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy;
22-
2324
import java.util.Date;
24-
25-
import static java.util.concurrent.TimeUnit.SECONDS;
25+
import lombok.RequiredArgsConstructor;
26+
import lombok.extern.slf4j.Slf4j;
2627

2728
/**
2829
* Abstract implementation for {@link RateLimiter}.
@@ -31,22 +32,34 @@
3132
* @author Marcos Barbero
3233
* @since 2017-08-28
3334
*/
35+
@Slf4j
36+
@RequiredArgsConstructor
3437
public abstract class AbstractRateLimiter implements RateLimiter {
3538

36-
protected abstract Rate getRate(String key);
39+
private final RateLimiterErrorHandler rateLimiterErrorHandler;
3740

41+
protected abstract Rate getRate(String key);
3842
protected abstract void saveRate(Rate rate);
3943

4044
@Override
4145
public synchronized Rate consume(final Policy policy, final String key, final Long requestTime) {
4246
Rate rate = this.create(policy, key);
4347
updateRate(policy, rate, requestTime);
44-
saveRate(rate);
48+
try {
49+
saveRate(rate);
50+
} catch (RuntimeException e) {
51+
rateLimiterErrorHandler.handleSaveError(key, e);
52+
}
4553
return rate;
4654
}
4755

4856
private Rate create(final Policy policy, final String key) {
49-
Rate rate = this.getRate(key);
57+
Rate rate = null;
58+
try {
59+
rate = this.getRate(key);
60+
} catch (RuntimeException e) {
61+
rateLimiterErrorHandler.handleFetchError(key, e);
62+
}
5063

5164
if (!isExpired(rate)) {
5265
return rate;

spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/ConsulRateLimiter.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,16 @@
1616

1717
package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository;
1818

19+
import static org.springframework.util.StringUtils.hasText;
20+
1921
import com.ecwid.consul.v1.ConsulClient;
2022
import com.ecwid.consul.v1.kv.model.GetValue;
2123
import com.fasterxml.jackson.core.JsonProcessingException;
2224
import com.fasterxml.jackson.databind.ObjectMapper;
2325
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate;
24-
2526
import java.io.IOException;
26-
27-
import lombok.RequiredArgsConstructor;
2827
import lombok.extern.slf4j.Slf4j;
2928

30-
import static org.springframework.util.StringUtils.hasText;
31-
3229
/**
3330
* Consul rate limiter configuration.
3431
*
@@ -37,12 +34,18 @@
3734
* @since 2017-08-15
3835
*/
3936
@Slf4j
40-
@RequiredArgsConstructor
4137
public class ConsulRateLimiter extends AbstractRateLimiter {
4238

4339
private final ConsulClient consulClient;
4440
private final ObjectMapper objectMapper;
4541

42+
public ConsulRateLimiter(RateLimiterErrorHandler rateLimiterErrorHandler,
43+
ConsulClient consulClient, ObjectMapper objectMapper) {
44+
super(rateLimiterErrorHandler);
45+
this.consulClient = consulClient;
46+
this.objectMapper = objectMapper;
47+
}
48+
4649
@Override
4750
protected Rate getRate(String key) {
4851
Rate rate = null;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
5+
@Slf4j
6+
public class DefaultRateLimiterErrorHandler implements RateLimiterErrorHandler {
7+
8+
@Override
9+
public void handleSaveError(String key, Exception e) {
10+
log.error("Failed saving rate for " + key + ", returning unsaved rate", e);
11+
}
12+
13+
@Override
14+
public void handleFetchError(String key, Exception e) {
15+
log.error("Failed retrieving rate for " + key + ", will create new rate", e);
16+
}
17+
18+
@Override
19+
public void handleError(String msg, Exception e) {
20+
log.error(msg, e);
21+
}
22+
}

spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/InMemoryRateLimiter.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ public class InMemoryRateLimiter extends AbstractRateLimiter {
3232

3333
private Map<String, Rate> repository = new ConcurrentHashMap<>();
3434

35+
public InMemoryRateLimiter(RateLimiterErrorHandler rateLimiterErrorHandler) {
36+
super(rateLimiterErrorHandler);
37+
}
38+
3539
@Override
3640
protected Rate getRate(String key) {
3741
return this.repository.get(key);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository;
2+
3+
public interface RateLimiterErrorHandler {
4+
5+
void handleSaveError(String key, Exception e);
6+
void handleFetchError(String key, Exception e);
7+
void handleError(String msg, Exception e);
8+
}

spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/RedisRateLimiter.java

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,27 @@
1616

1717
package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository;
1818

19+
import static java.util.concurrent.TimeUnit.SECONDS;
20+
1921
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate;
2022
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimiter;
2123
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy;
22-
23-
import org.springframework.data.redis.core.RedisTemplate;
24-
2524
import lombok.RequiredArgsConstructor;
26-
27-
import static java.util.concurrent.TimeUnit.SECONDS;
25+
import lombok.extern.slf4j.Slf4j;
26+
import org.springframework.data.redis.core.RedisTemplate;
2827

2928
/**
3029
* @author Marcos Barbero
3130
* @author Liel Chayoun
3231
*/
32+
@Slf4j
3333
@RequiredArgsConstructor
3434
@SuppressWarnings("unchecked")
3535
public class RedisRateLimiter implements RateLimiter {
3636

3737
private static final String QUOTA_SUFFIX = "-quota";
3838

39+
private final RateLimiterErrorHandler rateLimiterErrorHandler;
3940
private final RedisTemplate redisTemplate;
4041

4142
@Override
@@ -55,7 +56,13 @@ private void calcRemainingLimit(Long limit, Long refreshInterval,
5556
if (limit != null) {
5657
handleExpiration(key, refreshInterval, rate);
5758
long usage = requestTime == null ? 1L : 0L;
58-
Long current = this.redisTemplate.boundValueOps(key).increment(usage);
59+
Long current = 0L;
60+
try {
61+
current = this.redisTemplate.boundValueOps(key).increment(usage);
62+
} catch (RuntimeException e) {
63+
String msg = "Failed retrieving rate for " + key + ", will return limit";
64+
rateLimiterErrorHandler.handleError(msg, e);
65+
}
5966
rate.setRemaining(Math.max(-1, limit - current));
6067
}
6168
}
@@ -66,17 +73,29 @@ private void calcRemainingQuota(Long quota, Long refreshInterval,
6673
String quotaKey = key + QUOTA_SUFFIX;
6774
handleExpiration(quotaKey, refreshInterval, rate);
6875
Long usage = requestTime != null ? requestTime : 0L;
69-
Long current = this.redisTemplate.boundValueOps(quotaKey).increment(usage);
76+
Long current = 0L;
77+
try {
78+
current = this.redisTemplate.boundValueOps(quotaKey).increment(usage);
79+
} catch (RuntimeException e) {
80+
String msg = "Failed retrieving rate for " + quotaKey + ", will return quota limit";
81+
rateLimiterErrorHandler.handleError(msg, e);
82+
}
7083
rate.setRemainingQuota(Math.max(-1, quota - current));
7184
}
7285
}
7386

7487
private void handleExpiration(String key, Long refreshInterval, Rate rate) {
75-
Long expire = this.redisTemplate.getExpire(key);
76-
if (expire == null || expire == -1) {
77-
this.redisTemplate.expire(key, refreshInterval, SECONDS);
78-
expire = refreshInterval;
88+
Long expire = null;
89+
try {
90+
expire = this.redisTemplate.getExpire(key);
91+
if (expire == null || expire == -1) {
92+
this.redisTemplate.expire(key, refreshInterval, SECONDS);
93+
expire = refreshInterval;
94+
}
95+
} catch (RuntimeException e) {
96+
String msg = "Failed retrieving expiration for " + key + ", will reset now";
97+
rateLimiterErrorHandler.handleError(msg, e);
7998
}
80-
rate.setReset(SECONDS.toMillis(expire));
99+
rate.setReset(SECONDS.toMillis(expire == null ? 0L : expire));
81100
}
82101
}

spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/springdata/JpaRateLimiter.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818

1919
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate;
2020
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.AbstractRateLimiter;
21-
22-
import lombok.RequiredArgsConstructor;
21+
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.RateLimiterErrorHandler;
2322

2423
/**
2524
* In memory rate limiter configuration for dev environment.
@@ -28,11 +27,16 @@
2827
* @author Liel Chayoun
2928
* @since 2017-06-23
3029
*/
31-
@RequiredArgsConstructor
3230
public class JpaRateLimiter extends AbstractRateLimiter {
3331

3432
private final RateLimiterRepository repository;
3533

34+
public JpaRateLimiter(RateLimiterErrorHandler rateLimiterErrorHandler,
35+
RateLimiterRepository repository) {
36+
super(rateLimiterErrorHandler);
37+
this.repository = repository;
38+
}
39+
3640
@Override
3741
protected Rate getRate(String key) {
3842
return this.repository.findOne(key);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository;
2+
3+
import static org.mockito.ArgumentMatchers.any;
4+
import static org.mockito.ArgumentMatchers.matches;
5+
import static org.mockito.Mockito.doThrow;
6+
import static org.mockito.Mockito.verify;
7+
import static org.mockito.Mockito.when;
8+
9+
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy;
10+
import java.lang.reflect.Field;
11+
import java.lang.reflect.Modifier;
12+
import org.junit.Before;
13+
import org.junit.Test;
14+
import org.mockito.Mockito;
15+
16+
public class AbstractRateLimiterTest {
17+
18+
private AbstractRateLimiter abstractRateLimiter;
19+
private RateLimiterErrorHandler rateLimiterErrorHandler;
20+
21+
@Before
22+
public void setUp() throws Exception {
23+
abstractRateLimiter = Mockito.mock(AbstractRateLimiter.class, Mockito.CALLS_REAL_METHODS);
24+
Field rateLimiterErrorHandler = AbstractRateLimiter.class.getDeclaredField("rateLimiterErrorHandler");
25+
boolean accessible = rateLimiterErrorHandler.isAccessible();
26+
rateLimiterErrorHandler.setAccessible(true);
27+
Field modifiersField = Field.class.getDeclaredField("modifiers");
28+
modifiersField.setAccessible(true);
29+
modifiersField.setInt(rateLimiterErrorHandler, rateLimiterErrorHandler.getModifiers() & ~Modifier.FINAL);
30+
this.rateLimiterErrorHandler = Mockito.mock(RateLimiterErrorHandler.class);
31+
rateLimiterErrorHandler.set(abstractRateLimiter, this.rateLimiterErrorHandler);
32+
rateLimiterErrorHandler.setAccessible(accessible);
33+
modifiersField.setInt(rateLimiterErrorHandler, rateLimiterErrorHandler.getModifiers() & Modifier.FINAL);
34+
}
35+
36+
@Test
37+
public void testConsumeGetRateException() {
38+
when(abstractRateLimiter.getRate(any())).thenThrow(new RuntimeException());
39+
Policy policy = new Policy();
40+
policy.setLimit(100L);
41+
abstractRateLimiter.consume(policy, "key", 0L);
42+
verify(rateLimiterErrorHandler).handleFetchError(matches("key"), any());
43+
}
44+
45+
@Test
46+
public void testConsumeSaveRateException() {
47+
doThrow(new RuntimeException()).when(abstractRateLimiter).saveRate(any());
48+
Policy policy = new Policy();
49+
policy.setLimit(100L);
50+
abstractRateLimiter.consume(policy, "key", 0L);
51+
verify(rateLimiterErrorHandler).handleSaveError(matches("key"), any());
52+
}
53+
}

0 commit comments

Comments
 (0)