org.eclipse.microprofile.fault.tolerance.tck.TimeoutUninterruptableTest Maven / Gradle / Ivy
Show all versions of microprofile-fault-tolerance-tck Show documentation
/*
*******************************************************************************
* Copyright (c) 2018-2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* 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 org.eclipse.microprofile.fault.tolerance.tck;
import static org.eclipse.microprofile.fault.tolerance.tck.util.Exceptions.expect;
import static org.eclipse.microprofile.fault.tolerance.tck.util.Exceptions.expectTimeout;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThan;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.fail;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import org.eclipse.microprofile.fault.tolerance.tck.config.ConfigAnnotationAsset;
import org.eclipse.microprofile.fault.tolerance.tck.timeout.clientserver.UninterruptableTimeoutClient;
import org.eclipse.microprofile.fault.tolerance.tck.util.Packages;
import org.eclipse.microprofile.fault.tolerance.tck.util.TCKConfig;
import org.eclipse.microprofile.faulttolerance.exceptions.BulkheadException;
import org.eclipse.microprofile.faulttolerance.exceptions.TimeoutException;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.testng.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;
import jakarta.inject.Inject;
/**
* Test behaviour when a {@code @Timeout} is used but the method does not respond to interrupts.
*
* This provokes a lot of edge case interactions between Timeout and other annotations.
*
* Includes test for Timeout, Timeout + Async, Timeout + Async + Bulkhead, Timeout + Async + Retry.
*/
public class TimeoutUninterruptableTest extends Arquillian {
@Deployment
public static WebArchive deployment() {
ConfigAnnotationAsset timeoutConfig = new ConfigAnnotationAsset()
.autoscaleMethod(UninterruptableTimeoutClient.class, "serviceTimeout")
.autoscaleMethod(UninterruptableTimeoutClient.class, "serviceTimeoutAsync")
.autoscaleMethod(UninterruptableTimeoutClient.class, "serviceTimeoutAsyncCS")
.autoscaleMethod(UninterruptableTimeoutClient.class, "serviceTimeoutAsyncBulkhead")
.autoscaleMethod(UninterruptableTimeoutClient.class, "serviceTimeoutAsyncBulkheadQueueTimed")
.autoscaleMethod(UninterruptableTimeoutClient.class, "serviceTimeoutAsyncRetry")
.autoscaleMethod(UninterruptableTimeoutClient.class, "serviceTimeoutAsyncFallback");
JavaArchive testJar = ShrinkWrap.create(JavaArchive.class, "ftTimeoutUninterruptable.jar")
.addClass(UninterruptableTimeoutClient.class)
.addPackage(Packages.UTILS)
.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")
.addAsManifestResource(timeoutConfig, "microprofile-config.properties");
WebArchive testWar = ShrinkWrap.create(WebArchive.class, "ftTimeoutUninterruptable.war")
.addAsLibrary(testJar);
return testWar;
}
private List> waitingFutures = new ArrayList<>();
@Inject
private UninterruptableTimeoutClient client;
private final TCKConfig config = TCKConfig.getConfig();
@Test
public void testTimeout() {
long startTime = System.nanoTime();
expectTimeout(() -> client.serviceTimeout(config.getTimeoutInMillis(1000)));
long endTime = System.nanoTime();
// Interrupt flag should not be set
assertFalse(Thread.interrupted(), "Thread was still interrupted when method returned");
// Expect that execution will take the full 1000ms because the method does not respond to being interrupted
assertThat("Execution time", Duration.ofNanos(endTime - startTime),
greaterThanOrEqualTo(config.getTimeoutInDuration(800)));
}
@Test
public void testTimeoutAsync() throws Exception {
CompletableFuture waitingFuture = newWaitingFuture();
CompletableFuture completionFuture = new CompletableFuture();
long startTime = System.nanoTime();
Future result = client.serviceTimeoutAsync(waitingFuture, completionFuture);
expect(TimeoutException.class, result);
long resultTime = System.nanoTime();
// We should get the TimeoutException after 500ms, allow up to 1500ms
assertThat("Time for result to be complete", Duration.ofNanos(resultTime - startTime),
lessThan(config.getTimeoutInDuration(1500)));
assertFalse(completionFuture.isDone(), "Method should still be running");
// If we release the waitingFuture, the method should quickly complete
waitingFuture.complete(null);
completionFuture.get(config.getTimeoutInMillis(5000), TimeUnit.MILLISECONDS);
}
@Test
public void testTimeoutAsyncCS() throws InterruptedException {
AtomicBoolean wasInterrupted = new AtomicBoolean(false);
CompletableFuture completionFuture = new CompletableFuture<>();
AtomicLong endTime = new AtomicLong();
long startTime = System.nanoTime();
client.serviceTimeoutAsyncCS(config.getTimeoutInMillis(4000))
.thenRun(() -> completionFuture.complete(null))
.exceptionally((e) -> {
completionFuture.completeExceptionally(e);
return null;
})
.thenRun(() -> endTime.set(System.nanoTime()))
.thenRun(() -> wasInterrupted.set(Thread.interrupted()));
expect(TimeoutException.class, completionFuture);
// Interrupt flag should not be set
assertFalse(wasInterrupted.get(), "Thread was still interrupted when thenRun steps were run");
// Expect that the method will timeout after 500ms, allow up to 1500ms
assertThat("Execution time", Duration.ofNanos(endTime.get() - startTime),
lessThan(config.getTimeoutInDuration(1500)));
}
@Test
public void testTimeoutAsyncBulkhead() throws InterruptedException {
CompletableFuture> waitingFuture = newWaitingFuture();
long startTime = System.nanoTime();
Future resultA = client.serviceTimeoutAsyncBulkhead(waitingFuture);
expect(TimeoutException.class, resultA);
long resultTime = System.nanoTime();
// Should get the TimeoutException after 500ms, allow up to 1500ms
assertThat("Time for result to be complete", Duration.ofNanos(resultTime - startTime),
lessThan(config.getTimeoutInDuration(1500)));
// Should record one execution
assertEquals(client.getTimeoutAsyncBulkheadCounter(), 1, "Execution count after first call");
// At this point, the first execution should still be running, so the next one should get queued
startTime = System.nanoTime();
Future resultB = client.serviceTimeoutAsyncBulkhead(waitingFuture);
expect(TimeoutException.class, resultB);
resultTime = System.nanoTime();
// Should get the TimeoutException after 500ms, allow up to 1500ms
assertThat("Time for result to be complete", Duration.ofNanos(resultTime - startTime),
lessThan(config.getTimeoutInDuration(1500)));
// This time though, we shouldn't record a second execution since the request timed out before it got to start
// running
assertEquals(client.getTimeoutAsyncBulkheadCounter(), 1, "Execution count after second call");
// Make two more calls with a short gap
Future resultC = client.serviceTimeoutAsyncBulkhead(waitingFuture);
Thread.sleep(config.getTimeoutInMillis(100));
Future resultD = client.serviceTimeoutAsyncBulkhead(waitingFuture);
// The first call should be queued and eventually time out
// The second call should get a BulkheadException because the queue is full
expect(TimeoutException.class, resultC);
expect(BulkheadException.class, resultD);
// Lastly, neither of these calls actually got to run, so there should be no more executions
assertEquals(client.getTimeoutAsyncBulkheadCounter(), 1, "Execution count after fourth call");
// Complete the waiting future and check that, after a short wait, we have no additional executions
waitingFuture.complete(null);
Thread.sleep(config.getTimeoutInMillis(300));
assertEquals(client.getTimeoutAsyncBulkheadCounter(), 1, "Execution count after completing all tasks");
}
/**
* Test that the timeout timer is started when the execution is added to the queue
*
* @throws InterruptedException
* if the test is interrupted
* @throws ExecutionException
*/
@Test
public void testTimeoutAsyncBulkheadQueueTimed() throws InterruptedException, ExecutionException {
CompletableFuture waitingFutureA = newWaitingFuture();
CompletableFuture waitingFutureB = newWaitingFuture();
Future resultA = client.serviceTimeoutAsyncBulkheadQueueTimed(waitingFutureA);
Thread.sleep(config.getTimeoutInMillis(100));
long startTime = System.nanoTime();
Future resultB = client.serviceTimeoutAsyncBulkheadQueueTimed(waitingFutureB);
Thread.sleep(config.getTimeoutInMillis(300));
// Allow call A to finish, this should allow call B to start
waitingFutureA.complete(null);
// Assert there were no exceptions for call A
resultA.get();
// Wait for call B to time out
expect(TimeoutException.class, resultB);
long endTime = System.nanoTime();
// B should time out 500ms after it was submitted, even though it spent 300ms queued
// Allow up to 750ms. Bound is tighter here as more than 800ms would represent incorrect behavior
assertThat("Time taken for call B to timeout", Duration.ofNanos(endTime - startTime),
lessThan(config.getTimeoutInDuration(750)));
}
@Test
public void testTimeoutAsyncRetry() {
CompletableFuture> waitingFuture = newWaitingFuture();
// Expect to run method three times, each one timing out after 500ms
long startTime = System.nanoTime();
Future result = client.serviceTimeoutAsyncRetry(waitingFuture);
expect(TimeoutException.class, result);
long resultTime = System.nanoTime();
// Should get TimeoutException after 1500ms (3 attempts * 500ms), allow up to 3000ms
assertThat("Time for result to complete", Duration.ofNanos(resultTime - startTime),
lessThan(config.getTimeoutInDuration(3000)));
// Expect all executions to be run
assertEquals(client.getTimeoutAsyncRetryCounter(), 3, "Execution count after one call");
}
/**
* Test that the fallback is run as soon as the timeout occurs
*
* @throws InterruptedException
* if the test is interrupted
*/
@Test
public void testTimeoutAsyncFallback() throws InterruptedException {
CompletableFuture> waitingFuture = newWaitingFuture();
long startTime = System.nanoTime();
Future resultFuture = client.serviceTimeoutAsyncFallback(waitingFuture);
try {
// Expect TimeoutException causing fallback to be invoked and the result returned
assertEquals(resultFuture.get(config.getTimeoutInMillis(10000), TimeUnit.MILLISECONDS), "FALLBACK");
} catch (java.util.concurrent.TimeoutException e) {
fail("Method did not complete", e);
} catch (ExecutionException e) {
fail("Unexpected exception thrown", e);
}
long resultTime = System.nanoTime();
// Should get the TimeoutException + Fallback after 500ms, allow up to 1500ms
assertThat("Time for result to be complete", Duration.ofNanos(resultTime - startTime),
lessThan(config.getTimeoutInDuration(1500)));
}
/**
* Creates a waiting future and adds it to a list to be cleaned up at the end of the test
*
* @return the waiting future
*/
private CompletableFuture newWaitingFuture() {
CompletableFuture waitingFuture = new CompletableFuture();
waitingFutures.add(waitingFuture);
return waitingFuture;
}
/**
* Cleans up any waiting futures that have been created in the test
*/
@AfterMethod
public void cleanup() {
for (CompletableFuture future : waitingFutures) {
future.complete(null);
}
waitingFutures.clear();
}
}