/* * Copyright (c) 2018,2019 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.context.tck; import java.io.CharConversionException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.Phaser; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import javax.enterprise.inject.Instance; import javax.enterprise.inject.spi.CDI; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.transaction.Status; import javax.transaction.UserTransaction; import org.eclipse.microprofile.context.tck.contexts.buffer.Buffer; import org.eclipse.microprofile.context.tck.contexts.buffer.spi.BufferContextProvider; import org.eclipse.microprofile.context.tck.contexts.label.Label; import org.eclipse.microprofile.context.tck.contexts.label.spi.LabelContextProvider; import org.eclipse.microprofile.context.tck.contexts.priority.spi.ThreadPriorityContextProvider; import org.eclipse.microprofile.context.ManagedExecutor; import org.eclipse.microprofile.context.ThreadContext; import org.eclipse.microprofile.context.spi.ThreadContextProvider; 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.spec.JavaArchive; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.testng.Assert; import org.testng.ITestResult; import org.testng.annotations.Test; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; public class ManagedExecutorTest extends Arquillian { /** * Maximum tolerated wait for an asynchronous operation to complete. * This is important to ensure that tests don't hang waiting for asynchronous operations to complete. * Normally these sort of operations will complete in tiny fractions of a second, but we are specifying * an extremely generous value here to allow for the widest possible variety of test execution environments. */ private static final long MAX_WAIT_NS = TimeUnit.MINUTES.toNanos(2); /** * Pool of unmanaged threads (not context-aware) that can be used by tests. */ private ExecutorService unmanagedThreads; @AfterClass public void after() { unmanagedThreads.shutdownNow(); } @AfterMethod public void afterMethod(Method m, ITestResult result) { System.out.println("<<< END " + m.getClass().getSimpleName() + '.' + m.getName() + (result.isSuccess() ? " SUCCESS" : " FAILED")); Throwable failure = result.getThrowable(); if (failure != null) { failure.printStackTrace(System.out); } } @BeforeClass public void before() { unmanagedThreads = Executors.newFixedThreadPool(5); } @BeforeMethod public void beforeMethod(Method m) { System.out.println(">>> BEGIN " + m.getClass().getSimpleName() + '.' + m.getName()); } @Deployment public static WebArchive createDeployment() { // build a JAR that provides three fake context types: 'Buffer', 'Label', and 'ThreadPriority' JavaArchive fakeContextProviders = ShrinkWrap.create(JavaArchive.class, "fakeContextTypes.jar") .addPackages(true, "org.eclipse.microprofile.context.tck.contexts.buffer") .addPackages(true, "org.eclipse.microprofile.context.tck.contexts.label") .addPackage("org.eclipse.microprofile.context.tck.contexts.priority.spi") .addAsServiceProvider(ThreadContextProvider.class, BufferContextProvider.class, LabelContextProvider.class, ThreadPriorityContextProvider.class); return ShrinkWrap.create(WebArchive.class, ManagedExecutorTest.class.getSimpleName() + ".war") .addClass(ManagedExecutorTest.class) .addAsLibraries(fakeContextProviders); } @Test public void builderForManagedExecutorIsProvided() { Assert.assertNotNull(ManagedExecutor.builder(), "MicroProfile Context Propagation implementation does not provide a ManagedExecutor builder."); } /** * Verify that the ManagedExecutor implementation clears context * types that are not configured under propagated, or cleared. * * @throws TimeoutException indicates test failure * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure */ @Test public void clearUnspecifiedContexts() throws InterruptedException, ExecutionException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .propagated(Buffer.CONTEXT_NAME) .build(); int originalPriority = Thread.currentThread().getPriority(); try { // Set non-default values int newPriority = originalPriority == 3 ? 2 : 3; Thread.currentThread().setPriority(newPriority); Buffer.set(new StringBuffer("clearUnspecifiedContexts-test-buffer-A")); Future<Void> future = executor.completedFuture(1).thenRun(() -> { Assert.assertEquals(Buffer.get().toString(), "clearUnspecifiedContexts-test-buffer-A", "Context type was not propagated to contextual action."); Buffer.set(new StringBuffer("clearUnspecifiedContexts-test-buffer-B")); Assert.assertEquals(Thread.currentThread().getPriority(), Thread.NORM_PRIORITY, "Context type that remained unspecified was not cleared by default."); }); Assert.assertNull(future.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "Non-null value returned by stage that runs Runnable."); Assert.assertEquals(Buffer.get().toString(), "clearUnspecifiedContexts-test-buffer-A", "Previous context (Buffer) was not restored after context was propagated for contextual action."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Thread.currentThread().setPriority(originalPriority); } } /** * Verify that thread context is captured and propagated per the configuration of the * ManagedExecutor builder for all dependent stages of the completed future that is created * by the ManagedExecutor's completedFuture implementation. Thread context is captured * at each point where a dependent stage is added, rather than solely upon creation of the * initial stage or construction of the builder. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void completedFutureDependentStagesRunWithContext() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .propagated(Buffer.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { // Set non-default values Buffer.set(new StringBuffer("completedFuture-test-buffer-A")); Label.set("completedFuture-test-label"); CompletableFuture<Long> stage1a = executor.completedFuture(1000L); Assert.assertTrue(stage1a.isDone(), "Future created by completedFuture is not complete."); Assert.assertFalse(stage1a.isCompletedExceptionally(), "Future created by completedFuture reports exceptional completion."); Assert.assertEquals(stage1a.getNow(1234L), Long.valueOf(1000L), "Future created by completedFuture has result that differs from what was specified."); // The following incomplete future blocks subsequent stages from running inline on the current thread CompletableFuture<Long> stage1b = new CompletableFuture<Long>(); Buffer.set(new StringBuffer("completedFuture-test-buffer-B")); CompletableFuture<Long> stage2 = stage1a.thenCombine(stage1b, (a, b) -> { Assert.assertEquals(a, Long.valueOf(1000L), "First value supplied to BiFunction was lost or altered."); Assert.assertEquals(b, Long.valueOf(3L), "Second value supplied to BiFunction was lost or altered."); Assert.assertEquals(Buffer.get().toString(), "completedFuture-test-buffer-B", "Context type was not propagated to contextual action."); Assert.assertEquals(Label.get(), "", "Context type that is configured to be cleared was not cleared."); return a * b; }); Buffer.set(new StringBuffer("completedFuture-test-buffer-C")); CompletableFuture<Long> stage3 = stage2.thenApply(i -> { Assert.assertEquals(i, Long.valueOf(3000L), "Value supplied to third stage was lost or altered."); Assert.assertEquals(Buffer.get().toString(), "completedFuture-test-buffer-C", "Context type was not propagated to contextual action."); // This stage runs inline on the same thread as the test, so alter the // context here and later verify that the MicroProfile Context Propagation implementation // properly restores it to the thread's previous value, which will be // completedFuture-test-buffer-E at the point when this runs. Buffer.set(new StringBuffer("completedFuture-test-buffer-D")); Assert.assertEquals(Label.get(), "", "Context type that is configured to be cleared was not cleared."); return i - 300; }); Buffer.set(new StringBuffer("completedFuture-test-buffer-E")); // Complete stage 1b, allowing stage 2 and then 3 to run stage1b.complete(3L); Assert.assertEquals(stage3.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), Long.valueOf(2700L), "Unexpected result for stage 3."); Assert.assertEquals(stage2.getNow(3333L), Long.valueOf(3000L), "Unexpected or missing result for stage 2."); Assert.assertTrue(stage2.isDone(), "Second stage did not transition to done upon completion."); Assert.assertTrue(stage3.isDone(), "Third stage did not transition to done upon completion."); Assert.assertFalse(stage2.isCompletedExceptionally(), "Second stage should not report exceptional completion."); Assert.assertFalse(stage3.isCompletedExceptionally(), "Third stage should not report exceptional completion."); // Is context properly restored on current thread? Assert.assertEquals(Buffer.get().toString(), "completedFuture-test-buffer-E", "Previous context was not restored after context was cleared for managed executor tasks."); Assert.assertEquals(Label.get(), "completedFuture-test-label", "Previous context was not restored after context was propagated for managed executor tasks."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * Verify that thread context is captured and propagated per the configuration of the * ManagedExecutor builder for all dependent stages of the completed future that is created * by the ManagedExecutor's completedStage implementation. Thread context is captured * at each point where a dependent stage is added, rather than solely upon creation of the * initial stage or construction of the builder. * * @throws InterruptedException indicates test failure */ @Test public void completedStageDependentStagesRunWithContext() throws InterruptedException { ManagedExecutor executor = ManagedExecutor.builder() .propagated(Label.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { // Set non-default values Buffer.set(new StringBuffer("completedStage-test-buffer")); Label.set("completedStage-test-label-A"); CompletionStage<String> stage1 = executor.completedStage("5A"); // The following incomplete future prevents subsequent stages from completing CompletableFuture<Integer> stage2 = new CompletableFuture<Integer>(); Label.set("completedStage-test-label-B"); CompletionStage<Integer> stage3 = stage1.thenCompose(s -> { Assert.assertEquals(s, "5A", "Value supplied to compose function was lost or altered."); Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Assert.assertEquals(Label.get(), "completedStage-test-label-B", "Context type was not propagated to contextual action."); return stage2.thenApply(i -> i + Integer.parseInt(s, 16)); }); Label.set("completedStage-test-label-C"); CompletionStage<Integer> stage4 = stage3.applyToEither(new CompletableFuture<Integer>(), i -> { Assert.assertEquals(i, Integer.valueOf(99), "Value supplied to function was lost or altered."); Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Assert.assertEquals(Label.get(), "completedStage-test-label-C", "Context type was not propagated to contextual action."); return i + 1; }); Label.set("completedStage-test-label-D"); CountDownLatch completed = new CountDownLatch(1); AtomicInteger resultRef = new AtomicInteger(); stage4.whenComplete((result, failure) -> { resultRef.set(result); completed.countDown(); }); // allow stages 3 and 4 to complete stage2.complete(9); Assert.assertTrue(completed.await(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "Completion stage did not finish in a reasonable amount of time."); Assert.assertEquals(resultRef.get(), 100, "Unexpected result for stage 4."); // Is context properly restored on current thread? Assert.assertEquals(Buffer.get().toString(), "completedStage-test-buffer", "Previous context was not restored after context was cleared for managed executor tasks."); Assert.assertEquals(Label.get(), "completedStage-test-label-D", "Previous context was not restored after context was propagated for managed executor tasks."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * Verify the MicroProfile Context Propagation implementation of propagate(), and cleared() * for ManagedExecutor.Builder. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void contextControlsForManagedExecutorBuilder() throws InterruptedException, ExecutionException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .propagated(Buffer.CONTEXT_NAME) .cleared(Label.CONTEXT_NAME) .maxAsync(-1) .maxQueued(-1) .build(); try { ManagedExecutor.builder() .propagated(Buffer.CONTEXT_NAME) .cleared(Label.CONTEXT_NAME, Buffer.CONTEXT_NAME) .maxAsync(-1) .maxQueued(-1) .build(); Assert.fail("ManagedExecutor.Builder.build() should throw an IllegalStateException for set overlap between propagated and cleared"); } catch (IllegalStateException ISE) { // test passes } try { ManagedExecutor.builder() .propagated(Buffer.CONTEXT_NAME, "BOGUS_CONTEXT") .cleared(Label.CONTEXT_NAME) .maxAsync(-1) .maxQueued(-1) .build(); Assert.fail("ManagedExecutor.Builder.build() should throw an IllegalStateException for a nonexistent thread context type"); } catch (IllegalStateException ISE) { // test passes } try { // Set non-default values Buffer.get().append("contextControls-test-buffer-A"); Label.set("contextControls-test-label-A"); Future<Void> future = executor.submit(() -> { Assert.assertEquals(Buffer.get().toString(), "contextControls-test-buffer-A", "Context type was not propagated to contextual action."); Buffer.get().append("-B"); Assert.assertEquals(Label.get(), "", "Context type that is configured to be cleared was not cleared."); Label.set("contextControls-test-label-B"); return null; }); Assert.assertNull(future.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "Unexpected result of task."); Assert.assertEquals(Buffer.get().toString(), "contextControls-test-buffer-A-B", "Context type was not propagated to contextual action."); Assert.assertEquals(Label.get(), "contextControls-test-label-A", "Context type was not left unchanged by contextual action."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * When an already-contextualized Callable is specified as the action/task, * the action/task runs with its already-captured context rather than * capturing and applying context per the configuration of the managed executor. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void contextOfContextualCallableOverridesContextOfManagedExecutor() throws ExecutionException, InterruptedException, TimeoutException { ThreadContext bufferContext = ThreadContext.builder() .propagated(Buffer.CONTEXT_NAME) .unchanged() .cleared(ThreadContext.ALL_REMAINING) .build(); ManagedExecutor executor = ManagedExecutor.builder() .propagated(Label.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { Callable<String> getBuffer = () -> { Assert.assertEquals(Label.get(), "", "Context type not cleared from thread."); return Buffer.get().toString(); }; Callable<String> getLabel = () -> { Assert.assertEquals(Buffer.get().toString(), "", "Context type not cleared from thread."); return Label.get(); }; Buffer.set(new StringBuffer("contextualCallableOverride-buffer-1")); Label.set("contextualCallableOverride-label-1"); Callable<String> precontextualizedTask1 = bufferContext.contextualCallable(getBuffer); Buffer.set(new StringBuffer("contextualCallableOverride-buffer-2")); Label.set("contextualCallableOverride-label-2"); Callable<String> precontextualizedTask2 = bufferContext.contextualCallable(getBuffer); Buffer.set(new StringBuffer("contextualCallableOverride-buffer-3")); Label.set("contextualCallableOverride-label-3"); Future<String> future = executor.submit(precontextualizedTask1); Assert.assertEquals(future.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "contextualCallableOverride-buffer-1", "Previously captured context type not found on thread."); List<Future<String>> futures = executor.invokeAll( Arrays.asList(precontextualizedTask2, getLabel, precontextualizedTask1, precontextualizedTask2), MAX_WAIT_NS, TimeUnit.NANOSECONDS); future = futures.get(0); Assert.assertEquals(future.get(), "contextualCallableOverride-buffer-2", "Previously captured context type not found on thread."); future = futures.get(1); Assert.assertEquals(future.get(), "contextualCallableOverride-label-3", "Context type captured by managed executor not found on thread."); future = futures.get(2); Assert.assertEquals(future.get(), "contextualCallableOverride-buffer-1", "Previously captured context type not found on thread."); future = futures.get(3); Assert.assertEquals(future.get(), "contextualCallableOverride-buffer-2", "Previously captured context type not found on thread."); String result = executor.invokeAny( Arrays.asList(precontextualizedTask1, precontextualizedTask1), MAX_WAIT_NS, TimeUnit.NANOSECONDS); Assert.assertEquals(result, "contextualCallableOverride-buffer-1", "Previously captured context type not found on thread."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * When an already-contextualized Consumer or BiFunction is specified as the action/task, * the action/task runs with its already-captured context rather than * capturing and applying context per the configuration of the managed executor. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void contextOfContextualConsumerAndBiFunctionOverrideContextOfManagedExecutor() throws ExecutionException, InterruptedException, TimeoutException { ThreadContext labelContext = ThreadContext.builder() .propagated(Label.CONTEXT_NAME) .unchanged() .cleared(ThreadContext.ALL_REMAINING) .build(); ManagedExecutor executor = ManagedExecutor.builder() .propagated(Buffer.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { Buffer.set(new StringBuffer("contextualBiFunctionOverride-buffer-1")); Label.set("contextualBiFunctionOverride-label-1"); BiFunction<Integer, Throwable, Integer> precontextualizedFunction1 = labelContext.contextualFunction((result, failure) -> { Assert.assertEquals(Label.get(), "contextualBiFunctionOverride-label-1", "Previously captured context type not found on thread."); Assert.assertEquals(Buffer.get().toString(), "", "Context type not cleared from thread."); return failure == null ? result : 100; }); Buffer.set(new StringBuffer("contextualBiFunctionOverride-buffer-2")); Label.set("contextualBiFunctionOverride-label-2"); BiFunction<Integer, Integer, Integer> precontextualizedFunction2 = labelContext.contextualFunction((i, j) -> { Assert.assertEquals(Label.get(), "contextualBiFunctionOverride-label-2", "Previously captured context type not found on thread."); Assert.assertEquals(Buffer.get().toString(), "", "Context type not cleared from thread."); return i - j; }); Buffer.set(new StringBuffer("contextualConsumerOverride-buffer-3")); Label.set("contextualConsumerOverride-label-3"); Consumer<Integer> precontextualizedConsumer3 = labelContext.contextualConsumer(i -> { Assert.assertEquals(Label.get(), "contextualConsumerOverride-label-3", "Previously captured context type not found on thread."); Assert.assertEquals(Buffer.get().toString(), "", "Context type not cleared from thread."); }); Buffer.set(new StringBuffer("contextualConsuemrOverride-buffer-4")); Label.set("contextualConsumerOverride-label-4"); Consumer<Integer> precontextualizedConsumer4 = labelContext.contextualConsumer(i -> { Assert.assertEquals(Label.get(), "contextualConsumerOverride-label-4", "Previously captured context type not found on thread."); Assert.assertEquals(Buffer.get().toString(), "", "Context type not cleared from thread."); }); BiFunction<Void, Void, String> normalFunction5 = (unused1, unused2) -> { Assert.assertEquals(Buffer.get().toString(), "contextualConsumerAndBiFunctionOverride-buffer-5", "Previously captured context type not found on thread."); Assert.assertEquals(Label.get(), "", "Context type not cleared from thread."); return "done"; }; Buffer.set(new StringBuffer("contextualConsumerAndBiFunctionOverride-buffer-5")); Label.set("contextualConsumerAndBiFunctionOverride-label-5"); CompletableFuture<Integer> stage0 = executor.failedFuture(new ArrayIndexOutOfBoundsException("Expected error.")); CompletableFuture<Integer> stage1 = stage0.handleAsync(precontextualizedFunction1); CompletableFuture<Integer> stage2 = executor.completedFuture(200).thenCombineAsync(stage1, precontextualizedFunction2); CompletableFuture<Void> stage3 = stage2.thenAccept(precontextualizedConsumer3); CompletableFuture<Void> stage4 = stage2.acceptEitherAsync(stage1, precontextualizedConsumer4); CompletableFuture<String> stage5 = stage4.thenCombine(stage3, normalFunction5); Assert.assertEquals(stage5.join(), "done", "Unexpected result for completion stage."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * When an already-contextualized Function is specified as the action/task, * the action/task runs with its already-captured context rather than * capturing and applying context per the configuration of the managed executor. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void contextOfContextualFunctionOverridesContextOfManagedExecutor() throws ExecutionException, InterruptedException, TimeoutException { ThreadContext labelContext = ThreadContext.builder() .propagated(Label.CONTEXT_NAME) .unchanged() .cleared(ThreadContext.ALL_REMAINING) .build(); ManagedExecutor executor = ManagedExecutor.builder() .propagated(Buffer.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { Buffer.set(new StringBuffer("contextualFunctionOverride-buffer-1")); Label.set("contextualFunctionOverride-label-1"); Function<Integer, Integer> precontextualizedFunction1 = labelContext.contextualFunction(i -> { Assert.assertEquals(Label.get(), "contextualFunctionOverride-label-1", "Previously captured context type not found on thread."); Assert.assertEquals(Buffer.get().toString(), "", "Context type not cleared from thread."); return i + 1; }); Buffer.set(new StringBuffer("contextualFunctionOverride-buffer-2")); Label.set("contextualFunctionOverride-label-2"); Function<Integer, Integer> precontextualizedFunction2 = labelContext.contextualFunction(i -> { Assert.assertEquals(Label.get(), "contextualFunctionOverride-label-2", "Previously captured context type not found on thread."); Assert.assertEquals(Buffer.get().toString(), "", "Context type not cleared from thread."); return i + 20; }); Function<Throwable, Integer> precontextualizedErrorHandler = labelContext.contextualFunction(failure -> { Assert.assertEquals(Label.get(), "contextualFunctionOverride-label-2", "Previously captured context type not found on thread."); Assert.assertEquals(Buffer.get().toString(), "", "Context type not cleared from thread."); return -1; }); Buffer.set(new StringBuffer("contextualFunctionOverride-buffer-3")); Label.set("contextualFunctionOverride-label-3"); Function<Integer, Integer> normalFunction = i -> { Assert.assertEquals(Buffer.get().toString(), "contextualFunctionOverride-buffer-3", "Previously captured context type not found on thread."); Assert.assertEquals(Label.get(), "", "Context type not cleared from thread."); return i + 300; }; CompletableFuture<Integer> stage0 = executor.newIncompleteFuture(); CompletableFuture<Integer> stage1 = stage0.thenApplyAsync(precontextualizedFunction1); Buffer.set(new StringBuffer("contextualFunctionOverride-buffer-4")); Label.set("contextualFunctionOverride-label-4"); Function<Integer, CompletableFuture<Integer>> precontextualizedFunction4 = labelContext.contextualFunction(i -> { Assert.assertEquals(Label.get(), "contextualFunctionOverride-label-4", "Previously captured context type not found on thread."); Assert.assertEquals(Buffer.get().toString(), "", "Context type not cleared from thread."); return stage1; }); Buffer.set(new StringBuffer("contextualFunctionOverride-buffer-3")); Label.set("contextualFunctionOverride-label-3"); CompletableFuture<Integer> stage2 = stage0.thenComposeAsync(precontextualizedFunction4); CompletableFuture<Integer> stage3 = stage2.applyToEither(stage1, precontextualizedFunction2); CompletableFuture<Integer> stage4 = stage3.thenApply(normalFunction); CompletableFuture<Integer> stage5 = stage4.thenApply(i -> i / (i - 321)) // intentional ArithmeticException for division by 0 .exceptionally(precontextualizedErrorHandler); stage0.complete(0); Assert.assertEquals(stage2.join(), Integer.valueOf(1), "Unexpected result for completion stage."); Assert.assertEquals(stage5.join(), Integer.valueOf(-1), "Unexpected result for completion stage."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * When an already-contextualized Runnable is specified as the action/task, * the action/task runs with its already-captured context rather than * capturing and applying context per the configuration of the managed executor. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void contextOfContextualRunnableOverridesContextOfManagedExecutor() throws ExecutionException, InterruptedException, TimeoutException { ThreadContext labelContext = ThreadContext.builder() .propagated(Label.CONTEXT_NAME) .unchanged() .cleared(ThreadContext.ALL_REMAINING) .build(); ManagedExecutor executor = ManagedExecutor.builder() .propagated(Buffer.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { Buffer.set(new StringBuffer("contextualRunnableOverride-buffer-1")); Label.set("contextualRunnableOverride-label-1"); Runnable precontextualizedTask1 = labelContext.contextualRunnable(() -> { Assert.assertEquals(Label.get(), "contextualRunnableOverride-label-1", "Previously captured context type not found on thread."); Assert.assertEquals(Buffer.get().toString(), "", "Context type not cleared from thread."); }); Buffer.set(new StringBuffer("contextualRunnableOverride-buffer-2")); Label.set("contextualRunnableOverride-label-2"); Runnable precontextualizedTask2 = labelContext.contextualRunnable(() -> { Assert.assertEquals(Label.get(), "contextualRunnableOverride-label-2", "Previously captured context type not found on thread."); Assert.assertEquals(Buffer.get().toString(), "", "Context type not cleared from thread."); }); Buffer.set(new StringBuffer("contextualRunnableOverride-buffer-3")); Label.set("contextualRunnableOverride-label-3"); Runnable normalTask = () -> { Assert.assertEquals(Buffer.get().toString(), "contextualRunnableOverride-buffer-3", "Previously captured context type not found on thread."); Assert.assertEquals(Label.get(), "", "Context type not cleared from thread."); }; Future<Integer> future = executor.submit(precontextualizedTask1, 1); Assert.assertEquals(future.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), Integer.valueOf(1), "Unexpected result of task."); CompletableFuture<Void> stage0 = executor.runAsync(precontextualizedTask1); CompletableFuture<Void> stage1 = stage0.thenRunAsync(precontextualizedTask1); CompletableFuture<Void> stage2 = stage0.thenRun(precontextualizedTask2); CompletableFuture<Void> stage3 = stage1.runAfterEither(stage2, precontextualizedTask2); CompletableFuture<Void> stage4 = stage1.runAfterBothAsync(stage2, precontextualizedTask1); CompletableFuture<Void> stage5 = stage4.runAfterBoth(stage3, normalTask); stage5.join(); LinkedBlockingQueue<String> results = new LinkedBlockingQueue<String>(); Runnable precontextualizedTask3 = labelContext.contextualRunnable(() -> results.add(Label.get())); Buffer.set(new StringBuffer("contextualRunnableOverride-buffer-4")); Label.set("contextualRunnableOverride-label-4"); executor.execute(precontextualizedTask3); Assert.assertEquals(results.poll(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "contextualRunnableOverride-label-3", "Previously captured context type not found on thread."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * When an already-contextualized Supplier or BiFunction is specified as the action/task, * the action/task runs with its already-captured context rather than * capturing and applying context per the configuration of the managed executor. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void contextOfContextualSuppplierAndBiConsumerOverrideContextOfManagedExecutor() throws ExecutionException, InterruptedException, TimeoutException { ThreadContext bufferContext = ThreadContext.builder() .propagated(Buffer.CONTEXT_NAME) .unchanged() .cleared(ThreadContext.ALL_REMAINING) .build(); ManagedExecutor executor = ManagedExecutor.builder() .propagated(Label.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { Supplier<String> getBuffer = () -> { Assert.assertEquals(Label.get(), "", "Context type not cleared from thread."); return Buffer.get().toString(); }; Buffer.set(new StringBuffer("contextualSupplierOverride-buffer-1")); Label.set("contextualSupplierOverride-label-1"); Supplier<String> precontextualizedSupplier1 = bufferContext.contextualSupplier(getBuffer); Buffer.set(new StringBuffer("contextualSupplierOverride-buffer-2")); Label.set("contextualSupplierOverride-label-2"); Supplier<String> precontextualizedSupplier2 = bufferContext.contextualSupplier(getBuffer); Buffer.set(new StringBuffer("contextualBiConsumerOverride-buffer-3")); Label.set("contextualBiConsumerOverride-label-3"); BiConsumer<String, String> precontextualizedConsumer3 = bufferContext.contextualConsumer((b1, b2) -> { Assert.assertEquals(Buffer.get().toString(), "contextualBiConsumerOverride-buffer-3", "Previously captured context type not found on thread."); Assert.assertEquals(Label.get(), "", "Context type not cleared from thread."); Assert.assertEquals(b1, "contextualSupplierOverride-buffer-1", "Previously captured context type not found on Supplier's thread."); Assert.assertEquals(b2, "contextualSupplierOverride-buffer-2", "Previously captured context type not found on Supplier's thread."); }); Buffer.set(new StringBuffer("contextualBiConsumerOverride-buffer-4")); Label.set("contextualBiConsumerOverride-label-4"); BiConsumer<Void, Throwable> precontextualizedConsumer4 = bufferContext.contextualConsumer((unused, failure) -> { Assert.assertEquals(Buffer.get().toString(), "contextualBiConsumerOverride-buffer-4", "Previously captured context type not found on thread."); Assert.assertEquals(Label.get(), "", "Context type not cleared from thread."); }); Buffer.set(new StringBuffer("contextualSupplierAndBiConsumerOverride-buffer-5")); Label.set("contextualSupplierAndBiConsumerOverride-label-5"); CompletableFuture<String> stage1 = executor.supplyAsync(precontextualizedSupplier1); CompletableFuture<String> stage2 = executor.supplyAsync(precontextualizedSupplier2); CompletableFuture<Void> stage3 = stage1.thenAcceptBoth(stage2, precontextualizedConsumer3); CompletableFuture<Void> stage4 = stage3.whenCompleteAsync(precontextualizedConsumer4); CompletableFuture<Void> stage5 = stage4.whenComplete((unused, failure) -> { Assert.assertEquals(Label.get(), "contextualSupplierAndBiConsumerOverride-label-5", "Context type captured by managed executor not found on thread."); Assert.assertEquals(Buffer.get().toString(), "", "Context type not cleared from thread."); }); stage5.join(); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * Verify that thread context is cleared per the configuration of the ManagedExecutor builder * for all tasks that are executed via the execute method. This test supplies the ManagedExecutor * to a Java SE CompletableFuture, which invokes the execute method to run tasks asynchronously. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void executedTaskRunsWithClearedContext() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .propagated() .cleared(ThreadContext.ALL_REMAINING) .build(); try { Buffer.set(new StringBuffer("executed-task-test-buffer-A")); Label.set("executed-task-test-label-A"); CompletableFuture<Integer> cf1 = new CompletableFuture<Integer>(); CompletableFuture<Void> cf2 = cf1.thenAcceptAsync(i -> { Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Assert.assertEquals(Label.get(), "", "Context type that is configured to be cleared was not cleared."); Label.set("executed-task-test-label-B"); }, executor); cf1.complete(1000); cf2.join(); Assert.assertEquals(Buffer.get().toString(), "executed-task-test-buffer-A", "Context unexpectedly changed on thread."); Assert.assertEquals(Label.get(), "executed-task-test-label-A", "Context unexpectedly changed on thread."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * Verify that thread context is propagated per the configuration of the ManagedExecutor builder * for all tasks that are executed via the execute method. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void executedTaskRunsWithContext() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .propagated(Buffer.CONTEXT_NAME, Label.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { Buffer.set(new StringBuffer("executed-task-test-buffer-C")); Label.set("executed-task-test-label-C"); CompletableFuture<String> result = new CompletableFuture<String>(); executor.execute(() -> { try { Assert.assertEquals(Buffer.get().toString(), "executed-task-test-buffer-C", "Context type that is configured to be propagated was not propagated."); Assert.assertEquals(Label.get(), "executed-task-test-label-C", "Context type that is configured to be propagated was not propagated."); Label.set("executed-task-test-label-D"); result.complete("successful"); } catch (Throwable x) { result.completeExceptionally(x); } }); // Force exception to be raised, if any result.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS); Assert.assertEquals(Buffer.get().toString(), "executed-task-test-buffer-C", "Context unexpectedly changed on thread."); Assert.assertEquals(Label.get(), "executed-task-test-label-C", "Context unexpectedly changed on thread."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * Verify that thread context is captured and propagated per the configuration of the * ManagedExecutor builder for all dependent stages of the completed future that is created * by the ManagedExecutor's failedFuture implementation. Thread context is captured * at each point where a dependent stage is added, rather than solely upon creation of the * initial stage or construction of the builder. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void failedFutureDependentStagesRunWithContext() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .propagated(Buffer.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { // Set non-default values Buffer.set(new StringBuffer("failedFuture-test-buffer-1")); Label.set("failedFuture-test-label"); CompletableFuture<Character> stage1 = executor.failedFuture(new CharConversionException("A fake exception created by the test")); Assert.assertTrue(stage1.isDone(), "Future created by failedFuture is not complete."); Assert.assertTrue(stage1.isCompletedExceptionally(), "Future created by failedFuture does not report exceptional completion."); try { Character result = stage1.getNow('1'); Assert.fail("Failed future must raise exception. Instead, getNow returned: " + result); } catch (CompletionException x) { if (x.getCause() == null || !(x.getCause() instanceof CharConversionException) || !"A fake exception created by the test".equals(x.getCause().getMessage())) { throw x; } } Buffer.set(new StringBuffer("failedFuture-test-buffer-B")); CompletableFuture<Character> stage2a = stage1.exceptionally(x -> { Assert.assertEquals(x.getClass(), CharConversionException.class, "Wrong exception class supplied to 'exceptionally' method."); Assert.assertEquals(x.getMessage(), "A fake exception created by the test", "Exception message was lost or altered."); Assert.assertEquals(Buffer.get().toString(), "failedFuture-test-buffer-B", "Context type was not propagated to contextual action."); Assert.assertEquals(Label.get(), "", "Context type that is configured to be cleared was not cleared."); return 'A'; }); // The following incomplete future blocks subsequent stages from running inline on the current thread CompletableFuture<Character> stage2b = new CompletableFuture<Character>(); Buffer.set(new StringBuffer("failedFuture-test-buffer-C")); AtomicBoolean stage3Runs = new AtomicBoolean(); CompletableFuture<Void> stage3 = stage2a.runAfterBoth(stage2b, () -> { stage3Runs.set(true); Assert.assertEquals(Buffer.get().toString(), "failedFuture-test-buffer-C", "Context type was not propagated to contextual action."); Assert.assertEquals(Label.get(), "", "Context type that is configured to be cleared was not cleared."); }); Buffer.set(new StringBuffer("failedFuture-test-buffer-D")); Assert.assertFalse(stage3.isDone(), "Third stage should not report done until both of the stages upon which it depends complete."); // Complete stage 2b, allowing stage 3 to run stage2b.complete('B'); Assert.assertNull(stage3.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "Unexpected result for stage 3."); Assert.assertTrue(stage3Runs.get(), "The Runnable for stage 3 did not run."); Assert.assertEquals(stage2a.getNow('F'), Character.valueOf('A'), "Unexpected or missing result for stage 2."); Assert.assertTrue(stage2a.isDone(), "Second stage did not transition to done upon completion."); Assert.assertTrue(stage3.isDone(), "Third stage did not transition to done upon completion."); Assert.assertFalse(stage2a.isCompletedExceptionally(), "Second stage should not report exceptional completion."); Assert.assertFalse(stage3.isCompletedExceptionally(), "Third stage should not report exceptional completion."); // Is context properly restored on current thread? Assert.assertEquals(Buffer.get().toString(), "failedFuture-test-buffer-D", "Previous context was not restored after context was cleared for managed executor tasks."); Assert.assertEquals(Label.get(), "failedFuture-test-label", "Previous context was not restored after context was propagated for managed executor tasks."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * Verify that thread context is captured and propagated per the configuration of the * ManagedExecutor builder for all dependent stages of the completed future that is created * by the ManagedExecutor's failedStage implementation. Thread context is captured * at each point where a dependent stage is added, rather than solely upon creation of the * initial stage or construction of the builder. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void failedStageDependentStagesRunWithContext() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .propagated(Label.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { // Set non-default values Buffer.set(new StringBuffer("failedStage-test-buffer")); Label.set("failedStage-test-label-A"); CompletionStage<Integer> stage1 = executor.failedStage(new LinkageError("Error intentionally raised by test case")); Label.set("failedStage-test-label-B"); CompletionStage<Integer> stage2 = stage1.whenComplete((result, failure) -> { Assert.assertEquals(failure.getClass(), LinkageError.class, "Wrong exception class supplied to 'whenComplete' method."); Assert.assertEquals(failure.getMessage(), "Error intentionally raised by test case", "Error message was lost or altered."); Assert.assertNull(result, "Non-null result supplied to whenComplete for failed stage."); Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Assert.assertEquals(Label.get(), "failedStage-test-label-B", "Context type was not propagated to contextual action."); }); Label.set("failedStage-test-label-C"); CompletableFuture<Integer> future1 = stage1.toCompletableFuture(); try { Integer result = future1.join(); Assert.fail("The join operation did not raise the error from the failed stage. Instead: " + result); } catch (CompletionException x) { if (x.getCause() == null || !(x.getCause() instanceof LinkageError) || !"Error intentionally raised by test case".equals(x.getCause().getMessage())) { throw x; } } CompletableFuture<Integer> future2 = stage2.toCompletableFuture(); try { Integer result = future2.get(); Assert.fail("The get operation did not raise the error from the failed stage. Instead: " + result); } catch (ExecutionException x) { if (x.getCause() == null || !(x.getCause() instanceof LinkageError) || !"Error intentionally raised by test case".equals(x.getCause().getMessage())) { throw x; } } Assert.assertEquals(Buffer.get().toString(), "failedStage-test-buffer", "Previous context was not restored after context was cleared for managed executor tasks."); Assert.assertEquals(Label.get(), "failedStage-test-label-C", "Previous context was not restored after context was propagated for managed executor tasks."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * Verify that the ManagedExecutor implementation starts 2 async tasks/actions, and no more, * when maxAsync is configured to 2. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void maxAsync2() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .maxAsync(2) .propagated() .cleared(ThreadContext.ALL_REMAINING) .build(); Phaser barrier = new Phaser(2); try { // Use up both maxAsync slots on blocking operations and wait for them to start Future<Integer> future1 = executor.submit(() -> barrier.awaitAdvance(barrier.arriveAndAwaitAdvance())); CompletableFuture<Integer> future2 = executor.supplyAsync(() -> barrier.awaitAdvance(barrier.arriveAndAwaitAdvance())); barrier.awaitAdvanceInterruptibly(0, MAX_WAIT_NS, TimeUnit.NANOSECONDS); // This data structure holds the results of tasks which shouldn't be able to run yet LinkedBlockingQueue<String> results = new LinkedBlockingQueue<String>(); // Submit additional tasks/actions for async execution. // These should queue, but otherwise be unable to start yet due to maxAsync=2. CompletableFuture<Void> future3 = executor.runAsync(() -> results.offer("Result3")); CompletableFuture<Boolean> future4 = executor.supplyAsync(() -> results.offer("Result4")); Future<Boolean> future5 = executor.submit(() -> results.offer("Result5")); CompletableFuture<Boolean> future6 = executor.completedFuture("6") .thenApplyAsync(s -> results.offer("Result" + s)); // Detect whether any of the above tasks/actions run within the next 5 seconds Assert.assertNull(results.poll(5, TimeUnit.SECONDS), "Should not be able start more than 2 async tasks when maxAsync is 2."); // unblock and allow tasks to finish barrier.arrive(); barrier.arrive(); // there are 2 parties in each phase Assert.assertNotNull(results.poll(MAX_WAIT_NS, TimeUnit.SECONDS), "None of the queued tasks ran."); Assert.assertNotNull(results.poll(MAX_WAIT_NS, TimeUnit.SECONDS), "Only 1 of the queued tasks ran."); Assert.assertNotNull(results.poll(MAX_WAIT_NS, TimeUnit.SECONDS), "Only 2 of the queued tasks ran."); Assert.assertNotNull(results.poll(MAX_WAIT_NS, TimeUnit.SECONDS), "Only 3 of the queued tasks ran."); Assert.assertEquals(future1.get(), Integer.valueOf(2), "Unexpected result of first task."); Assert.assertEquals(future2.get(), Integer.valueOf(2), "Unexpected result of second task."); Assert.assertNull(future3.join(), "Unexpected result of third task."); Assert.assertEquals(future4.join(), Boolean.TRUE, "Unexpected result of fourth task."); Assert.assertEquals(future5.get(), Boolean.TRUE, "Unexpected result of fifth task."); Assert.assertEquals(future6.get(), Boolean.TRUE, "Unexpected result of sixth task."); } finally { barrier.forceTermination(); executor.shutdownNow(); } } /** * Attempt to specify invalid values (less than -1 and 0) for maxAsync. * Require this to be rejected upon the maxQueued operation per JavaDoc * rather than from the build method. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void maxAsyncInvalidValues() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor.Builder builder = ManagedExecutor.builder(); builder.propagated(ThreadContext.ALL_REMAINING); builder.cleared(ThreadContext.TRANSACTION); try { builder.maxAsync(-10); Assert.fail("ManagedExecutor builder permitted value of -10 for maxAsync."); } catch (IllegalArgumentException x) { // test passes } try { builder.maxAsync(-2); Assert.fail("ManagedExecutor builder permitted value of -2 for maxAsync."); } catch (IllegalArgumentException x) { // test passes } try { builder.maxQueued(0); Assert.fail("ManagedExecutor builder permitted value of 0 for maxAsync."); } catch (IllegalArgumentException x) { // test passes } // builder remains usable ManagedExecutor executor = builder.build(); try { // neither of the invalid values apply - can run a task Future<String> future = executor.submit(() -> "it worked!"); Assert.assertEquals(future.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "it worked!", "Task had missing or unexpected result."); } finally { executor.shutdownNow(); } } /** * Verify that 3 tasks/actions, and no more, can be queued when maxQueued is configured to 3. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void maxQueued3() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .maxAsync(1) .maxQueued(3) .propagated() .cleared(ThreadContext.ALL_REMAINING) .build(); Phaser barrier = new Phaser(1); try { // First, use up the single maxAsync slot with a blocking task and wait for it to start executor.submit(() -> barrier.awaitAdvanceInterruptibly(barrier.arrive() + 1)); barrier.awaitAdvanceInterruptibly(0, MAX_WAIT_NS, TimeUnit.NANOSECONDS); // Use up first queue position Future<Integer> future1 = executor.submit(() -> 101); // Use up second queue position CompletableFuture<Void> future2 = executor.runAsync(() -> System.out.println("second task running")); // Use up third queue position Future<Integer> future3 = executor.submit(() -> 103); // Fourth attempt to queue a task must be rejected try { Future<Integer> future4 = executor.submit(() -> 104); Assert.fail("Exceeded maxQueued of 3. Future for 4th queued task/action is " + future4); } catch (RejectedExecutionException x) { // test passes } // Fifth attempt to queue a task must also be rejected try { CompletableFuture<Integer> future5 = executor.supplyAsync(() -> 105); Assert.fail("Exceeded maxQueued of 3. Future for 5th queued task/action is " + future5); } catch (RejectedExecutionException x) { // test passes } // unblock and allow tasks to finish barrier.arrive(); Assert.assertEquals(future1.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), Integer.valueOf(101), "Unexpected result of first task."); Assert.assertNull(future2.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "Unexpected result of second task."); // At least 2 queue positions must be available at this point Future<Integer> future6 = executor.submit(() -> 106); CompletableFuture<Integer> future7 = executor.supplyAsync(() -> 107); Assert.assertEquals(future3.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), Integer.valueOf(103), "Unexpected result of third task."); Assert.assertEquals(future6.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), Integer.valueOf(106), "Unexpected result of sixth task."); Assert.assertEquals(future7.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), Integer.valueOf(107), "Unexpected result of seventh task."); } finally { barrier.forceTermination(); executor.shutdownNow(); } } /** * Attempt to specify invalid values (less than -1 and 0) for maxQueued. * Require this to be rejected upon the maxQueued operation per JavaDoc * rather than from the build method. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void maxQueuedInvalidValues() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor.Builder builder = ManagedExecutor.builder() .propagated() .cleared(ThreadContext.ALL_REMAINING); try { builder.maxQueued(-2); Assert.fail("ManagedExecutor builder permitted value of -2 for maxQueued."); } catch (IllegalArgumentException x) { // test passes } try { builder.maxQueued(0); Assert.fail("ManagedExecutor builder permitted value of 0 for maxQueued."); } catch (IllegalArgumentException x) { // test passes } // builder remains usable ManagedExecutor executor = builder.build(); try { // neither of the invalid values apply - can queue a task and run it Future<String> future = executor.submit(() -> "successful!"); Assert.assertEquals(future.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "successful!", "Task had missing or unexpected result."); } finally { executor.shutdownNow(); } } /** * Verify that thread context is captured and propagated per the configuration of the * ManagedExecutor builder for all dependent stages of the incomplete future that is created * by the ManagedExecutor's newIncompleteFuture implementation. Thread context is captured * at each point where a dependent stage is added, rather than solely upon creation of the * initial stage or construction of the builder. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure */ @Test public void newIncompleteFutureDependentStagesRunWithContext() throws ExecutionException, InterruptedException { ManagedExecutor executor = ManagedExecutor.builder() .propagated(Label.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { CompletableFuture<Integer> stage1 = executor.newIncompleteFuture(); Assert.assertFalse(stage1.isDone(), "Completable future created by newIncompleteFuture did not start out as incomplete."); // Set non-default values Buffer.get().append("newIncompleteFuture-test-buffer"); Label.set("newIncompleteFuture-test-label-A"); CompletableFuture<Integer> stage2 = stage1.thenApply(i -> { Assert.assertEquals(i, Integer.valueOf(10), "Value supplied to second stage was lost or altered."); Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Assert.assertEquals(Label.get(), "newIncompleteFuture-test-label-A", "Context type was not correctly propagated to contextual action."); return i * 2; }); Label.set("newIncompleteFuture-test-label-B"); CompletableFuture<Integer> stage3 = stage2.thenApply(i -> { Assert.assertEquals(i, Integer.valueOf(20), "Value supplied to third stage was lost or altered."); Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Assert.assertEquals(Label.get(), "newIncompleteFuture-test-label-B", "Context type was not correctly propagated to contextual action."); return i + 10; }); Label.set("newIncompleteFuture-test-label-C"); // To avoid the possibility that CompletableFuture.get might cause the action to run // on the current thread, which would bypass the intent of testing context propagation, // use a countdown latch to independently wait for completion. CountDownLatch completed = new CountDownLatch(1); stage3.whenComplete((result, failure) -> completed.countDown()); Assert.assertTrue(stage1.complete(10), "Unable to complete the future that was created by newIncompleteFuture."); Assert.assertTrue(completed.await(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "Completable future did not finish in a reasonable amount of time."); Assert.assertTrue(stage1.isDone(), "First stage did not transition to done upon completion."); Assert.assertTrue(stage2.isDone(), "Second stage did not transition to done upon completion."); Assert.assertTrue(stage3.isDone(), "Third stage did not transition to done upon completion."); Assert.assertEquals(stage1.get(), Integer.valueOf(10), "Result of first stage does not match the value with which it was completed."); Assert.assertEquals(stage2.getNow(22), Integer.valueOf(20), "Result of second stage was lost or altered."); Assert.assertEquals(stage3.join(), Integer.valueOf(30), "Result of third stage was lost or altered."); Assert.assertFalse(stage1.isCompletedExceptionally(), "First stage should not report exceptional completion."); Assert.assertFalse(stage2.isCompletedExceptionally(), "Second stage should not report exceptional completion."); Assert.assertFalse(stage3.isCompletedExceptionally(), "Third stage should not report exceptional completion."); // Is context properly restored on current thread? Assert.assertEquals(Buffer.get().toString(), "newIncompleteFuture-test-buffer", "Previous context was not restored after context was cleared for managed executor tasks."); Assert.assertEquals(Label.get(), "newIncompleteFuture-test-label-C", "Previous context was not restored after context was propagated for managed executor tasks."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * Verify that Application context makes the application's thread context class loader available to the task. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void propagateApplicationContext() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor.Builder builder = ManagedExecutor.builder() .propagated(ThreadContext.APPLICATION) .cleared(ThreadContext.ALL_REMAINING); ManagedExecutor executor; try { executor = builder.build(); } catch (IllegalStateException x) { return; // Skip test if Application context is not supported. } try { CompletableFuture<Class<?>> cf = executor.supplyAsync(() -> { try { // load a class from the application ClassLoader loader = Thread.currentThread().getContextClassLoader(); return loader.loadClass("org.eclipse.microprofile.context.tck.contexts.label.Label"); } catch (ClassNotFoundException x) { throw new CompletionException(x); } }); Assert.assertEquals(cf.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), Label.class, "Could not load class from application's class loader."); } finally { executor.shutdownNow(); } } /** * Verify that when JTA transactions are supported and configuration of propagated=TRANSACTION * is permitted, it is at least possible to propagate the absence of a transaction, * and if context is allowed to be captured while a transaction is active, then the task or action * runs with a transaction on the thread. It would be nice to test participation in the same * transaction, but that isn't possible without the TCK having a dependency on a particular * type of transactional resource. * * The test tries to get hold of {@code UserTransaction} via JNDI and via CDI. * Should neither work, it still doesn't throw exception but instead returns. * * @throws Exception indicates test failure */ @Test public void propagateTransactionContextJTA() throws Exception { ManagedExecutor executor; try { executor = ManagedExecutor.builder() .propagated(ThreadContext.TRANSACTION) .cleared(ThreadContext.ALL_REMAINING) .build(); } catch (IllegalStateException x) { System.out.println("Skipping test propagateTransactionContextJTA. Transaction context propagation is not supported."); return; } // try to get UserTransaction via JNDI UserTransaction txFromJNDI = null; try { txFromJNDI = InitialContext.doLookup("java:comp/UserTransaction"); } catch (NamingException x) { // JTA UserTransaction not available in JNDI } // try to get UserTransaction via CDI UserTransaction txFromCDI = null; try { CDI<Object> cdi = CDI.current(); Instance<UserTransaction> transactionInstance = cdi.select(UserTransaction.class); if (transactionInstance.isResolvable()) { // UserTransaction available via CDI txFromCDI = transactionInstance.get(); } else { System.out.println("CDI implementation is present, but UserTransaction cannot be retrieved."); } } catch (IllegalStateException x) { System.out.println("CDI implementation not present, cannot retrieve UserTransaction from CDI." + x); } UserTransaction tx = txFromJNDI == null ? txFromCDI : txFromJNDI; if (tx == null) { System.out.println("Skipping test propagateTransactionContextJTA. JTA transactions are not supported."); return; } // Propagate context from thread where no transaction is active CompletableFuture<String> stage0 = executor.newIncompleteFuture(); CompletableFuture<String> stage1 = stage0.thenApply(s -> { try { Assert.assertEquals(tx.getStatus(), Status.STATUS_NO_TRANSACTION, "Transaction status should indicate no transaction is active on thread."); tx.begin(); tx.commit(); return "SUCCESS1"; } catch (Exception x) { throw new CompletionException(x); } }); CompletableFuture<String> stage2; boolean txPropagationRejected = false; tx.begin(); try { // Force stage1 to run on thread where transaction context is already present Assert.assertTrue(stage0.complete("READY")); Assert.assertEquals(stage1.join(), "SUCCESS1"); Assert.assertEquals(tx.getStatus(), Status.STATUS_ACTIVE, "Transaction no longer active after running task."); // Attempt to propagate this transaction to another thread. try { stage2 = stage1.thenApplyAsync(s -> { try { Assert.assertEquals(tx.getStatus(), Status.STATUS_ACTIVE, "Transaction context not propagated."); return "SUCCESS2"; } catch (Exception x) { throw new CompletionException(x); } }); } catch (IllegalStateException x) { System.out.println("Skipping portion of test propagateTransactionContextJTA. Propagation of active transaction is not supported."); txPropagationRejected = true; return; } String result; try { result = stage2.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS); } catch (ExecutionException x) { if (x.getCause() instanceof IllegalStateException) { System.out.println("Skipping portion of test propagateTransactionContextJTA. " + "Propagation of active transaction to multiple threads in parallel is not supported."); txPropagationRejected = true; return; } else { throw x; } } Assert.assertEquals(result, "SUCCESS2"); } finally { if (txPropagationRejected) { tx.rollback(); } else { tx.commit(); } } } /** * Verify that the ManagedExecutor shutdownNow method prevents additional tasks from being submitted * and cancels tasks that are currently in progress or queued. * Also verify that once the tasks and actions terminate, the ManagedExecutor transitions to terminated state. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void shutdownNowPreventsAdditionalSubmitsAndCancelsTasks() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .maxAsync(1) .maxQueued(4) .propagated() .cleared(ThreadContext.ALL_REMAINING) .build(); Phaser barrier = new Phaser(1); Future<Integer> future1; CompletableFuture<Void> future2; CompletableFuture<String> future3; Future<String> future4; Future<Boolean> future5 = null; AtomicInteger task2ResultRef = new AtomicInteger(-1); List<Runnable> tasksThatDidNotStart; try { try { // Block the single maxAsync slot future1 = executor.submit(() -> barrier.awaitAdvanceInterruptibly(barrier.arrive() + 1)); barrier.awaitAdvanceInterruptibly(0, MAX_WAIT_NS, TimeUnit.NANOSECONDS); // Queue up some tasks future2 = executor.runAsync(() -> task2ResultRef.set(20)); future3 = executor.supplyAsync(() -> "Q30"); future4 = executor.submit(() -> "Q40"); Assert.assertFalse(executor.isTerminated(), "ManagedExecutor should not report being terminated when tasks are still running/queued."); // Await termination from a different executor, future5 = unmanagedThreads.submit(() -> executor.awaitTermination(MAX_WAIT_NS, TimeUnit.NANOSECONDS)); } finally { tasksThatDidNotStart = executor.shutdownNow(); } Assert.assertNotNull(tasksThatDidNotStart, "Null list returned by ManagedExecutor.shutdownNow."); Assert.assertEquals(tasksThatDidNotStart.size(), 3, "List of tasks that did not start should correspond to the tasks/actions that are queued. Observed: " + tasksThatDidNotStart); Assert.assertTrue(executor.isShutdown(), "ManagedExecutor reported that it has not been shut down after we shut it down."); // additional submits of async tasks/actions must be rejected try { Future<Integer> future6 = executor.submit(() -> 60); Assert.fail("Should not be possible to submit new task after shutdownNow. Future: " + future6); } catch (RejectedExecutionException x) { // test passes } try { Future<Integer> future7 = executor.supplyAsync(() -> 70); Assert.fail("Should not be possible to create new async action after shutdownNow. Future: " + future7); } catch (RejectedExecutionException x) { // test passes } Assert.assertTrue(executor.awaitTermination(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "ManagedExecutor did not reach terminated state within a reasonable amount of time."); Assert.assertTrue(executor.isTerminated(), "ManagedExecutor did not report being terminated after running/queued tasks were canceled and ended."); // assert that future 1 was completed but ended with ExecutionException because it was running on executor // when shutdownNow() was invoked try { Assert.assertTrue(future1.isDone()); Integer result1 = future1.get(1, TimeUnit.SECONDS); Assert.fail("Running task should not complete successfully after shutdownNow. Result: " + result1); } catch (ExecutionException x) { if (!(x.getCause() instanceof InterruptedException)) { throw x; } // test passes } catch (CancellationException x) { // test passes, impl may chose to mark such task as cancelled } // assert that future 2,3,4 weren't executed (based on impl they are either neither done nor cancelled // or they are done and cancelled) if (future2.isDone()) { try { Object result2 = future2.join(); Assert.fail("Queued action should not run after shutdownNow. Result: " + result2); } catch (CancellationException x) { // test passes } } else { Assert.assertTrue(!future2.isCancelled(), "Running task should not complete after shutdownNow() invocation."); } if (future3.isDone()) { try { String result3 = future3.getNow("333"); Assert.fail("Queued action should not run after shutdownNow. Result: " + result3); } catch (CancellationException x) { // test passes } } else { Assert.assertTrue(!future3.isCancelled(), "Running task should not complete after shutdownNow() invocation."); } if (future4.isDone()) { try { String result4 = future4.get(1, TimeUnit.SECONDS); Assert.fail("Queued task should not run after shutdownNow. Result: " + result4); } catch (CancellationException x) { // test passes } } else { Assert.assertTrue(!future4.isCancelled(), "Running task should not complete after shutdownNow() invocation."); } Assert.assertEquals(task2ResultRef.get(), -1, "Queued action should not start running after shutdownNow."); Assert.assertEquals(future5.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), Boolean.TRUE, "Notification of termination was not received in a reasonable amount of time by the " + "awaitTermination request that was issued before shutdownNow"); } finally { barrier.forceTermination(); if (future5 != null) { future5.cancel(true); } } } /** * Verify that the ManagedExecutor shutdown method prevents additional tasks from being submitted * but does not interfere with tasks and actions that are running or queued. * Also verify that once the tasks and actions finish, the ManagedExecutor transitions to terminated state. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void shutdownPreventsAdditionalSubmits() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .maxAsync(1) .maxQueued(10) .propagated() .cleared(ThreadContext.ALL_REMAINING) .build(); Phaser barrier = new Phaser(1); CompletableFuture<Integer> future1; CompletableFuture<String> future2; Future<String> future3; Future<Boolean> future4 = null; Future<Boolean> future5 = null; try { try { // Block the single maxAsync slot future1 = executor.supplyAsync(() -> barrier.awaitAdvance(barrier.arrive() + 1)); barrier.awaitAdvanceInterruptibly(0, MAX_WAIT_NS, TimeUnit.NANOSECONDS); // Queue up some tasks future2 = executor.supplyAsync(() -> "Q2"); future3 = executor.submit(() -> "Q3"); Assert.assertFalse(executor.isShutdown(), "ManagedExecutor reportd that it has been shut down even though we did not shut it down yet."); // Await termination from a different executor, future4 = unmanagedThreads.submit(() -> executor.awaitTermination(MAX_WAIT_NS, TimeUnit.NANOSECONDS)); } finally { executor.shutdown(); } Assert.assertTrue(executor.isShutdown(), "ManagedExecutor reported that it has not been shut down after we shut it down."); // additional submits of async tasks/actions must be rejected try { Future<Integer> future6 = executor.submit(() -> 60); Assert.fail("Should not be possible to submit new task after shutdown. Future: " + future6); } catch (RejectedExecutionException x) { // test passes } try { Future<Integer> future7 = executor.supplyAsync(() -> 70); Assert.fail("Should not be possible to create new async action after shutdown. Future: " + future7); } catch (RejectedExecutionException x) { // test passes } Assert.assertFalse(future1.isDone(), "Task should remain running after shutdown is invoked."); Assert.assertFalse(future2.isDone(), "Action should remain queued after shutdown is invoked."); Assert.assertFalse(future3.isDone(), "Task should remain queued after shutdown is invoked."); Assert.assertFalse(executor.isTerminated(), "ManagedExecutor should not report being terminated when tasks are still running/queued."); // Extra invocation of shutdown has no effect per ExecutorService JavaDoc, executor.shutdown(); // Await termination from a different executor, future5 = unmanagedThreads.submit(() -> executor.awaitTermination(MAX_WAIT_NS, TimeUnit.NANOSECONDS)); // Let the running task finish and the queued tasks run barrier.arrive(); Assert.assertEquals(future1.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), Integer.valueOf(2), "Unexpected result for action that was running when shutdown was requested."); Assert.assertEquals(future2.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "Q2", "Unexpected result for action that was in the queue when shutdown was requested."); Assert.assertEquals(future3.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "Q3", "Unexpected result for task that was in the queue when shutdown was requested."); Assert.assertEquals(future4.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), Boolean.TRUE, "Notification of termination was not received in a reasonable amount of time by the " + "awaitTermination request that was issued prior to shutdown"); Assert.assertEquals(future5.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), Boolean.TRUE, "Notification of termination was not received in a reasonable amount of time by the " + "awaitTermination request that was issued after shutdown"); Assert.assertTrue(executor.isTerminated(), "ManagedExecutor did not report being terminated after running/queued tasks completed."); } finally { barrier.forceTermination(); if (future4 != null) { future4.cancel(true); } if (future5 != null) { future5.cancel(true); } } } /** * Verify that the ManagedExecutor.Builder can be used to create multiple ManagedExecutors with * different configured contexts. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void reuseManagedExecutorBuilder() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor.Builder builder = ManagedExecutor.builder() .propagated() .cleared(Buffer.CONTEXT_NAME); ManagedExecutor clearingExecutor = builder.build(); ManagedExecutor propagatingExecutor = builder.propagated(Buffer.CONTEXT_NAME) .cleared() .build(); try { // Set non-default value Buffer.set(new StringBuffer("reuseBuilder-test-buffer-A")); Future<Void> clearedFuture = clearingExecutor.completedFuture(1).thenRun(() -> { Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Buffer.set(new StringBuffer("reuseBuilder-test-buffer-B")); }); Future<Void> propagatedFuture = propagatingExecutor.completedFuture(1).thenRunAsync(() -> { Assert.assertEquals(Buffer.get().toString(), "reuseBuilder-test-buffer-A", "Context type was not propagated to contextual action."); Buffer.set(new StringBuffer("reuseBuilder-test-buffer-C")); }); Assert.assertNull(propagatedFuture.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "Non-null value returned by stage that runs Runnable."); Assert.assertNull(clearedFuture.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "Non-null value returned by stage that runs Runnable."); Assert.assertEquals(Buffer.get().toString(), "reuseBuilder-test-buffer-A", "Previous context (Buffer) was not restored after context was propagated for contextual action."); } finally { clearingExecutor.shutdownNow(); propagatingExecutor.shutdownNow(); // Restore original value Buffer.set(null); } } /** * Verify that thread context is captured and propagated per the configuration of the * ManagedExecutor builder for all dependent stages as well as the initial stage created * by the ManagedExecutor's runAsync implementation. Thread context is captured * at each point where a dependent stage is added, rather than solely upon creation of the * initial stage or construction of the builder. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure */ @Test public void runAsyncStageAndDependentStagesRunWithContext() throws ExecutionException, InterruptedException { ManagedExecutor executor = ManagedExecutor.builder() .propagated(Label.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { // Set non-default values Buffer.get().append("runAsync-test-buffer"); Label.set("runAsync-test-label-A"); CompletableFuture<Void> stage1 = executor.runAsync(() -> { Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Assert.assertEquals(Label.get(), "runAsync-test-label-A", "Context type was not correctly propagated to contextual action."); }); Label.set("runAsync-test-label-B"); CompletableFuture<Void> stage2 = stage1.thenRunAsync(() -> { Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Assert.assertEquals(Label.get(), "runAsync-test-label-B", "Context type was not correctly propagated to contextual action."); }); Label.set("runAsync-test-label-C"); CompletableFuture<Void> stage3 = stage2.thenRunAsync(() -> { Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Assert.assertEquals(Label.get(), "runAsync-test-label-C", "Context type was not correctly propagated to contextual action."); }, unmanagedThreads); // supplied executor runs the action, but does not determine context propagation Label.set("runAsync-test-label-D"); CompletableFuture<Void> stage4 = stage3.thenRun(() -> { Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Assert.assertEquals(Label.get(), "runAsync-test-label-D", "Context type was not correctly propagated to contextual action."); throw new NegativeArraySizeException("Fake exception raised by test"); }); Label.set("runAsync-test-label-E"); CompletableFuture<Character> stage5 = stage4.handle((v, x) -> { Assert.assertNull(v, "Non-null value supplied to 'handle' method."); Assert.assertEquals(x.getClass(), CompletionException.class, "Exception parameter to 'handle' method is inconsistent with java.util.concurrent.CompletableFuture."); Throwable cause = x.getCause(); Assert.assertNotNull(cause, "CompletionException supplied to 'handle' method lacks cause."); Assert.assertEquals(cause.getClass(), NegativeArraySizeException.class, "Wrong exception class supplied to 'handle' method."); Assert.assertEquals(cause.getMessage(), "Fake exception raised by test", "Exception message was lost or altered."); Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Assert.assertEquals(Label.get(), "runAsync-test-label-E", "Context type was not correctly propagated to contextual action."); return 'E'; }); Label.set("runAsync-test-label-F"); CompletableFuture<Void> stage6 = stage5.thenAccept(c -> { Assert.assertEquals(c, Character.valueOf('E'), "Value supplied to Consumer was lost or altered."); Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Assert.assertEquals(Label.get(), "runAsync-test-label-F", "Context type was not correctly propagated to contextual action."); }); Label.set("runAsync-test-label-G"); // use a countdown latch to independently wait for completion. CountDownLatch completed = new CountDownLatch(1); stage6.whenComplete((result, failure) -> completed.countDown()); Assert.assertTrue(completed.await(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "Completable future did not finish in a reasonable amount of time."); Assert.assertTrue(stage1.isDone(), "First stage did not transition to done upon completion."); Assert.assertTrue(stage2.isDone(), "Second stage did not transition to done upon completion."); Assert.assertTrue(stage3.isDone(), "Third stage did not transition to done upon completion."); Assert.assertTrue(stage4.isDone(), "Fourth stage did not transition to done upon completion."); Assert.assertTrue(stage5.isDone(), "Fifth stage did not transition to done upon completion."); Assert.assertTrue(stage6.isDone(), "Sixth stage did not transition to done upon completion."); try { Object result = stage4.join(); Assert.fail("The join method must not return value " + result + " for stage with exceptional completion."); } catch (CompletionException x) { if (x.getCause() == null || !(x.getCause() instanceof NegativeArraySizeException) || !"Fake exception raised by test".equals(x.getCause().getMessage())) { throw x; } } Assert.assertEquals(stage5.join(), Character.valueOf('E'), "Return value of 'handle' method was lost or altered."); Assert.assertFalse(stage1.isCompletedExceptionally(), "First stage should not report exceptional completion."); Assert.assertFalse(stage2.isCompletedExceptionally(), "Second stage should not report exceptional completion."); Assert.assertFalse(stage3.isCompletedExceptionally(), "Third stage should not report exceptional completion."); Assert.assertTrue(stage4.isCompletedExceptionally(), "Fourth stage did not report exceptional completion."); Assert.assertFalse(stage5.isCompletedExceptionally(), "Fifth stage should not report exceptional completion."); Assert.assertFalse(stage6.isCompletedExceptionally(), "Sixth stage should not report exceptional completion."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * Verify that thread context is captured and propagated per the configuration of the * ManagedExecutor builder for all tasks that are submitted via the submit(Callable), * submit(Runnable) and submit(Runnable, result) methods. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void submittedTasksRunWithContext() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .propagated(Label.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { Buffer.set(new StringBuffer("submitted-tasks-test-buffer-A")); Label.set("submitted-tasks-test-label-A"); Future<?> futureA = executor.submit(() -> { Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Assert.assertEquals(Label.get(), "submitted-tasks-test-label-A", "Context type was not correctly propagated to contextual action."); throw new Error("Fake error intentionally raised by test Runnable."); }); Label.set("submitted-tasks-test-label-B"); Future<String> futureB = executor.submit(() -> { Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Assert.assertEquals(Label.get(), "submitted-tasks-test-label-B", "Context type was not correctly propagated to contextual action."); }, "Task-B-Result"); Label.set("submitted-tasks-test-label-C"); Future<String> futureC = executor.submit(() -> { Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Assert.assertEquals(Label.get(), "submitted-tasks-test-label-C", "Context type was not correctly propagated to contextual action."); return "Task-C-Result"; }); try { Object result = futureA.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS); Assert.fail("Result of " + result + " returned for Runnable that throws an Error."); } catch (ExecutionException x) { if (x.getCause() == null || (!(x.getCause() instanceof Error)) || (!"Fake error intentionally raised by test Runnable.".equals(x.getCause().getMessage()))) { throw x; } } Assert.assertEquals("Task-B-Result", futureB.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "Result does not match the predetermined result that was specified when submitting the Runnable."); Assert.assertEquals("Task-C-Result", futureC.get(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "Result does not match the result returned by the Callable."); Assert.assertTrue(futureA.isDone(), "Future for first Runnable should report being done after test case awaits its completion."); Assert.assertTrue(futureB.isDone(), "Future for second Runnable should report being done after test case awaits its completion."); Assert.assertTrue(futureC.isDone(), "Future for Callable should report being done after test case awaits its completion."); Assert.assertFalse(futureA.isCancelled(), "Future for first Runnable should not be canceled because the test case did not cancel it."); Assert.assertFalse(futureB.isCancelled(), "Future for second Runnable should not be canceled because the test case did not cancel it."); Assert.assertFalse(futureC.isCancelled(), "Future for Callable should not be canceled because the test case did not cancel it."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * Verify that thread context is captured and propagated per the configuration of the * ManagedExecutor builder for all dependent stages as well as the initial stage created * by the ManagedExecutor's supplyAsync implementation. Thread context is captured * at each point where a dependent stage is added, rather than solely upon creation of the * initial stage or construction of the builder. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void supplyAsyncStageAndDependentStagesRunWithContext() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .propagated(Buffer.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { // Set non-default values Buffer.set(new StringBuffer("supplyAsync-test-buffer-A")); Label.set("supplyAsync-test-label"); CompletableFuture<Long> stage1 = executor.supplyAsync(() -> { Assert.assertEquals(Buffer.get().toString(), "supplyAsync-test-buffer-A", "Context type was not correctly propagated to contextual action."); Assert.assertEquals(Label.get(), "", "Context type that is configured to be cleared was not cleared."); return 100L; }); Buffer.set(new StringBuffer("supplyAsync-test-buffer-B")); CompletableFuture<Long> stage2 = stage1.thenApply(i -> { Assert.assertEquals(i, Long.valueOf(100), "Value supplied to Function was lost or altered."); Assert.assertEquals(Buffer.get().toString(), "supplyAsync-test-buffer-B", "Context type was not correctly propagated to contextual action."); Assert.assertEquals(Label.get(), "", "Context type that is configured to be cleared was not cleared."); return 200L; }); Buffer.set(new StringBuffer("supplyAsync-test-buffer-C")); CompletableFuture<Long> stage3 = stage1.thenApply(i -> { Assert.assertEquals(i, Long.valueOf(100), "Value supplied to Function was lost or altered."); Assert.assertEquals(Buffer.get().toString(), "supplyAsync-test-buffer-C", "Context type was not correctly propagated to contextual action."); Assert.assertEquals(Label.get(), "", "Context type that is configured to be cleared was not cleared."); return 300L; }); Buffer.set(new StringBuffer("supplyAsync-test-buffer-D")); CompletableFuture<Void> stage4 = stage2.thenAcceptBoth(stage3, (i, j) -> { Assert.assertEquals(i, Long.valueOf(200), "First value supplied to BiConsumer was lost or altered."); Assert.assertEquals(j, Long.valueOf(300), "Second value supplied to BiConsumer was lost or altered."); Assert.assertEquals(Buffer.get().toString(), "supplyAsync-test-buffer-D", "Context type was not correctly propagated to contextual action."); Assert.assertEquals(Label.get(), "", "Context type that is configured to be cleared was not cleared."); }); Buffer.set(new StringBuffer("supplyAsync-test-buffer-D")); // use a countdown latch to independently wait for completion. CountDownLatch completed = new CountDownLatch(1); stage4.handleAsync((result, x) -> { completed.countDown(); return result; }, unmanagedThreads); Assert.assertTrue(completed.await(MAX_WAIT_NS, TimeUnit.NANOSECONDS), "Completable future did not finish in a reasonable amount of time."); Assert.assertEquals(stage1.get(10, TimeUnit.SECONDS), Long.valueOf(100), "Unexpected result for first stage."); Assert.assertEquals(stage2.join(), Long.valueOf(200), "Unexpected result for second stage."); Assert.assertEquals(stage3.getNow(33L), Long.valueOf(300), "Unexpected result for third stage."); Assert.assertNull(stage4.join(), "Unexpected result for fourth stage."); // Is context properly restored on current thread? Assert.assertEquals(Buffer.get().toString(), "supplyAsync-test-buffer-D", "Previous context was not restored after context was propagated for managed executor tasks."); Assert.assertEquals(Label.get(), "supplyAsync-test-label", "Previous context was not restored after context was cleared for managed executor tasks."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * Verify that thread context is captured and propagated per the configuration of the * ManagedExecutor builder for all tasks that are submitted via the ManagedExecutor's * timed invokeAll operation. Thread context is captured at the point where invokeAll is * invoked, rather than upon creation of the executor or construction of the builder. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void timedInvokeAllRunsTasksWithContext() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .propagated(Buffer.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { // Set non-default values Buffer.set(new StringBuffer("timed-invokeAll-test-buffer-A")); Label.set("timed-invokeAll-test-label-A"); List<Future<String>> futures = executor.invokeAll(Arrays.<Callable<String>>asList( () -> { Assert.assertEquals(Buffer.get().toString(), "timed-invokeAll-test-buffer-A", "Context type was not propagated to contextual action."); Assert.assertEquals(Label.get(), "", "Context type that is configured to be cleared was not cleared."); Label.set("timed-invokeAll-test-label-B"); Buffer.set(new StringBuffer("timed-invokeAll-test-buffer-B")); return "B"; }, () -> { Assert.assertEquals(Buffer.get().toString(), "timed-invokeAll-test-buffer-A", "Context type was not propagated to contextual action."); Assert.assertEquals(Label.get(), "", "Context type that is configured to be cleared was not cleared."); Label.set("timed-invokeAll-test-label-C"); Buffer.set(new StringBuffer("invokeAll-test-buffer-C")); return "C"; }), MAX_WAIT_NS, TimeUnit.NANOSECONDS); Assert.assertEquals(futures.size(), 2, "Number of futures does not match the number of tasks. " + futures); Assert.assertTrue(futures.get(0).isDone(), "Future for first task does not indicate it is done after invokeAll."); Assert.assertEquals(futures.get(0).get(), "B", "Future for first task returned wrong value."); Assert.assertTrue(futures.get(1).isDone(), "Future for second task does not indicate it is done after invokeAll."); Assert.assertEquals(futures.get(1).get(1, TimeUnit.SECONDS), "C", "Future for second task returned wrong value."); Assert.assertEquals(Label.get(), "timed-invokeAll-test-label-A", "Previous context was not restored after context was propagated for managed executor tasks."); Assert.assertEquals(Buffer.get().toString(), "timed-invokeAll-test-buffer-A", "Previous context was not restored after context was cleared for managed executor tasks."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * Verify that thread context is captured and propagated per the configuration of the * ManagedExecutor builder for one or more tasks that are submitted via the ManagedExecutor's * timed invokeAny operation. Thread context is captured at the point where invokeAny is * invoked, rather than upon creation of the executor or construction of the builder. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void timedInvokeAnyRunsTaskWithContext() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .propagated(Buffer.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { // Set non-default values Buffer.set(new StringBuffer("timed-invokeAny-test-buffer-A")); Label.set("timed-invokeAny-test-label-A"); Character result = executor.invokeAny(Arrays.<Callable<Character>>asList( () -> { Assert.assertEquals(Buffer.get().toString(), "timed-invokeAny-test-buffer-A", "Context type was not propagated to contextual action."); Assert.assertEquals(Label.get(), "", "Context type that is configured to be cleared was not cleared."); Label.set("timed-invokeAny-test-label-B"); Buffer.set(new StringBuffer("timed-invokeAny-test-buffer-B")); return 'b'; }, () -> { Assert.assertEquals(Buffer.get().toString(), "timed-invokeAny-test-buffer-A", "Context type was not propagated to contextual action."); Assert.assertEquals(Label.get(), "", "Context type that is configured to be cleared was not cleared."); Label.set("timed-invokeAny-test-label-C"); Buffer.set(new StringBuffer("invokeAny-test-buffer-C")); return 'c'; }), MAX_WAIT_NS, TimeUnit.NANOSECONDS); Assert.assertTrue(Character.valueOf('b').equals(result) || Character.valueOf('c').equals(result), "Result of invokeAny, " + result + ", does not match the result of either of the tasks."); Assert.assertEquals(Label.get(), "timed-invokeAny-test-label-A", "Previous context was not restored after context was propagated for managed executor tasks."); Assert.assertEquals(Buffer.get().toString(), "timed-invokeAny-test-buffer-A", "Previous context was not restored after context was cleared for managed executor tasks."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * Verify that thread context is captured and propagated per the configuration of the * ManagedExecutor builder for all tasks that are submitted via the ManagedExecutor's * untimed invokeAll operation. Thread context is captured at the point where invokeAll is * invoked, rather than upon creation of the executor or construction of the builder. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void untimedInvokeAllRunsTasksWithContext() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .maxAsync(1) .propagated(Label.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); // Verify that maxAsync=1 is enforced by recording the thread id upon which // the managed executor runs tasks asynchronous to the requesting thread. // If this value is ever found to be non-zero when set to a new thread id, // this indicates that multiple tasks are running asynchronously to the // requesting thread, which is a violation of maxAsync=1. AtomicLong asyncThreadIdRef = new AtomicLong(); long testThreadId = Thread.currentThread().getId(); try { // Set non-default values Buffer.set(new StringBuffer("untimed-invokeAll-test-buffer-A")); Label.set("untimed-invokeAll-test-label-A"); List<Future<Integer>> futures = executor.invokeAll(Arrays.<Callable<Integer>>asList( () -> { long threadId = Thread.currentThread().getId(); if (threadId != testThreadId) { Assert.assertEquals(asyncThreadIdRef.getAndSet(threadId), 0L, "Thread ID indicates that ManagedExecutor invokeAll operation exceeded maxAsync of 1."); } Assert.assertEquals(Label.get(), "untimed-invokeAll-test-label-A", "Context type was not propagated to contextual action."); Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Label.set("untimed-invokeAll-test-label-B"); Buffer.set(new StringBuffer("untimed-invokeAll-test-buffer-B")); asyncThreadIdRef.compareAndSet(threadId, 0L); return 66; }, () -> { long threadId = Thread.currentThread().getId(); if (threadId != testThreadId) { Assert.assertEquals(asyncThreadIdRef.getAndSet(threadId), 0L, "Thread ID indicates that ManagedExecutor invokeAll operation exceeded maxAsync of 1."); } Assert.assertEquals(Label.get(), "untimed-invokeAll-test-label-A", "Context type was not propagated to contextual action."); Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Label.set("untimed-invokeAll-test-label-C"); Buffer.set(new StringBuffer("uninvokeAll-test-buffer-C")); asyncThreadIdRef.compareAndSet(threadId, 0L); return 67; }, () -> { long threadId = Thread.currentThread().getId(); if (threadId != testThreadId) { Assert.assertEquals(asyncThreadIdRef.getAndSet(threadId), 0L, "Thread ID indicates that ManagedExecutor invokeAll operation exceeded maxAsync of 1."); } Assert.assertEquals(Label.get(), "untimed-invokeAll-test-label-A", "Context type was not propagated to contextual action."); Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Label.set("untimed-invokeAll-test-label-D"); Buffer.set(new StringBuffer("untimed-invokeAll-test-buffer-D")); asyncThreadIdRef.compareAndSet(threadId, 0L); return 68; }, () -> { long threadId = Thread.currentThread().getId(); if (threadId != testThreadId) { Assert.assertEquals(asyncThreadIdRef.getAndSet(threadId), 0L, "Thread ID indicates that ManagedExecutor invokeAll operation exceeded maxAsync of 1."); } Assert.assertEquals(Label.get(), "untimed-invokeAll-test-label-A", "Context type was not propagated to contextual action."); Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Label.set("untimed-invokeAll-test-label-E"); Buffer.set(new StringBuffer("untimed-invokeAll-test-buffer-E")); asyncThreadIdRef.compareAndSet(threadId, 0L); return 69; }), MAX_WAIT_NS, TimeUnit.NANOSECONDS); Assert.assertEquals(futures.size(), 4, "Number of futures does not match the number of tasks. " + futures); Assert.assertTrue(futures.get(0).isDone(), "Future for first task does not indicate it is done after invokeAll."); Assert.assertEquals(futures.get(0).get(), Integer.valueOf(66), "Future for first task returned wrong value."); Assert.assertTrue(futures.get(1).isDone(), "Future for second task does not indicate it is done after invokeAll."); Assert.assertEquals(futures.get(1).get(1, TimeUnit.SECONDS), Integer.valueOf(67), "Future for second task returned wrong value."); Assert.assertTrue(futures.get(2).isDone(), "Future for third task does not indicate it is done after invokeAll."); Assert.assertEquals(futures.get(2).get(), Integer.valueOf(68), "Future for third task returned wrong value."); Assert.assertTrue(futures.get(3).isDone(), "Future for fourth task does not indicate it is done after invokeAll."); Assert.assertEquals(futures.get(3).get(), Integer.valueOf(69), "Future for fourth task returned wrong value."); Assert.assertEquals(Label.get(), "untimed-invokeAll-test-label-A", "Previous context was not restored after context was propagated for managed executor tasks."); Assert.assertEquals(Buffer.get().toString(), "untimed-invokeAll-test-buffer-A", "Previous context was not restored after context was cleared for managed executor tasks."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } /** * Verify that thread context is captured and propagated per the configuration of the * ManagedExecutor builder for one or more tasks that are submitted via the ManagedExecutor's * untimed invokeAny operation. Thread context is captured at the point where invokeAny is * invoked, rather than upon creation of the executor or construction of the builder. * * @throws ExecutionException indicates test failure * @throws InterruptedException indicates test failure * @throws TimeoutException indicates test failure */ @Test public void untimedInvokeAnyRunsTasksWithContext() throws ExecutionException, InterruptedException, TimeoutException { ManagedExecutor executor = ManagedExecutor.builder() .maxAsync(1) .propagated(Label.CONTEXT_NAME) .cleared(ThreadContext.ALL_REMAINING) .build(); try { // Set non-default values Buffer.set(new StringBuffer("untimed-invokeAny-test-buffer-A")); Label.set("untimed-invokeAny-test-label-A"); String result = executor.invokeAny(Arrays.<Callable<String>>asList( () -> { Assert.assertEquals(Label.get(), "untimed-invokeAny-test-label-A", "Context type was not propagated to contextual action."); Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Label.set("untimed-invokeAny-test-label-B"); Buffer.set(new StringBuffer("untimed-invokeAny-test-buffer-B")); return "Bb"; }, () -> { Assert.assertEquals(Label.get(), "untimed-invokeAny-test-label-A", "Context type was not propagated to contextual action."); Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Label.set("untimed-invokeAny-test-label-C"); Buffer.set(new StringBuffer("uninvokeAny-test-buffer-C")); return "Cc"; }, () -> { Assert.assertEquals(Label.get(), "untimed-invokeAny-test-label-A", "Context type was not propagated to contextual action."); Assert.assertEquals(Buffer.get().toString(), "", "Context type that is configured to be cleared was not cleared."); Label.set("untimed-invokeAny-test-label-D"); Buffer.set(new StringBuffer("untimed-invokeAny-test-buffer-D")); return "Dd"; }), MAX_WAIT_NS, TimeUnit.NANOSECONDS); Assert.assertTrue("Bb".equals(result) || "Cc".equals(result) || "Dd".equals(result), "Result of invokeAny, " + result + ", does not match the result of any of the tasks."); Assert.assertEquals(Label.get(), "untimed-invokeAny-test-label-A", "Previous context was not restored after context was propagated for managed executor tasks."); Assert.assertEquals(Buffer.get().toString(), "untimed-invokeAny-test-buffer-A", "Previous context was not restored after context was cleared for managed executor tasks."); } finally { executor.shutdownNow(); // Restore original values Buffer.set(null); Label.set(null); } } }