diff --git a/resilience4j-annotations/src/main/java/io/github/resilience4j/timelimiter/annotation/TimeLimiter.java b/resilience4j-annotations/src/main/java/io/github/resilience4j/timelimiter/annotation/TimeLimiter.java new file mode 100644 index 0000000000..d016c814ed --- /dev/null +++ b/resilience4j-annotations/src/main/java/io/github/resilience4j/timelimiter/annotation/TimeLimiter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.annotation; + +import java.lang.annotation.*; + +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = {ElementType.METHOD, ElementType.TYPE}) +@Documented +public @interface TimeLimiter { + + /** + * Name of the sync timeLimiter. + * + * @return the name of the sync timeLimiter. + */ + String name(); + + /** + * fallbackMethod method name. + * + * @return fallbackMethod method name. + */ + String fallbackMethod() default ""; + +} diff --git a/resilience4j-framework-common/build.gradle b/resilience4j-framework-common/build.gradle index 34363a0fe1..6f5390c5cc 100644 --- a/resilience4j-framework-common/build.gradle +++ b/resilience4j-framework-common/build.gradle @@ -4,7 +4,7 @@ dependencies { compile project(':resilience4j-ratelimiter') compile project(':resilience4j-retry') compile project(':resilience4j-bulkhead') - + compile project(':resilience4j-timelimiter') } ext.moduleName = 'io.github.resilience4j.framework-common' \ No newline at end of file diff --git a/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/timelimiter/configuration/TimeLimiterConfigurationProperties.java b/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/timelimiter/configuration/TimeLimiterConfigurationProperties.java new file mode 100644 index 0000000000..81296e7447 --- /dev/null +++ b/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/timelimiter/configuration/TimeLimiterConfigurationProperties.java @@ -0,0 +1,150 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.common.timelimiter.configuration; + +import io.github.resilience4j.common.utils.ConfigUtils; +import io.github.resilience4j.core.ConfigurationNotFoundException; +import io.github.resilience4j.core.StringUtils; +import io.github.resilience4j.core.lang.Nullable; +import io.github.resilience4j.timelimiter.TimeLimiterConfig; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class TimeLimiterConfigurationProperties { + + private final Map instances = new HashMap<>(); + private final Map configs = new HashMap<>(); + + public Map getInstances() { + return instances; + } + + public Map getConfigs() { + return configs; + } + + /** + * @param backend timeLimiter backend name + * @return the configured spring backend properties + */ + @Nullable + public InstanceProperties getInstanceProperties(String backend) { + return instances.get(backend); + } + + public TimeLimiterConfig createTimeLimiterConfig(@Nullable InstanceProperties instanceProperties) { + if (instanceProperties == null) { + return TimeLimiterConfig.ofDefaults(); + } + if (StringUtils.isNotEmpty(instanceProperties.getBaseConfig())) { + InstanceProperties baseProperties = configs.get(instanceProperties.getBaseConfig()); + if (baseProperties == null) { + throw new ConfigurationNotFoundException(instanceProperties.getBaseConfig()); + } + return buildConfigFromBaseConfig(baseProperties, instanceProperties); + } + return buildTimeLimiterConfig(TimeLimiterConfig.custom(), instanceProperties); + } + + private static TimeLimiterConfig buildConfigFromBaseConfig( + InstanceProperties baseProperties, InstanceProperties instanceProperties) { + ConfigUtils.mergePropertiesIfAny(baseProperties, instanceProperties); + TimeLimiterConfig baseConfig = buildTimeLimiterConfig(TimeLimiterConfig.custom(), baseProperties); + return buildTimeLimiterConfig(TimeLimiterConfig.from(baseConfig), instanceProperties); + } + + private static TimeLimiterConfig buildTimeLimiterConfig( + TimeLimiterConfig.Builder builder, @Nullable InstanceProperties instanceProperties) { + if (instanceProperties == null) { + return builder.build(); + } + + if (instanceProperties.getTimeoutDuration() != null) { + builder.timeoutDuration(instanceProperties.getTimeoutDuration()); + } + + if (instanceProperties.getCancelRunningFuture() != null) { + builder.cancelRunningFuture(instanceProperties.getCancelRunningFuture()); + } + + return builder.build(); + } + + public TimeLimiterConfig createTimeLimiterConfig(String limiter) { + return createTimeLimiterConfig(getInstanceProperties(limiter)); + } + + public static class InstanceProperties { + + private Duration timeoutDuration; + private Boolean cancelRunningFuture; + @Nullable + private Integer eventConsumerBufferSize; + + @Nullable + private String baseConfig; + + public Duration getTimeoutDuration() { + return timeoutDuration; + } + + public InstanceProperties setTimeoutDuration(Duration timeoutDuration) { + Objects.requireNonNull(timeoutDuration); + if (timeoutDuration.toMillis() < 0) { + throw new IllegalArgumentException( + "timeoutDuration must be greater than or equal to 0."); + } + this.timeoutDuration = timeoutDuration; + return this; + } + + public Boolean getCancelRunningFuture() { + return cancelRunningFuture; + } + + public InstanceProperties setCancelRunningFuture(Boolean cancelRunningFuture) { + this.cancelRunningFuture = cancelRunningFuture; + return this; + } + + public Integer getEventConsumerBufferSize() { + return eventConsumerBufferSize; + } + + public InstanceProperties setEventConsumerBufferSize(Integer eventConsumerBufferSize) { + Objects.requireNonNull(eventConsumerBufferSize); + if (eventConsumerBufferSize < 1) { + throw new IllegalArgumentException("eventConsumerBufferSize must be greater than or equal to 1."); + } + + this.eventConsumerBufferSize = eventConsumerBufferSize; + return this; + } + + @Nullable + public String getBaseConfig() { + return baseConfig; + } + + public void setBaseConfig(@Nullable String baseConfig) { + this.baseConfig = baseConfig; + } + } +} diff --git a/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/timelimiter/monitoring/endpoint/TimeLimiterEndpointResponse.java b/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/timelimiter/monitoring/endpoint/TimeLimiterEndpointResponse.java new file mode 100644 index 0000000000..02a60253b1 --- /dev/null +++ b/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/timelimiter/monitoring/endpoint/TimeLimiterEndpointResponse.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.common.timelimiter.monitoring.endpoint; + +import io.github.resilience4j.core.lang.Nullable; + +import java.util.List; + +public class TimeLimiterEndpointResponse { + + @Nullable + private List timeLimiters; + + public TimeLimiterEndpointResponse(){ + } + + public TimeLimiterEndpointResponse(List timeLimiters){ + this.timeLimiters = timeLimiters; + } + + @Nullable + public List getTimeLimiters() { + return timeLimiters; + } + + public void setTimeLimiters(List timeLimiters) { + this.timeLimiters = timeLimiters; + } + +} diff --git a/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/timelimiter/monitoring/endpoint/TimeLimiterEventDTO.java b/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/timelimiter/monitoring/endpoint/TimeLimiterEventDTO.java new file mode 100644 index 0000000000..a7899123b2 --- /dev/null +++ b/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/timelimiter/monitoring/endpoint/TimeLimiterEventDTO.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.common.timelimiter.monitoring.endpoint; + +import io.github.resilience4j.core.lang.Nullable; +import io.github.resilience4j.timelimiter.event.TimeLimiterEvent; + +public class TimeLimiterEventDTO { + + @Nullable + private String timeLimiterName; + @Nullable private TimeLimiterEvent.Type type; + @Nullable private String creationTime; + + public static TimeLimiterEventDTO createTimeLimiterEventDTO(TimeLimiterEvent timeLimiterEvent) { + TimeLimiterEventDTO dto = new TimeLimiterEventDTO(); + dto.setTimeLimiterName(timeLimiterEvent.getTimeLimiterName()); + dto.setType(timeLimiterEvent.getEventType()); + dto.setCreationTime(timeLimiterEvent.getCreationTime().toString()); + return dto; + } + + @Nullable + public String getTimeLimiterName() { + return timeLimiterName; + } + + public void setTimeLimiterName(@Nullable String timeLimiterName) { + this.timeLimiterName = timeLimiterName; + } + + @Nullable + public TimeLimiterEvent.Type getType() { + return type; + } + + public void setType(@Nullable TimeLimiterEvent.Type type) { + this.type = type; + } + + @Nullable + public String getCreationTime() { + return creationTime; + } + + public void setCreationTime(@Nullable String creationTime) { + this.creationTime = creationTime; + } +} diff --git a/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/timelimiter/monitoring/endpoint/TimeLimiterEventsEndpointResponse.java b/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/timelimiter/monitoring/endpoint/TimeLimiterEventsEndpointResponse.java new file mode 100644 index 0000000000..6c388cb652 --- /dev/null +++ b/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/timelimiter/monitoring/endpoint/TimeLimiterEventsEndpointResponse.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.common.timelimiter.monitoring.endpoint; + +import io.github.resilience4j.core.lang.Nullable; + +import java.util.List; + +public class TimeLimiterEventsEndpointResponse { + + @Nullable + private List timeLimiterEvents; + + public TimeLimiterEventsEndpointResponse() { + } + + public TimeLimiterEventsEndpointResponse(@Nullable List timeLimiterEvents) { + this.timeLimiterEvents = timeLimiterEvents; + } + + @Nullable + public List getTimeLimiterEvents() { + return timeLimiterEvents; + } + + public void setTimeLimiterEvents(@Nullable List timeLimiterEvents) { + this.timeLimiterEvents = timeLimiterEvents; + } + +} diff --git a/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/utils/ConfigUtils.java b/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/utils/ConfigUtils.java index 309aadb98c..6972b7bd1d 100644 --- a/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/utils/ConfigUtils.java +++ b/resilience4j-framework-common/src/main/java/io/github/resilience4j/common/utils/ConfigUtils.java @@ -19,6 +19,7 @@ import io.github.resilience4j.common.circuitbreaker.configuration.CircuitBreakerConfigurationProperties; import io.github.resilience4j.common.ratelimiter.configuration.RateLimiterConfigurationProperties; import io.github.resilience4j.common.retry.configuration.RetryConfigurationProperties; +import io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties; /** * resilience4j configuration util @@ -127,4 +128,18 @@ public static void mergePropertiesIfAny( } } + /** + * merge only properties that are not part of timeLimiter config if any match the conditions of merge + * + * @param baseProperties base config properties + * @param instanceProperties instance properties + */ + public static void mergePropertiesIfAny(TimeLimiterConfigurationProperties.InstanceProperties baseProperties, + TimeLimiterConfigurationProperties.InstanceProperties instanceProperties) { + if (instanceProperties.getEventConsumerBufferSize() == null + && baseProperties.getEventConsumerBufferSize() != null) { + instanceProperties.setEventConsumerBufferSize(baseProperties.getEventConsumerBufferSize()); + } + } + } diff --git a/resilience4j-framework-common/src/test/java/io/github/resilience4j/common/timelimiter/configuration/TimeLimiterConfigurationPropertiesTest.java b/resilience4j-framework-common/src/test/java/io/github/resilience4j/common/timelimiter/configuration/TimeLimiterConfigurationPropertiesTest.java new file mode 100644 index 0000000000..192eeab2b8 --- /dev/null +++ b/resilience4j-framework-common/src/test/java/io/github/resilience4j/common/timelimiter/configuration/TimeLimiterConfigurationPropertiesTest.java @@ -0,0 +1,120 @@ +package io.github.resilience4j.common.timelimiter.configuration; + +import io.github.resilience4j.core.ConfigurationNotFoundException; +import io.github.resilience4j.timelimiter.TimeLimiterConfig; +import org.junit.Test; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TimeLimiterConfigurationPropertiesTest { + + @Test + public void testTimeLimiterProperties() { + // Given + TimeLimiterConfigurationProperties.InstanceProperties instanceProperties1 = new TimeLimiterConfigurationProperties.InstanceProperties(); + instanceProperties1.setTimeoutDuration(Duration.ofSeconds(3)); + instanceProperties1.setCancelRunningFuture(true); + instanceProperties1.setEventConsumerBufferSize(200); + + TimeLimiterConfigurationProperties.InstanceProperties instanceProperties2 = new TimeLimiterConfigurationProperties.InstanceProperties(); + instanceProperties2.setTimeoutDuration(Duration.ofSeconds(5)); + instanceProperties2.setCancelRunningFuture(false); + instanceProperties2.setEventConsumerBufferSize(500); + + TimeLimiterConfigurationProperties timeLimiterConfigurationProperties = new TimeLimiterConfigurationProperties(); + timeLimiterConfigurationProperties.getInstances().put("backend1", instanceProperties1); + timeLimiterConfigurationProperties.getInstances().put("backend2", instanceProperties2); + + // Then + assertThat(timeLimiterConfigurationProperties.getInstances().size()).isEqualTo(2); + final TimeLimiterConfig timeLimiter1 = timeLimiterConfigurationProperties.createTimeLimiterConfig("backend1"); + final TimeLimiterConfig timeLimiter2 = timeLimiterConfigurationProperties.createTimeLimiterConfig("backend2"); + + TimeLimiterConfigurationProperties.InstanceProperties instancePropertiesForTimeLimiter1 = timeLimiterConfigurationProperties.getInstances().get("backend1"); + + assertThat(instancePropertiesForTimeLimiter1.getTimeoutDuration().toMillis()).isEqualTo(3000); + assertThat(timeLimiter1).isNotNull(); + assertThat(timeLimiter1.shouldCancelRunningFuture()).isEqualTo(true); + + assertThat(timeLimiter2).isNotNull(); + assertThat(timeLimiter2.getTimeoutDuration().toMillis()).isEqualTo(5000); + } + + @Test + public void testCreateTimeLimiterPropertiesWithSharedConfigs() { + // Given + TimeLimiterConfigurationProperties.InstanceProperties defaultProperties = new TimeLimiterConfigurationProperties.InstanceProperties(); + defaultProperties.setTimeoutDuration(Duration.ofSeconds(3)); + defaultProperties.setCancelRunningFuture(true); + defaultProperties.setEventConsumerBufferSize(200); + + TimeLimiterConfigurationProperties.InstanceProperties sharedProperties = new TimeLimiterConfigurationProperties.InstanceProperties(); + sharedProperties.setTimeoutDuration(Duration.ofSeconds(5)); + sharedProperties.setCancelRunningFuture(false); + sharedProperties.setEventConsumerBufferSize(500); + + TimeLimiterConfigurationProperties.InstanceProperties backendWithDefaultConfig = new TimeLimiterConfigurationProperties.InstanceProperties(); + backendWithDefaultConfig.setBaseConfig("default"); + backendWithDefaultConfig.setTimeoutDuration(Duration.ofMillis(200L)); + + TimeLimiterConfigurationProperties.InstanceProperties backendWithSharedConfig = new TimeLimiterConfigurationProperties.InstanceProperties(); + backendWithSharedConfig.setBaseConfig("sharedConfig"); + backendWithSharedConfig.setTimeoutDuration(Duration.ofMillis(300L)); + + TimeLimiterConfigurationProperties timeLimiterConfigurationProperties = new TimeLimiterConfigurationProperties(); + timeLimiterConfigurationProperties.getConfigs().put("default", defaultProperties); + timeLimiterConfigurationProperties.getConfigs().put("sharedConfig", sharedProperties); + + timeLimiterConfigurationProperties.getInstances().put("backendWithDefaultConfig", backendWithDefaultConfig); + timeLimiterConfigurationProperties.getInstances().put("backendWithSharedConfig", backendWithSharedConfig); + + //Then + // Should get default config and overwrite max attempt and wait time + TimeLimiterConfig timeLimiter1 = timeLimiterConfigurationProperties.createTimeLimiterConfig("backendWithDefaultConfig"); + assertThat(timeLimiter1).isNotNull(); + assertThat(timeLimiter1.shouldCancelRunningFuture()).isEqualTo(true); + assertThat(timeLimiter1.getTimeoutDuration().toMillis()).isEqualTo(200); + + // Should get shared config and overwrite wait time + TimeLimiterConfig timeLimiter2 = timeLimiterConfigurationProperties.createTimeLimiterConfig("backendWithSharedConfig"); + assertThat(timeLimiter2).isNotNull(); + assertThat(timeLimiter2.shouldCancelRunningFuture()).isEqualTo(false); + assertThat(timeLimiter2.getTimeoutDuration().toMillis()).isEqualTo(300); + + // Unknown backend should get default config of Registry + TimeLimiterConfig timeLimiter3 = timeLimiterConfigurationProperties.createTimeLimiterConfig("unknownBackend"); + assertThat(timeLimiter3).isNotNull(); + assertThat(timeLimiter3.getTimeoutDuration().toMillis()).isEqualTo(1000); + + } + + @Test + public void testCreatePropertiesWithUnknownConfig() { + TimeLimiterConfigurationProperties timeLimiterConfigurationProperties = new TimeLimiterConfigurationProperties(); + + TimeLimiterConfigurationProperties.InstanceProperties instanceProperties = new TimeLimiterConfigurationProperties.InstanceProperties(); + instanceProperties.setBaseConfig("unknownConfig"); + timeLimiterConfigurationProperties.getInstances().put("backend", instanceProperties); + + //then + assertThatThrownBy(() -> timeLimiterConfigurationProperties.createTimeLimiterConfig("backend")) + .isInstanceOf(ConfigurationNotFoundException.class) + .hasMessage("Configuration with name 'unknownConfig' does not exist"); + } + + @Test(expected = IllegalArgumentException.class) + public void testIllegalArgumentOnEventConsumerBufferSize() { + TimeLimiterConfigurationProperties.InstanceProperties defaultProperties = new TimeLimiterConfigurationProperties.InstanceProperties(); + defaultProperties.setEventConsumerBufferSize(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void testIllegalArgumentOnTimeoutDuration() { + TimeLimiterConfigurationProperties.InstanceProperties defaultProperties = new TimeLimiterConfigurationProperties.InstanceProperties(); + defaultProperties.setTimeoutDuration(Duration.ofMillis(-1000)); + } + +} \ No newline at end of file diff --git a/resilience4j-framework-common/src/test/java/io/github/resilience4j/common/utils/SpringConfigUtilsTest.java b/resilience4j-framework-common/src/test/java/io/github/resilience4j/common/utils/SpringConfigUtilsTest.java index 3b4318fb4a..dc4c55d4dc 100644 --- a/resilience4j-framework-common/src/test/java/io/github/resilience4j/common/utils/SpringConfigUtilsTest.java +++ b/resilience4j-framework-common/src/test/java/io/github/resilience4j/common/utils/SpringConfigUtilsTest.java @@ -15,6 +15,11 @@ */ package io.github.resilience4j.common.utils; +import io.github.resilience4j.common.bulkhead.configuration.BulkheadConfigurationProperties; +import io.github.resilience4j.common.circuitbreaker.configuration.CircuitBreakerConfigurationProperties; +import io.github.resilience4j.common.ratelimiter.configuration.RateLimiterConfigurationProperties; +import io.github.resilience4j.common.retry.configuration.RetryConfigurationProperties; +import io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties; import org.junit.Test; import java.time.Duration; @@ -28,12 +33,12 @@ public class SpringConfigUtilsTest { @Test public void testBulkHeadMergeSpringProperties() { - io.github.resilience4j.common.bulkhead.configuration.BulkheadConfigurationProperties.InstanceProperties shared = new io.github.resilience4j.common.bulkhead.configuration.BulkheadConfigurationProperties.InstanceProperties(); + BulkheadConfigurationProperties.InstanceProperties shared = new BulkheadConfigurationProperties.InstanceProperties(); shared.setMaxConcurrentCalls(3); shared.setEventConsumerBufferSize(200); assertThat(shared.getEventConsumerBufferSize()).isEqualTo(200); - io.github.resilience4j.common.bulkhead.configuration.BulkheadConfigurationProperties.InstanceProperties instanceProperties = new io.github.resilience4j.common.bulkhead.configuration.BulkheadConfigurationProperties.InstanceProperties(); + BulkheadConfigurationProperties.InstanceProperties instanceProperties = new BulkheadConfigurationProperties.InstanceProperties(); instanceProperties.setMaxConcurrentCalls(3); assertThat(instanceProperties.getEventConsumerBufferSize()).isNull(); @@ -45,14 +50,14 @@ public void testBulkHeadMergeSpringProperties() { @Test public void testCircuitBreakerMergeSpringProperties() { - io.github.resilience4j.common.circuitbreaker.configuration.CircuitBreakerConfigurationProperties.InstanceProperties sharedProperties = new io.github.resilience4j.common.circuitbreaker.configuration.CircuitBreakerConfigurationProperties.InstanceProperties(); + CircuitBreakerConfigurationProperties.InstanceProperties sharedProperties = new CircuitBreakerConfigurationProperties.InstanceProperties(); sharedProperties.setRingBufferSizeInClosedState(1337); sharedProperties.setRingBufferSizeInHalfOpenState(1000); sharedProperties.setRegisterHealthIndicator(true); sharedProperties.setAllowHealthIndicatorToFail(true); sharedProperties.setEventConsumerBufferSize(200); - io.github.resilience4j.common.circuitbreaker.configuration.CircuitBreakerConfigurationProperties.InstanceProperties backendWithDefaultConfig = new io.github.resilience4j.common.circuitbreaker.configuration.CircuitBreakerConfigurationProperties.InstanceProperties(); + CircuitBreakerConfigurationProperties.InstanceProperties backendWithDefaultConfig = new CircuitBreakerConfigurationProperties.InstanceProperties(); backendWithDefaultConfig.setRingBufferSizeInHalfOpenState(99); assertThat(backendWithDefaultConfig.getEventConsumerBufferSize()).isNull(); assertThat(backendWithDefaultConfig.getAllowHealthIndicatorToFail()).isNull(); @@ -66,14 +71,14 @@ public void testCircuitBreakerMergeSpringProperties() { @Test public void testRetrySpringProperties() { - io.github.resilience4j.common.retry.configuration.RetryConfigurationProperties.InstanceProperties sharedProperties = new io.github.resilience4j.common.retry.configuration.RetryConfigurationProperties.InstanceProperties(); + RetryConfigurationProperties.InstanceProperties sharedProperties = new RetryConfigurationProperties.InstanceProperties(); sharedProperties.setMaxRetryAttempts(2); sharedProperties.setWaitDuration(Duration.ofMillis(100)); sharedProperties.setEnableRandomizedWait(true); sharedProperties.setExponentialBackoffMultiplier(0.1); sharedProperties.setEnableExponentialBackoff(false); - io.github.resilience4j.common.retry.configuration.RetryConfigurationProperties.InstanceProperties backendWithDefaultConfig = new io.github.resilience4j.common.retry.configuration.RetryConfigurationProperties.InstanceProperties(); + RetryConfigurationProperties.InstanceProperties backendWithDefaultConfig = new RetryConfigurationProperties.InstanceProperties(); backendWithDefaultConfig.setBaseConfig("default"); backendWithDefaultConfig.setWaitDuration(Duration.ofMillis(200L)); assertThat(backendWithDefaultConfig.getEnableExponentialBackoff()).isNull(); @@ -89,14 +94,14 @@ public void testRetrySpringProperties() { @Test public void testRateLimiterSpringProperties() { - io.github.resilience4j.common.ratelimiter.configuration.RateLimiterConfigurationProperties.InstanceProperties sharedProperties = new io.github.resilience4j.common.ratelimiter.configuration.RateLimiterConfigurationProperties.InstanceProperties(); + RateLimiterConfigurationProperties.InstanceProperties sharedProperties = new RateLimiterConfigurationProperties.InstanceProperties(); sharedProperties.setLimitForPeriod(2); sharedProperties.setLimitRefreshPeriod(Duration.ofMillis(6000000)); sharedProperties.setSubscribeForEvents(true); sharedProperties.setRegisterHealthIndicator(true); sharedProperties.setEventConsumerBufferSize(200); - io.github.resilience4j.common.ratelimiter.configuration.RateLimiterConfigurationProperties.InstanceProperties backendWithDefaultConfig = new io.github.resilience4j.common.ratelimiter.configuration.RateLimiterConfigurationProperties.InstanceProperties(); + RateLimiterConfigurationProperties.InstanceProperties backendWithDefaultConfig = new RateLimiterConfigurationProperties.InstanceProperties(); backendWithDefaultConfig.setBaseConfig("default"); backendWithDefaultConfig.setLimitForPeriod(200); assertThat(backendWithDefaultConfig.getRegisterHealthIndicator()).isNull(); @@ -110,4 +115,20 @@ public void testRateLimiterSpringProperties() { assertThat(backendWithDefaultConfig.getSubscribeForEvents()).isTrue(); } + @Test + public void testTimeLimiterSpringProperties() { + + TimeLimiterConfigurationProperties.InstanceProperties sharedProperties = new TimeLimiterConfigurationProperties.InstanceProperties(); + sharedProperties.setTimeoutDuration(Duration.ofSeconds(20)); + sharedProperties.setCancelRunningFuture(false); + sharedProperties.setEventConsumerBufferSize(200); + + TimeLimiterConfigurationProperties.InstanceProperties backendWithDefaultConfig = new TimeLimiterConfigurationProperties.InstanceProperties(); + sharedProperties.setCancelRunningFuture(true); + assertThat(backendWithDefaultConfig.getEventConsumerBufferSize()).isNull(); + + ConfigUtils.mergePropertiesIfAny(sharedProperties, backendWithDefaultConfig); + + assertThat(backendWithDefaultConfig.getEventConsumerBufferSize()).isEqualTo(200); + } } diff --git a/resilience4j-framework-common/src/test/java/io/github/resilience4j/timelimiter/monitoring/endpoint/TimeLimiterEventDTOTest.java b/resilience4j-framework-common/src/test/java/io/github/resilience4j/timelimiter/monitoring/endpoint/TimeLimiterEventDTOTest.java new file mode 100644 index 0000000000..be26443ab2 --- /dev/null +++ b/resilience4j-framework-common/src/test/java/io/github/resilience4j/timelimiter/monitoring/endpoint/TimeLimiterEventDTOTest.java @@ -0,0 +1,50 @@ +package io.github.resilience4j.timelimiter.monitoring.endpoint; + +import io.github.resilience4j.common.timelimiter.monitoring.endpoint.TimeLimiterEventDTO; +import io.github.resilience4j.timelimiter.event.TimeLimiterEvent; +import io.github.resilience4j.timelimiter.event.TimeLimiterOnErrorEvent; +import io.github.resilience4j.timelimiter.event.TimeLimiterOnSuccessEvent; +import io.github.resilience4j.timelimiter.event.TimeLimiterOnTimeoutEvent; +import org.junit.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + + +public class TimeLimiterEventDTOTest { + + @Test + public void shouldMapTimeLimiterOnSuccessEvent(){ + TimeLimiterOnSuccessEvent event = new TimeLimiterOnSuccessEvent("name"); + + TimeLimiterEventDTO timeLimiterEventDTO = TimeLimiterEventDTO.createTimeLimiterEventDTO(event); + + assertThat(timeLimiterEventDTO.getTimeLimiterName()).isEqualTo("name"); + assertThat(timeLimiterEventDTO.getType()).isEqualTo(TimeLimiterEvent.Type.SUCCESS); + assertThat(timeLimiterEventDTO.getCreationTime()).isNotNull(); + } + + @Test + public void shouldMapTimeLimiterOnErrorEvent(){ + TimeLimiterOnErrorEvent event = new TimeLimiterOnErrorEvent("name", new IOException("Error message")); + + TimeLimiterEventDTO timeLimiterEventDTO = TimeLimiterEventDTO.createTimeLimiterEventDTO(event); + + assertThat(timeLimiterEventDTO.getTimeLimiterName()).isEqualTo("name"); + assertThat(timeLimiterEventDTO.getType()).isEqualTo(TimeLimiterEvent.Type.ERROR); + assertThat(timeLimiterEventDTO.getCreationTime()).isNotNull(); + } + + + @Test + public void shouldMapTimeLimiterOnTimeoutEvent(){ + TimeLimiterOnTimeoutEvent event = new TimeLimiterOnTimeoutEvent("name"); + + TimeLimiterEventDTO timeLimiterEventDTO = TimeLimiterEventDTO.createTimeLimiterEventDTO(event); + + assertThat(timeLimiterEventDTO.getTimeLimiterName()).isEqualTo("name"); + assertThat(timeLimiterEventDTO.getType()).isEqualTo(TimeLimiterEvent.Type.TIMEOUT); + assertThat(timeLimiterEventDTO.getCreationTime()).isNotNull(); + } +} \ No newline at end of file diff --git a/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/AbstractTimeLimiterConfigurationOnMissingBean.java b/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/AbstractTimeLimiterConfigurationOnMissingBean.java new file mode 100644 index 0000000000..7567f9f07e --- /dev/null +++ b/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/AbstractTimeLimiterConfigurationOnMissingBean.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.autoconfigure; + +import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.core.registry.RegistryEventConsumer; +import io.github.resilience4j.fallback.FallbackDecorators; +import io.github.resilience4j.fallback.autoconfigure.FallbackConfigurationOnMissingBean; +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.github.resilience4j.timelimiter.configure.*; +import io.github.resilience4j.timelimiter.event.TimeLimiterEvent; +import io.github.resilience4j.utils.AspectJOnClasspathCondition; +import io.github.resilience4j.utils.ReactorOnClasspathCondition; +import io.github.resilience4j.utils.RxJava2OnClasspathCondition; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.*; + +import java.util.List; +import java.util.Optional; + +@Configuration +@Import(FallbackConfigurationOnMissingBean.class) +public abstract class AbstractTimeLimiterConfigurationOnMissingBean { + protected final TimeLimiterConfiguration timeLimiterConfiguration; + + protected AbstractTimeLimiterConfigurationOnMissingBean() { + this.timeLimiterConfiguration = new TimeLimiterConfiguration(); + } + + @Bean + @ConditionalOnMissingBean + public TimeLimiterRegistry timeLimiterRegistry(TimeLimiterConfigurationProperties timeLimiterProperties, + EventConsumerRegistry timeLimiterEventsConsumerRegistry, + RegistryEventConsumer timeLimiterRegistryEventConsumer) { + return timeLimiterConfiguration.timeLimiterRegistry( + timeLimiterProperties, timeLimiterEventsConsumerRegistry, timeLimiterRegistryEventConsumer); + } + + @Bean + @Primary + public RegistryEventConsumer timeLimiterRegistryEventConsumer( + Optional>> optionalRegistryEventConsumers) { + return timeLimiterConfiguration.timeLimiterRegistryEventConsumer(optionalRegistryEventConsumers); + } + + @Bean + @Conditional(AspectJOnClasspathCondition.class) + @ConditionalOnMissingBean + public TimeLimiterAspect timeLimiterAspect(TimeLimiterConfigurationProperties timeLimiterProperties, + TimeLimiterRegistry timeLimiterRegistry, + @Autowired(required = false) List timeLimiterAspectExtList, + FallbackDecorators fallbackDecorators) { + return timeLimiterConfiguration.timeLimiterAspect( + timeLimiterProperties, timeLimiterRegistry, timeLimiterAspectExtList, fallbackDecorators); + } + + @Bean + @Conditional({RxJava2OnClasspathCondition.class, AspectJOnClasspathCondition.class}) + @ConditionalOnMissingBean + public RxJava2TimeLimiterAspectExt rxJava2TimeLimiterAspectExt() { + return timeLimiterConfiguration.rxJava2TimeLimiterAspectExt(); + } + + @Bean + @Conditional({ReactorOnClasspathCondition.class, AspectJOnClasspathCondition.class}) + @ConditionalOnMissingBean + public ReactorTimeLimiterAspectExt reactorTimeLimiterAspectExt() { + return timeLimiterConfiguration.reactorTimeLimiterAspectExt(); + } + +} diff --git a/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterProperties.java b/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterProperties.java new file mode 100644 index 0000000000..b36ffc21fb --- /dev/null +++ b/resilience4j-spring-boot-common/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterProperties.java @@ -0,0 +1,24 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.autoconfigure; + +import io.github.resilience4j.timelimiter.configure.TimeLimiterConfigurationProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "resilience4j.timelimiter") +public class TimeLimiterProperties extends TimeLimiterConfigurationProperties { +} diff --git a/resilience4j-spring-boot-common/src/test/java/io/github/resilience4j/SpringBootCommonTest.java b/resilience4j-spring-boot-common/src/test/java/io/github/resilience4j/SpringBootCommonTest.java index 2d4734b643..723e1a9a47 100644 --- a/resilience4j-spring-boot-common/src/test/java/io/github/resilience4j/SpringBootCommonTest.java +++ b/resilience4j-spring-boot-common/src/test/java/io/github/resilience4j/SpringBootCommonTest.java @@ -37,6 +37,9 @@ import io.github.resilience4j.retry.RetryRegistry; import io.github.resilience4j.retry.autoconfigure.AbstractRetryConfigurationOnMissingBean; import io.github.resilience4j.retry.configure.RetryConfigurationProperties; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.github.resilience4j.timelimiter.autoconfigure.AbstractTimeLimiterConfigurationOnMissingBean; +import io.github.resilience4j.timelimiter.configure.TimeLimiterConfigurationProperties; import org.junit.Test; import java.util.Arrays; @@ -124,6 +127,22 @@ public void testRateLimiterCommonConfig() { .rateLimiterRegistryEventConsumer(Optional.empty())).isNotNull(); } + @Test + public void testTimeLimiterCommonConfig() { + TimeLimiterConfigurationOnMissingBean timeLimiterConfigurationOnMissingBean = new TimeLimiterConfigurationOnMissingBean(); + assertThat(timeLimiterConfigurationOnMissingBean.reactorTimeLimiterAspectExt()).isNotNull(); + assertThat(timeLimiterConfigurationOnMissingBean.rxJava2TimeLimiterAspectExt()).isNotNull(); + assertThat(timeLimiterConfigurationOnMissingBean + .timeLimiterRegistry(new TimeLimiterConfigurationProperties(), + new DefaultEventConsumerRegistry<>(), + new CompositeRegistryEventConsumer<>(Collections.emptyList()))).isNotNull(); + assertThat(timeLimiterConfigurationOnMissingBean + .timeLimiterAspect(new TimeLimiterConfigurationProperties(), + TimeLimiterRegistry.ofDefaults(), Collections.emptyList(), + new FallbackDecorators(Arrays.asList(new CompletionStageFallbackDecorator())))).isNotNull(); + assertThat(timeLimiterConfigurationOnMissingBean + .timeLimiterRegistryEventConsumer(Optional.empty())).isNotNull(); + } // testing config samples class BulkheadConfigurationOnMissingBean extends AbstractBulkheadConfigurationOnMissingBean { @@ -160,4 +179,7 @@ class RateLimiterConfigurationOnMissingBean extends AbstractRateLimiterConfigurationOnMissingBean { } + + class TimeLimiterConfigurationOnMissingBean extends AbstractTimeLimiterConfigurationOnMissingBean { + } } diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterAutoConfiguration.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterAutoConfiguration.java new file mode 100644 index 0000000000..f396e03fde --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterAutoConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.autoconfigure; + +import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.github.resilience4j.timelimiter.event.TimeLimiterEvent; +import io.github.resilience4j.timelimiter.monitoring.endpoint.TimeLimiterEndpoint; +import io.github.resilience4j.timelimiter.monitoring.endpoint.TimeLimiterEventsEndpoint; +import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; +import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +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 org.springframework.context.annotation.Import; + +@Configuration +@ConditionalOnClass(TimeLimiter.class) +@EnableConfigurationProperties(TimeLimiterProperties.class) +@Import(TimeLimiterConfigurationOnMissingBean.class) +@AutoConfigureBefore(EndpointAutoConfiguration.class) +public class TimeLimiterAutoConfiguration { + + @Configuration + @ConditionalOnClass(Endpoint.class) + public static class TimeLimiterEndpointConfiguration { + + @Bean + public TimeLimiterEndpoint timeLimiterEndpoint(TimeLimiterRegistry timeLimiterRegistry) { + return new TimeLimiterEndpoint(timeLimiterRegistry); + } + + @Bean + public TimeLimiterEventsEndpoint timeLimiterEventsEndpoint(EventConsumerRegistry eventsConsumerRegistry) { + return new TimeLimiterEventsEndpoint(eventsConsumerRegistry); + } + + } +} diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterConfigurationOnMissingBean.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterConfigurationOnMissingBean.java new file mode 100644 index 0000000000..a795ec99c9 --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterConfigurationOnMissingBean.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.autoconfigure; + +import io.github.resilience4j.common.IntegerToDurationConverter; +import io.github.resilience4j.common.StringToDurationConverter; +import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.timelimiter.event.TimeLimiterEvent; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({IntegerToDurationConverter.class, StringToDurationConverter.class}) +public class TimeLimiterConfigurationOnMissingBean extends AbstractTimeLimiterConfigurationOnMissingBean { + + @Bean + public EventConsumerRegistry timeLimiterEventsConsumerRegistry() { + return timeLimiterConfiguration.timeLimiterEventsConsumerRegistry(); + } + +} diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterMetricsAutoConfiguration.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterMetricsAutoConfiguration.java new file mode 100644 index 0000000000..b2ae579222 --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterMetricsAutoConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.autoconfigure; + +import com.codahale.metrics.MetricRegistry; +import io.github.resilience4j.metrics.TimeLimiterMetrics; +import io.github.resilience4j.metrics.publisher.TimeLimiterMetricsPublisher; +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import org.springframework.boot.actuate.autoconfigure.MetricRepositoryAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.MetricsDropwizardAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnClass({MetricRegistry.class, TimeLimiter.class, TimeLimiterMetricsPublisher.class}) +@AutoConfigureAfter(MetricsDropwizardAutoConfiguration.class) +@AutoConfigureBefore(MetricRepositoryAutoConfiguration.class) +@ConditionalOnProperty(value = "resilience4j.timelimiter.metrics.enabled", matchIfMissing = true) +public class TimeLimiterMetricsAutoConfiguration { + + @Bean + @ConditionalOnProperty(value = "resilience4j.timelimiter.metrics.legacy.enabled", havingValue = "true") + @ConditionalOnMissingBean + public TimeLimiterMetrics registerTimeLimiterMetrics(TimeLimiterRegistry timeLimiterRegistry, + MetricRegistry metricRegistry) { + return TimeLimiterMetrics.ofTimeLimiterRegistry(timeLimiterRegistry, metricRegistry); + } + + @Bean + @ConditionalOnProperty(value = "resilience4j.timelimiter.metrics.legacy.enabled", havingValue = "false", matchIfMissing = true) + @ConditionalOnMissingBean + public TimeLimiterMetricsPublisher timeLimiterMetricsPublisher(MetricRegistry metricRegistry) { + return new TimeLimiterMetricsPublisher(metricRegistry); + } + +} diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterMicrometerAutoConfiguration.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterMicrometerAutoConfiguration.java new file mode 100644 index 0000000000..694954a45e --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterMicrometerAutoConfiguration.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.autoconfigure; + +import io.github.resilience4j.micrometer.tagged.TaggedTimeLimiterMetrics; +import io.github.resilience4j.micrometer.tagged.TaggedTimeLimiterMetricsPublisher; +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.spring.autoconfigure.MetricsAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnClass({MetricsAutoConfiguration.class, TimeLimiter.class, + TaggedTimeLimiterMetricsPublisher.class}) +@AutoConfigureAfter(MetricsAutoConfiguration.class) +@ConditionalOnProperty(value = "resilience4j.timelimiter.metrics.enabled", matchIfMissing = true) +public class TimeLimiterMicrometerAutoConfiguration { + + @Bean + @ConditionalOnProperty(value = "resilience4j.timelimiter.metrics.legacy.enabled", havingValue = "true") + @ConditionalOnMissingBean + public TaggedTimeLimiterMetrics registerTimeLimiterMetrics(TimeLimiterRegistry timeLimiterRegistry) { + return TaggedTimeLimiterMetrics.ofTimeLimiterRegistry(timeLimiterRegistry); + } + + @Bean + @ConditionalOnBean(MeterRegistry.class) + @ConditionalOnProperty(value = "resilience4j.timelimiter.metrics.legacy.enabled", havingValue = "false", matchIfMissing = true) + @ConditionalOnMissingBean + public TaggedTimeLimiterMetricsPublisher taggedTimeLimiterMetricsPublisher(MeterRegistry meterRegistry) { + return new TaggedTimeLimiterMetricsPublisher(meterRegistry); + } + +} diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterPrometheusAutoConfiguration.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterPrometheusAutoConfiguration.java new file mode 100644 index 0000000000..7703300cb8 --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterPrometheusAutoConfiguration.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.autoconfigure; + +import io.github.resilience4j.prometheus.collectors.TimeLimiterMetricsCollector; +import io.github.resilience4j.prometheus.publisher.TimeLimiterMetricsPublisher; +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.prometheus.client.GaugeMetricFamily; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnClass({GaugeMetricFamily.class, TimeLimiter.class, TimeLimiterMetricsPublisher.class}) +@ConditionalOnProperty(value = "resilience4j.timelimiter.metrics.enabled", matchIfMissing = true) +public class TimeLimiterPrometheusAutoConfiguration { + + @Bean + @ConditionalOnProperty(value = "resilience4j.timelimiter.metrics.legacy.enabled", havingValue = "true") + @ConditionalOnMissingBean + public TimeLimiterMetricsCollector timeLimiterPrometheusCollector( + TimeLimiterRegistry timeLimiterRegistry) { + TimeLimiterMetricsCollector collector = TimeLimiterMetricsCollector + .ofTimeLimiterRegistry(timeLimiterRegistry); + collector.register(); + return collector; + } + + @Bean + @ConditionalOnProperty(value = "resilience4j.timelimiter.metrics.legacy.enabled", havingValue = "false", matchIfMissing = true) + @ConditionalOnMissingBean + public TimeLimiterMetricsPublisher timeLimiterPrometheusPublisher() { + return new TimeLimiterMetricsPublisher(); + } + +} diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/monitoring/endpoint/TimeLimiterEndpoint.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/monitoring/endpoint/TimeLimiterEndpoint.java new file mode 100644 index 0000000000..a91cc1d3c5 --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/monitoring/endpoint/TimeLimiterEndpoint.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.monitoring.endpoint; + +import io.github.resilience4j.common.timelimiter.monitoring.endpoint.TimeLimiterEndpointResponse; +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +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 TimeLimiter. + */ +@ConfigurationProperties(prefix = "endpoints.timelimiter") +public class TimeLimiterEndpoint extends AbstractEndpoint { + + private final TimeLimiterRegistry timeLimiterRegistry; + + public TimeLimiterEndpoint(TimeLimiterRegistry timeLimiterRegistry) { + super("timelimiter"); + this.timeLimiterRegistry = timeLimiterRegistry; + } + + @Override + public Object invoke() { + List names = timeLimiterRegistry.getAllTimeLimiters() + .map(TimeLimiter::getName).sorted().toJavaList(); + return ResponseEntity.ok(new TimeLimiterEndpointResponse(names)); + } + +} diff --git a/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/monitoring/endpoint/TimeLimiterEventsEndpoint.java b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/monitoring/endpoint/TimeLimiterEventsEndpoint.java new file mode 100644 index 0000000000..40bea68310 --- /dev/null +++ b/resilience4j-spring-boot/src/main/java/io/github/resilience4j/timelimiter/monitoring/endpoint/TimeLimiterEventsEndpoint.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.monitoring.endpoint; + +import io.github.resilience4j.common.timelimiter.monitoring.endpoint.TimeLimiterEventDTO; +import io.github.resilience4j.common.timelimiter.monitoring.endpoint.TimeLimiterEventsEndpointResponse; +import io.github.resilience4j.consumer.CircularEventConsumer; +import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.timelimiter.event.TimeLimiterEvent; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.Comparator; +import java.util.List; + +@Controller +@RequestMapping(value = "timelimiter/") +public class TimeLimiterEventsEndpoint { + + private final EventConsumerRegistry eventsConsumerRegistry; + + public TimeLimiterEventsEndpoint(EventConsumerRegistry eventsConsumerRegistry) { + this.eventsConsumerRegistry = eventsConsumerRegistry; + } + + @GetMapping(value = "events", produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public TimeLimiterEventsEndpointResponse getAllTimeLimiterEvents() { + List eventsList = eventsConsumerRegistry.getAllEventConsumer() + .flatMap(CircularEventConsumer::getBufferedEvents) + .sorted(Comparator.comparing(TimeLimiterEvent::getCreationTime)) + .map(TimeLimiterEventDTO::createTimeLimiterEventDTO).toJavaList(); + return new TimeLimiterEventsEndpointResponse(eventsList); + } + + @GetMapping(value = "events/{timeLimiterName}", produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public TimeLimiterEventsEndpointResponse getEventsFilteredByTimeLimiterName( + @PathVariable("timeLimiterName") String timeLimiterName) { + List eventsList = eventsConsumerRegistry.getEventConsumer(timeLimiterName).getBufferedEvents() + .filter(event -> event.getTimeLimiterName().equals(timeLimiterName)) + .map(TimeLimiterEventDTO::createTimeLimiterEventDTO).toJavaList(); + return new TimeLimiterEventsEndpointResponse(eventsList); + } + + @GetMapping(value = "events/{timeLimiterName}/{eventType}", produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public TimeLimiterEventsEndpointResponse getEventsFilteredByTimeLimiterNameAndEventType( + @PathVariable("timeLimiterName") String timeLimiterName, + @PathVariable("eventType") String eventType) { + TimeLimiterEvent.Type targetType = TimeLimiterEvent.Type.valueOf(eventType.toUpperCase()); + List eventsList = eventsConsumerRegistry.getEventConsumer(timeLimiterName) + .getBufferedEvents() + .filter(event -> event.getTimeLimiterName().equals(timeLimiterName)) + .filter(event -> event.getEventType() == targetType) + .map(TimeLimiterEventDTO::createTimeLimiterEventDTO).toJavaList(); + return new TimeLimiterEventsEndpointResponse(eventsList); + } +} diff --git a/resilience4j-spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/resilience4j-spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 8810fe7f43..fd50f06aee 100644 --- a/resilience4j-spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/resilience4j-spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -71,6 +71,18 @@ "type": "java.lang.Boolean", "description": "Whether to enable the legacy-retry-metrics.", "defaultValue": "false" + }, + { + "name": "resilience4j.timelimiter.metrics.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable the timelimiter-metrics.", + "defaultValue": "true" + }, + { + "name": "resilience4j.timelimiter.metrics.legacy.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable the legacy-timelimiter-metrics.", + "defaultValue": "false" } ] } 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 09d5274f24..6b4f0968e4 100644 --- a/resilience4j-spring-boot/src/main/resources/META-INF/spring.factories +++ b/resilience4j-spring-boot/src/main/resources/META-INF/spring.factories @@ -18,4 +18,8 @@ io.github.resilience4j.ratelimiter.autoconfigure.RateLimiterAutoConfiguration,\ io.github.resilience4j.ratelimiter.autoconfigure.RateLimiterMetricsAutoConfiguration,\ io.github.resilience4j.ratelimiter.autoconfigure.RateLimiterMicrometerAutoConfiguration,\ io.github.resilience4j.ratelimiter.autoconfigure.RateLimiterPrometheusAutoConfiguration,\ -io.github.resilience4j.ratelimiter.autoconfigure.RateLimitersHealthIndicatorAutoConfiguration +io.github.resilience4j.ratelimiter.autoconfigure.RateLimitersHealthIndicatorAutoConfiguration,\ +io.github.resilience4j.timelimiter.autoconfigure.TimeLimiterAutoConfiguration,\ +io.github.resilience4j.timelimiter.autoconfigure.TimeLimiterMetricsAutoConfiguration,\ +io.github.resilience4j.timelimiter.autoconfigure.TimeLimiterMicrometerAutoConfiguration,\ +io.github.resilience4j.timelimiter.autoconfigure.TimeLimiterPrometheusAutoConfiguration \ No newline at end of file diff --git a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/service/test/TimeLimiterDummyService.java b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/service/test/TimeLimiterDummyService.java new file mode 100644 index 0000000000..f58ae1e720 --- /dev/null +++ b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/service/test/TimeLimiterDummyService.java @@ -0,0 +1,10 @@ +package io.github.resilience4j.service.test; + +import java.util.concurrent.CompletionStage; + +public interface TimeLimiterDummyService { + String BACKEND = "backend"; + + CompletionStage doSomething(boolean timeout) throws Exception; + +} diff --git a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/service/test/TimeLimiterDummyServiceImpl.java b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/service/test/TimeLimiterDummyServiceImpl.java new file mode 100644 index 0000000000..2671680ef8 --- /dev/null +++ b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/service/test/TimeLimiterDummyServiceImpl.java @@ -0,0 +1,26 @@ +package io.github.resilience4j.service.test; + +import io.github.resilience4j.timelimiter.annotation.TimeLimiter; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +@Component +public class TimeLimiterDummyServiceImpl implements TimeLimiterDummyService { + + @TimeLimiter(name = TimeLimiterDummyService.BACKEND) + @Override + public CompletionStage doSomething(boolean timeout) throws Exception { + return CompletableFuture.supplyAsync(() -> { + if (timeout) { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + // Do nothing + } + } + return "something"; + }); + } +} diff --git a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterAutoConfigurationTest.java b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterAutoConfigurationTest.java new file mode 100644 index 0000000000..71c437ca5c --- /dev/null +++ b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterAutoConfigurationTest.java @@ -0,0 +1,98 @@ +package io.github.resilience4j.timelimiter; + +import io.github.resilience4j.common.timelimiter.monitoring.endpoint.TimeLimiterEndpointResponse; +import io.github.resilience4j.common.timelimiter.monitoring.endpoint.TimeLimiterEventDTO; +import io.github.resilience4j.common.timelimiter.monitoring.endpoint.TimeLimiterEventsEndpointResponse; +import io.github.resilience4j.service.test.TestApplication; +import io.github.resilience4j.service.test.TimeLimiterDummyService; +import io.github.resilience4j.timelimiter.autoconfigure.TimeLimiterProperties; +import io.github.resilience4j.timelimiter.configure.TimeLimiterAspect; +import io.github.resilience4j.timelimiter.event.TimeLimiterEvent; +import io.prometheus.client.CollectorRegistry; +import org.junit.BeforeClass; +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 java.time.Duration; +import java.util.List; +import java.util.stream.Collectors; + +import static io.github.resilience4j.service.test.TimeLimiterDummyService.BACKEND; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = TestApplication.class) +public class TimeLimiterAutoConfigurationTest { + + @Autowired + private TimeLimiterRegistry timeLimiterRegistry; + + @Autowired + private TimeLimiterProperties timeLimiterProperties; + + @Autowired + private TimeLimiterAspect timeLimiterAspect; + + @Autowired + private TimeLimiterDummyService dummyService; + + @Autowired + private TestRestTemplate restTemplate; + + @BeforeClass + public static void setUp() { + // Need to clear this static registry out since multiple tests register collectors that could collide. + CollectorRegistry.defaultRegistry.clear(); + } + + /** + * The test verifies that a TimeLimiter instance is created and configured properly when the DummyService is invoked and + * that the TimeLimiter records successful and failed calls. + */ + @Test + public void testTimeLimiterAutoConfiguration() throws Exception { + assertThat(timeLimiterRegistry).isNotNull(); + assertThat(timeLimiterProperties).isNotNull(); + + try { + dummyService.doSomething(true).toCompletableFuture().get(); + } catch (Exception ex) { + // Do nothing. The Exception is recorded by the timelimiter TimeoutException + } + // The invocation is recorded by TimeLimiter as a success. + dummyService.doSomething(false).toCompletableFuture().get(); + + TimeLimiter timeLimiter = timeLimiterRegistry.timeLimiter(BACKEND); + assertThat(timeLimiter).isNotNull(); + + assertThat(timeLimiter.getTimeLimiterConfig().getTimeoutDuration()).isEqualTo(Duration.ofSeconds(1)); + assertThat(timeLimiter.getName()).isEqualTo(BACKEND); + + // expect TimeLimiter actuator endpoint contains both timeLimiters + ResponseEntity timeLimiterList = restTemplate.getForEntity("/timelimiter", TimeLimiterEndpointResponse.class); + assertThat(timeLimiterList.getBody().getTimeLimiters()).hasSize(1).containsOnly(BACKEND); + + // expect TimeLimiter-event actuator endpoint recorded both events + ResponseEntity timeLimiterEventList = restTemplate.getForEntity("/timelimiter/events/" + BACKEND, TimeLimiterEventsEndpointResponse.class); + + List timeLimiterEvents = timeLimiterEventList.getBody().getTimeLimiterEvents(); + + assertThat(timeLimiterEvents.stream() + .filter(event -> event.getType() == TimeLimiterEvent.Type.SUCCESS) + .collect(Collectors.toList())).hasSize(1); + + assertThat(timeLimiterEvents.stream() + .filter(event -> event.getType() == TimeLimiterEvent.Type.TIMEOUT) + .collect(Collectors.toList())).hasSize(1); + + // expect aspect configured as defined in application.yml + assertThat(timeLimiterAspect.getOrder()).isEqualTo(500); + + } +} diff --git a/resilience4j-spring-boot/src/test/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterConfigurationOnMissingBeanTest.java b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterConfigurationOnMissingBeanTest.java new file mode 100644 index 0000000000..1b2010616e --- /dev/null +++ b/resilience4j-spring-boot/src/test/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterConfigurationOnMissingBeanTest.java @@ -0,0 +1,105 @@ +package io.github.resilience4j.timelimiter.autoconfigure; + +import io.github.resilience4j.consumer.DefaultEventConsumerRegistry; +import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.fallback.FallbackDecorators; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.github.resilience4j.timelimiter.configure.TimeLimiterAspect; +import io.github.resilience4j.timelimiter.configure.TimeLimiterAspectExt; +import io.github.resilience4j.timelimiter.configure.TimeLimiterConfiguration; +import io.github.resilience4j.timelimiter.configure.TimeLimiterConfigurationProperties; +import io.github.resilience4j.timelimiter.event.TimeLimiterEvent; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.lang.reflect.Method; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = { + TimeLimiterConfigurationOnMissingBeanTest.ConfigWithOverrides.class, + TimeLimiterAutoConfiguration.class, + TimeLimiterConfigurationOnMissingBean.class +}) +@EnableConfigurationProperties(TimeLimiterProperties.class) +public class TimeLimiterConfigurationOnMissingBeanTest { + + @Autowired + public ConfigWithOverrides configWithOverrides; + + @Autowired + private TimeLimiterRegistry timeLimiterRegistry; + + @Autowired + private TimeLimiterAspect timeLimiterAspect; + + @Autowired + private EventConsumerRegistry timeLimiterEventsConsumerRegistry; + + @Test + public void testAllBeansFromTimeLimiterConfigurationHasOnMissingBean() throws NoSuchMethodException { + final Class originalClass = TimeLimiterConfiguration.class; + final Class onMissingBeanClass = TimeLimiterConfigurationOnMissingBean.class; + + for (Method methodTimeLimiterConfiguration : originalClass.getMethods()) { + if (methodTimeLimiterConfiguration.isAnnotationPresent(Bean.class)) { + final Method methodOnMissing = onMissingBeanClass + .getMethod(methodTimeLimiterConfiguration.getName(), methodTimeLimiterConfiguration.getParameterTypes()); + + assertThat(methodOnMissing.isAnnotationPresent(Bean.class)).isTrue(); + + if (!"timeLimiterEventsConsumerRegistry".equals(methodOnMissing.getName()) && + !"timeLimiterRegistryEventConsumer".equals(methodOnMissing.getName())) { + assertThat(methodOnMissing.isAnnotationPresent(ConditionalOnMissingBean.class)).isTrue(); + } + } + } + } + + @Test + public void testAllCircuitBreakerConfigurationBeansOverridden() { + assertEquals(timeLimiterRegistry, configWithOverrides.timeLimiterRegistry); + assertEquals(timeLimiterAspect, configWithOverrides.timeLimiterAspect); + assertNotEquals(timeLimiterEventsConsumerRegistry, configWithOverrides.timeLimiterEventsConsumerRegistry); + } + + @Configuration + public static class ConfigWithOverrides { + + private TimeLimiterRegistry timeLimiterRegistry; + + private TimeLimiterAspect timeLimiterAspect; + + private EventConsumerRegistry timeLimiterEventsConsumerRegistry; + + @Bean + public TimeLimiterRegistry timeLimiterRegistry() { + timeLimiterRegistry = TimeLimiterRegistry.ofDefaults(); + return timeLimiterRegistry; + } + + @Bean + public TimeLimiterAspect timeLimiterAspect(TimeLimiterRegistry timeLimiterRegistry, @Autowired(required = false) List timeLimiterAspectExtList, FallbackDecorators fallbackDecorators) { + timeLimiterAspect = new TimeLimiterAspect(timeLimiterRegistry, new TimeLimiterConfigurationProperties(), timeLimiterAspectExtList, fallbackDecorators); + return timeLimiterAspect; + } + + @Bean + public EventConsumerRegistry timeLimiterEventsConsumerRegistry() { + timeLimiterEventsConsumerRegistry = new DefaultEventConsumerRegistry<>(); + return timeLimiterEventsConsumerRegistry; + } + } + +} diff --git a/resilience4j-spring-boot/src/test/resources/application.yaml b/resilience4j-spring-boot/src/test/resources/application.yaml index 660bab20b8..6d2b7c031e 100644 --- a/resilience4j-spring-boot/src/test/resources/application.yaml +++ b/resilience4j-spring-boot/src/test/resources/application.yaml @@ -89,6 +89,12 @@ resilience4j.bulkhead: maxWaitDuration: 10 maxConcurrentCalls: 2 +resilience4j.timelimiter: + time-limiter-aspect-order: 500 + instances: + backend: + timeoutDuration: 1s + management.security.enabled: false management.endpoint.health.show-details: always diff --git a/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterAutoConfiguration.java b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterAutoConfiguration.java new file mode 100644 index 0000000000..53fae21ff0 --- /dev/null +++ b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterAutoConfiguration.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.autoconfigure; + +import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.fallback.autoconfigure.FallbackConfigurationOnMissingBean; +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.github.resilience4j.timelimiter.event.TimeLimiterEvent; +import io.github.resilience4j.timelimiter.monitoring.endpoint.TimeLimiterEndpoint; +import io.github.resilience4j.timelimiter.monitoring.endpoint.TimeLimiterEventsEndpoint; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +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 org.springframework.context.annotation.Import; + +@Configuration +@ConditionalOnClass(TimeLimiter.class) +@EnableConfigurationProperties(TimeLimiterProperties.class) +@Import({TimeLimiterConfigurationOnMissingBean.class, FallbackConfigurationOnMissingBean.class}) +public class TimeLimiterAutoConfiguration { + + @Configuration + @ConditionalOnClass(Endpoint.class) + static class TimeLimiterAutoEndpointConfiguration { + + @Bean + @ConditionalOnEnabledEndpoint + public TimeLimiterEndpoint timeLimiterEndpoint(TimeLimiterRegistry timeLimiterRegistry) { + return new TimeLimiterEndpoint(timeLimiterRegistry); + } + + @Bean + @ConditionalOnEnabledEndpoint + public TimeLimiterEventsEndpoint timeLimiterEventsEndpoint(EventConsumerRegistry eventConsumerRegistry) { + return new TimeLimiterEventsEndpoint(eventConsumerRegistry); + } + } + +} diff --git a/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterConfigurationOnMissingBean.java b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterConfigurationOnMissingBean.java new file mode 100644 index 0000000000..8555a10de2 --- /dev/null +++ b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterConfigurationOnMissingBean.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.autoconfigure; + +import io.github.resilience4j.consumer.DefaultEventConsumerRegistry; +import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.timelimiter.event.TimeLimiterEvent; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class TimeLimiterConfigurationOnMissingBean extends AbstractTimeLimiterConfigurationOnMissingBean { + + /** + * The EventConsumerRegistry is used to manage EventConsumer instances. + * The EventConsumerRegistry is used by the TimeLimiter events monitor to show the latest TimeLimiterEvent events + * for each TimeLimiter instance. + * + * @return a default EventConsumerRegistry {@link DefaultEventConsumerRegistry} + */ + @Bean + @ConditionalOnMissingBean(value = TimeLimiterEvent.class, parameterizedContainer = EventConsumerRegistry.class) + public EventConsumerRegistry timeLimiterEventsConsumerRegistry() { + return timeLimiterConfiguration.timeLimiterEventsConsumerRegistry(); + } + +} diff --git a/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterMetricsAutoConfiguration.java b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterMetricsAutoConfiguration.java new file mode 100644 index 0000000000..9f50a0695a --- /dev/null +++ b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/timelimiter/autoconfigure/TimeLimiterMetricsAutoConfiguration.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.autoconfigure; + +import io.github.resilience4j.micrometer.tagged.TaggedTimeLimiterMetrics; +import io.github.resilience4j.micrometer.tagged.TaggedTimeLimiterMetricsPublisher; +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnClass({MeterRegistry.class, TimeLimiter.class, TaggedTimeLimiterMetricsPublisher.class}) +@AutoConfigureAfter({MetricsAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class}) +@ConditionalOnProperty(value = "resilience4j.timelimiter.metrics.enabled", matchIfMissing = true) +public class TimeLimiterMetricsAutoConfiguration { + + @Bean + @ConditionalOnProperty(value = "resilience4j.timelimiter.metrics.legacy.enabled", havingValue = "true") + @ConditionalOnMissingBean + public TaggedTimeLimiterMetrics registerTimeLimiterMetrics(TimeLimiterRegistry timeLimiterRegistry) { + return TaggedTimeLimiterMetrics.ofTimeLimiterRegistry(timeLimiterRegistry); + } + + @Bean + @ConditionalOnBean(MeterRegistry.class) + @ConditionalOnProperty(value = "resilience4j.timelimiter.metrics.legacy.enabled", havingValue = "false", matchIfMissing = true) + @ConditionalOnMissingBean + public TaggedTimeLimiterMetricsPublisher taggedTimeLimiterMetricsPublisher(MeterRegistry meterRegistry) { + return new TaggedTimeLimiterMetricsPublisher(meterRegistry); + } + +} diff --git a/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/timelimiter/monitoring/endpoint/TimeLimiterEndpoint.java b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/timelimiter/monitoring/endpoint/TimeLimiterEndpoint.java new file mode 100644 index 0000000000..f340e649de --- /dev/null +++ b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/timelimiter/monitoring/endpoint/TimeLimiterEndpoint.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.monitoring.endpoint; + +import io.github.resilience4j.common.timelimiter.monitoring.endpoint.TimeLimiterEndpointResponse; +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; + +import java.util.List; + +@Endpoint(id = "timelimiters") +public class TimeLimiterEndpoint { + private final TimeLimiterRegistry timeLimiterRegistry; + + public TimeLimiterEndpoint(TimeLimiterRegistry timeLimiterRegistry) { + this.timeLimiterRegistry = timeLimiterRegistry; + } + + @ReadOperation + public TimeLimiterEndpointResponse getAllTimeLimiters() { + List timeLimiters = timeLimiterRegistry.getAllTimeLimiters() + .map(TimeLimiter::getName).sorted().toJavaList(); + return new TimeLimiterEndpointResponse(timeLimiters); + } + +} diff --git a/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/timelimiter/monitoring/endpoint/TimeLimiterEventsEndpoint.java b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/timelimiter/monitoring/endpoint/TimeLimiterEventsEndpoint.java new file mode 100644 index 0000000000..87052afec3 --- /dev/null +++ b/resilience4j-spring-boot2/src/main/java/io/github/resilience4j/timelimiter/monitoring/endpoint/TimeLimiterEventsEndpoint.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.monitoring.endpoint; + +import io.github.resilience4j.common.timelimiter.monitoring.endpoint.TimeLimiterEventDTO; +import io.github.resilience4j.common.timelimiter.monitoring.endpoint.TimeLimiterEventsEndpointResponse; +import io.github.resilience4j.consumer.CircularEventConsumer; +import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.timelimiter.event.TimeLimiterEvent; +import io.vavr.collection.List; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; + +import java.util.Comparator; + +@Endpoint(id = "timelimiterevents") +public class TimeLimiterEventsEndpoint { + + private final EventConsumerRegistry eventsConsumerRegistry; + + public TimeLimiterEventsEndpoint(EventConsumerRegistry eventsConsumerRegistry) { + this.eventsConsumerRegistry = eventsConsumerRegistry; + } + + @ReadOperation + public TimeLimiterEventsEndpointResponse getAllTimeLimiterEvents() { + return new TimeLimiterEventsEndpointResponse(eventsConsumerRegistry.getAllEventConsumer() + .flatMap(CircularEventConsumer::getBufferedEvents) + .sorted(Comparator.comparing(TimeLimiterEvent::getCreationTime)) + .map(TimeLimiterEventDTO::createTimeLimiterEventDTO).toJavaList()); + } + + @ReadOperation + public TimeLimiterEventsEndpointResponse getEventsFilteredByTimeLimiterName(@Selector String name) { + return new TimeLimiterEventsEndpointResponse(getTimeLimiterEvents(name) + .map(TimeLimiterEventDTO::createTimeLimiterEventDTO).toJavaList()); + } + + @ReadOperation + public TimeLimiterEventsEndpointResponse getEventsFilteredByTimeLimiterNameAndEventType(@Selector String name, + @Selector String eventType) { + TimeLimiterEvent.Type targetType = TimeLimiterEvent.Type.valueOf(eventType.toUpperCase()); + return new TimeLimiterEventsEndpointResponse(getTimeLimiterEvents(name) + .filter(event -> event.getEventType() == targetType) + .map(TimeLimiterEventDTO::createTimeLimiterEventDTO).toJavaList()); + } + + private List getTimeLimiterEvents(String name) { + CircularEventConsumer eventConsumer = eventsConsumerRegistry.getEventConsumer(name); + if(eventConsumer != null){ + return eventConsumer.getBufferedEvents() + .filter(event -> event.getTimeLimiterName().equals(name)); + }else{ + return List.empty(); + } + } + +} diff --git a/resilience4j-spring-boot2/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/resilience4j-spring-boot2/src/main/resources/META-INF/additional-spring-configuration-metadata.json index cc377bb84a..0338839aed 100644 --- a/resilience4j-spring-boot2/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/resilience4j-spring-boot2/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -71,6 +71,18 @@ "type": "java.lang.Boolean", "description": "Whether to enable the legacy-retry-metrics.", "defaultValue": "false" + }, + { + "name": "resilience4j.timelimiter.metrics.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable the timelimiter-metrics.", + "defaultValue": "true" + }, + { + "name": "resilience4j.timelimiter.metrics.legacy.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable the legacy-timelimiter-metrics.", + "defaultValue": "false" } ] } diff --git a/resilience4j-spring-boot2/src/main/resources/META-INF/spring.factories b/resilience4j-spring-boot2/src/main/resources/META-INF/spring.factories index 7ba44261c1..e19f24a8ff 100644 --- a/resilience4j-spring-boot2/src/main/resources/META-INF/spring.factories +++ b/resilience4j-spring-boot2/src/main/resources/META-INF/spring.factories @@ -9,4 +9,6 @@ io.github.resilience4j.circuitbreaker.autoconfigure.CircuitBreakerMetricsAutoCon io.github.resilience4j.circuitbreaker.autoconfigure.CircuitBreakersHealthIndicatorAutoConfiguration,\ io.github.resilience4j.ratelimiter.autoconfigure.RateLimiterAutoConfiguration,\ io.github.resilience4j.ratelimiter.autoconfigure.RateLimiterMetricsAutoConfiguration,\ -io.github.resilience4j.ratelimiter.autoconfigure.RateLimitersHealthIndicatorAutoConfiguration +io.github.resilience4j.ratelimiter.autoconfigure.RateLimitersHealthIndicatorAutoConfiguration,\ +io.github.resilience4j.timelimiter.autoconfigure.TimeLimiterAutoConfiguration,\ +io.github.resilience4j.timelimiter.autoconfigure.TimeLimiterMetricsAutoConfiguration diff --git a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/LegacyMetricsAutoConfigurationTest.java b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/LegacyMetricsAutoConfigurationTest.java index b4213d8908..0b65d43e6a 100644 --- a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/LegacyMetricsAutoConfigurationTest.java +++ b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/LegacyMetricsAutoConfigurationTest.java @@ -37,7 +37,8 @@ "resilience4j.thread-pool-bulkhead.metrics.legacy.enabled=true", "resilience4j.circuitbreaker.metrics.legacy.enabled=true", "resilience4j.ratelimiter.metrics.legacy.enabled=true", - "resilience4j.retry.metrics.legacy.enabled=true" + "resilience4j.retry.metrics.legacy.enabled=true", + "resilience4j.timelimiter.metrics.legacy.enabled=true" }) public class LegacyMetricsAutoConfigurationTest { @@ -59,6 +60,9 @@ public class LegacyMetricsAutoConfigurationTest { @Autowired(required = false) TaggedRetryMetrics taggedRetryMetrics; + @Autowired(required = false) + TaggedTimeLimiterMetrics taggedTimeLimiterMetrics; + @Test public void newMetricsPublisherIsNotBound() { assertThat(metricsPublishers).isEmpty(); @@ -89,4 +93,9 @@ public void legacyRetryBinderIsBound() { assertThat(taggedRetryMetrics).isNotNull(); } + @Test + public void legacyTimeLimiterBinderIsBound() { + assertThat(taggedTimeLimiterMetrics).isNotNull(); + } + } diff --git a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/MetricsAutoConfigurationTest.java b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/MetricsAutoConfigurationTest.java index c5f03a69e2..d3117f6a1a 100644 --- a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/MetricsAutoConfigurationTest.java +++ b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/MetricsAutoConfigurationTest.java @@ -44,6 +44,9 @@ public class MetricsAutoConfigurationTest { @Autowired(required = false) TaggedRetryMetricsPublisher taggedRetryMetricsPublisher; + @Autowired(required = false) + TaggedTimeLimiterMetricsPublisher taggedTimeLimiterMetricsPublisher; + @Test public void newCircuitBreakerPublisherIsBound() { assertThat(taggedCircuitBreakerMetricsPublisher).isNotNull(); @@ -69,4 +72,8 @@ public void newRetryPublisherIsBound() { assertThat(taggedRetryMetricsPublisher).isNotNull(); } + @Test + public void newTimeLimiterPublisherIsBound() { + assertThat(taggedTimeLimiterMetricsPublisher).isNotNull(); + } } diff --git a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerAutoConfigurationTest.java b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerAutoConfigurationTest.java index b387fcfe9c..dfac412c2b 100644 --- a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerAutoConfigurationTest.java +++ b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/circuitbreaker/CircuitBreakerAutoConfigurationTest.java @@ -110,8 +110,7 @@ public void testCircuitBreakerAutoConfiguration() throws IOException { CircuitBreakerEventsEndpointResponse circuitBreakerEventsBefore = circuitBreakerEvents( "/actuator/circuitbreakerevents"); CircuitBreakerEventsEndpointResponse circuitBreakerEventsForABefore = circuitBreakerEvents( - "/actuator" + - "/circuitbreakerevents/backendA"); + "/actuator/circuitbreakerevents/backendA"); try { dummyService.doSomething(true); diff --git a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/service/test/DummyServiceImpl.java b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/service/test/DummyServiceImpl.java index 4c35e6ff66..8f74662dc7 100644 --- a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/service/test/DummyServiceImpl.java +++ b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/service/test/DummyServiceImpl.java @@ -3,6 +3,8 @@ import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import io.github.resilience4j.ratelimiter.annotation.RateLimiter; +import io.github.resilience4j.retry.annotation.Retry; +import io.github.resilience4j.timelimiter.annotation.TimeLimiter; import org.springframework.stereotype.Component; import java.io.IOException; @@ -21,6 +23,7 @@ public void doSomething(boolean throwBackendTrouble) throws IOException { } @Override + @TimeLimiter(name = DummyService.BACKEND) public CompletableFuture doSomethingAsync(boolean throwBackendTrouble) throws IOException { if (throwBackendTrouble) { diff --git a/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterAutoConfigurationTest.java b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterAutoConfigurationTest.java new file mode 100644 index 0000000000..1a8999c209 --- /dev/null +++ b/resilience4j-spring-boot2/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterAutoConfigurationTest.java @@ -0,0 +1,84 @@ +package io.github.resilience4j.timelimiter; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import io.github.resilience4j.common.timelimiter.monitoring.endpoint.TimeLimiterEventsEndpointResponse; +import io.github.resilience4j.service.test.DummyService; +import io.github.resilience4j.service.test.TestApplication; +import io.github.resilience4j.timelimiter.autoconfigure.TimeLimiterProperties; +import io.github.resilience4j.timelimiter.configure.TimeLimiterAspect; +import org.junit.Rule; +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.test.context.junit4.SpringJUnit4ClassRunner; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = TestApplication.class) +public class TimeLimiterAutoConfigurationTest { + + @Autowired + TimeLimiterRegistry timeLimiterRegistry; + + @Autowired + TimeLimiterProperties timeLimiterProperties; + + @Autowired + TimeLimiterAspect timeLimiterAspect; + + @Autowired + private DummyService dummyService; + + @Autowired + private TestRestTemplate restTemplate; + + @Rule + public WireMockRule wireMockRule = new WireMockRule(8090); + + @Test + public void testTimeLimiterAutoConfigurationTest() throws Exception { + assertThat(timeLimiterRegistry).isNotNull(); + assertThat(timeLimiterProperties).isNotNull(); + + TimeLimiterEventsEndpointResponse timeLimiterEventsBefore = + timeLimiterEvents("/actuator/timelimiterevents"); + TimeLimiterEventsEndpointResponse timeLimiterEventsForABefore = + timeLimiterEvents("/actuator/timelimiterevents/backendA"); + + try { + dummyService.doSomethingAsync(true).get(); + } catch (Exception ex) { + // Do nothing. The IOException is recorded by the TimeLimiter as a failure. + } + + final CompletableFuture stringCompletionStage = dummyService.doSomethingAsync(false); + assertThat(stringCompletionStage.get()).isEqualTo("Test result"); + + TimeLimiter timeLimiter = timeLimiterRegistry.timeLimiter(DummyService.BACKEND); + assertThat(timeLimiter).isNotNull(); + + assertThat(timeLimiter.getTimeLimiterConfig().getTimeoutDuration()).isEqualTo(Duration.ofSeconds(5)); + + TimeLimiterEventsEndpointResponse timeLimiterEventList = timeLimiterEvents("/actuator/timelimiterevents"); + assertThat(timeLimiterEventList.getTimeLimiterEvents()) + .hasSize(timeLimiterEventsBefore.getTimeLimiterEvents().size() + 2); + + timeLimiterEventList = timeLimiterEvents("/actuator/timelimiterevents/backendA"); + assertThat(timeLimiterEventList.getTimeLimiterEvents()) + .hasSize(timeLimiterEventsForABefore.getTimeLimiterEvents().size() + 2); + + assertThat(timeLimiterAspect.getOrder()).isEqualTo(398); + } + + private TimeLimiterEventsEndpointResponse timeLimiterEvents(String s) { + return restTemplate.getForEntity(s, TimeLimiterEventsEndpointResponse.class).getBody(); + } + +} diff --git a/resilience4j-spring-boot2/src/test/resources/application.yaml b/resilience4j-spring-boot2/src/test/resources/application.yaml index 11b31764a6..48909548c1 100644 --- a/resilience4j-spring-boot2/src/test/resources/application.yaml +++ b/resilience4j-spring-boot2/src/test/resources/application.yaml @@ -163,6 +163,15 @@ resilience4j.thread-pool-bulkhead": contextPropagator: - io.github.resilience4j.TestThreadLocalContextPropagator +resilience4j.timelimiter: + time-limiter-aspect-order: 398 + configs: + default: + timeoutDuration: 1s + cancelRunningFuture: false + instances: + backendA: + timeoutDuration: 5s management.security.enabled: false management.endpoints.web.exposure.include: '*' diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadAspectExt.java b/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadAspectExt.java index ae28d78db1..93ecafc82f 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadAspectExt.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/bulkhead/configure/BulkheadAspectExt.java @@ -15,6 +15,7 @@ */ package io.github.resilience4j.bulkhead.configure; +import io.github.resilience4j.bulkhead.Bulkhead; import org.aspectj.lang.ProceedingJoinPoint; /** @@ -24,6 +25,6 @@ public interface BulkheadAspectExt { boolean canHandleReturnType(Class returnType); - Object handle(ProceedingJoinPoint proceedingJoinPoint, - io.github.resilience4j.bulkhead.Bulkhead bulkhead, String methodName) throws Throwable; + Object handle(ProceedingJoinPoint proceedingJoinPoint, Bulkhead bulkhead, String methodName) + throws Throwable; } diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerAspectExt.java b/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerAspectExt.java index 527670a710..3fbf3a82c0 100644 --- a/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerAspectExt.java +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/circuitbreaker/configure/CircuitBreakerAspectExt.java @@ -15,6 +15,7 @@ */ package io.github.resilience4j.circuitbreaker.configure; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; import org.aspectj.lang.ProceedingJoinPoint; /** @@ -25,6 +26,5 @@ public interface CircuitBreakerAspectExt { boolean canHandleReturnType(Class returnType); Object handle(ProceedingJoinPoint proceedingJoinPoint, - io.github.resilience4j.circuitbreaker.CircuitBreaker circuitBreaker, String methodName) - throws Throwable; + CircuitBreaker circuitBreaker, String methodName) throws Throwable; } diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/ReactorTimeLimiterAspectExt.java b/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/ReactorTimeLimiterAspectExt.java new file mode 100644 index 0000000000..79a0ef9bf4 --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/ReactorTimeLimiterAspectExt.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.configure; + +import io.github.resilience4j.reactor.timelimiter.TimeLimiterOperator; +import io.github.resilience4j.timelimiter.TimeLimiter; +import org.aspectj.lang.ProceedingJoinPoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class ReactorTimeLimiterAspectExt implements TimeLimiterAspectExt{ + + private static final Logger logger = LoggerFactory.getLogger(ReactorTimeLimiterAspectExt.class); + + /** + * @param returnType the AOP method return type class + * @return boolean if the method has Reactor return type + */ + @Override + public boolean canHandleReturnType(Class returnType) { + return Flux.class.isAssignableFrom(returnType) || Mono.class.isAssignableFrom(returnType); + } + + /** + * handle the Spring web flux (Flux /Mono) return types AOP based into reactor time limiter + * See {@link TimeLimiter} for details. + * + * @param proceedingJoinPoint Spring AOP proceedingJoinPoint + * @param timeLimiter the configured rateLimiter + * @param methodName the method name + * @return the result object + * @throws Throwable exception in case of faulty flow + */ + @Override + public Object handle(ProceedingJoinPoint proceedingJoinPoint, + TimeLimiter timeLimiter, String methodName) throws Throwable { + Object returnValue = proceedingJoinPoint.proceed(); + if (Flux.class.isAssignableFrom(returnValue.getClass())) { + Flux fluxReturnValue = (Flux) returnValue; + return fluxReturnValue.compose(TimeLimiterOperator.of(timeLimiter)); + } else if (Mono.class.isAssignableFrom(returnValue.getClass())) { + Mono monoReturnValue = (Mono) returnValue; + return monoReturnValue.compose(TimeLimiterOperator.of(timeLimiter)); + } else { + logger.error("Unsupported type for Reactor timeLimiter {}", returnValue.getClass().getTypeName()); + throw new IllegalArgumentException( + "Not Supported type for the timeLimiter in Reactor :" + returnValue.getClass().getName()); + + } + } + +} diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/RxJava2TimeLimiterAspectExt.java b/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/RxJava2TimeLimiterAspectExt.java new file mode 100644 index 0000000000..abadf12b8e --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/RxJava2TimeLimiterAspectExt.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.configure; + +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.transformer.TimeLimiterTransformer; +import io.reactivex.*; +import org.aspectj.lang.ProceedingJoinPoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Set; + +import static io.github.resilience4j.utils.AspectUtil.newHashSet; + +public class RxJava2TimeLimiterAspectExt implements TimeLimiterAspectExt { + + private static final Logger logger = LoggerFactory.getLogger(RxJava2TimeLimiterAspectExt.class); + private final Set> rxSupportedTypes = newHashSet(ObservableSource.class, + SingleSource.class, CompletableSource.class, MaybeSource.class, Flowable.class); + + /** + * @param returnType the AOP method return type class + * @return boolean if the method has Rx java 2 rerun type + */ + @Override + public boolean canHandleReturnType(Class returnType) { + return rxSupportedTypes.stream().anyMatch(classType -> classType.isAssignableFrom(returnType)); + } + + /** + * @param proceedingJoinPoint Spring AOP proceedingJoinPoint + * @param timeLimiter the configured timeLimiter + * @param methodName the method name + * @return the result object + * @throws Throwable exception in case of faulty flow + */ + @Override + public Object handle(ProceedingJoinPoint proceedingJoinPoint, TimeLimiter timeLimiter, String methodName) + throws Throwable { + TimeLimiterTransformer timeLimiterTransformer = TimeLimiterTransformer.of(timeLimiter); + Object returnValue = proceedingJoinPoint.proceed(); + return executeRxJava2Aspect(timeLimiterTransformer, returnValue); + } + + @SuppressWarnings("unchecked") + private static Object executeRxJava2Aspect(TimeLimiterTransformer timeLimiterTransformer, Object returnValue) { + if (returnValue instanceof ObservableSource) { + Observable observable = (Observable) returnValue; + return observable.compose(timeLimiterTransformer); + } else if (returnValue instanceof SingleSource) { + Single single = (Single) returnValue; + return single.compose(timeLimiterTransformer); + } else if (returnValue instanceof CompletableSource) { + Completable completable = (Completable) returnValue; + return completable.compose(timeLimiterTransformer); + } else if (returnValue instanceof MaybeSource) { + Maybe maybe = (Maybe) returnValue; + return maybe.compose(timeLimiterTransformer); + } else if (returnValue instanceof Flowable) { + Flowable flowable = (Flowable) returnValue; + return flowable.compose(timeLimiterTransformer); + } else { + logger.error("Unsupported type for TimeLimiter RxJava2 {}", returnValue.getClass().getTypeName()); + throw new IllegalArgumentException( + "Not Supported type for the TimeLimiter in RxJava2 :" + returnValue.getClass().getName()); + } + } +} diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/TimeLimiterAspect.java b/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/TimeLimiterAspect.java new file mode 100644 index 0000000000..a30d1f5ac2 --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/TimeLimiterAspect.java @@ -0,0 +1,178 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.configure; + +import io.github.resilience4j.core.lang.Nullable; +import io.github.resilience4j.fallback.FallbackDecorators; +import io.github.resilience4j.fallback.FallbackMethod; +import io.github.resilience4j.timelimiter.TimeLimiterConfig; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.github.resilience4j.timelimiter.annotation.TimeLimiter; +import io.github.resilience4j.utils.AnnotationExtractor; +import io.github.resilience4j.utils.ValueResolver; +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 org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.core.Ordered; +import org.springframework.util.StringUtils; +import org.springframework.util.StringValueResolver; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.List; +import java.util.concurrent.*; + +@Aspect +public class TimeLimiterAspect implements EmbeddedValueResolverAware, Ordered { + private static final Logger logger = LoggerFactory.getLogger(TimeLimiterAspect.class); + + private final TimeLimiterRegistry timeLimiterRegistry; + private final TimeLimiterConfigurationProperties properties; + private static final ScheduledExecutorService timeLimiterExecutorService = Executors + .newScheduledThreadPool(Runtime.getRuntime().availableProcessors()); + @Nullable + private final List timeLimiterAspectExtList; + private final FallbackDecorators fallbackDecorators; + private StringValueResolver embeddedValueResolver; + + public TimeLimiterAspect(TimeLimiterRegistry timeLimiterRegistry, + TimeLimiterConfigurationProperties properties, + @Nullable List timeLimiterAspectExtList, + FallbackDecorators fallbackDecorators) { + this.timeLimiterRegistry = timeLimiterRegistry; + this.properties = properties; + this.timeLimiterAspectExtList = timeLimiterAspectExtList; + this.fallbackDecorators = fallbackDecorators; + cleanup(); + } + + @Pointcut(value = "@within(timeLimiter) || @annotation(timeLimiter)", argNames = "timeLimiter") + public void matchAnnotatedClassOrMethod(TimeLimiter timeLimiter) { + } + + @Around(value = "matchAnnotatedClassOrMethod(timeLimiterAnnotation)", argNames = "proceedingJoinPoint, timeLimiterAnnotation") + public Object timeLimiterAroundAdvice(ProceedingJoinPoint proceedingJoinPoint, + @Nullable TimeLimiter timeLimiterAnnotation) throws Throwable { + Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod(); + String methodName = method.getDeclaringClass().getName() + "#" + method.getName(); + if (timeLimiterAnnotation == null) { + timeLimiterAnnotation = getTimeLimiterAnnotation(proceedingJoinPoint); + } + if(timeLimiterAnnotation == null) { + return proceedingJoinPoint.proceed(); + } + String name = timeLimiterAnnotation.name(); + io.github.resilience4j.timelimiter.TimeLimiter timeLimiter = + getOrCreateTimeLimiter(methodName, name); + Class returnType = method.getReturnType(); + + String fallbackMethodValue = ValueResolver.resolve( + this.embeddedValueResolver, timeLimiterAnnotation.fallbackMethod()); + if (StringUtils.isEmpty(fallbackMethodValue)) { + return proceed(proceedingJoinPoint, methodName, timeLimiter, returnType); + } + FallbackMethod fallbackMethod = FallbackMethod + .create(fallbackMethodValue, method, + proceedingJoinPoint.getArgs(), proceedingJoinPoint.getTarget()); + return fallbackDecorators.decorate(fallbackMethod, + () -> proceed(proceedingJoinPoint, methodName, timeLimiter, returnType)).apply(); + } + + private Object proceed(ProceedingJoinPoint proceedingJoinPoint, String methodName, + io.github.resilience4j.timelimiter.TimeLimiter timeLimiter, Class returnType) + throws Throwable { + if (timeLimiterAspectExtList != null && !timeLimiterAspectExtList.isEmpty()) { + for (TimeLimiterAspectExt timeLimiterAspectExt : timeLimiterAspectExtList) { + if (timeLimiterAspectExt.canHandleReturnType(returnType)) { + return timeLimiterAspectExt.handle(proceedingJoinPoint, timeLimiter, methodName); + } + } + } + + if (!CompletionStage.class.isAssignableFrom(returnType)) { + throw new IllegalStateException("Not supported type by TimeLimiterAspect"); + } + + return handleJoinPointCompletableFuture(proceedingJoinPoint, timeLimiter); + } + + private io.github.resilience4j.timelimiter.TimeLimiter getOrCreateTimeLimiter(String methodName, String name) { + io.github.resilience4j.timelimiter.TimeLimiter timeLimiter = timeLimiterRegistry.timeLimiter(name); + + if (logger.isDebugEnabled()) { + TimeLimiterConfig timeLimiterConfig = timeLimiter.getTimeLimiterConfig(); + logger.debug( + "Created or retrieved time limiter '{}' with timeout duration '{}' and cancelRunningFuture '{}' for method: '{}'", + name, timeLimiterConfig.getTimeoutDuration(), timeLimiterConfig.shouldCancelRunningFuture(), methodName + ); + } + + return timeLimiter; + } + + @Nullable + private static TimeLimiter getTimeLimiterAnnotation(ProceedingJoinPoint proceedingJoinPoint) { + if (proceedingJoinPoint.getTarget() instanceof Proxy) { + logger.debug("The TimeLimiter annotation is kept on a interface which is acting as a proxy"); + return AnnotationExtractor.extractAnnotationFromProxy(proceedingJoinPoint.getTarget(), TimeLimiter.class); + } else { + return AnnotationExtractor.extract(proceedingJoinPoint.getTarget().getClass(), TimeLimiter.class); + } + } + + private static Object handleJoinPointCompletableFuture( + ProceedingJoinPoint proceedingJoinPoint, io.github.resilience4j.timelimiter.TimeLimiter timeLimiter) throws Throwable { + return timeLimiter.executeCompletionStage(timeLimiterExecutorService, () -> { + try { + return (CompletionStage) proceedingJoinPoint.proceed(); + } catch (Throwable throwable) { + throw new CompletionException(throwable); + } + }); + } + + private void cleanup() { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + timeLimiterExecutorService.shutdown(); + try { + if (!timeLimiterExecutorService.awaitTermination(5, TimeUnit.SECONDS)) { + timeLimiterExecutorService.shutdownNow(); + } + } catch (InterruptedException e) { + if (!timeLimiterExecutorService.isTerminated()) { + timeLimiterExecutorService.shutdownNow(); + } + Thread.currentThread().interrupt(); + } + })); + } + + @Override + public int getOrder() { + return properties.getTimeLimiterAspectOrder(); + } + + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.embeddedValueResolver = resolver; + } +} diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/TimeLimiterAspectExt.java b/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/TimeLimiterAspectExt.java new file mode 100644 index 0000000000..5ee167ca6e --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/TimeLimiterAspectExt.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.configure; + +import io.github.resilience4j.timelimiter.TimeLimiter; +import org.aspectj.lang.ProceedingJoinPoint; + +public interface TimeLimiterAspectExt { + + boolean canHandleReturnType(Class returnType); + + Object handle(ProceedingJoinPoint proceedingJoinPoint, TimeLimiter timeLimiter, String methodName) throws Throwable; + +} diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/TimeLimiterConfiguration.java b/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/TimeLimiterConfiguration.java new file mode 100644 index 0000000000..d7c57c43f9 --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/TimeLimiterConfiguration.java @@ -0,0 +1,135 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.configure; + +import io.github.resilience4j.consumer.DefaultEventConsumerRegistry; +import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.core.registry.CompositeRegistryEventConsumer; +import io.github.resilience4j.core.registry.RegistryEventConsumer; +import io.github.resilience4j.fallback.FallbackDecorators; +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.TimeLimiterConfig; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.github.resilience4j.timelimiter.event.TimeLimiterEvent; +import io.github.resilience4j.utils.AspectJOnClasspathCondition; +import io.github.resilience4j.utils.ReactorOnClasspathCondition; +import io.github.resilience4j.utils.RxJava2OnClasspathCondition; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * {@link Configuration} for resilience4j-timelimiter. + */ +@Configuration +public class TimeLimiterConfiguration { + + @Bean + public TimeLimiterRegistry timeLimiterRegistry(TimeLimiterConfigurationProperties timeLimiterConfigurationProperties, + EventConsumerRegistry timeLimiterEventConsumerRegistry, + RegistryEventConsumer timeLimiterRegistryEventConsumer) { + TimeLimiterRegistry timeLimiterRegistry = + createTimeLimiterRegistry(timeLimiterConfigurationProperties, timeLimiterRegistryEventConsumer); + registerEventConsumer(timeLimiterRegistry, timeLimiterEventConsumerRegistry, timeLimiterConfigurationProperties); + timeLimiterConfigurationProperties.getInstances().forEach((name, properties) -> + timeLimiterRegistry.timeLimiter(name, timeLimiterConfigurationProperties.createTimeLimiterConfig(properties))); + return timeLimiterRegistry; + } + + @Bean + @Primary + public RegistryEventConsumer timeLimiterRegistryEventConsumer( + Optional>> optionalRegistryEventConsumers) { + return new CompositeRegistryEventConsumer<>(optionalRegistryEventConsumers.orElseGet(ArrayList::new)); + } + + @Bean + @Conditional(AspectJOnClasspathCondition.class) + public TimeLimiterAspect timeLimiterAspect(TimeLimiterConfigurationProperties timeLimiterConfigurationProperties, TimeLimiterRegistry timeLimiterRegistry, + @Autowired(required = false) List timeLimiterAspectExtList, + FallbackDecorators fallbackDecorators) { + return new TimeLimiterAspect(timeLimiterRegistry, timeLimiterConfigurationProperties, timeLimiterAspectExtList, fallbackDecorators); + } + + @Bean + @Conditional({RxJava2OnClasspathCondition.class, AspectJOnClasspathCondition.class}) + public RxJava2TimeLimiterAspectExt rxJava2TimeLimiterAspectExt() { + return new RxJava2TimeLimiterAspectExt(); + } + + @Bean + @Conditional({ReactorOnClasspathCondition.class, AspectJOnClasspathCondition.class}) + public ReactorTimeLimiterAspectExt reactorTimeLimiterAspectExt() { + return new ReactorTimeLimiterAspectExt(); + } + + /** + * The EventConsumerRegistry is used to manage EventConsumer instances. + * The EventConsumerRegistry is used by the TimeLimiter events monitor to show the latest TimeLimiter events + * for each TimeLimiter instance. + * + * @return a default EventConsumerRegistry {@link DefaultEventConsumerRegistry} + */ + @Bean + public EventConsumerRegistry timeLimiterEventsConsumerRegistry() { + return new DefaultEventConsumerRegistry<>(); + } + /** + * Initializes a timeLimiter registry. + * + * @param timeLimiterConfigurationProperties The timeLimiter configuration properties. + * @return a timeLimiterRegistry + */ + private static TimeLimiterRegistry createTimeLimiterRegistry(TimeLimiterConfigurationProperties timeLimiterConfigurationProperties, + RegistryEventConsumer timeLimiterRegistryEventConsumer) { + Map configs = timeLimiterConfigurationProperties.getConfigs() + .entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, + entry -> timeLimiterConfigurationProperties.createTimeLimiterConfig(entry.getValue()))); + + return TimeLimiterRegistry.of(configs, timeLimiterRegistryEventConsumer); + } + + /** + * Registers the post creation consumer function that registers the consumer events to the timeLimiters. + * + * @param timeLimiterRegistry The timeLimiter registry. + * @param eventConsumerRegistry The event consumer registry. + * @param timeLimiterConfigurationProperties timeLimiter configuration properties + */ + private static void registerEventConsumer(TimeLimiterRegistry timeLimiterRegistry, + EventConsumerRegistry eventConsumerRegistry, + TimeLimiterConfigurationProperties timeLimiterConfigurationProperties) { + timeLimiterRegistry.getEventPublisher().onEntryAdded(event -> registerEventConsumer(eventConsumerRegistry, event.getAddedEntry(), timeLimiterConfigurationProperties)); + } + + private static void registerEventConsumer(EventConsumerRegistry eventConsumerRegistry, TimeLimiter timeLimiter, + TimeLimiterConfigurationProperties timeLimiterConfigurationProperties) { + int eventConsumerBufferSize = Optional.ofNullable(timeLimiterConfigurationProperties.getInstanceProperties(timeLimiter.getName())) + .map(io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties.InstanceProperties::getEventConsumerBufferSize) + .orElse(100); + timeLimiter.getEventPublisher().onEvent(eventConsumerRegistry.createEventConsumer(timeLimiter.getName(), eventConsumerBufferSize)); + } + +} diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/TimeLimiterConfigurationProperties.java b/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/TimeLimiterConfigurationProperties.java new file mode 100644 index 0000000000..ae937541c0 --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/TimeLimiterConfigurationProperties.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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.timelimiter.configure; + +import org.springframework.core.Ordered; + +public class TimeLimiterConfigurationProperties extends + io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties { + + private int timeLimiterAspectOrder = Ordered.LOWEST_PRECEDENCE - 4; + + public int getTimeLimiterAspectOrder() { + return timeLimiterAspectOrder; + } + + /** + * set timeLimiter aspect order + * + * @param timeLimiterAspectOrder timeLimiter aspect target order + */ + public void setTimeLimiterAspectOrder(int timeLimiterAspectOrder) { + this.timeLimiterAspectOrder = timeLimiterAspectOrder; + } + +} diff --git a/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/package-info.java b/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/package-info.java new file mode 100644 index 0000000000..c4bba1b68a --- /dev/null +++ b/resilience4j-spring/src/main/java/io/github/resilience4j/timelimiter/configure/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2020 Ingyu Hwang + * + * 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. + */ +@NonNullApi +@NonNullFields +package io.github.resilience4j.timelimiter.configure; + +import io.github.resilience4j.core.lang.NonNullApi; +import io.github.resilience4j.core.lang.NonNullFields; \ No newline at end of file diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/TestApplication.java b/resilience4j-spring/src/test/java/io/github/resilience4j/TestApplication.java index a5ca1ec444..1935b426d6 100644 --- a/resilience4j-spring/src/test/java/io/github/resilience4j/TestApplication.java +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/TestApplication.java @@ -25,6 +25,9 @@ import io.github.resilience4j.retry.RetryConfig; import io.github.resilience4j.retry.RetryRegistry; import io.github.resilience4j.retry.configure.RetryConfigurationProperties; +import io.github.resilience4j.timelimiter.TimeLimiterConfig; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.github.resilience4j.timelimiter.configure.TimeLimiterConfigurationProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @@ -85,4 +88,16 @@ public RetryRegistry retryRegistry() { public RetryConfigurationProperties retryConfigurationProperties() { return new RetryConfigurationProperties(); } + + @Bean + public TimeLimiterRegistry timeLimiterRegistry() { + TimeLimiterConfig config = TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(1)).build(); + return TimeLimiterRegistry.of(config); + } + + @Bean + public TimeLimiterConfigurationProperties timeLimiterConfigurationProperties() { + return new TimeLimiterConfigurationProperties(); + } + } diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/TimeLimiterDummyService.java b/resilience4j-spring/src/test/java/io/github/resilience4j/TimeLimiterDummyService.java new file mode 100644 index 0000000000..14866ed79c --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/TimeLimiterDummyService.java @@ -0,0 +1,85 @@ +package io.github.resilience4j; + +import io.github.resilience4j.timelimiter.annotation.TimeLimiter; +import io.reactivex.*; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.concurrent.CompletionStage; + +@Component +public class TimeLimiterDummyService implements TestDummyService { + + @Override + public String sync() { + //no-op + return null; + } + + @Override + public CompletionStage asyncThreadPool() { + //no-op + return null; + } + + @Override + public CompletionStage asyncThreadPoolSuccess() { + // no-op + return null; + } + + @Override + @TimeLimiter(name = BACKEND, fallbackMethod = "completionStageRecovery") + public CompletionStage async() { + return asyncError(); + } + + @Override + @TimeLimiter(name = BACKEND, fallbackMethod = "fluxRecovery") + public Flux flux() { + return fluxError(); + } + + @Override + @TimeLimiter(name = BACKEND, fallbackMethod = "monoRecovery") + public Mono mono(String parameter) { + return monoError(parameter); + } + + @Override + @TimeLimiter(name = BACKEND, fallbackMethod = "observableRecovery") + public Observable observable() { + return observableError(); + } + + @Override + @TimeLimiter(name = BACKEND, fallbackMethod = "singleRecovery") + public Single single() { + return singleError(); + } + + @Override + @TimeLimiter(name = BACKEND, fallbackMethod = "completableRecovery") + public Completable completable() { + return completableError(); + } + + @Override + @TimeLimiter(name = BACKEND, fallbackMethod = "maybeRecovery") + public Maybe maybe() { + return maybeError(); + } + + @Override + @TimeLimiter(name = BACKEND, fallbackMethod = "flowableRecovery") + public Flowable flowable() { + return flowableError(); + } + + @Override + @TimeLimiter(name = BACKEND, fallbackMethod = "#{'recovery'}") + public String spelSync() { + return syncError(); + } +} diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/timelimiter/configure/ReactorTimeLimiterAspectExtTest.java b/resilience4j-spring/src/test/java/io/github/resilience4j/timelimiter/configure/ReactorTimeLimiterAspectExtTest.java new file mode 100644 index 0000000000..1b47aa3f12 --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/timelimiter/configure/ReactorTimeLimiterAspectExtTest.java @@ -0,0 +1,50 @@ +package io.github.resilience4j.timelimiter.configure; + +import io.github.resilience4j.timelimiter.TimeLimiter; +import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ReactorTimeLimiterAspectExtTest { + + @Mock + ProceedingJoinPoint proceedingJoinPoint; + + @InjectMocks + ReactorTimeLimiterAspectExt reactorTimeLimiterAspectExt; + + + @Test + public void testCheckTypes() { + assertThat(reactorTimeLimiterAspectExt.canHandleReturnType(Mono.class)).isTrue(); + assertThat(reactorTimeLimiterAspectExt.canHandleReturnType(Flux.class)).isTrue(); + } + + @Test + public void testReactorTypes() throws Throwable { + TimeLimiter timeLimiter = TimeLimiter.ofDefaults("test"); + + when(proceedingJoinPoint.proceed()).thenReturn(Mono.just("Test")); + assertThat(reactorTimeLimiterAspectExt.handle(proceedingJoinPoint, timeLimiter, "testMethod")).isNotNull(); + + when(proceedingJoinPoint.proceed()).thenReturn(Flux.just("Test")); + assertThat(reactorTimeLimiterAspectExt.handle(proceedingJoinPoint, timeLimiter, "testMethod")).isNotNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowIllegalArgumentExceptionWithNotReactorType() throws Throwable{ + TimeLimiter timeLimiter = TimeLimiter.ofDefaults("test"); + when(proceedingJoinPoint.proceed()).thenReturn("NOT REACTOR TYPE"); + reactorTimeLimiterAspectExt.handle(proceedingJoinPoint, timeLimiter, "testMethod"); + } + +} \ No newline at end of file diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/timelimiter/configure/RxJava2TimeLimiterAspectExtTest.java b/resilience4j-spring/src/test/java/io/github/resilience4j/timelimiter/configure/RxJava2TimeLimiterAspectExtTest.java new file mode 100644 index 0000000000..34a6ba5f06 --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/timelimiter/configure/RxJava2TimeLimiterAspectExtTest.java @@ -0,0 +1,59 @@ +package io.github.resilience4j.timelimiter.configure; + +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.reactivex.*; +import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RxJava2TimeLimiterAspectExtTest { + @Mock + ProceedingJoinPoint proceedingJoinPoint; + + @InjectMocks + RxJava2TimeLimiterAspectExt rxJava2TimeLimiterAspectExt; + + @Test + public void testCheckTypes() { + assertThat(rxJava2TimeLimiterAspectExt.canHandleReturnType(Flowable.class)).isTrue(); + assertThat(rxJava2TimeLimiterAspectExt.canHandleReturnType(Single.class)).isTrue(); + assertThat(rxJava2TimeLimiterAspectExt.canHandleReturnType(Observable.class)).isTrue(); + assertThat(rxJava2TimeLimiterAspectExt.canHandleReturnType(Completable.class)).isTrue(); + assertThat(rxJava2TimeLimiterAspectExt.canHandleReturnType(Maybe.class)).isTrue(); + } + + @Test + public void testRxJava2Types() throws Throwable { + TimeLimiter timeLimiter = TimeLimiter.ofDefaults("test"); + + when(proceedingJoinPoint.proceed()).thenReturn(Single.just("Test")); + assertThat(rxJava2TimeLimiterAspectExt.handle(proceedingJoinPoint, timeLimiter, "testMethod")).isNotNull(); + + when(proceedingJoinPoint.proceed()).thenReturn(Flowable.just("Test")); + assertThat(rxJava2TimeLimiterAspectExt.handle(proceedingJoinPoint, timeLimiter, "testMethod")).isNotNull(); + + when(proceedingJoinPoint.proceed()).thenReturn(Observable.just("Test")); + assertThat(rxJava2TimeLimiterAspectExt.handle(proceedingJoinPoint, timeLimiter, "testMethod")).isNotNull(); + + when(proceedingJoinPoint.proceed()).thenReturn(Completable.complete()); + assertThat(rxJava2TimeLimiterAspectExt.handle(proceedingJoinPoint, timeLimiter, "testMethod")).isNotNull(); + + when(proceedingJoinPoint.proceed()).thenReturn(Maybe.just("Test")); + assertThat(rxJava2TimeLimiterAspectExt.handle(proceedingJoinPoint, timeLimiter, "testMethod")).isNotNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowIllegalArgumentExceptionWithNotRxJava2Type() throws Throwable{ + TimeLimiter timeLimiter = TimeLimiter.ofDefaults("test"); + when(proceedingJoinPoint.proceed()).thenReturn("NOT RXJAVA2 TYPE"); + rxJava2TimeLimiterAspectExt.handle(proceedingJoinPoint, timeLimiter, "testMethod"); + } + +} \ No newline at end of file diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/timelimiter/configure/TimeLimiterConfigurationSpringTest.java b/resilience4j-spring/src/test/java/io/github/resilience4j/timelimiter/configure/TimeLimiterConfigurationSpringTest.java new file mode 100644 index 0000000000..c6fc7f7d65 --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/timelimiter/configure/TimeLimiterConfigurationSpringTest.java @@ -0,0 +1,92 @@ +package io.github.resilience4j.timelimiter.configure; + +import io.github.resilience4j.consumer.DefaultEventConsumerRegistry; +import io.github.resilience4j.consumer.EventConsumerRegistry; +import io.github.resilience4j.fallback.FallbackDecorators; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.github.resilience4j.timelimiter.event.TimeLimiterEvent; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.time.Duration; +import java.util.List; + +import static org.junit.Assert.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = { + TimeLimiterConfigurationSpringTest.ConfigWithOverrides.class +}) +public class TimeLimiterConfigurationSpringTest { + + @Autowired + private ConfigWithOverrides configWithOverrides; + + + @Test + public void testAllCircuitBreakerConfigurationBeansOverridden() { + assertNotNull(configWithOverrides.timeLimiterRegistry); + assertNotNull(configWithOverrides.timeLimiterAspect); + assertNotNull(configWithOverrides.timeLimiterEventEventConsumerRegistry); + assertNotNull(configWithOverrides.timeLimiterConfigurationProperties); + assertEquals(1, configWithOverrides.timeLimiterConfigurationProperties.getConfigs().size()); + } + + + @Configuration + @ComponentScan({"io.github.resilience4j.timelimiter","io.github.resilience4j.fallback"}) + public static class ConfigWithOverrides { + + private TimeLimiterRegistry timeLimiterRegistry; + + private TimeLimiterAspect timeLimiterAspect; + + private EventConsumerRegistry timeLimiterEventEventConsumerRegistry; + + private TimeLimiterConfigurationProperties timeLimiterConfigurationProperties; + + @Bean + public TimeLimiterRegistry timeLimiterRegistry() { + timeLimiterRegistry = TimeLimiterRegistry.ofDefaults(); + return timeLimiterRegistry; + } + + @Bean + public TimeLimiterAspect timeLimiterAspect(TimeLimiterRegistry timeLimiterRegistry, + @Autowired(required = false) List timeLimiterAspectExtList, + FallbackDecorators recoveryDecorators) { + timeLimiterAspect = new TimeLimiterAspect(timeLimiterRegistry, timeLimiterConfigurationProperties(), timeLimiterAspectExtList, recoveryDecorators); + return timeLimiterAspect; + } + + @Bean + public EventConsumerRegistry eventConsumerRegistry() { + timeLimiterEventEventConsumerRegistry = new DefaultEventConsumerRegistry<>(); + return timeLimiterEventEventConsumerRegistry; + } + + @Bean + public TimeLimiterConfigurationProperties timeLimiterConfigurationProperties() { + timeLimiterConfigurationProperties = new TimeLimiterConfigurationPropertiesTest(); + return timeLimiterConfigurationProperties; + } + + private static class TimeLimiterConfigurationPropertiesTest extends TimeLimiterConfigurationProperties { + + TimeLimiterConfigurationPropertiesTest() { + InstanceProperties instanceProperties = new InstanceProperties(); + instanceProperties.setBaseConfig("sharedConfig"); + instanceProperties.setTimeoutDuration(Duration.ofSeconds(3)); + getConfigs().put("sharedBackend", instanceProperties); + } + + } + } + +} diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/timelimiter/configure/TimeLimiterConfigurationTest.java b/resilience4j-spring/src/test/java/io/github/resilience4j/timelimiter/configure/TimeLimiterConfigurationTest.java new file mode 100644 index 0000000000..f3462da09e --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/timelimiter/configure/TimeLimiterConfigurationTest.java @@ -0,0 +1,128 @@ +package io.github.resilience4j.timelimiter.configure; + +import io.github.resilience4j.consumer.DefaultEventConsumerRegistry; +import io.github.resilience4j.core.ConfigurationNotFoundException; +import io.github.resilience4j.core.registry.CompositeRegistryEventConsumer; +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.github.resilience4j.timelimiter.event.TimeLimiterEvent; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.time.Duration; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@RunWith(MockitoJUnitRunner.class) +public class TimeLimiterConfigurationTest { + + @Test + public void testTimeLimiterRegistry() { + + // Given + io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties.InstanceProperties instanceProperties1 = new io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties.InstanceProperties(); + instanceProperties1.setTimeoutDuration(Duration.ofSeconds(3)); + + io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties.InstanceProperties instanceProperties2 = new io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties.InstanceProperties(); + instanceProperties2.setTimeoutDuration(Duration.ofSeconds(2)); + + TimeLimiterConfigurationProperties timeLimiterConfigurationProperties = new TimeLimiterConfigurationProperties(); + timeLimiterConfigurationProperties.getInstances().put("backend1", instanceProperties1); + timeLimiterConfigurationProperties.getInstances().put("backend2", instanceProperties2); + timeLimiterConfigurationProperties.setTimeLimiterAspectOrder(200); + + TimeLimiterConfiguration timeLimiterConfiguration = new TimeLimiterConfiguration(); + DefaultEventConsumerRegistry eventConsumerRegistry = new DefaultEventConsumerRegistry<>(); + + // When + TimeLimiterRegistry timeLimiterRegistry = timeLimiterConfiguration.timeLimiterRegistry(timeLimiterConfigurationProperties, eventConsumerRegistry, new CompositeRegistryEventConsumer<>(emptyList())); + + // Then + assertThat(timeLimiterConfigurationProperties.getTimeLimiterAspectOrder()).isEqualTo(200); + assertThat(timeLimiterRegistry.getAllTimeLimiters().size()).isEqualTo(2); + TimeLimiter timeLimiter1 = timeLimiterRegistry.timeLimiter("backend1"); + assertThat(timeLimiter1).isNotNull(); + assertThat(timeLimiter1.getTimeLimiterConfig().getTimeoutDuration()).isEqualTo(Duration.ofSeconds(3)); + + TimeLimiter timeLimiter2 = timeLimiterRegistry.timeLimiter("backend2"); + assertThat(timeLimiter2).isNotNull(); + assertThat(timeLimiter2.getTimeLimiterConfig().getTimeoutDuration()).isEqualTo(Duration.ofSeconds(2)); + + assertThat(eventConsumerRegistry.getAllEventConsumer()).hasSize(2); + } + + @Test + public void testCreateTimeLimiterRegistryWithSharedConfigs() { + // Given + io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties.InstanceProperties defaultProperties = new io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties.InstanceProperties(); + defaultProperties.setTimeoutDuration(Duration.ofSeconds(3)); + defaultProperties.setCancelRunningFuture(true); + + io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties.InstanceProperties sharedProperties = new io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties.InstanceProperties(); + sharedProperties.setTimeoutDuration(Duration.ofSeconds(2)); + sharedProperties.setCancelRunningFuture(false); + + io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties.InstanceProperties backendWithDefaultConfig = new io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties.InstanceProperties(); + backendWithDefaultConfig.setBaseConfig("default"); + backendWithDefaultConfig.setTimeoutDuration(Duration.ofSeconds(5)); + + io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties.InstanceProperties backendWithSharedConfig = new io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties.InstanceProperties(); + backendWithSharedConfig.setBaseConfig("sharedConfig"); + backendWithSharedConfig.setCancelRunningFuture(true); + + TimeLimiterConfigurationProperties timeLimiterConfigurationProperties = new TimeLimiterConfigurationProperties(); + timeLimiterConfigurationProperties.getConfigs().put("default", defaultProperties); + timeLimiterConfigurationProperties.getConfigs().put("sharedConfig", sharedProperties); + + timeLimiterConfigurationProperties.getInstances().put("backendWithDefaultConfig", backendWithDefaultConfig); + timeLimiterConfigurationProperties.getInstances().put("backendWithSharedConfig", backendWithSharedConfig); + + TimeLimiterConfiguration timeLimiterConfiguration = new TimeLimiterConfiguration(); + DefaultEventConsumerRegistry eventConsumerRegistry = new DefaultEventConsumerRegistry<>(); + + // When + TimeLimiterRegistry timeLimiterRegistry = timeLimiterConfiguration.timeLimiterRegistry(timeLimiterConfigurationProperties, eventConsumerRegistry, new CompositeRegistryEventConsumer<>(emptyList())); + + // Then + assertThat(timeLimiterRegistry.getAllTimeLimiters().size()).isEqualTo(2); + + // Should get default config and overwrite timeout duration + TimeLimiter timeLimiter1 = timeLimiterRegistry.timeLimiter("backendWithDefaultConfig"); + assertThat(timeLimiter1).isNotNull(); + assertThat(timeLimiter1.getTimeLimiterConfig().getTimeoutDuration()).isEqualTo(Duration.ofSeconds(5)); + assertThat(timeLimiter1.getTimeLimiterConfig().shouldCancelRunningFuture()).isEqualTo(true); + + // Should get shared config and overwrite cancelRunningFuture + TimeLimiter timeLimiter2 = timeLimiterRegistry.timeLimiter("backendWithSharedConfig"); + assertThat(timeLimiter2).isNotNull(); + assertThat(timeLimiter2.getTimeLimiterConfig().getTimeoutDuration()).isEqualTo(Duration.ofSeconds(2)); + assertThat(timeLimiter2.getTimeLimiterConfig().shouldCancelRunningFuture()).isEqualTo(true); + + // Unknown backend should get default config of Registry + TimeLimiter timeLimiter3 = timeLimiterRegistry.timeLimiter("unknownBackend"); + assertThat(timeLimiter3).isNotNull(); + assertThat(timeLimiter3.getTimeLimiterConfig().getTimeoutDuration()).isEqualTo(Duration.ofSeconds(3)); + + assertThat(eventConsumerRegistry.getAllEventConsumer()).hasSize(3); + } + + @Test + public void testCreateTimeLimiterRegistryWithUnknownConfig() { + TimeLimiterConfigurationProperties timeLimiterConfigurationProperties = new TimeLimiterConfigurationProperties(); + + io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties.InstanceProperties instanceProperties = new io.github.resilience4j.common.timelimiter.configuration.TimeLimiterConfigurationProperties.InstanceProperties(); + instanceProperties.setBaseConfig("unknownConfig"); + timeLimiterConfigurationProperties.getInstances().put("backend", instanceProperties); + + TimeLimiterConfiguration timeLimiterConfiguration = new TimeLimiterConfiguration(); + DefaultEventConsumerRegistry eventConsumerRegistry = new DefaultEventConsumerRegistry<>(); + + //When + assertThatThrownBy(() -> timeLimiterConfiguration.timeLimiterRegistry(timeLimiterConfigurationProperties, eventConsumerRegistry, new CompositeRegistryEventConsumer<>(emptyList()))) + .isInstanceOf(ConfigurationNotFoundException.class) + .hasMessage("Configuration with name 'unknownConfig' does not exist"); + } +} \ No newline at end of file diff --git a/resilience4j-spring/src/test/java/io/github/resilience4j/timelimiter/configure/TimeLimiterRecoveryTest.java b/resilience4j-spring/src/test/java/io/github/resilience4j/timelimiter/configure/TimeLimiterRecoveryTest.java new file mode 100644 index 0000000000..10ed372b55 --- /dev/null +++ b/resilience4j-spring/src/test/java/io/github/resilience4j/timelimiter/configure/TimeLimiterRecoveryTest.java @@ -0,0 +1,68 @@ +package io.github.resilience4j.timelimiter.configure; + +import io.github.resilience4j.TestApplication; +import io.github.resilience4j.TestDummyService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = TestApplication.class) +public class TimeLimiterRecoveryTest { + @Autowired + @Qualifier("timeLimiterDummyService") + TestDummyService testDummyService; + + @Test + public void testAsyncRecovery() throws Exception { + assertThat(testDummyService.async().toCompletableFuture().get(5, TimeUnit.SECONDS)).isEqualTo("recovered"); + } + + @Test + public void testMonoRecovery() { + assertThat(testDummyService.mono("test").block()).isEqualTo("test"); + } + + @Test + public void testFluxRecovery() { + assertThat(testDummyService.flux().blockFirst()).isEqualTo("recovered"); + } + + @Test + public void testObservableRecovery() { + assertThat(testDummyService.observable().blockingFirst()).isEqualTo("recovered"); + } + + @Test + public void testSingleRecovery() { + assertThat(testDummyService.single().blockingGet()).isEqualTo("recovered"); + } + + @Test + public void testCompletableRecovery() { + assertThat(testDummyService.completable().blockingGet()).isNull(); + } + + @Test + public void testMaybeRecovery() { + assertThat(testDummyService.maybe().blockingGet()).isEqualTo("recovered"); + } + + @Test + public void testFlowableRecovery() { + assertThat(testDummyService.flowable().blockingFirst()).isEqualTo("recovered"); + } + + @Test + public void testSpelRecovery() { + assertThat(testDummyService.spelSync()).isEqualTo("recovered"); + } + +} diff --git a/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/TimeLimiter.java b/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/TimeLimiter.java index eefd02c22d..f9a1ace557 100644 --- a/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/TimeLimiter.java +++ b/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/TimeLimiter.java @@ -30,6 +30,15 @@ static TimeLimiter ofDefaults() { return new TimeLimiterImpl(DEFAULT_NAME, TimeLimiterConfig.ofDefaults()); } + /** + * Creates a TimeLimiter decorator with a default TimeLimiterConfig configuration. + * + * @return The {@link TimeLimiter} + */ + static TimeLimiter ofDefaults(String name) { + return new TimeLimiterImpl(name, TimeLimiterConfig.ofDefaults()); + } + /** * Creates a TimeLimiter decorator with a TimeLimiterConfig configuration. * diff --git a/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/TimeLimiterConfig.java b/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/TimeLimiterConfig.java index e9adc644d9..bc68944bc2 100644 --- a/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/TimeLimiterConfig.java +++ b/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/TimeLimiterConfig.java @@ -23,6 +23,9 @@ public static Builder custom() { return new Builder(); } + public static Builder from(TimeLimiterConfig baseConfig) { + return new Builder(baseConfig); + } /** * Creates a default TimeLimiter configuration. * @@ -54,7 +57,16 @@ public String toString() { public static class Builder { - private TimeLimiterConfig config = new TimeLimiterConfig(); + private Duration timeoutDuration = Duration.ofSeconds(1); + private boolean cancelRunningFuture = true; + + public Builder() { + } + + public Builder(TimeLimiterConfig baseConfig) { + this.timeoutDuration = baseConfig.timeoutDuration; + this.cancelRunningFuture = baseConfig.cancelRunningFuture; + } /** * Builds a TimeLimiterConfig @@ -62,6 +74,9 @@ public static class Builder { * @return the TimeLimiterConfig */ public TimeLimiterConfig build() { + TimeLimiterConfig config = new TimeLimiterConfig(); + config.timeoutDuration = timeoutDuration; + config.cancelRunningFuture = cancelRunningFuture; return config; } @@ -72,7 +87,7 @@ public TimeLimiterConfig build() { * @return the TimeLimiterConfig.Builder */ public Builder timeoutDuration(final Duration timeoutDuration) { - config.timeoutDuration = checkTimeoutDuration(timeoutDuration); + this.timeoutDuration = checkTimeoutDuration(timeoutDuration); return this; } @@ -83,7 +98,7 @@ public Builder timeoutDuration(final Duration timeoutDuration) { * @return the TimeLimiterConfig.Builder */ public Builder cancelRunningFuture(final boolean cancelRunningFuture) { - config.cancelRunningFuture = cancelRunningFuture; + this.cancelRunningFuture = cancelRunningFuture; return this; } diff --git a/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterConfigTest.java b/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterConfigTest.java index 798f88af85..3c5632b735 100644 --- a/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterConfigTest.java +++ b/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterConfigTest.java @@ -50,4 +50,20 @@ public void builderTimeoutIsNull() { public void configToString() { then(TimeLimiterConfig.ofDefaults().toString()).isEqualTo(TIMEOUT_TO_STRING); } + + @Test + public void shouldUseBaseConfigAndOverwriteProperties() { + TimeLimiterConfig baseConfig = TimeLimiterConfig.custom() + .timeoutDuration(Duration.ofSeconds(5)) + .cancelRunningFuture(false) + .build(); + + TimeLimiterConfig extendedConfig = TimeLimiterConfig.from(baseConfig) + .timeoutDuration(Duration.ofSeconds(20)) + .build(); + + then(extendedConfig.getTimeoutDuration()).isEqualTo(Duration.ofSeconds(20)); + then(extendedConfig.shouldCancelRunningFuture()).isEqualTo(false); + } + } diff --git a/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterTest.java b/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterTest.java index 64da742dda..09c8a4852b 100644 --- a/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterTest.java +++ b/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterTest.java @@ -149,4 +149,10 @@ public void unwrapExecutionException() { assertThat(decoratedResult.getCause() instanceof RuntimeException).isTrue(); } + + @Test + public void shouldSetGivenName() { + TimeLimiter timeLimiter = TimeLimiter.ofDefaults("TEST"); + assertThat(timeLimiter.getName()).isEqualTo("TEST"); + } }