diff --git a/resilience4j-metrics/build.gradle b/resilience4j-metrics/build.gradle index e453e3a434..4a60f7df1c 100644 --- a/resilience4j-metrics/build.gradle +++ b/resilience4j-metrics/build.gradle @@ -4,10 +4,12 @@ dependencies { compileOnly project(':resilience4j-circuitbreaker') compileOnly project(':resilience4j-retry') compileOnly project(':resilience4j-ratelimiter') + compileOnly project(':resilience4j-timelimiter') testCompile project(':resilience4j-test') testCompile project(':resilience4j-bulkhead') testCompile project(':resilience4j-circuitbreaker') testCompile project(':resilience4j-ratelimiter') + testCompile project(':resilience4j-timelimiter') testCompile project(':resilience4j-retry') testCompile project(':resilience4j-test') testCompile project(':resilience4j-circuitbreaker') diff --git a/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/TimeLimiterMetrics.java b/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/TimeLimiterMetrics.java new file mode 100644 index 0000000000..be4a87bdd9 --- /dev/null +++ b/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/TimeLimiterMetrics.java @@ -0,0 +1,148 @@ +/* + * + * Copyright 2019 authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ +package io.github.resilience4j.metrics; + +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.vavr.collection.Array; + +import java.util.Map; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Metric; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.MetricSet; + +import static com.codahale.metrics.MetricRegistry.name; +import static io.github.resilience4j.timelimiter.utils.MetricNames.DEFAULT_PREFIX; +import static io.github.resilience4j.timelimiter.utils.MetricNames.FAILED; +import static io.github.resilience4j.timelimiter.utils.MetricNames.SUCCESSFUL; +import static io.github.resilience4j.timelimiter.utils.MetricNames.TIMEOUT; +import static java.util.Objects.requireNonNull; + +/** + * An adapter which exports TimeLimiter's events as Dropwizard Metrics. + */ +public class TimeLimiterMetrics implements MetricSet { + + private static final String PREFIX_NULL = "Prefix must not be null"; + private static final String ITERABLE_NULL = "TimeLimiters iterable must not be null"; + + private final MetricRegistry metricRegistry; + + private TimeLimiterMetrics(Iterable timeLimiters) { + this(DEFAULT_PREFIX, timeLimiters, new MetricRegistry()); + } + + private TimeLimiterMetrics(String prefix, Iterable timeLimiters, MetricRegistry metricRegistry) { + requireNonNull(prefix, PREFIX_NULL); + requireNonNull(timeLimiters, ITERABLE_NULL); + requireNonNull(metricRegistry); + this.metricRegistry = metricRegistry; + timeLimiters.forEach(timeLimiter -> { + String name = timeLimiter.getName(); + Counter successes = metricRegistry.counter(name(prefix, name, SUCCESSFUL)); + Counter failures = metricRegistry.counter(name(prefix, name, FAILED)); + Counter timeouts = metricRegistry.counter(name(prefix, name, TIMEOUT)); + timeLimiter.getEventPublisher().onSuccess(event -> successes.inc()); + timeLimiter.getEventPublisher().onError(event -> failures.inc()); + timeLimiter.getEventPublisher().onTimeout(event -> timeouts.inc()); + } + ); + } + + /** + * Creates a new instance {@link TimeLimiterMetrics} with specified metrics names prefix and + * a {@link TimeLimiterRegistry} as a source. + * + * @param prefix the prefix of metrics names + * @param timeLimiterRegistry the registry of time limiters + * @param metricRegistry the metric registry + */ + public static TimeLimiterMetrics ofTimeLimiterRegistry(String prefix, TimeLimiterRegistry timeLimiterRegistry, MetricRegistry metricRegistry) { + return new TimeLimiterMetrics(prefix, timeLimiterRegistry.getAllTimeLimiters(), metricRegistry); + } + + /** + * Creates a new instance {@link TimeLimiterMetrics} with specified metrics names prefix and + * a {@link TimeLimiterRegistry} as a source. + * + * @param prefix the prefix of metrics names + * @param timeLimiterRegistry the registry of time limiters + */ + public static TimeLimiterMetrics ofTimeLimiterRegistry(String prefix, TimeLimiterRegistry timeLimiterRegistry) { + return new TimeLimiterMetrics(prefix, timeLimiterRegistry.getAllTimeLimiters(), new MetricRegistry()); + } + + /** + * Creates a new instance {@link TimeLimiterMetrics} with + * a {@link TimeLimiterRegistry} as a source. + * + * @param timeLimiterRegistry the registry of time limiters + * @param metricRegistry the metric registry + */ + public static TimeLimiterMetrics ofTimeLimiterRegistry(TimeLimiterRegistry timeLimiterRegistry, MetricRegistry metricRegistry) { + return new TimeLimiterMetrics(DEFAULT_PREFIX, timeLimiterRegistry.getAllTimeLimiters(), metricRegistry); + } + + /** + * Creates a new instance {@link TimeLimiterMetrics} with + * a {@link TimeLimiterRegistry} as a source. + * + * @param timeLimiterRegistry the registry of time limiters + */ + public static TimeLimiterMetrics ofTimeLimiterRegistry(TimeLimiterRegistry timeLimiterRegistry) { + return new TimeLimiterMetrics(timeLimiterRegistry.getAllTimeLimiters()); + } + + /** + * Creates a new instance {@link TimeLimiterMetrics} with + * an {@link Iterable} of time limiters as a source. + * + * @param timeLimiters the time limiters + */ + public static TimeLimiterMetrics ofIterable(Iterable timeLimiters) { + return new TimeLimiterMetrics(timeLimiters); + } + + /** + * Creates a new instance {@link TimeLimiterMetrics} with + * an {@link Iterable} of time limiters as a source. + * + * @param timeLimiters the time limiters + */ + public static TimeLimiterMetrics ofIterable(String prefix, Iterable timeLimiters) { + return new TimeLimiterMetrics(prefix, timeLimiters, new MetricRegistry()); + } + + + /** + * Creates a new instance of {@link TimeLimiterMetrics} with a time limiter as a source. + * + * @param timeLimiter the time limiter + */ + public static TimeLimiterMetrics ofTimeLimiter(TimeLimiter timeLimiter) { + return new TimeLimiterMetrics(Array.of(timeLimiter)); + } + + @Override + public Map getMetrics() { + return metricRegistry.getMetrics(); + } +} diff --git a/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/TimeLimiterMetricsTest.java b/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/TimeLimiterMetricsTest.java new file mode 100644 index 0000000000..a39de01bcb --- /dev/null +++ b/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/TimeLimiterMetricsTest.java @@ -0,0 +1,157 @@ +/* + * + * Copyright 2019 authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ +package io.github.resilience4j.metrics; + +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.TimeLimiterConfig; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.vavr.control.Try; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +import org.junit.Before; +import org.junit.Test; + +import com.codahale.metrics.MetricRegistry; + +import static io.github.resilience4j.metrics.assertion.MetricRegistryAssert.assertThat; +import static org.assertj.core.api.BDDAssertions.then; + +public class TimeLimiterMetricsTest { + + private static final String DEFAULT_PREFIX = "resilience4j.timelimiter.UNDEFINED."; + private static final String SUCCESSFUL = "successful"; + private static final String FAILED = "failed"; + private static final String TIMEOUT = "timeout"; + + private MetricRegistry metricRegistry; + + @Before + public void setUp() { + metricRegistry = new MetricRegistry(); + } + + @Test + public void shouldRegisterMetrics() throws Exception { + TimeLimiterRegistry timeLimiterRegistry = TimeLimiterRegistry.ofDefaults(); + TimeLimiter timeLimiter = timeLimiterRegistry.timeLimiter("testLimit"); + metricRegistry.registerAll(TimeLimiterMetrics.ofTimeLimiterRegistry(timeLimiterRegistry)); + String expectedPrefix = "resilience4j.timelimiter.testLimit."; + Supplier> futureSupplier = () -> + CompletableFuture.completedFuture("Hello world"); + + String result = timeLimiter.decorateFutureSupplier(futureSupplier).call(); + + then(result).isEqualTo("Hello world"); + assertThat(metricRegistry).hasMetricsSize(3); + assertThat(metricRegistry).counter(expectedPrefix + SUCCESSFUL) + .hasValue(1L); + assertThat(metricRegistry).counter(expectedPrefix + FAILED) + .hasValue(0L); + assertThat(metricRegistry).counter(expectedPrefix + TIMEOUT) + .hasValue(0L); + } + + @Test + public void shouldUseCustomPrefix() throws Exception { + TimeLimiterRegistry timeLimiterRegistry = TimeLimiterRegistry.ofDefaults(); + TimeLimiter timeLimiter = timeLimiterRegistry.timeLimiter("testLimit"); + metricRegistry.registerAll(TimeLimiterMetrics.ofIterable("testPre", timeLimiterRegistry.getAllTimeLimiters())); + String expectedPrefix = "testPre.testLimit."; + Supplier> futureSupplier = () -> + CompletableFuture.completedFuture("Hello world"); + + String result = timeLimiter.decorateFutureSupplier(futureSupplier).call(); + + then(result).isEqualTo("Hello world"); + assertThat(metricRegistry).hasMetricsSize(3); + assertThat(metricRegistry).counter(expectedPrefix + SUCCESSFUL) + .hasValue(1L); + assertThat(metricRegistry).counter(expectedPrefix + FAILED) + .hasValue(0L); + assertThat(metricRegistry).counter(expectedPrefix + TIMEOUT) + .hasValue(0L); + } + + @Test + public void shouldRecordSuccesses() { + TimeLimiter timeLimiter = TimeLimiter.of(TimeLimiterConfig.ofDefaults()); + metricRegistry.registerAll(TimeLimiterMetrics.ofTimeLimiter(timeLimiter)); + Supplier> futureSupplier = () -> + CompletableFuture.completedFuture("Hello world"); + + Try.ofCallable(timeLimiter.decorateFutureSupplier(futureSupplier)); + Try.ofCallable(timeLimiter.decorateFutureSupplier(futureSupplier)); + + assertThat(metricRegistry).hasMetricsSize(3); + assertThat(metricRegistry).counter(DEFAULT_PREFIX + SUCCESSFUL) + .hasValue(2L); + assertThat(metricRegistry).counter(DEFAULT_PREFIX + FAILED) + .hasValue(0L); + assertThat(metricRegistry).counter(DEFAULT_PREFIX + TIMEOUT) + .hasValue(0L); + } + + @Test + public void shouldRecordErrors() { + TimeLimiter timeLimiter = TimeLimiter.of(TimeLimiterConfig.ofDefaults()); + metricRegistry.registerAll(TimeLimiterMetrics.ofTimeLimiter(timeLimiter)); + Supplier> futureSupplier = () -> + CompletableFuture.supplyAsync(this::fail); + + Try.ofCallable(timeLimiter.decorateFutureSupplier(futureSupplier)); + Try.ofCallable(timeLimiter.decorateFutureSupplier(futureSupplier)); + + assertThat(metricRegistry).hasMetricsSize(3); + assertThat(metricRegistry).counter(DEFAULT_PREFIX + SUCCESSFUL) + .hasValue(0L); + assertThat(metricRegistry).counter(DEFAULT_PREFIX + FAILED) + .hasValue(2L); + assertThat(metricRegistry).counter(DEFAULT_PREFIX + TIMEOUT) + .hasValue(0L); + } + + @Test + public void shouldRecordTimeouts() { + TimeLimiter timeLimiter = TimeLimiter.of(TimeLimiterConfig.custom() + .timeoutDuration(Duration.ZERO) + .build()); + metricRegistry.registerAll(TimeLimiterMetrics.ofTimeLimiter(timeLimiter)); + Supplier> futureSupplier = () -> + CompletableFuture.supplyAsync(this::fail); + + Try.ofCallable(timeLimiter.decorateFutureSupplier(futureSupplier)); + Try.ofCallable(timeLimiter.decorateFutureSupplier(futureSupplier)); + + assertThat(metricRegistry).hasMetricsSize(3); + assertThat(metricRegistry).counter(DEFAULT_PREFIX + SUCCESSFUL) + .hasValue(0L); + assertThat(metricRegistry).counter(DEFAULT_PREFIX + FAILED) + .hasValue(0L); + assertThat(metricRegistry).counter(DEFAULT_PREFIX + TIMEOUT) + .hasValue(2L); + } + + private String fail() { + throw new RuntimeException(); + } + +} diff --git a/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/assertion/CounterAssert.java b/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/assertion/CounterAssert.java new file mode 100644 index 0000000000..61199a3daf --- /dev/null +++ b/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/assertion/CounterAssert.java @@ -0,0 +1,24 @@ +package io.github.resilience4j.metrics.assertion; + +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.Assertions; + +import com.codahale.metrics.Counter; + +public class CounterAssert extends AbstractAssert { + + public CounterAssert(Counter actual) { + super(actual, CounterAssert.class); + } + + public static CounterAssert assertThat(Counter actual) { + return new CounterAssert(actual); + } + + public CounterAssert hasValue(T expected) { + isNotNull(); + Assertions.assertThat(actual.getCount()) + .isEqualTo(expected); + return this; + } +} \ No newline at end of file diff --git a/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/assertion/MetricRegistryAssert.java b/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/assertion/MetricRegistryAssert.java new file mode 100644 index 0000000000..52e23eb13d --- /dev/null +++ b/resilience4j-metrics/src/test/java/io/github/resilience4j/metrics/assertion/MetricRegistryAssert.java @@ -0,0 +1,30 @@ +package io.github.resilience4j.metrics.assertion; + +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.Assertions; + +import com.codahale.metrics.MetricRegistry; + +public class MetricRegistryAssert extends AbstractAssert { + + public MetricRegistryAssert(MetricRegistry actual) { + super(actual, MetricRegistryAssert.class); + } + + public static MetricRegistryAssert assertThat(MetricRegistry actual) { + return new MetricRegistryAssert(actual); + } + + public MetricRegistryAssert hasMetricsSize(int size) { + isNotNull(); + Assertions.assertThat(actual.getMetrics()) + .hasSize(size); + return this; + } + + public CounterAssert counter(String name) { + isNotNull(); + Assertions.assertThat(actual.getCounters()).containsKey(name); + return CounterAssert.assertThat(actual.getCounters().get(name)); + } +} \ No newline at end of file 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 f73b34df23..1d73bbc622 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 @@ -75,6 +75,8 @@ static > Callable decorateFutureSupplier(TimeLimiter t return timeLimiter.decorateFutureSupplier(futureSupplier); } + String getName(); + /** * Get the TimeLimiterConfig of this TimeLimiter decorator. * @@ -105,6 +107,11 @@ default > T executeFutureSupplier(Supplier futureSuppl */ > Callable decorateFutureSupplier(Supplier futureSupplier); + /** + * Returns an EventPublisher which can be used to register event consumers. + * + * @return an EventPublisher + */ EventPublisher getEventPublisher(); /** diff --git a/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/TimeLimiterRegistry.java b/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/TimeLimiterRegistry.java new file mode 100644 index 0000000000..ddb14fc2ca --- /dev/null +++ b/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/TimeLimiterRegistry.java @@ -0,0 +1,103 @@ +/* + * + * Copyright 2019 authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ +package io.github.resilience4j.timelimiter; + +import io.github.resilience4j.core.Registry; +import io.github.resilience4j.timelimiter.internal.InMemoryTimeLimiterRegistry; +import io.vavr.collection.Seq; + +import java.util.Map; +import java.util.function.Supplier; + +/** + * Manages all TimeLimiter instances. + */ +public interface TimeLimiterRegistry extends Registry { + + /** + * Returns all managed {@link TimeLimiter} instances. + * + * @return all managed {@link TimeLimiter} instances. + */ + Seq getAllTimeLimiters(); + + /** + * Returns a managed {@link TimeLimiter} or creates a new one with the default TimeLimiter configuration. + * + * @param name the name of the TimeLimiter + * @return The {@link TimeLimiter} + */ + TimeLimiter timeLimiter(String name); + + /** + * Returns a managed {@link TimeLimiter} or creates a new one with a custom TimeLimiter configuration. + * + * @param name the name of the TimeLimiter + * @param timeLimiterConfig a custom TimeLimiter configuration + * @return The {@link TimeLimiter} + */ + TimeLimiter timeLimiter(String name, TimeLimiterConfig timeLimiterConfig); + + /** + * Returns a managed {@link TimeLimiterConfig} or creates a new one with a custom TimeLimiterConfig configuration. + * + * @param name the name of the TimeLimiterConfig + * @param timeLimiterConfigSupplier a supplier of a custom TimeLimiterConfig configuration + * @return The {@link TimeLimiterConfig} + */ + TimeLimiter timeLimiter(String name, Supplier timeLimiterConfigSupplier); + + /** + * Returns a managed {@link TimeLimiter} or creates a new one with a custom TimeLimiter configuration. + * + * @param name the name of the TimeLimiter + * @param configName a custom TimeLimiter configuration name + * @return The {@link TimeLimiter} + */ + TimeLimiter timeLimiter(String name, String configName); + + /** + * Creates a TimeLimiterRegistry with a custom TimeLimiter configuration. + * + * @param defaultTimeLimiterConfig a custom TimeLimiter configuration + * @return a TimeLimiterRegistry instance backed by a custom TimeLimiter configuration + */ + static TimeLimiterRegistry of(TimeLimiterConfig defaultTimeLimiterConfig) { + return new InMemoryTimeLimiterRegistry(defaultTimeLimiterConfig); + } + + /** + * Returns a managed {@link TimeLimiterConfig} or creates a new one with a default TimeLimiter configuration. + * + * @return The {@link TimeLimiterConfig} + */ + static TimeLimiterRegistry ofDefaults() { + return new InMemoryTimeLimiterRegistry(TimeLimiterConfig.ofDefaults()); + } + + /** + * Creates a ThreadPoolBulkheadRegistry with a Map of shared TimeLimiter configurations. + * + * @param configs a Map of shared TimeLimiter configurations + * @return a ThreadPoolBulkheadRegistry with a Map of shared TimeLimiter configurations. + */ + static TimeLimiterRegistry of(Map configs) { + return new InMemoryTimeLimiterRegistry(configs); + } +} diff --git a/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/internal/InMemoryTimeLimiterRegistry.java b/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/internal/InMemoryTimeLimiterRegistry.java new file mode 100644 index 0000000000..244f85dce3 --- /dev/null +++ b/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/internal/InMemoryTimeLimiterRegistry.java @@ -0,0 +1,105 @@ +/* + * + * Copyright 2019 authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ +package io.github.resilience4j.timelimiter.internal; + +import io.github.resilience4j.core.ConfigurationNotFoundException; +import io.github.resilience4j.core.registry.AbstractRegistry; +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.TimeLimiterConfig; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; +import io.vavr.collection.Array; +import io.vavr.collection.Seq; + +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Backend TimeLimiter manager. + * Constructs backend TimeLimiters according to configuration values. + */ +public class InMemoryTimeLimiterRegistry extends AbstractRegistry implements TimeLimiterRegistry { + + /** + * The constructor with default default. + */ + public InMemoryTimeLimiterRegistry() { + this(TimeLimiterConfig.ofDefaults()); + } + + public InMemoryTimeLimiterRegistry(Map configs) { + this(configs.getOrDefault(DEFAULT_CONFIG, TimeLimiterConfig.ofDefaults())); + this.configurations.putAll(configs); + } + + /** + * The constructor with custom default config. + * + * @param defaultConfig The default config. + */ + public InMemoryTimeLimiterRegistry(TimeLimiterConfig defaultConfig) { + super(defaultConfig); + } + + /** + * {@inheritDoc} + */ + @Override + public Seq getAllTimeLimiters() { + return Array.ofAll(entryMap.values()); + } + + /** + * {@inheritDoc} + */ + @Override + public TimeLimiter timeLimiter(final String name) { + return timeLimiter(name, getDefaultConfig()); + } + + /** + * {@inheritDoc} + */ + @Override + public TimeLimiter timeLimiter(final String name, final TimeLimiterConfig config) { + return computeIfAbsent(name, () -> new TimeLimiterImpl(name, Objects.requireNonNull(config, CONFIG_MUST_NOT_BE_NULL))); + } + + /** + * {@inheritDoc} + */ + @Override + public TimeLimiter timeLimiter(final String name, final Supplier timeLimiterConfigSupplier) { + return computeIfAbsent(name, () -> { + TimeLimiterConfig config = Objects.requireNonNull(timeLimiterConfigSupplier, SUPPLIER_MUST_NOT_BE_NULL).get(); + return new TimeLimiterImpl(name, Objects.requireNonNull(config, CONFIG_MUST_NOT_BE_NULL)); + }); + } + + /** + * {@inheritDoc} + */ + @Override + public TimeLimiter timeLimiter(String name, String configName) { + return computeIfAbsent(name, () -> { + TimeLimiterConfig config = getConfiguration(configName).orElseThrow(() -> new ConfigurationNotFoundException(configName)); + return TimeLimiter.of(name, config); + }); + } +} diff --git a/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/internal/TimeLimiterImpl.java b/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/internal/TimeLimiterImpl.java index d989ae20d3..d919b8e6ef 100644 --- a/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/internal/TimeLimiterImpl.java +++ b/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/internal/TimeLimiterImpl.java @@ -6,8 +6,6 @@ import io.github.resilience4j.timelimiter.event.TimeLimiterOnErrorEvent; import io.github.resilience4j.timelimiter.event.TimeLimiterOnSuccessEvent; import io.github.resilience4j.timelimiter.event.TimeLimiterOnTimeoutEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -16,6 +14,9 @@ import java.util.concurrent.TimeoutException; import java.util.function.Supplier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class TimeLimiterImpl implements TimeLimiter { private static final Logger LOG = LoggerFactory.getLogger(TimeLimiterImpl.class); @@ -59,6 +60,11 @@ public > Callable decorateFutureSupplier(Supplier f }; } + @Override + public String getName() { + return name; + } + @Override public TimeLimiterConfig getTimeLimiterConfig() { return timeLimiterConfig; @@ -79,23 +85,33 @@ public void onSuccess() { @Override public void onError(Throwable throwable) { + if (throwable instanceof TimeoutException) { + onTimeout(); + } else { + onFailure(throwable); + } + } + + private void onTimeout() { if (!eventProcessor.hasConsumers()) { return; } - if (throwable instanceof TimeoutException) { - publishEvent(new TimeLimiterOnTimeoutEvent(name)); - } else { - publishEvent(new TimeLimiterOnErrorEvent(name, throwable)); + publishEvent(new TimeLimiterOnTimeoutEvent(name)); + } + + private void onFailure(Throwable throwable) { + if (!eventProcessor.hasConsumers()) { + return; } + publishEvent(new TimeLimiterOnErrorEvent(name, throwable)); } private void publishEvent(TimeLimiterEvent event) { - try{ + try { eventProcessor.consumeEvent(event); LOG.debug("Event {} published: {}", event.getEventType(), event); - }catch (Throwable t){ + } catch (Throwable t) { LOG.warn("Failed to handle event {}", event.getEventType(), t); } } - -} +} \ No newline at end of file diff --git a/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/utils/MetricNames.java b/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/utils/MetricNames.java new file mode 100644 index 0000000000..34baf95b7d --- /dev/null +++ b/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/utils/MetricNames.java @@ -0,0 +1,8 @@ +package io.github.resilience4j.timelimiter.utils; + +public class MetricNames { + public static final String DEFAULT_PREFIX = "resilience4j.timelimiter"; + public static final String SUCCESSFUL = "successful"; + public static final String FAILED = "failed"; + public static final String TIMEOUT = "timeout"; +} diff --git a/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/utils/package-info.java b/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/utils/package-info.java new file mode 100644 index 0000000000..e099da7e0f --- /dev/null +++ b/resilience4j-timelimiter/src/main/java/io/github/resilience4j/timelimiter/utils/package-info.java @@ -0,0 +1,24 @@ +/* + * + * Copyright 2019 authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ +@NonNullApi +@NonNullFields +package io.github.resilience4j.timelimiter.utils; + +import io.github.resilience4j.core.lang.NonNullApi; +import io.github.resilience4j.core.lang.NonNullFields; \ No newline at end of file diff --git a/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterEventPublisherTest.java b/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterEventPublisherTest.java index 44b2f63dc0..23525674b2 100644 --- a/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterEventPublisherTest.java +++ b/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterEventPublisherTest.java @@ -18,8 +18,6 @@ */ package io.github.resilience4j.timelimiter; -import io.github.resilience4j.timelimiter.event.TimeLimiterEvent; -import io.github.resilience4j.timelimiter.event.TimeLimiterOnErrorEvent; import io.vavr.control.Try; import java.time.Duration; @@ -32,7 +30,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; public class TimeLimiterEventPublisherTest { @@ -62,7 +59,7 @@ public void shouldConsumeOnSuccessEvent() throws Exception { String result = timeLimiter.decorateFutureSupplier(futureSupplier).call(); assertThat(result).isEqualTo("Hello world"); - then(logger).should(times(1)).info("SUCCESS"); + then(logger).should().info("SUCCESS"); } @Test @@ -75,7 +72,7 @@ public void shouldConsumeOnTimeoutEvent() { Try.ofCallable(timeLimiter.decorateFutureSupplier(futureSupplier)); - then(logger).should(times(1)).info("TIMEOUT"); + then(logger).should().info("TIMEOUT"); } @Test @@ -88,7 +85,7 @@ public void shouldConsumeOnErrorEvent() { Try.ofCallable(timeLimiter.decorateFutureSupplier(futureSupplier)); - then(logger).should(times(1)).info("ERROR java.lang.RuntimeException"); + then(logger).should().info("ERROR java.lang.RuntimeException"); } private String fail() { diff --git a/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterRegistryTest.java b/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterRegistryTest.java new file mode 100644 index 0000000000..c7d4d652fb --- /dev/null +++ b/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/TimeLimiterRegistryTest.java @@ -0,0 +1,55 @@ +package io.github.resilience4j.timelimiter; + +import io.github.resilience4j.core.ConfigurationNotFoundException; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + + +public class TimeLimiterRegistryTest { + + @Test + public void testCreateWithConfigurationMap() { + Map configs = new HashMap<>(); + configs.put("default", TimeLimiterConfig.ofDefaults()); + configs.put("custom", TimeLimiterConfig.ofDefaults()); + + TimeLimiterRegistry timeLimiterRegistry = TimeLimiterRegistry.of(configs); + + assertThat(timeLimiterRegistry.getDefaultConfig()).isNotNull(); + assertThat(timeLimiterRegistry.getConfiguration("custom")).isNotNull(); + } + + @Test + public void testCreateWithConfigurationMapWithoutDefaultConfig() { + Map configs = new HashMap<>(); + configs.put("custom", TimeLimiterConfig.ofDefaults()); + + TimeLimiterRegistry timeLimiterRegistry = TimeLimiterRegistry.of(configs); + + assertThat(timeLimiterRegistry.getDefaultConfig()).isNotNull(); + assertThat(timeLimiterRegistry.getConfiguration("custom")).isNotNull(); + } + + @Test + public void testAddConfiguration() { + TimeLimiterRegistry timeLimiterRegistry = TimeLimiterRegistry.ofDefaults(); + timeLimiterRegistry.addConfiguration("custom", TimeLimiterConfig.custom().build()); + + assertThat(timeLimiterRegistry.getDefaultConfig()).isNotNull(); + assertThat(timeLimiterRegistry.getConfiguration("custom")).isNotNull(); + } + + @Test + public void testWithNotExistingConfig() { + TimeLimiterRegistry timeLimiterRegistry = TimeLimiterRegistry.ofDefaults(); + + assertThatThrownBy(() -> timeLimiterRegistry.timeLimiter("test", "doesNotExist")) + .isInstanceOf(ConfigurationNotFoundException.class); + } +} diff --git a/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/internal/InMemoryTimeLimiterRegistryTest.java b/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/internal/InMemoryTimeLimiterRegistryTest.java new file mode 100644 index 0000000000..de79568399 --- /dev/null +++ b/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/internal/InMemoryTimeLimiterRegistryTest.java @@ -0,0 +1,144 @@ +/* + * + * Copyright 2019 authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ +package io.github.resilience4j.timelimiter.internal; + +import io.github.resilience4j.timelimiter.TimeLimiter; +import io.github.resilience4j.timelimiter.TimeLimiterConfig; +import io.github.resilience4j.timelimiter.TimeLimiterRegistry; + +import java.util.function.Supplier; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class InMemoryTimeLimiterRegistryTest { + + private static final String CONFIG_MUST_NOT_BE_NULL = "Config must not be null"; + private static final String NAME_MUST_NOT_BE_NULL = "Name must not be null"; + @Rule + public ExpectedException exception = ExpectedException.none(); + private TimeLimiterConfig config; + + @Before + public void init() { + config = TimeLimiterConfig.ofDefaults(); + } + + @Test + public void timeLimiterPositive() { + TimeLimiterRegistry registry = TimeLimiterRegistry.of(config); + + TimeLimiter firstTimeLimiter = registry.timeLimiter("test"); + TimeLimiter anotherLimit = registry.timeLimiter("test1"); + TimeLimiter sameAsFirst = registry.timeLimiter("test"); + + then(firstTimeLimiter).isEqualTo(sameAsFirst); + then(firstTimeLimiter).isNotEqualTo(anotherLimit); + } + + @Test + @SuppressWarnings("unchecked") + public void timeLimiterPositiveWithSupplier() { + TimeLimiterRegistry registry = new InMemoryTimeLimiterRegistry(config); + Supplier timeLimiterConfigSupplier = mock(Supplier.class); + given(timeLimiterConfigSupplier.get()).willReturn(config); + + TimeLimiter firstTimeLimiter = registry.timeLimiter("test", timeLimiterConfigSupplier); + verify(timeLimiterConfigSupplier, times(1)).get(); + TimeLimiter sameAsFirst = registry.timeLimiter("test", timeLimiterConfigSupplier); + verify(timeLimiterConfigSupplier, times(1)).get(); + TimeLimiter anotherLimit = registry.timeLimiter("test1", timeLimiterConfigSupplier); + verify(timeLimiterConfigSupplier, times(2)).get(); + + then(firstTimeLimiter).isEqualTo(sameAsFirst); + then(firstTimeLimiter).isNotEqualTo(anotherLimit); + } + + @Test + public void timeLimiterConfigIsNull() { + exception.expect(NullPointerException.class); + exception.expectMessage(CONFIG_MUST_NOT_BE_NULL); + + new InMemoryTimeLimiterRegistry((TimeLimiterConfig) null); + } + + @Test + public void timeLimiterNewWithNullName() { + exception.expect(NullPointerException.class); + exception.expectMessage(NAME_MUST_NOT_BE_NULL); + TimeLimiterRegistry registry = new InMemoryTimeLimiterRegistry(config); + + registry.timeLimiter(null); + } + + @Test + public void timeLimiterNewWithNullNonDefaultConfig() { + exception.expect(NullPointerException.class); + exception.expectMessage(CONFIG_MUST_NOT_BE_NULL); + TimeLimiterRegistry registry = new InMemoryTimeLimiterRegistry(config); + + registry.timeLimiter("name", (TimeLimiterConfig) null); + } + + @Test + public void timeLimiterNewWithNullNameAndNonDefaultConfig() { + exception.expect(NullPointerException.class); + exception.expectMessage(NAME_MUST_NOT_BE_NULL); + TimeLimiterRegistry registry = new InMemoryTimeLimiterRegistry(config); + + registry.timeLimiter(null, config); + } + + @Test + public void timeLimiterNewWithNullNameAndConfigSupplier() { + exception.expect(NullPointerException.class); + exception.expectMessage(NAME_MUST_NOT_BE_NULL); + TimeLimiterRegistry registry = new InMemoryTimeLimiterRegistry(config); + + registry.timeLimiter(null, () -> config); + } + + @Test + public void timeLimiterNewWithNullConfigSupplier() { + exception.expect(NullPointerException.class); + exception.expectMessage("Supplier must not be null"); + TimeLimiterRegistry registry = new InMemoryTimeLimiterRegistry(config); + + registry.timeLimiter("name", (Supplier) null); + } + + @Test + public void timeLimiterGetAllTimeLimiters() { + TimeLimiterRegistry registry = new InMemoryTimeLimiterRegistry(config); + + registry.timeLimiter("foo"); + + then(registry.getAllTimeLimiters().size()).isEqualTo(1); + then(registry.getAllTimeLimiters().get(0).getName()).isEqualTo("foo"); + } + +} \ No newline at end of file diff --git a/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/internal/TimeLimiterImplTest.java b/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/internal/TimeLimiterImplTest.java index 0d27a15dc1..fc6f7afcfe 100644 --- a/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/internal/TimeLimiterImplTest.java +++ b/resilience4j-timelimiter/src/test/java/io/github/resilience4j/timelimiter/internal/TimeLimiterImplTest.java @@ -1,6 +1,10 @@ package io.github.resilience4j.timelimiter.internal; +import io.github.resilience4j.timelimiter.TimeLimiter; import io.github.resilience4j.timelimiter.TimeLimiterConfig; + +import java.time.Duration; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -8,16 +12,15 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; -import java.time.Duration; - import static org.assertj.core.api.BDDAssertions.then; @RunWith(PowerMockRunner.class) @PrepareForTest(TimeLimiterImpl.class) public class TimeLimiterImplTest { + private static final String NAME = "name"; private TimeLimiterConfig timeLimiterConfig; - private TimeLimiterImpl timeout; + private TimeLimiter timeLimiter; @Before public void init() { @@ -25,11 +28,16 @@ public void init() { .timeoutDuration(Duration.ZERO) .build(); TimeLimiterImpl testTimeout = new TimeLimiterImpl("name", timeLimiterConfig); - timeout = PowerMockito.spy(testTimeout); + timeLimiter = PowerMockito.spy(testTimeout); } @Test public void configPropagation() { - then(timeout.getTimeLimiterConfig()).isEqualTo(timeLimiterConfig); + then(timeLimiter.getTimeLimiterConfig()).isEqualTo(timeLimiterConfig); + } + + @Test + public void namePropagation() { + then(timeLimiter.getName()).isEqualTo(NAME); } }