diff --git a/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/RateLimiterConfig.java b/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/RateLimiterConfig.java index 41fa6ff421..2e5d996f23 100644 --- a/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/RateLimiterConfig.java +++ b/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/RateLimiterConfig.java @@ -64,6 +64,14 @@ public int getLimitForPeriod() { return limitForPeriod; } + @Override public String toString() { + return "RateLimiterConfig{" + + "timeoutDuration=" + timeoutDuration + + ", limitRefreshPeriod=" + limitRefreshPeriod + + ", limitForPeriod=" + limitForPeriod + + '}'; + } + public static class Builder { private RateLimiterConfig config = new RateLimiterConfig(); diff --git a/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/internal/AtomicRateLimiter.java b/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/internal/AtomicRateLimiter.java index 69b9912436..94fef43e1b 100644 --- a/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/internal/AtomicRateLimiter.java +++ b/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/internal/AtomicRateLimiter.java @@ -280,6 +280,13 @@ public Flowable getEventStream() { return eventPublisher; } + @Override public String toString() { + return "AtomicRateLimiter{" + + "name='" + name + '\'' + + ", rateLimiterConfig=" + rateLimiterConfig + + '}'; + } + /** * Get the enhanced Metrics with some implementation specific details. * diff --git a/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/internal/SemaphoreBasedRateLimiter.java b/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/internal/SemaphoreBasedRateLimiter.java index 06b410c7ac..28d2f7bf91 100644 --- a/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/internal/SemaphoreBasedRateLimiter.java +++ b/resilience4j-ratelimiter/src/main/java/io/github/resilience4j/ratelimiter/internal/SemaphoreBasedRateLimiter.java @@ -157,6 +157,13 @@ public RateLimiterConfig getRateLimiterConfig() { return this.rateLimiterConfig; } + @Override public String toString() { + return "SemaphoreBasedRateLimiter{" + + "name='" + name + '\'' + + ", rateLimiterConfig=" + rateLimiterConfig + + '}'; + } + /** * {@inheritDoc} */ diff --git a/resilience4j-spring-boot/README.adoc b/resilience4j-spring-boot/README.adoc index e8bd7369fa..1799a1fef1 100644 --- a/resilience4j-spring-boot/README.adoc +++ b/resilience4j-spring-boot/README.adoc @@ -18,12 +18,15 @@ dependencies { == Spring AOP -The demo shows how to use the `CircuitBreaker` annotation to make your Spring Boot application more fault tolerant. You can either annotate a class in order to protect all public methods or just some specific methods. +The demo shows how to use the `CircuitBreaker` and `RateLimiter` annotations +to make your Spring Boot application more fault tolerant. +You can either annotate a class in order to protect all public methods or just some specific methods. For example: [source,java] ---- @CircuitBreaker(backend = "backendA") +@RateLimiter("backendA") @Component(value = "backendAConnector") public class BackendAConnector implements Connector { ... @@ -68,7 +71,13 @@ public Observable methodWhichReturnsAStream() { == Monitoring -Spring Boot Actuator health information can be used to check the status of your running application. It is often used by monitoring software to alert someone if a production system has serious issues. This demo publishes the status and metrics of all CircuitBreakers via a custom `CircuitBreakerHealthIndicator`. A closed CircuitBreaker state is mapped to UP, an open state to DOWN and a half-open state to UNKNOWN. +Spring Boot Actuator health information can be used to check the status of your running application. +It is often used by monitoring software to alert someone if a production system has serious issues. + +=== CircuitBreaker +This demo publishes the status and metrics of all CircuitBreakers via a custom `CircuitBreakerHealthIndicator`. +A closed CircuitBreaker state is mapped to UP, an open state to DOWN and a half-open state to UNKNOWN. + For example: [source,json] @@ -142,8 +151,28 @@ resilience4j_circuitbreaker_states{name="backendA",state="open",} 0.0 resilience4j_circuitbreaker_states{name="backendA",state="half_open",} 0.0 ---- +=== RateLimiter +This demo publishes the status and metrics of all RateLimiter via a custom `RateLimiterHealthIndicator`. +RateLimiterHealthIndicator changes its state DOWN only if there is some permission waiting threads +and they won't be able to unblock until timeout. + +For example: + +[source,json] +---- +{ + "status": "UP", + "backendA": { + "status": "UP", + "availablePermissions": 10, + "numberOfWaitingThreads": 0 + } +} +---- + == Configuration +=== CircuitBreaker You can configure your CircuitBreakers in Spring Boot's `application.yml` config file. For example @@ -164,7 +193,28 @@ resilience4j.circuitbreaker: eventConsumerBufferSize: 10 ---- -== CircuitBreaker Event Monitoring +=== RateLimiter +You can configure your CircuitBreakers in Spring Boot's `application.yml` config file. +For example + +---- +resilience4j.ratelimiter: + limiters: + backendA: + limitForPeriod: 10 + limitRefreshPeriodInMillis: 1000 + timeoutInMillis: 0 + subscribeForEvents: true + registerHealthIndicator: true + backendB: + limitForPeriod: 6 + limitRefreshPeriodInMillis: 500 + timeoutInMillis: 3000 +---- + +== Event Monitoring + +=== CircuitBreaker The emitted CircuitBreaker events are stored in a separate circular event consumer buffers. The size of a event consumer buffer can be configured per CircuitBreaker in the application.yml file (eventConsumerBufferSize). The demo adds a custom Spring Boot Actuator endpoint which can be used to monitor the emitted events of your CircuitBreakers. @@ -284,9 +334,48 @@ For example `/management/circuitbreaker/events/backendA/ERROR`: } ---- +=== RateLimiter +WARNING: Unlike the CircuitBreaker events, RateLimiter events require explicit subscription. +Use property resilience4j.ratelimiter.limiters.{yourBackendName}.registerHealthIndicator=true + +There are literally the same endpoints implemented for RateLimiter, +so for detailed documentation please refer to previous section: + +List of available endpoints: + +* `/ratelimiter/events` +* `/ratelimiter/stream/events` +* `/ratelimiter/events/{rateLimiterName}` +* `/ratelimiter/stream/events/{rateLimiterName}` +* `/ratelimiter/events/{rateLimiterName}/{eventType}` +* `/ratelimiter/stream/events/{rateLimiterName}/{eventType}` + +Example of response: +---- +{ + "eventsList": [ + { + "rateLimiterName": "backendA", + "rateLimiterEventType": "SUCCESSFUL_ACQUIRE", + "rateLimiterCreationTime": "2017-05-05T21:29:40.463+03:00[Europe/Uzhgorod]" + }, + { + "rateLimiterName": "backendA", + "rateLimiterEventType": "SUCCESSFUL_ACQUIRE", + "rateLimiterCreationTime": "2017-05-05T21:29:40.469+03:00[Europe/Uzhgorod]" + }, + { + "rateLimiterName": "backendA", + "rateLimiterEventType": "FAILED_ACQUIRE", + "rateLimiterCreationTime": "2017-05-05T21:29:41.268+03:00[Europe/Uzhgorod]" + } + ] +} +---- + == License -Copyright 2017 Robert Winkler +Copyright 2017 Robert Winkler and Bohdan Storozhuk 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 diff --git a/resilience4j-spring-boot/build.gradle b/resilience4j-spring-boot/build.gradle index 093b08bb85..cc71ee8669 100644 --- a/resilience4j-spring-boot/build.gradle +++ b/resilience4j-spring-boot/build.gradle @@ -3,6 +3,7 @@ dependencies { compile ( libraries.spring_boot_actuator ) compile ( libraries.spring_boot_web ) compile project(':resilience4j-circuitbreaker') + compile project(':resilience4j-ratelimiter') compile project(':resilience4j-consumer') compileOnly project(':resilience4j-prometheus') compileOnly project(':resilience4j-metrics') diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerAspect.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerAspect.java index aac2db91d0..4358805ad9 100644 --- a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerAspect.java +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerAspect.java @@ -36,7 +36,7 @@ @Aspect public class CircuitBreakerAspect { - private static Logger logger = LoggerFactory.getLogger(CircuitBreakerAspect.class); + private static final Logger logger = LoggerFactory.getLogger(CircuitBreakerAspect.class); private final CircuitBreakerProperties circuitBreakerProperties; private final CircuitBreakerRegistry circuitBreakerRegistry; diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerAutoConfiguration.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerAutoConfiguration.java index 11ae750f5c..f12e7ded97 100644 --- a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerAutoConfiguration.java +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/circuitbreaker/autoconfigure/CircuitBreakerAutoConfiguration.java @@ -60,10 +60,9 @@ public CircuitBreakerEndpoint circuitBreakerEndpoint(CircuitBreakerRegistry circ } @Bean - public CircuitBreakerEventsEndpoint circuitBreakerEventsEndpoint(CircuitBreakerEndpoint circuitBreakerEndpoint, - EventConsumerRegistry eventConsumerRegistry, + public CircuitBreakerEventsEndpoint circuitBreakerEventsEndpoint(EventConsumerRegistry eventConsumerRegistry, CircuitBreakerRegistry circuitBreakerRegistry) { - return new CircuitBreakerEventsEndpoint(circuitBreakerEndpoint, eventConsumerRegistry, circuitBreakerRegistry); + return new CircuitBreakerEventsEndpoint(eventConsumerRegistry, circuitBreakerRegistry); } /** diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/circuitbreaker/monitoring/endpoint/CircuitBreakerEventsEndpoint.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/circuitbreaker/monitoring/endpoint/CircuitBreakerEventsEndpoint.java index 63abdd3859..e66bbde4df 100644 --- a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/circuitbreaker/monitoring/endpoint/CircuitBreakerEventsEndpoint.java +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/circuitbreaker/monitoring/endpoint/CircuitBreakerEventsEndpoint.java @@ -16,16 +16,14 @@ package io.github.resilience4j.circuitbreaker.monitoring.endpoint; -import org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter; import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import java.util.Comparator; - import io.github.resilience4j.circuitbreaker.CircuitBreaker; import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; import io.github.resilience4j.circuitbreaker.event.CircuitBreakerEvent; @@ -34,18 +32,19 @@ import io.reactivex.Flowable; import javaslang.collection.Seq; +import java.util.Comparator; + -@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) -public class CircuitBreakerEventsEndpoint extends EndpointMvcAdapter { +@Controller +@RequestMapping(value = "circuitbreaker/", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) +public class CircuitBreakerEventsEndpoint { private static final String MEDIA_TYPE_TEXT_EVENT_STREAM = "text/event-stream"; private final EventConsumerRegistry eventConsumerRegistry; private final CircuitBreakerRegistry circuitBreakerRegistry; - public CircuitBreakerEventsEndpoint(CircuitBreakerEndpoint circuitBreakerEndpoint, - EventConsumerRegistry eventConsumerRegistry, + public CircuitBreakerEventsEndpoint(EventConsumerRegistry eventConsumerRegistry, CircuitBreakerRegistry circuitBreakerRegistry) { - super(circuitBreakerEndpoint); this.eventConsumerRegistry = eventConsumerRegistry; this.circuitBreakerRegistry = circuitBreakerRegistry; } diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/annotation/RateLimiter.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/annotation/RateLimiter.java new file mode 100644 index 0000000000..48ec8ad984 --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/annotation/RateLimiter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017 Bohdan Storozhuk + * + * 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.github.resilience4j.ratelimiter.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation can be applied to a class or a specific method. + * Applying it on a class is equivalent to applying it on all its public methods. + * The annotation enables throttling for all methods where it is applied. + * Throttling monitoring is performed via a rate limiter. + * See {@link io.github.resilience4j.ratelimiter.RateLimiter} for details. + */ +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = {ElementType.METHOD, ElementType.TYPE}) +@Documented +public @interface RateLimiter { + /** + * Name of the rate limiter + * @return + */ + String name(); +} diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterAspect.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterAspect.java new file mode 100644 index 0000000000..29c06e980a --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterAspect.java @@ -0,0 +1,114 @@ +/* + * Copyright 2017 Bohdan Storozhuk + * + * 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.github.resilience4j.ratelimiter.autoconfigure; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.resilience4j.ratelimiter.RateLimiterConfig; +import io.github.resilience4j.ratelimiter.RateLimiterRegistry; +import io.github.resilience4j.ratelimiter.annotation.RateLimiter; + +import java.lang.reflect.Method; + +/** + * This Spring AOP aspect intercepts all methods which are annotated with a {@link RateLimiter} annotation. + * The aspect protects an annotated method with a RateLimiter. The RateLimiterRegistry is used to retrieve an instance of a RateLimiter for + * a specific backend. + */ + +@Aspect +public class RateLimiterAspect { + private static final Logger logger = LoggerFactory.getLogger(RateLimiterAspect.class); + public static final String RATE_LIMITER_RECEIVED = "Created or retrieved rate limiter '{}' with period: '{}'; limit for period: '{}'; timeout: '{}'; method: '{}'"; + + private final RateLimiterRegistry rateLimiterRegistry; + + public RateLimiterAspect(RateLimiterRegistry rateLimiterRegistry) { + this.rateLimiterRegistry = rateLimiterRegistry; + } + + /** + * Method used as pointcut + * @param rateLimiter - matched annotation + */ + @Pointcut(value = "@within(rateLimiter) || @annotation(rateLimiter)", argNames = "rateLimiter") + public void matchAnnotatedClassOrMethod(RateLimiter rateLimiter) { + // Method used as pointcut + } + + @Around(value = "matchAnnotatedClassOrMethod(limitedService)", argNames = "proceedingJoinPoint, limitedService") + public Object rateLimiterAroundAdvice(ProceedingJoinPoint proceedingJoinPoint, RateLimiter limitedService) throws Throwable { + RateLimiter targetService = limitedService; + Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod(); + String methodName = method.getDeclaringClass().getName() + "#" + method.getName(); + if (targetService == null) { + targetService = getRateLimiterAnnotation(proceedingJoinPoint); + } + String name = targetService.name(); + io.github.resilience4j.ratelimiter.RateLimiter rateLimiter = getOrCreateRateLimiter(methodName, name); + return handleJoinPoint(proceedingJoinPoint, rateLimiter, methodName); + } + + private io.github.resilience4j.ratelimiter.RateLimiter getOrCreateRateLimiter(String methodName, String name) { + io.github.resilience4j.ratelimiter.RateLimiter rateLimiter = rateLimiterRegistry.rateLimiter(name); + + if (logger.isDebugEnabled()) { + RateLimiterConfig rateLimiterConfig = rateLimiter.getRateLimiterConfig(); + logger.debug( + RATE_LIMITER_RECEIVED, + name, rateLimiterConfig.getLimitRefreshPeriod(), rateLimiterConfig.getLimitForPeriod(), + rateLimiterConfig.getTimeoutDuration(), methodName + ); + } + + return rateLimiter; + } + + private RateLimiter getRateLimiterAnnotation(ProceedingJoinPoint proceedingJoinPoint) { + RateLimiter rateLimiter = null; + Class targetClass = proceedingJoinPoint.getTarget().getClass(); + if (targetClass.isAnnotationPresent(RateLimiter.class)) { + rateLimiter = targetClass.getAnnotation(RateLimiter.class); + if (rateLimiter == null) { + rateLimiter = targetClass.getDeclaredAnnotation(RateLimiter.class); + } + if (rateLimiter == null) { + logger.debug("TargetClass has no declared annotation 'RateLimiter'"); + } + } + return rateLimiter; + } + + private Object handleJoinPoint(ProceedingJoinPoint proceedingJoinPoint, + io.github.resilience4j.ratelimiter.RateLimiter rateLimiter, String methodName) + throws Throwable { + try { + io.github.resilience4j.ratelimiter.RateLimiter.waitForPermission(rateLimiter); + return proceedingJoinPoint.proceed(); + } catch (Exception exception) { + if (logger.isDebugEnabled()) { + logger.debug("Invocation of method '" + methodName + "' failed!", exception); + } + throw exception; + } + } +} diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterAutoConfiguration.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterAutoConfiguration.java new file mode 100644 index 0000000000..b0c38f9245 --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterAutoConfiguration.java @@ -0,0 +1,117 @@ +/* + * Copyright 2017 Bohdan Storozhuk + * + * 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.github.resilience4j.ratelimiter.autoconfigure; + +import static io.github.resilience4j.ratelimiter.autoconfigure.RateLimiterProperties.createRateLimiterConfig; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.github.resilience4j.consumer.DefaultEventConsumerRegistry; +import io.github.resilience4j.consumer.EventConsumer; +import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.ratelimiter.RateLimiter; +import io.github.resilience4j.ratelimiter.RateLimiterConfig; +import io.github.resilience4j.ratelimiter.RateLimiterRegistry; +import io.github.resilience4j.ratelimiter.event.RateLimiterEvent; +import io.github.resilience4j.ratelimiter.internal.InMemoryRateLimiterRegistry; +import io.github.resilience4j.ratelimiter.monitoring.endpoint.RateLimiterEndpoint; +import io.github.resilience4j.ratelimiter.monitoring.endpoint.RateLimiterEventsEndpoint; +import io.github.resilience4j.ratelimiter.monitoring.health.RateLimiterHealthIndicator; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} for resilience4j ratelimiter. + */ +@Configuration +@ConditionalOnClass(RateLimiter.class) +@EnableConfigurationProperties(RateLimiterProperties.class) +public class RateLimiterAutoConfiguration { + + private static final Logger logger = LoggerFactory.getLogger(RateLimiterAutoConfiguration.class); + + @Bean + public RateLimiterRegistry rateLimiterRegistry(RateLimiterProperties rateLimiterProperties, + EventConsumerRegistry rateLimiterEventsConsumerRegistry, + ConfigurableBeanFactory beanFactory) { + RateLimiterRegistry rateLimiterRegistry = new InMemoryRateLimiterRegistry(RateLimiterConfig.ofDefaults()); + rateLimiterProperties.getLimiters().forEach( + (name, properties) -> { + RateLimiter rateLimiter = createRateLimiter(rateLimiterRegistry, name, properties); + if (properties.getSubscribeForEvents()) { + subscribeToLimiterEvents(rateLimiterEventsConsumerRegistry, name, properties, rateLimiter); + } + if (properties.getRegisterHealthIndicator()) { + createHealthIndicatorForLimiter(beanFactory, name, rateLimiter); + } + } + ); + return rateLimiterRegistry; + } + + @Bean + public RateLimiterAspect rateLimiterAspect(RateLimiterRegistry rateLimiterRegistry) { + return new RateLimiterAspect(rateLimiterRegistry); + } + + @Bean + public RateLimiterEndpoint rateLimiterEndpoint(RateLimiterRegistry rateLimiterRegistry) { + return new RateLimiterEndpoint(rateLimiterRegistry); + } + + @Bean + public RateLimiterEventsEndpoint rateLimiterEventsEndpoint(EventConsumerRegistry rateLimiterEventsConsumerRegistry, + RateLimiterRegistry rateLimiterRegistry) { + return new RateLimiterEventsEndpoint(rateLimiterEventsConsumerRegistry, rateLimiterRegistry); + } + + /** + * The EventConsumerRegistry is used to manage EventConsumer instances. + * The EventConsumerRegistry is used by the RateLimiterHealthIndicator to show the latest RateLimiterEvents events + * for each RateLimiter instance. + */ + @Bean + public EventConsumerRegistry rateLimiterEventsConsumerRegistry() { + return new DefaultEventConsumerRegistry<>(); + } + + private void createHealthIndicatorForLimiter(ConfigurableBeanFactory beanFactory, String name, RateLimiter rateLimiter) { + beanFactory.registerSingleton( + name + "HealthIndicator", + new RateLimiterHealthIndicator(rateLimiter) + ); + } + + private void subscribeToLimiterEvents(EventConsumerRegistry rateLimiterEventsConsumerRegistry, String name, RateLimiterProperties.LimiterProperties properties, RateLimiter rateLimiter) { + EventConsumer eventConsumer = rateLimiterEventsConsumerRegistry + .createEventConsumer(name, properties.getEventConsumerBufferSize()); + rateLimiter.getEventStream().subscribe(eventConsumer); + + logger.debug("Autoconfigure subscription for Rate Limiter {}", rateLimiter); + } + + private RateLimiter createRateLimiter(RateLimiterRegistry rateLimiterRegistry, String name, RateLimiterProperties.LimiterProperties properties) { + RateLimiter rateLimiter = + rateLimiterRegistry.rateLimiter(name, createRateLimiterConfig(properties)); + logger.debug("Autoconfigure Rate Limiter registered. {}", rateLimiter); + return rateLimiter; + } +} diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterProperties.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterProperties.java new file mode 100644 index 0000000000..ddecd6aff4 --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/autoconfigure/RateLimiterProperties.java @@ -0,0 +1,171 @@ +/* + * Copyright 2017 Bohdan Storozhuk + * + * 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.github.resilience4j.ratelimiter.autoconfigure; + +import io.github.resilience4j.ratelimiter.RateLimiterConfig; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +@ConfigurationProperties(prefix = "resilience4j.ratelimiter") +@Component +public class RateLimiterProperties { + + private Map limiters = new HashMap<>(); + + private LimiterProperties getLimiterProperties(String limiter) { + return limiters.get(limiter); + } + + public RateLimiterConfig createRateLimiterConfig(String limiter) { + return createRateLimiterConfig(getLimiterProperties(limiter)); + } + + public static RateLimiterConfig createRateLimiterConfig(LimiterProperties limiterProperties) { + if (limiterProperties == null) { + return RateLimiterConfig.ofDefaults(); + } + + RateLimiterConfig.Builder rateLimiterConfigBuilder = RateLimiterConfig.custom(); + + if (limiterProperties.getLimitForPeriod() != null) { + rateLimiterConfigBuilder.limitForPeriod(limiterProperties.getLimitForPeriod()); + } + + if (limiterProperties.getLimitRefreshPeriodInMillis() != null) { + rateLimiterConfigBuilder.limitRefreshPeriod(Duration.ofMillis(limiterProperties.getLimitRefreshPeriodInMillis())); + } + + if (limiterProperties.getTimeoutInMillis() != null) { + rateLimiterConfigBuilder.timeoutDuration(Duration.ofMillis(limiterProperties.getTimeoutInMillis())); + } + + return rateLimiterConfigBuilder.build(); + } + + public Map getLimiters() { + return limiters; + } + + /** + * Class storing property values for configuring {@link io.github.resilience4j.ratelimiter.RateLimiterConfig} instances. + */ + public static class LimiterProperties { + + private Integer limitForPeriod; + private Integer limitRefreshPeriodInMillis; + private Integer timeoutInMillis; + private Boolean subscribeForEvents = false; + private Boolean registerHealthIndicator = false; + private Integer eventConsumerBufferSize = 100; + + /** + * Configures the permissions limit for refresh period. + * Count of permissions available during one rate limiter period + * specified by {@link RateLimiterConfig#limitRefreshPeriod} value. + * Default value is 50. + * + * @return the permissions limit for refresh period + */ + public Integer getLimitForPeriod() { + return limitForPeriod; + } + + /** + * Configures the permissions limit for refresh period. + * Count of permissions available during one rate limiter period + * specified by {@link RateLimiterConfig#limitRefreshPeriod} value. + * Default value is 50. + * + * @param limitForPeriod the permissions limit for refresh period + */ + public void setLimitForPeriod(Integer limitForPeriod) { + this.limitForPeriod = limitForPeriod; + } + + /** + * Configures the period of limit refresh. + * After each period rate limiter sets its permissions + * count to {@link RateLimiterConfig#limitForPeriod} value. + * Default value is 500 nanoseconds. + * + * @return the period of limit refresh + */ + public Integer getLimitRefreshPeriodInMillis() { + return limitRefreshPeriodInMillis; + } + + /** + * Configures the period of limit refresh. + * After each period rate limiter sets its permissions + * count to {@link RateLimiterConfig#limitForPeriod} value. + * Default value is 500 nanoseconds. + * + * @param limitRefreshPeriodInMillis the period of limit refresh + */ + public void setLimitRefreshPeriodInMillis(Integer limitRefreshPeriodInMillis) { + this.limitRefreshPeriodInMillis = limitRefreshPeriodInMillis; + } + + /** + * Configures the default wait for permission duration. + * Default value is 5 seconds. + * + * @return wait for permission duration + */ + public Integer getTimeoutInMillis() { + return timeoutInMillis; + } + + /** + * Configures the default wait for permission duration. + * Default value is 5 seconds. + * + * @param timeoutInMillis wait for permission duration + */ + public void setTimeoutInMillis(Integer timeoutInMillis) { + this.timeoutInMillis = timeoutInMillis; + } + + public Boolean getSubscribeForEvents() { + return subscribeForEvents; + } + + public void setSubscribeForEvents(Boolean subscribeForEvents) { + this.subscribeForEvents = subscribeForEvents; + } + + public Integer getEventConsumerBufferSize() { + return eventConsumerBufferSize; + } + + public void setEventConsumerBufferSize(Integer eventConsumerBufferSize) { + this.eventConsumerBufferSize = eventConsumerBufferSize; + } + + public Boolean getRegisterHealthIndicator() { + return registerHealthIndicator; + } + + public void setRegisterHealthIndicator(Boolean registerHealthIndicator) { + this.registerHealthIndicator = registerHealthIndicator; + } + } + +} diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/endpoint/RateLimiterEndpoint.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/endpoint/RateLimiterEndpoint.java new file mode 100644 index 0000000000..99b2dbb6cb --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/endpoint/RateLimiterEndpoint.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 Bohdan Storozhuk + * + * 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.github.resilience4j.ratelimiter.monitoring.endpoint; + +import io.github.resilience4j.ratelimiter.RateLimiter; +import io.github.resilience4j.ratelimiter.RateLimiterRegistry; +import io.github.resilience4j.ratelimiter.monitoring.model.RateLimiterEndpointResponse; + +import org.springframework.boot.actuate.endpoint.AbstractEndpoint; +import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.ResponseEntity; + +import java.util.List; + +/** + * {@link Endpoint} to expose RateLimiter events. + */ +@ConfigurationProperties(prefix = "endpoints.ratelimiter") +public class RateLimiterEndpoint extends AbstractEndpoint { + + private final RateLimiterRegistry rateLimiterRegistry; + + public RateLimiterEndpoint(RateLimiterRegistry rateLimiterRegistry) { + super("ratelimiter"); + this.rateLimiterRegistry = rateLimiterRegistry; + } + + @Override + public Object invoke() { + List names = rateLimiterRegistry.getAllRateLimiters() + .map(RateLimiter::getName).sorted().toJavaList(); + return ResponseEntity.ok(new RateLimiterEndpointResponse(names)); + } +} diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/endpoint/RateLimiterEventsEmitter.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/endpoint/RateLimiterEventsEmitter.java new file mode 100644 index 0000000000..cecb08b50c --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/endpoint/RateLimiterEventsEmitter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017 Bohdan Storozhuk + * + * 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.github.resilience4j.ratelimiter.monitoring.endpoint; + +import org.springframework.http.MediaType; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import io.github.resilience4j.ratelimiter.event.RateLimiterEvent; +import io.github.resilience4j.ratelimiter.monitoring.model.RateLimiterEventDTO; +import io.reactivex.Flowable; +import io.reactivex.disposables.Disposable; + +import java.io.IOException; + +public class RateLimiterEventsEmitter { + private final SseEmitter sseEmitter; + private final Disposable disposable; + + public RateLimiterEventsEmitter(Flowable flowable) { + this.sseEmitter = new SseEmitter(); + this.sseEmitter.onCompletion(this::unsubscribe); + this.sseEmitter.onTimeout(this::unsubscribe); + this.disposable = flowable.subscribe(this::notify, + this.sseEmitter::completeWithError, + this.sseEmitter::complete); + } + + private void notify(RateLimiterEventDTO rateLimiterEventDTO) throws IOException { + sseEmitter.send(rateLimiterEventDTO, MediaType.APPLICATION_JSON); + } + + private void unsubscribe() { + this.disposable.dispose(); + } + + public static SseEmitter createSseEmitter(Flowable eventStream) { + Flowable flowable = eventStream.map(RateLimiterEventDTO::createRateLimiterEventDTO); + return new RateLimiterEventsEmitter(flowable).sseEmitter; + } +} diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/endpoint/RateLimiterEventsEndpoint.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/endpoint/RateLimiterEventsEndpoint.java new file mode 100644 index 0000000000..a8fd4ab217 --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/endpoint/RateLimiterEventsEndpoint.java @@ -0,0 +1,112 @@ +/* + * Copyright 2017 Bohdan Storozhuk + * + * 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.github.resilience4j.ratelimiter.monitoring.endpoint; + +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import io.github.resilience4j.consumer.EventConsumer; +import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.ratelimiter.RateLimiter; +import io.github.resilience4j.ratelimiter.RateLimiterRegistry; +import io.github.resilience4j.ratelimiter.event.RateLimiterEvent; +import io.github.resilience4j.ratelimiter.monitoring.model.RateLimiterEventDTO; +import io.github.resilience4j.ratelimiter.monitoring.model.RateLimiterEventsEndpointResponse; +import io.reactivex.Flowable; +import javaslang.collection.Seq; + +import java.util.Comparator; +import java.util.List; + +@Controller +@RequestMapping(value = "ratelimiter/", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) +public class RateLimiterEventsEndpoint { + private static final String MEDIA_TYPE_TEXT_EVENT_STREAM = "text/event-stream"; + private final EventConsumerRegistry eventsConsumerRegistry; + private final RateLimiterRegistry rateLimiterRegistry; + + public RateLimiterEventsEndpoint(EventConsumerRegistry eventsConsumerRegistry, + RateLimiterRegistry rateLimiterRegistry) { + this.eventsConsumerRegistry = eventsConsumerRegistry; + this.rateLimiterRegistry = rateLimiterRegistry; + } + + + @RequestMapping(value = "events", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public RateLimiterEventsEndpointResponse getAllRateLimiterEvents() { + List eventsList = eventsConsumerRegistry.getAllEventConsumer() + .flatMap(EventConsumer::getBufferedEvents) + .sorted(Comparator.comparing(RateLimiterEvent::getCreationTime)) + .map(RateLimiterEventDTO::createRateLimiterEventDTO).toJavaList(); + return new RateLimiterEventsEndpointResponse(eventsList); + } + + @RequestMapping(value = "stream/events", produces = MEDIA_TYPE_TEXT_EVENT_STREAM) + public SseEmitter getAllRateLimiterEventsStream() { + Seq> eventStreams = rateLimiterRegistry.getAllRateLimiters() + .map(RateLimiter::getEventStream); + return RateLimiterEventsEmitter.createSseEmitter(Flowable.merge(eventStreams)); + } + + @RequestMapping(value = "events/{rateLimiterName}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public RateLimiterEventsEndpointResponse getEventsFilteredByRateLimiterName(@PathVariable("rateLimiterName") String rateLimiterName) { + List eventsList = eventsConsumerRegistry.getEventConsumer(rateLimiterName).getBufferedEvents() + .filter(event -> event.getRateLimiterName().equals(rateLimiterName)) + .map(RateLimiterEventDTO::createRateLimiterEventDTO).toJavaList(); + return new RateLimiterEventsEndpointResponse(eventsList); + } + + @RequestMapping(value = "stream/events/{rateLimiterName}", produces = MEDIA_TYPE_TEXT_EVENT_STREAM) + public SseEmitter getEventsStreamFilteredByRateLimiterName(@PathVariable("rateLimiterName") String rateLimiterName) { + RateLimiter rateLimiter = rateLimiterRegistry.getAllRateLimiters() + .find(rL -> rL.getName().equals(rateLimiterName)) + .getOrElseThrow(() -> + new IllegalArgumentException(String.format("rate limiter with name %s not found", rateLimiterName))); + return RateLimiterEventsEmitter.createSseEmitter(rateLimiter.getEventStream()); + } + + @RequestMapping(value = "events/{rateLimiterName}/{eventType}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public RateLimiterEventsEndpointResponse getEventsFilteredByRateLimiterNameAndEventType(@PathVariable("rateLimiterName") String rateLimiterName, + @PathVariable("eventType") String eventType) { + RateLimiterEvent.Type targetType = RateLimiterEvent.Type.valueOf(eventType.toUpperCase()); + List eventsList = eventsConsumerRegistry.getEventConsumer(rateLimiterName).getBufferedEvents() + .filter(event -> event.getRateLimiterName().equals(rateLimiterName)) + .filter(event -> event.getEventType() == targetType) + .map(RateLimiterEventDTO::createRateLimiterEventDTO).toJavaList(); + return new RateLimiterEventsEndpointResponse(eventsList); + } + + @RequestMapping(value = "stream/events/{rateLimiterName}/{eventType}", produces = MEDIA_TYPE_TEXT_EVENT_STREAM) + public SseEmitter getEventsStreamFilteredByRateLimiterNameAndEventType(@PathVariable("rateLimiterName") String rateLimiterName, + @PathVariable("eventType") String eventType) { + RateLimiterEvent.Type targetType = RateLimiterEvent.Type.valueOf(eventType.toUpperCase()); + RateLimiter rateLimiter = rateLimiterRegistry.getAllRateLimiters() + .find(rL -> rL.getName().equals(rateLimiterName)) + .getOrElseThrow(() -> + new IllegalArgumentException(String.format("rate limiter with name %s not found", rateLimiterName))); + Flowable eventStream = rateLimiter.getEventStream() + .filter(event -> event.getEventType() == targetType); + return RateLimiterEventsEmitter.createSseEmitter(eventStream); + } +} diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/health/RateLimiterHealthIndicator.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/health/RateLimiterHealthIndicator.java new file mode 100644 index 0000000000..b1d028fa9c --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/health/RateLimiterHealthIndicator.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017 Bohdan Storozhuk + * + * 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.github.resilience4j.ratelimiter.monitoring.health; + +import io.github.resilience4j.ratelimiter.RateLimiter; +import io.github.resilience4j.ratelimiter.internal.AtomicRateLimiter; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.Status; + +public class RateLimiterHealthIndicator implements HealthIndicator { + + private final RateLimiter rateLimiter; + private final long timeoutInNanos; + + public RateLimiterHealthIndicator(RateLimiter rateLimiter) { + this.rateLimiter = rateLimiter; + timeoutInNanos = rateLimiter.getRateLimiterConfig().getTimeoutDuration().toNanos(); + } + + @Override + public Health health() { + RateLimiter.Metrics metrics = rateLimiter.getMetrics(); + int availablePermissions = metrics.getAvailablePermissions(); + int numberOfWaitingThreads = metrics.getNumberOfWaitingThreads(); + if (availablePermissions > 0 || numberOfWaitingThreads == 0) { + return rateLimiterHealth(Status.UP, availablePermissions, numberOfWaitingThreads); + } + if (rateLimiter instanceof AtomicRateLimiter) { + AtomicRateLimiter atomicRateLimiter = (AtomicRateLimiter) this.rateLimiter; + AtomicRateLimiter.AtomicRateLimiterMetrics detailedMetrics = atomicRateLimiter.getDetailedMetrics(); + if (detailedMetrics.getNanosToWait() > timeoutInNanos) { + rateLimiterHealth(Status.DOWN, availablePermissions, numberOfWaitingThreads); + } + } + return rateLimiterHealth(Status.UNKNOWN, availablePermissions, numberOfWaitingThreads); + } + + private Health rateLimiterHealth(Status status, int availablePermissions, int numberOfWaitingThreads) { + return Health.status(status) + .withDetail("availablePermissions", availablePermissions) + .withDetail("numberOfWaitingThreads", numberOfWaitingThreads) + .build(); + } +} \ No newline at end of file diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/model/RateLimiterEndpointResponse.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/model/RateLimiterEndpointResponse.java new file mode 100644 index 0000000000..86dab35fba --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/model/RateLimiterEndpointResponse.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Bohdan Storozhuk + * + * 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.github.resilience4j.ratelimiter.monitoring.model; + +import java.util.List; + + +public class RateLimiterEndpointResponse { + + private List rateLimitersNames; + + // created for spring to be able to construct POJO + public RateLimiterEndpointResponse() {} + + public RateLimiterEndpointResponse(List rateLimitersNames) { + this.rateLimitersNames = rateLimitersNames; + } + + public List getRateLimitersNames() { + return rateLimitersNames; + } + + public void setRateLimitersNames(List rateLimitersNames) { + this.rateLimitersNames = rateLimitersNames; + } +} diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/model/RateLimiterEventDTO.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/model/RateLimiterEventDTO.java new file mode 100644 index 0000000000..8cd5c99268 --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/model/RateLimiterEventDTO.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017 Bohdan Storozhuk + * + * 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.github.resilience4j.ratelimiter.monitoring.model; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import io.github.resilience4j.ratelimiter.event.RateLimiterEvent; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class RateLimiterEventDTO { + + private String rateLimiterName; + private RateLimiterEvent.Type rateLimiterEventType; + private String rateLimiterCreationTime; + + public static RateLimiterEventDTO createRateLimiterEventDTO(RateLimiterEvent rateLimiterEvent) { + RateLimiterEventDTO dto = new RateLimiterEventDTO(); + dto.setRateLimiterName(rateLimiterEvent.getRateLimiterName()); + dto.setRateLimiterEventType(rateLimiterEvent.getEventType()); + dto.setRateLimiterCreationTime(rateLimiterEvent.getCreationTime().toString()); + return dto; + } + + public String getRateLimiterName() { + return rateLimiterName; + } + + public void setRateLimiterName(String rateLimiterName) { + this.rateLimiterName = rateLimiterName; + } + + public RateLimiterEvent.Type getRateLimiterEventType() { + return rateLimiterEventType; + } + + public void setRateLimiterEventType(RateLimiterEvent.Type rateLimiterEventType) { + this.rateLimiterEventType = rateLimiterEventType; + } + + public String getRateLimiterCreationTime() { + return rateLimiterCreationTime; + } + + public void setRateLimiterCreationTime(String rateLimiterCreationTime) { + this.rateLimiterCreationTime = rateLimiterCreationTime; + } +} diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/model/RateLimiterEventsEndpointResponse.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/model/RateLimiterEventsEndpointResponse.java new file mode 100644 index 0000000000..8c55ee6276 --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/ratelimiter/monitoring/model/RateLimiterEventsEndpointResponse.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Bohdan Storozhuk + * + * 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.github.resilience4j.ratelimiter.monitoring.model; + +import java.util.List; + +public class RateLimiterEventsEndpointResponse { + + private List eventsList; + + public RateLimiterEventsEndpointResponse() { + } + + public RateLimiterEventsEndpointResponse(List eventsList) { + this.eventsList = eventsList; + } + + public List getEventsList() { + return eventsList; + } + + public void setEventsList(List eventsList) { + this.eventsList = eventsList; + } +} diff --git a/resilience4j-spring-boot/src/main/resources/META-INF/spring.factories b/resilience4j-spring-boot/src/main/resources/META-INF/spring.factories index e1b3e648ae..51c7249fbe 100644 --- a/resilience4j-spring-boot/src/main/resources/META-INF/spring.factories +++ b/resilience4j-spring-boot/src/main/resources/META-INF/spring.factories @@ -1,4 +1,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ io.github.resilience4j.circuitbreaker.autoconfigure.CircuitBreakerAutoConfiguration,\ io.github.resilience4j.circuitbreaker.autoconfigure.MetricsAutoConfiguration,\ -io.github.resilience4j.circuitbreaker.autoconfigure.PrometheusAutoConfiguration +io.github.resilience4j.circuitbreaker.autoconfigure.PrometheusAutoConfiguration,\ +io.github.resilience4j.ratelimiter.autoconfigure.RateLimiterAutoConfiguration diff --git a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerAutoConfigurationTest.java b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerAutoConfigurationTest.java index 833076223d..1ccb849246 100644 --- a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerAutoConfigurationTest.java +++ b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerAutoConfigurationTest.java @@ -15,35 +15,27 @@ */ package io.github.resilience4j.circuitbreaker; +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.context.annotation.Bean; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import java.io.IOException; - import io.github.resilience4j.circuitbreaker.autoconfigure.CircuitBreakerProperties; -import io.github.resilience4j.circuitbreaker.event.CircuitBreakerEvent; import io.github.resilience4j.circuitbreaker.monitoring.endpoint.CircuitBreakerEndpointResponse; import io.github.resilience4j.circuitbreaker.monitoring.endpoint.CircuitBreakerEventsEndpointResponse; -import io.github.resilience4j.circuitbreaker.monitoring.health.CircuitBreakerHealthIndicator; -import io.github.resilience4j.circuitbreaker.test.DummyService; -import io.github.resilience4j.consumer.EventConsumerRegistry; -import io.prometheus.client.spring.boot.EnablePrometheusEndpoint; -import io.prometheus.client.spring.boot.EnableSpringBootMetricsCollector; +import io.github.resilience4j.service.test.DummyService; +import io.github.resilience4j.service.test.TestApplication; -import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = CircuitBreakerAutoConfigurationTest.TestApplication.class) + classes = TestApplication.class) public class CircuitBreakerAutoConfigurationTest { @Autowired @@ -69,7 +61,7 @@ public void testCircuitBreakerAutoConfiguration() throws IOException { try { dummyService.doSomething(true); - }catch (IOException ex){ + } catch (IOException ex) { // Do nothing. The IOException is recorded by the CircuitBreaker as a failure. } // The invocation is recorded by the CircuitBreaker as a success. @@ -95,33 +87,4 @@ public void testCircuitBreakerAutoConfiguration() throws IOException { ResponseEntity circuitBreakerEventList = restTemplate.getForEntity("/circuitbreaker/events", CircuitBreakerEventsEndpointResponse.class); assertThat(circuitBreakerEventList.getBody().getCircuitBreakerEvents()).hasSize(2); } - - @SpringBootApplication - @EnableSpringBootMetricsCollector - @EnablePrometheusEndpoint - public static class TestApplication{ - public static void main(String[] args) { - SpringApplication.run(TestApplication.class, args); - } - - @Bean - public HealthIndicator backendA(CircuitBreakerRegistry circuitBreakerRegistry, - EventConsumerRegistry eventConsumerRegistry, - CircuitBreakerProperties circuitBreakerProperties){ - return new CircuitBreakerHealthIndicator(circuitBreakerRegistry, - eventConsumerRegistry, - circuitBreakerProperties, - "backendA"); - } - - @Bean - public HealthIndicator backendB(CircuitBreakerRegistry circuitBreakerRegistry, - EventConsumerRegistry eventConsumerRegistry, - CircuitBreakerProperties circuitBreakerProperties){ - return new CircuitBreakerHealthIndicator(circuitBreakerRegistry, - eventConsumerRegistry, - circuitBreakerProperties, - "backendB"); - } - } } diff --git a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/ratelimiter/RateLimiterAutoConfigurationTest.java b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/ratelimiter/RateLimiterAutoConfigurationTest.java new file mode 100644 index 0000000000..a357dd2765 --- /dev/null +++ b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/ratelimiter/RateLimiterAutoConfigurationTest.java @@ -0,0 +1,115 @@ +/* + * Copyright 2017 Bohdan Storozhuk + * + * 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.github.resilience4j.ratelimiter; + +import static com.jayway.awaitility.Awaitility.await; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import io.github.resilience4j.ratelimiter.autoconfigure.RateLimiterProperties; +import io.github.resilience4j.ratelimiter.event.RateLimiterEvent; +import io.github.resilience4j.ratelimiter.monitoring.model.RateLimiterEndpointResponse; +import io.github.resilience4j.ratelimiter.monitoring.model.RateLimiterEventDTO; +import io.github.resilience4j.ratelimiter.monitoring.model.RateLimiterEventsEndpointResponse; +import io.github.resilience4j.service.test.DummyService; +import io.github.resilience4j.service.test.TestApplication; + +import java.io.IOException; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = TestApplication.class) +public class RateLimiterAutoConfigurationTest { + + @Autowired + private RateLimiterRegistry rateLimiterRegistry; + + @Autowired + private RateLimiterProperties rateLimiterProperties; + + @Autowired + private DummyService dummyService; + + @Autowired + private TestRestTemplate restTemplate; + + /** + * The test verifies that a RateLimiter instance is created and configured properly when the DummyService is invoked and + * that the RateLimiter records successful and failed calls. + */ + @Test + public void testRateLimiterAutoConfiguration() throws IOException { + assertThat(rateLimiterRegistry).isNotNull(); + assertThat(rateLimiterProperties).isNotNull(); + + RateLimiter rateLimiter = rateLimiterRegistry.rateLimiter(DummyService.BACKEND); + assertThat(rateLimiter).isNotNull(); + await() + .atMost(1, TimeUnit.SECONDS) + .until(() -> rateLimiter.getMetrics().getAvailablePermissions() == 10); + + try { + dummyService.doSomething(true); + } catch (IOException ex) { + // Do nothing. + } + dummyService.doSomething(false); + + assertThat(rateLimiter.getMetrics().getAvailablePermissions()).isEqualTo(8); + assertThat(rateLimiter.getMetrics().getNumberOfWaitingThreads()).isEqualTo(0); + + assertThat(rateLimiter.getRateLimiterConfig().getLimitForPeriod()).isEqualTo(10); + assertThat(rateLimiter.getRateLimiterConfig().getLimitRefreshPeriod()).isEqualTo(Duration.ofSeconds(1)); + assertThat(rateLimiter.getRateLimiterConfig().getTimeoutDuration()).isEqualTo(Duration.ofSeconds(0)); + + // Test Actuator endpoints + + ResponseEntity rateLimiterList = restTemplate + .getForEntity("/ratelimiter", RateLimiterEndpointResponse.class); + + assertThat(rateLimiterList.getBody().getRateLimitersNames()).hasSize(2).containsExactly("backendA", "backendB"); + + try { + for (int i = 0; i < 11; i++) { + dummyService.doSomething(false); + } + } catch (RequestNotPermitted e) { + // Do nothing + } + + ResponseEntity rateLimiterEventList = restTemplate + .getForEntity("/ratelimiter/events", RateLimiterEventsEndpointResponse.class); + + List eventsList = rateLimiterEventList.getBody().getEventsList(); + assertThat(eventsList).isNotEmpty(); + RateLimiterEventDTO lastEvent = eventsList.get(eventsList.size() - 1); + assertThat(lastEvent.getRateLimiterEventType()).isEqualTo(RateLimiterEvent.Type.FAILED_ACQUIRE); + + await() + .atMost(1, TimeUnit.SECONDS) + .until(() -> rateLimiter.getMetrics().getAvailablePermissions() == 10); + } +} diff --git a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/circuitbreaker/test/DummyService.java b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/service/test/DummyService.java similarity index 75% rename from resilience4j-spring-boot/src/test/java/io/github/resilience4j/circuitbreaker/test/DummyService.java rename to resilience4j-spring-boot/src/test/java/io/github/resilience4j/service/test/DummyService.java index 138b6c9dc8..4cf402b5c0 100644 --- a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/circuitbreaker/test/DummyService.java +++ b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/service/test/DummyService.java @@ -1,4 +1,4 @@ -package io.github.resilience4j.circuitbreaker.test; +package io.github.resilience4j.service.test; import java.io.IOException; diff --git a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/circuitbreaker/test/DummyServiceImpl.java b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/service/test/DummyServiceImpl.java similarity index 75% rename from resilience4j-spring-boot/src/test/java/io/github/resilience4j/circuitbreaker/test/DummyServiceImpl.java rename to resilience4j-spring-boot/src/test/java/io/github/resilience4j/service/test/DummyServiceImpl.java index b6e446f1b3..4ef41d4eb8 100644 --- a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/circuitbreaker/test/DummyServiceImpl.java +++ b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/service/test/DummyServiceImpl.java @@ -1,4 +1,4 @@ -package io.github.resilience4j.circuitbreaker.test; +package io.github.resilience4j.service.test; import org.springframework.stereotype.Component; @@ -6,8 +6,10 @@ import java.io.IOException; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.ratelimiter.annotation.RateLimiter; @CircuitBreaker(backend = DummyService.BACKEND) +@RateLimiter(name = DummyService.BACKEND) @Component public class DummyServiceImpl implements DummyService { @Override diff --git a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/service/test/TestApplication.java b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/service/test/TestApplication.java new file mode 100644 index 0000000000..56dfe46374 --- /dev/null +++ b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/service/test/TestApplication.java @@ -0,0 +1,46 @@ +package io.github.resilience4j.service.test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; +import io.github.resilience4j.circuitbreaker.autoconfigure.CircuitBreakerProperties; +import io.github.resilience4j.circuitbreaker.event.CircuitBreakerEvent; +import io.github.resilience4j.circuitbreaker.monitoring.health.CircuitBreakerHealthIndicator; +import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.prometheus.client.spring.boot.EnablePrometheusEndpoint; +import io.prometheus.client.spring.boot.EnableSpringBootMetricsCollector; + +/** + * @author bstorozhuk + */ +@SpringBootApplication(scanBasePackages = "io.github.resilience4j") +@EnableSpringBootMetricsCollector +@EnablePrometheusEndpoint +public class TestApplication { + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + + @Bean + public HealthIndicator backendA(CircuitBreakerRegistry circuitBreakerRegistry, + EventConsumerRegistry eventConsumerRegistry, + CircuitBreakerProperties circuitBreakerProperties) { + return new CircuitBreakerHealthIndicator(circuitBreakerRegistry, + eventConsumerRegistry, + circuitBreakerProperties, + "backendA"); + } + + @Bean + public HealthIndicator backendB(CircuitBreakerRegistry circuitBreakerRegistry, + EventConsumerRegistry eventConsumerRegistry, + CircuitBreakerProperties circuitBreakerProperties) { + return new CircuitBreakerHealthIndicator(circuitBreakerRegistry, + eventConsumerRegistry, + circuitBreakerProperties, + "backendB"); + } +} diff --git a/resilience4j-spring-boot/src/test/resources/application.yaml b/resilience4j-spring-boot/src/test/resources/application.yaml index 07435fb33b..ce63a3b3b0 100644 --- a/resilience4j-spring-boot/src/test/resources/application.yaml +++ b/resilience4j-spring-boot/src/test/resources/application.yaml @@ -11,4 +11,17 @@ resilience4j.circuitbreaker: ringBufferSizeInHalfOpenState: 5 waitInterval: 5000 failureRateThreshold: 50 - eventConsumerBufferSize: 10 \ No newline at end of file + eventConsumerBufferSize: 10 + +resilience4j.ratelimiter: + limiters: + backendA: + limitForPeriod: 10 + limitRefreshPeriodInMillis: 1000 + timeoutInMillis: 0 + subscribeForEvents: true + registerHealthIndicator: true + backendB: + limitForPeriod: 6 + limitRefreshPeriodInMillis: 500 + timeoutInMillis: 3000 \ No newline at end of file