From b501d5d0857fb629ee4c4989ad2993b3c36ba546 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Thu, 11 Jun 2020 14:05:43 +0200 Subject: [PATCH] add TCK tests for configuring @CircuitBreaker attributes Signed-off-by: Ladislav Thon --- .../tck/config/CircuitBreakerConfigBean.java | 58 +++++- .../tck/config/CircuitBreakerConfigTest.java | 182 ++++++++++++++++-- .../fault/tolerance/tck/util/Exceptions.java | 16 +- 3 files changed, 236 insertions(+), 20 deletions(-) diff --git a/tck/src/main/java/org/eclipse/microprofile/fault/tolerance/tck/config/CircuitBreakerConfigBean.java b/tck/src/main/java/org/eclipse/microprofile/fault/tolerance/tck/config/CircuitBreakerConfigBean.java index cd27cf33..40edc47f 100644 --- a/tck/src/main/java/org/eclipse/microprofile/fault/tolerance/tck/config/CircuitBreakerConfigBean.java +++ b/tck/src/main/java/org/eclipse/microprofile/fault/tolerance/tck/config/CircuitBreakerConfigBean.java @@ -23,16 +23,72 @@ import org.eclipse.microprofile.faulttolerance.CircuitBreaker; import javax.enterprise.context.Dependent; +import java.time.temporal.ChronoUnit; +/** + * Suite of methods for testing the various parameters of CircuitBreaker + */ @Dependent public class CircuitBreakerConfigBean { /** - * Method throws TestConfigExceptionA which will result in CircuitBreakerOpenException on the third call, unless skipOn is configured + * Method throws TestConfigExceptionA which will NOT result in CircuitBreakerOpenException on the third call, + * unless failOn is configured to TestConfigExceptionA. + */ + @CircuitBreaker(requestVolumeThreshold = 2, failOn = TestConfigExceptionB.class) + public void failOnMethod() { + throw new TestConfigExceptionA(); + } + + /** + * Method throws TestConfigExceptionA which will result in CircuitBreakerOpenException on the third call, + * unless skipOn is configured to TestConfigExceptionA. */ @CircuitBreaker(requestVolumeThreshold = 2) public void skipOnMethod() { throw new TestConfigExceptionA(); } + /** + * This method's circuit breaker moves from open to half-open after 10 micros, + * unless delay and delayUnit are configured differently. + */ + @CircuitBreaker(requestVolumeThreshold = 2, delay = 20, delayUnit = ChronoUnit.MICROS) + public void delayMethod(boolean fail) { + if (fail) { + throw new TestConfigExceptionA(); + } + } + + /** + * Method throws TestConfigExceptionA which will result in CircuitBreakerOpenException on the third call, + * unless requestVolumeThreshold is configured to a greater number. + */ + @CircuitBreaker(requestVolumeThreshold = 2) + public void requestVolumeThresholdMethod() { + throw new TestConfigExceptionA(); + } + + /** + * This method's circuit breaker moves from closed to open after 10 consecutive failures, + * unless failureRatio is configured differently. + */ + @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 1.0) + public void failureRatioMethod(boolean fail) { + if (fail) { + throw new TestConfigExceptionA(); + } + } + + /** + * This method's circuit breaker moves from half-open to closed after 4 consecutive successes, + * unless successThreshold is configured differently. + */ + @CircuitBreaker(requestVolumeThreshold = 10, successThreshold = 4, delay = 1000) + public void successThresholdMethod(boolean fail) { + if (fail) { + throw new TestConfigExceptionA(); + } + } + } diff --git a/tck/src/main/java/org/eclipse/microprofile/fault/tolerance/tck/config/CircuitBreakerConfigTest.java b/tck/src/main/java/org/eclipse/microprofile/fault/tolerance/tck/config/CircuitBreakerConfigTest.java index abcce1f9..6beda9e9 100644 --- a/tck/src/main/java/org/eclipse/microprofile/fault/tolerance/tck/config/CircuitBreakerConfigTest.java +++ b/tck/src/main/java/org/eclipse/microprofile/fault/tolerance/tck/config/CircuitBreakerConfigTest.java @@ -20,10 +20,8 @@ package org.eclipse.microprofile.fault.tolerance.tck.config; -import javax.inject.Inject; - -import org.eclipse.microprofile.fault.tolerance.tck.util.Exceptions; import org.eclipse.microprofile.fault.tolerance.tck.util.Packages; +import org.eclipse.microprofile.fault.tolerance.tck.util.TCKConfig; import org.eclipse.microprofile.faulttolerance.CircuitBreaker; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.testng.Arquillian; @@ -33,6 +31,18 @@ import org.jboss.shrinkwrap.api.spec.WebArchive; import org.testng.annotations.Test; +import javax.inject.Inject; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.eclipse.microprofile.fault.tolerance.tck.util.Exceptions.expect; +import static org.eclipse.microprofile.fault.tolerance.tck.util.Exceptions.expectCbOpen; +import static org.eclipse.microprofile.fault.tolerance.tck.util.Exceptions.expectNoException; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.lessThan; + /** * Test configuration of parameters of {@link CircuitBreaker} */ @@ -40,33 +50,169 @@ public class CircuitBreakerConfigTest extends Arquillian { @Deployment public static WebArchive create() { - ConfigAnnotationAsset config = new ConfigAnnotationAsset(); - config.set(CircuitBreakerConfigBean.class, "skipOnMethod", CircuitBreaker.class, "skipOn", TestConfigExceptionA.class.getCanonicalName()); - + ConfigAnnotationAsset config = new ConfigAnnotationAsset() + .set(CircuitBreakerConfigBean.class, "skipOnMethod", CircuitBreaker.class, + "skipOn", TestConfigExceptionA.class.getName()) + .set(CircuitBreakerConfigBean.class, "failOnMethod", CircuitBreaker.class, + "failOn", TestConfigExceptionA.class.getName()) + .set(CircuitBreakerConfigBean.class, "delayMethod", CircuitBreaker.class, + "delay", TCKConfig.getConfig().getTimeoutInStr(1000)) + .set(CircuitBreakerConfigBean.class, "delayMethod", CircuitBreaker.class, + "delayUnit", "MILLIS") + .set(CircuitBreakerConfigBean.class, "requestVolumeThresholdMethod", CircuitBreaker.class, + "requestVolumeThreshold", "4") + .set(CircuitBreakerConfigBean.class, "failureRatioMethod", CircuitBreaker.class, + "failureRatio","0.8") + .set(CircuitBreakerConfigBean.class, "successThresholdMethod", CircuitBreaker.class, + "successThreshold", "2") + // only changing value here to scale the original, not for the purpose of this test + .set(CircuitBreakerConfigBean.class, "successThresholdMethod", CircuitBreaker.class, + "delay", TCKConfig.getConfig().getTimeoutInStr(1000)); + JavaArchive jar = ShrinkWrap .create(JavaArchive.class, "ftCircuitBreakerConfig.jar") - .addPackage(CircuitBreakerConfigTest.class.getPackage()) + .addClasses(CircuitBreakerConfigBean.class, TestConfigExceptionA.class, TestConfigExceptionB.class) .addPackage(Packages.UTILS) .addAsManifestResource(config, "microprofile-config.properties") .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"); - - WebArchive war = ShrinkWrap + + return ShrinkWrap .create(WebArchive.class, "ftCircuitBreakerConfig.war") .addAsLibraries(jar); - - return war; } - + @Inject private CircuitBreakerConfigBean bean; - + + @Test + public void testConfigureFailOn() { + // In annotation: requestVolumeThreshold = 2 + // skipOn = {} + // failOn = {TestConfigExceptionB.class} + // In config: failOn = {TestConfigExceptionA.class} + + expect(TestConfigExceptionA.class, () -> bean.failOnMethod()); + expect(TestConfigExceptionA.class, () -> bean.failOnMethod()); + + // If failOn is not configured to include TestConfigExceptionA, this would throw a TestConfigExceptionA + expectCbOpen(() -> bean.failOnMethod()); + } + @Test public void testConfigureSkipOn() { - Exceptions.expect(TestConfigExceptionA.class, () -> bean.skipOnMethod()); - Exceptions.expect(TestConfigExceptionA.class, () -> bean.skipOnMethod()); - + // In annotation: requestVolumeThreshold = 2 + // failOn = {Throwable.class} + // skipOn = {} + // In config: skipOn = {TestConfigExceptionA.class} + + expect(TestConfigExceptionA.class, () -> bean.skipOnMethod()); + expect(TestConfigExceptionA.class, () -> bean.skipOnMethod()); + // If skipOn is not configured to include TestConfigExceptionA, this would throw a CircuitBreakerOpenException - Exceptions.expect(TestConfigExceptionA.class, () -> bean.skipOnMethod()); + expect(TestConfigExceptionA.class, () -> bean.skipOnMethod()); + } + + @Test + public void testConfigureDelay() { + // In annotation: requestVolumeThreshold = 2 + // delay = 20 + // delayUnit = MICROS + // In config: delay = 1000 + // delayUnit = MILLIS + + expect(TestConfigExceptionA.class, () -> bean.delayMethod(true)); + expect(TestConfigExceptionA.class, () -> bean.delayMethod(true)); + + expectCbOpen(() -> bean.delayMethod(false)); + // CB is now open, wait until it moves to half-open (and time how long that took) + + long start = System.nanoTime(); + await().atMost(1, TimeUnit.MINUTES).untilAsserted(() -> { + expectNoException(() -> bean.delayMethod(false)); + }); + long end = System.nanoTime(); + + long durationInMillis = Duration.ofNanos(end - start).toMillis(); + assertThat(durationInMillis, greaterThan(TCKConfig.getConfig().getTimeoutInMillis(800))); + assertThat(durationInMillis, lessThan(TCKConfig.getConfig().getTimeoutInMillis(2000))); } - + + @Test + public void testConfigureRequestVolumeThreshold() { + // In annotation: requestVolumeThreshold = 2 + // In config: requestVolumeThreshold = 4 + + expect(TestConfigExceptionA.class, () -> bean.requestVolumeThresholdMethod()); + expect(TestConfigExceptionA.class, () -> bean.requestVolumeThresholdMethod()); + + // If requestVolumeThreshold is not configured to 4, this would throw a CircuitBreakerOpenException + expect(TestConfigExceptionA.class, () -> bean.requestVolumeThresholdMethod()); + expect(TestConfigExceptionA.class, () -> bean.requestVolumeThresholdMethod()); + + // If requestVolumeThreshold is not configured to 4, this would NOT throw a CircuitBreakerOpenException + expectCbOpen(() -> bean.requestVolumeThresholdMethod()); + } + + @Test + public void testConfigureFailureRatio() { + // In annotation: requestVolumeThreshold = 10 + // failureRatio = 1.0 + // In config: failureRatio = 0.8 + + expect(TestConfigExceptionA.class, () -> bean.failureRatioMethod(true)); + expect(TestConfigExceptionA.class, () -> bean.failureRatioMethod(true)); + expectNoException(() -> bean.failureRatioMethod(false)); + expect(TestConfigExceptionA.class, () -> bean.failureRatioMethod(true)); + expect(TestConfigExceptionA.class, () -> bean.failureRatioMethod(true)); + expect(TestConfigExceptionA.class, () -> bean.failureRatioMethod(true)); + expectNoException(() -> bean.failureRatioMethod(false)); + expect(TestConfigExceptionA.class, () -> bean.failureRatioMethod(true)); + expect(TestConfigExceptionA.class, () -> bean.failureRatioMethod(true)); + expect(TestConfigExceptionA.class, () -> bean.failureRatioMethod(true)); + + // If failureRatio is not configured to 0.8, this would NOT throw a CircuitBreakerOpenException: + // - if failureRatio > 0.8, this would throw TestConfigExceptionA + // - if failureRatio < 0.8, CircuitBreakerOpenException would be thrown sooner + expectCbOpen(() -> bean.failureRatioMethod(false)); + } + + @Test + public void testConfigureSuccessThreshold() { + // In annotation: delay = 1000 + // requestVolumeThreshold = 10 + // successThreshold = 4 + // In config: successThreshold = 2 + + for (int i = 0; i < 10; i++) { + expect(TestConfigExceptionA.class, () -> bean.successThresholdMethod(true)); + } + + // CB is now open, wait until it moves to half-open + expectCbOpen(() -> bean.successThresholdMethod(false)); + await().atMost(1, TimeUnit.MINUTES).untilAsserted(() -> { + expectNoException(() -> bean.successThresholdMethod(false)); + }); + + // CB is now half-open and 1st successful invocation already occured + expectNoException(() -> bean.successThresholdMethod(false)); + + // 2nd successful invocation occured, CB is now closed + for (int i = 0; i < 10; i++) { + // 10 because after moving to close, we start with a new, empty rolling window + expect(TestConfigExceptionA.class, () -> bean.successThresholdMethod(true)); + } + + // CB is now open, wait until it moves to half-open + expectCbOpen(() -> bean.successThresholdMethod(false)); + await().atMost(1, TimeUnit.MINUTES).untilAsserted(() -> { + expectNoException(() -> bean.successThresholdMethod(false)); + }); + + // CB is now half-open and 1st successful invocation already occured + expect(TestConfigExceptionA.class, () -> bean.successThresholdMethod(true)); + + // 2nd invocation was a failure, CB is back to open + expectCbOpen(() -> bean.successThresholdMethod(false)); + } + } diff --git a/tck/src/main/java/org/eclipse/microprofile/fault/tolerance/tck/util/Exceptions.java b/tck/src/main/java/org/eclipse/microprofile/fault/tolerance/tck/util/Exceptions.java index 9b9d285e..a3bffbaf 100644 --- a/tck/src/main/java/org/eclipse/microprofile/fault/tolerance/tck/util/Exceptions.java +++ b/tck/src/main/java/org/eclipse/microprofile/fault/tolerance/tck/util/Exceptions.java @@ -123,7 +123,21 @@ public static void expect(Class expectedException, Exceptio } } } - + + /** + * Run an action and ensure that no exception is thrown + * + * @param action the action to run + */ + public static void expectNoException(ExceptionThrowingAction action) { + try { + action.call(); + } + catch (Exception e) { + fail("Unexpected exception thrown", e); + } + } + @FunctionalInterface public static interface ExceptionThrowingAction { public void call() throws Exception;