All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ee.jakarta.tck.concurrent.spec.ContextService.contextPropagate.ContextServiceDefinitionServlet Maven / Gradle / Ivy

/*
 * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */
package ee.jakarta.tck.concurrent.spec.ContextService.contextPropagate;

import static jakarta.enterprise.concurrent.ContextServiceDefinition.ALL_REMAINING;
import static jakarta.enterprise.concurrent.ContextServiceDefinition.APPLICATION;
import static jakarta.enterprise.concurrent.ContextServiceDefinition.TRANSACTION;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.AbstractMap.SimpleEntry;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SubmissionPublisher;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.Flow.Processor;
import java.util.concurrent.Flow.Subscription;
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.naming.InitialContext;
import javax.naming.NamingException;

import ee.jakarta.tck.concurrent.common.context.IntContext;
import ee.jakarta.tck.concurrent.common.context.StringContext;
import ee.jakarta.tck.concurrent.framework.TestServlet;
import jakarta.annotation.Resource;
import jakarta.enterprise.concurrent.ContextService;
import jakarta.enterprise.concurrent.ContextServiceDefinition;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.transaction.Status;
import jakarta.transaction.SystemException;
import jakarta.transaction.UserTransaction;

@ContextServiceDefinition(name = "java:app/concurrent/ContextA", propagated = { APPLICATION,
        IntContext.NAME }, cleared = StringContext.NAME, unchanged = TRANSACTION)
@ContextServiceDefinition(name = "java:module/concurrent/ContextB", cleared = TRANSACTION, unchanged = { APPLICATION,
        IntContext.NAME }, propagated = ALL_REMAINING)
@ContextServiceDefinition(name = "java:comp/concurrent/ContextC")
@WebServlet("/ContextServiceDefinitionServlet")
public class ContextServiceDefinitionServlet extends TestServlet {
    private static final long serialVersionUID = 1L;
    private static final long MAX_WAIT_SECONDS = TimeUnit.MINUTES.toSeconds(2);

    /**
     * Java SE thread pool with threads that lack context.
     */
    private ExecutorService unmanagedThreads;

    @Resource
    private UserTransaction tx;

    @Override
    public void destroy() {
        unmanagedThreads.shutdownNow();
    }

    @Override
    public void before() throws ServletException {
        unmanagedThreads = Executors.newFixedThreadPool(10);
    }

    /**
     * A ContextServiceDefinition with all attributes configured
     * propagates/clears/ignores context types as configured. ContextA, which is
     * tested here, propagates Application context and IntContext, clears
     * StringContext, and leaves Transaction context unchanged.
     */
    public void testContextServiceDefinitionAllAttributes() throws Throwable {
        ContextService contextServiceA = InitialContext.doLookup("java:app/concurrent/ContextA");

        BiFunction fn = (results, jndiName) -> {
            try {
                results[0] = InitialContext.doLookup(jndiName);
            } catch (Throwable x) {
                results[0] = x;
            }
            results[1] = IntContext.get();
            results[2] = StringContext.get();
            try {
                results[3] = tx.getStatus();
            } catch (SystemException x) {
                throw new CompletionException(x);
            }
            return results;
        };

        CompletableFuture future;
        try {
            StringContext.set("testContextServiceDefinitionAllAttributes-1");
            IntContext.set(101);

            BiFunction contextualFunction = contextServiceA.contextualFunction(fn);

            future = CompletableFuture.completedFuture(new Object[4]).thenCombineAsync(
                    CompletableFuture.completedFuture("java:app/concurrent/ContextA"), contextualFunction,
                    unmanagedThreads);

            // change context of the current thread
            StringContext.set("testContextServiceDefinitionAllAttributes-2");
            IntContext.set(102);
            tx.begin();

            // run inline
            Object[] results = contextualFunction.apply(new Object[4], "java:app/concurrent/ContextA");
            if (results[0] instanceof Throwable)
                throw new AssertionError("Application context must be propagated to inline contextual BiFunction "
                        + "to perform lookup of java:app/concurrent/ContextA").initCause((Throwable) results[0]);
            assertTrue(results[0] instanceof ContextService,
                    "Application context must be propagated to inline contextual BiFunction "
                            + "per java:app/concurrent/ContextA configuration.");
            assertEquals(results[1], Integer.valueOf(101),
                    "Third-party context type IntContext must be propagated to inline contextual BiFunction "
                            + "per java:app/concurrent/ContextA configuration.");
            assertEquals(results[2], "",
                    "Third-party context type StringContext must be cleared from inline contextual BiFunction "
                            + "per java:app/concurrent/ContextA configuration.");
            assertEquals(results[3], Integer.valueOf(Status.STATUS_ACTIVE),
                    "Transaction context must be left unchanged on inline contextual BiFunction "
                            + "per java:app/concurrent/ContextA configuration.");

            // context from before the inline contextual BiFunction must be restored to
            // thread
            assertNotNull(InitialContext.doLookup("java:app/concurrent/ContextA"),
                    "Previous Application context must be present after inline contextual BiFunction.");
            assertEquals(IntContext.get(), 102,
                    "Third-party context type IntContext must be restored after inline contextual BiFunction.");
            assertEquals(StringContext.get(), "testContextServiceDefinitionAllAttributes-2",
                    "Third-party context type StringContext must be restored after inline contextual BiFunction.");
            assertEquals(tx.getStatus(), Status.STATUS_ACTIVE,
                    "Transaction context must remain on thread after inline contextual BiFunction "
                            + "because it is to be left unchanged per java:app/concurrent/ContextA configuration.");
        } finally {
            StringContext.set("");
            IntContext.set(0);
            if (tx.getStatus() != Status.STATUS_NO_TRANSACTION)
                tx.rollback();
        }

        Object[] results = future.get(MAX_WAIT_SECONDS, TimeUnit.SECONDS);

        if (results[0] instanceof Throwable)
            throw new AssertionError("Application context must be propagated to async contextual BiFunction "
                    + "to perform lookup of java:app/concurrent/ContextA").initCause((Throwable) results[0]);
        assertTrue(results[0] instanceof ContextService,
                "Application context must be propagated to async contextual BiFunction "
                        + "per java:app/concurrent/ContextA configuration.");
        assertEquals(results[1], Integer.valueOf(101),
                "Third-party context type IntContext must be propagated to async contextual BiFunction "
                        + "per java:app/concurrent/ContextA configuration.");
        assertEquals(results[2], "",
                "Third-party context type StringContext must be cleared from async contextual BiFunction "
                        + "per java:app/concurrent/ContextA configuration.");
        assertEquals(results[3], Integer.valueOf(Status.STATUS_NO_TRANSACTION),
                "Transaction context must be left unchanged on async contextual BiFunction "
                        + "per java:app/concurrent/ContextA configuration.");
    }

    /**
     * A ContextServiceDefinition with minimal attributes configured clears
     * transaction context and propagates other types.
     */
    public void testContextServiceDefinitionDefaults() throws Throwable {
        ContextService contextService = InitialContext.doLookup("java:comp/concurrent/ContextC");

        LinkedBlockingQueue results = new LinkedBlockingQueue();
        try {
            IntContext.set(13);
            new Thread(contextService.contextualRunnable(() -> {
                results.add(IntContext.get());
                try {
                    results.add(InitialContext.doLookup("java:comp/concurrent/ContextC"));
                } catch (Throwable x) {
                    results.add(x);
                }
            })).start();
        } finally {
            IntContext.set(0);
        }

        tx.begin();
        try {
            StringContext.set("testContextServiceDefinitionDefaults-1");

            Callable callable = contextService.contextualCallable(() -> {
                // Transaction context is cleared by default, so we must be
                // able to start another transaction inline:
                UserTransaction tran = InitialContext.doLookup("java:comp/UserTransaction");
                tran.begin();
                tran.commit();
                return StringContext.get();
            });

            StringContext.set("testContextServiceDefinitionDefaults-2");

            assertEquals(callable.call(), "testContextServiceDefinitionDefaults-1",
                    "Third-party context type StringContext must be propagated to contextual Callable.");

            assertEquals(tx.getStatus(), Status.STATUS_ACTIVE,
                    "Transaction must be restored on thread after contextual proxy completes.");
        } finally {
            StringContext.set(null);
            tx.rollback();
        }

        Object result;
        
        result = results.poll(MAX_WAIT_SECONDS, TimeUnit.SECONDS);
        assertNotNull(result, "Contextual runnable did not start on thread.");
        assertEquals(result, Integer.valueOf(13),
                "Third-party context type IntContext must be propagated to contextual Runnable.");

        result = results.poll(MAX_WAIT_SECONDS, TimeUnit.SECONDS);
        assertNotNull(result, "Contextual runnable did not complete on thread.");
        if (result instanceof Throwable)
            throw new AssertionError("Unable to look up java:comp name from contextual Runnable.")
                    .initCause((Throwable) result);
        assertTrue(result instanceof ContextService,
                "Application context must be propagated to contextual Runnable, but instead lookup found: " + result);
    }

    /**
     * A ContextServiceDefinition can specify a third-party context type to be
     * propagated/cleared/ignored. This test uses 2 ContextServiceDefinitions:
     * ContextA with IntContext propagated and StringContext cleared. ContextB with
     * IntContext unchanged and StringContext propagated (per ALL_REMAINING).
     */
    public void testContextServiceDefinitionWithThirdPartyContext() throws Throwable {
        try {
            StringContext.set("testCSDWithThirdPartyContext-1");
            IntContext.set(31);

            ContextService contextServiceA = InitialContext.doLookup("java:app/concurrent/ContextA");
            BiConsumer, Queue> consumerA = contextServiceA.contextualConsumer((intQ, strQ) -> {
                intQ.add(IntContext.get());
                strQ.add(StringContext.get());
            });
            BlockingQueue queueA = new LinkedBlockingQueue();
            CompletableFuture> qFutureA = CompletableFuture.completedFuture(queueA);
            qFutureA.thenAcceptBothAsync(qFutureA, consumerA, unmanagedThreads);

            ContextService contextServiceB = InitialContext.doLookup("java:module/concurrent/ContextB");
            BiConsumer, Queue> consumerB = contextServiceB.contextualConsumer((intQ, strQ) -> {
                intQ.add(IntContext.get());
                strQ.add(StringContext.get());
            });
            BlockingQueue queueB = new LinkedBlockingQueue();
            CompletableFuture> qFutureB = CompletableFuture.completedFuture(queueB);
            qFutureB.thenAcceptBothAsync(qFutureB, consumerB);

            StringContext.set("testCSDWithThirdPartyContext-2");
            IntContext.set(32);

            // Run inline
            Queue results = new LinkedList();
            consumerA.accept(results, results);
            assertEquals(results.poll(), Integer.valueOf(31),
                    "Third-party context type IntContext must be propagated to inline contextual BiConsumer "
                            + "per java:app/concurrent/ContextA configuration.");
            assertEquals(results.poll(), "",
                    "Third-party context type StringContext must be cleared from inline contextual BiConsumer "
                            + "per java:app/concurrent/ContextA configuration.");

            consumerB.accept(results, results);
            assertEquals(results.poll(), Integer.valueOf(32),
                    "Third-party context type IntContext must be left unchanged on inline contextual BiConsumer "
                            + "per java:module/concurrent/ContextB configuration.");
            assertEquals(results.poll(), "testCSDWithThirdPartyContext-1",
                    "Third-party context type StringContext must be propagated to inline contextual BiConsumer "
                            + "per java:module/concurrent/ContextB configuration.");

            // Check the thread context of async consumers
            assertEquals(queueA.poll(MAX_WAIT_SECONDS, TimeUnit.SECONDS), Integer.valueOf(31),
                    "Third-party context type IntContext must be propagated to async contextual BiConsumer "
                            + "per java:app/concurrent/ContextA configuration.");
            assertEquals(queueA.poll(MAX_WAIT_SECONDS, TimeUnit.SECONDS), "",
                    "Third-party context type StringContext must be cleared from async contextual BiConsumer "
                            + "per java:app/concurrent/ContextA configuration.");

            assertEquals(queueB.poll(MAX_WAIT_SECONDS, TimeUnit.SECONDS), Integer.valueOf(0),
                    "Third-party context type IntContext must be left unchanged on async contextual BiConsumer "
                            + "per java:module/concurrent/ContextB configuration.");
            assertEquals(queueB.poll(MAX_WAIT_SECONDS, TimeUnit.SECONDS), "testCSDWithThirdPartyContext-1",
                    "Third-party context type StringContext must be propagated to async contextual BiConsumer "
                            + "per java:module/concurrent/ContextB configuration.");
        } finally {
            StringContext.set(null);
            IntContext.set(0);
        }
    }

    /**
     * A ContextService contextualizes a Consumer, which can be supplied as a
     * dependent stage action to an unmanaged CompletableFuture. The dependent stage
     * action runs with the thread context of the thread that contextualizes the
     * Consumer, per the configuration of the ContextServiceDefinition.
     */
    public void testContextualConsumer() throws Throwable {
        ContextService contextService = InitialContext.doLookup("java:app/concurrent/ContextA");
        LinkedBlockingQueue results = new LinkedBlockingQueue();
        try {
            StringContext.set("testContextualConsumer-1");
            IntContext.set(121);

            Consumer> contextualConsumer = contextService.contextualConsumer(queue -> {
                queue.add(IntContext.get());
                queue.add(StringContext.get());
                try {
                    queue.add(InitialContext.doLookup("java:app/concurrent/ContextA"));
                } catch (Throwable x) {
                    queue.add(x);
                }
            });

            CompletableFuture.completedFuture(results).thenAcceptAsync(contextualConsumer, unmanagedThreads);
        } finally {
            StringContext.set(null);
            IntContext.set(0);
        }

        Object result;
        
        result = results.poll(MAX_WAIT_SECONDS, TimeUnit.SECONDS);
        assertNotNull(result, "Contextual Consumer did not start running.");
        assertEquals(result, Integer.valueOf(121),
                "Third-party context type IntContext must be propagated to contextual Consumer "
                        + "per java:app/concurrent/ContextA configuration.");
        
        result = results.poll(MAX_WAIT_SECONDS, TimeUnit.SECONDS);
        assertNotNull(result, "Contextual Consumer did not continue running.");
        assertEquals(result, "", "Third-party context type StringContext must be cleared on contextual Consumer "
                + "per java:app/concurrent/ContextA configuration.");
        
        result = results.poll(MAX_WAIT_SECONDS, TimeUnit.SECONDS);
        assertNotNull(result, "Contextual Consumer did not complete.");
        if (result instanceof Throwable)
            throw new AssertionError("Application context must be propagated to contextual Consumer")
                    .initCause((Throwable) result);
        assertTrue(result instanceof ContextService, "Application context must be propagated to contextual Consumer "
                + "per java:app/concurrent/ContextA configuration.");
    }

    /**
     * A ContextService contextualizes a Function, which can be supplied as a
     * dependent stage action to an unmanaged CompletableFuture. The dependent stage
     * action runs with the thread context of the thread that contextualizes the
     * Function, per the configuration of the ContextServiceDefinition.
     */
    public void testContextualFunction() throws Throwable {
        ContextService contextService = InitialContext.doLookup("java:module/concurrent/ContextB");
        CompletableFuture future;
        tx.begin();
        try {
            StringContext.set("testContextualFunction-1");
            IntContext.set(151);

            Function contextualFunction = contextService.contextualFunction(jndiName -> {
                Object[] results = new Object[4];
                try {
                    results[0] = InitialContext.doLookup(jndiName);
                } catch (NamingException x) {
                    results[0] = x;
                }
                results[1] = IntContext.get();
                results[2] = StringContext.get();
                try {
                    results[3] = tx.getStatus();
                } catch (SystemException x) {
                    throw new CompletionException(x);
                }
                return results;
            });

            future = CompletableFuture.completedFuture("java:module/concurrent/ContextB")
                    .thenApplyAsync(contextualFunction);
        } finally {
            StringContext.set(null);
            IntContext.set(0);
            tx.rollback();
        }

        Object[] results = future.get(MAX_WAIT_SECONDS, TimeUnit.SECONDS);
        assertTrue(results[0] instanceof NamingException,
                "Application context must remain unchanged on contextual Function "
                        + "per java:module/concurrent/ContextB configuration. Result: " + results[0]);
        assertEquals(results[1], Integer.valueOf(0),
                "Third-party context type IntContext must remain unchanged on contextual Function "
                        + "per java:module/concurrent/ContextB configuration.");
        assertEquals(results[2], "testContextualFunction-1",
                "Third-party context type StringContext must be propagated to contextual Function "
                        + "per java:module/concurrent/ContextB configuration.");
        assertEquals(results[3], Integer.valueOf(Status.STATUS_NO_TRANSACTION),
                "Transaction context must be cleared from contextual Function "
                        + "per java:module/concurrent/ContextB configuration.");
    }

    /**
     * A ContextService contextualizes a Supplier, which can be supplied as a
     * dependent stage action to an unmanaged CompletableFuture. The dependent stage
     * action runs with the thread context of the thread that contextualizes the
     * Supplier, per the configuration of the ContextServiceDefinition.
     */
    public void testContextualSupplier() throws Throwable {
        try {
            StringContext.set("testContextualSupplier-1");
            IntContext.set(61);

            ContextService contextServiceA = InitialContext.doLookup("java:app/concurrent/ContextA");
            Supplier> supplierA = contextServiceA
                    .contextualSupplier(() -> new SimpleEntry(IntContext.get(), StringContext.get()));
            CompletableFuture> futureA = CompletableFuture.supplyAsync(supplierA,
                    unmanagedThreads);

            StringContext.set("testContextualSupplier-2");
            IntContext.set(62);

            Map.Entry results = futureA.get(MAX_WAIT_SECONDS, TimeUnit.SECONDS);
            assertEquals(results.getKey(), Integer.valueOf(61),
                    "Third-party context type IntContext must be propagated to async contextual Supplier "
                            + "per java:app/concurrent/ContextA configuration.");
            assertEquals(results.getValue(), "",
                    "Third-party context type StringContext must be cleared from async contextual Supplier "
                            + "per java:app/concurrent/ContextA configuration.");
        } finally {
            StringContext.set(null);
            IntContext.set(0);
        }
    }

    /**
     * A ContextService contextualizes a Flow.Subscriber, which is subscribed to an unmanaged Flow.Producer.
     * The Flow.Subscriber methods are run with the thread context of the thread which contextualizes the Flow.Subscriber
     * per the configuration of the ContextServiceDefinition.
     *
     * publisher.subscribe triggers onSubscribe
     * publisher.offer triggers onNext
     * publisher.close (implicit from AutoClosable) triggers onComplete
     * publisher.closeExceptionally triggers onError
     */
    public void testContextualFlowSubscriber() throws Throwable {
        ContextService contextService = InitialContext.doLookup("java:app/concurrent/ContextA");
        CountDownLatch publisherClosed = new CountDownLatch(1);

        Subscriber subscriber = new Subscriber() {

                private Subscription subscription;
                
                @Override
                public void onSubscribe(final Subscription subscription) {
                    this.subscription = subscription;
                    assertEquals(IntContext.get(), Integer.valueOf(61),
                        "Third-party context type IntContext must be propagated to async contextual Flow.Subscriber "
                             + "per java:app/concurrent/ContextA configuration.");
                    assertEquals(StringContext.get(), "",
                        "Third-party context type StringContext must be cleared from async contextual Flow.Subscriber  "
                             + "per java:app/concurrent/ContextA configuration.");
                    subscription.request(1);
                }

                @Override
                public void onNext(final String item) {
                    assertEquals(IntContext.get(), Integer.valueOf(61),
                        "Third-party context type IntContext must be propagated to async contextual Flow.Subscriber  "
                            + "per java:app/concurrent/ContextA configuration.");
                    assertEquals(StringContext.get(), "",
                         "Third-party context type StringContext must be cleared from async contextual Flow.Subscriber  "
                             + "per java:app/concurrent/ContextA configuration.");
                    subscription.request(1);
                }

                @Override
                public void onError(final Throwable throwable) {
                    assertEquals(IntContext.get(), Integer.valueOf(61),
                        "Third-party context type IntContext must be propagated to async contextual Flow.Subscriber  "
                            + "per java:app/concurrent/ContextA configuration.");
                    assertEquals(StringContext.get(), "",
                        "Third-party context type StringContext must be cleared from async contextual Flow.Subscriber  "
                            + "per java:app/concurrent/ContextA configuration.");
                    publisherClosed.countDown();
                }

                @Override
                public void onComplete() {
                    assertEquals(IntContext.get(), Integer.valueOf(61),
                        "Third-party context type IntContext must be propagated to async contextual Flow.Subscriber  "
                             + "per java:app/concurrent/ContextA configuration.");
                    assertEquals(StringContext.get(), "",
                        "Third-party context type StringContext must be cleared from async contextual Flow.Subscriber  "
                            + "per java:app/concurrent/ContextA configuration.");
                    publisherClosed.countDown();
                }
                
            };


        try (SubmissionPublisher publisher =
                new SubmissionPublisher(unmanagedThreads, 2, null)) {
            StringContext.set("testContextualFlowSubscriber-1");
            IntContext.set(61);
               
            publisher.subscribe(contextService.contextualSubscriber(subscriber));
            
            publisher.offer("", MAX_WAIT_SECONDS, TimeUnit.SECONDS, null);
        } finally {
            StringContext.set(null);
            IntContext.set(0);
        }


        try (SubmissionPublisher publisher =
                 new SubmissionPublisher(unmanagedThreads, 2, null)) {
            
            StringContext.set("testContextualFlowSubscriber-1");
            IntContext.set(61);

            publisher.subscribe(contextService.contextualSubscriber(subscriber));

            publisher.closeExceptionally(new Exception("Test onError"));

        } finally {
            StringContext.set(null);
            IntContext.set(0);
        }
        
        assertTrue(publisherClosed.await(MAX_WAIT_SECONDS, TimeUnit.SECONDS));
    }

    /**
     * A ContextService contextualizes a Flow.Processor, which is subscribed to an unmanaged Flow.Producer.
     * The Flow.Subscriber methods are run with the thread context of the thread which contextualizes the Flow.Processor
     * per the configuration of the ContextServiceDefinition.
     *
     * processor.subscribe triggers subscribe
     * publisher.subscribe triggers onSubscribe
     * publisher.offer triggers onNext
     * publisher.close (implicit from AutoClosable) triggers onComplete
     * publisher.closeExceptionally triggers onError
     */
    public void testContextualFlowProcessor() throws Throwable {
        ContextService contextService = InitialContext.doLookup("java:app/concurrent/ContextA");
        CountDownLatch publisherClosed = new CountDownLatch(2);

        Subscriber emptySubscriber = new Subscriber() {

            private Subscription subscription;
            
            @Override
            public void onSubscribe(final Subscription subscription) {
                this.subscription = subscription;
                subscription.request(1);
            }

            @Override
            public void onNext(final String item) {
                subscription.request(1);
            }

            @Override
            public void onError(final Throwable throwable) {
                publisherClosed.countDown();
            }

            @Override
            public void onComplete() {
                publisherClosed.countDown();
            }
                
        };

        try (SubmissionPublisher publisher =
            new SubmissionPublisher(unmanagedThreads, 2, null)) {
            StringContext.set("testContextualFlowSubscriber-1");
            IntContext.set(61);

            Processor processor = contextService.contextualProcessor(new ProcessorImpl());
   
            processor.subscribe(emptySubscriber);
            publisher.subscribe(processor);

            publisher.offer("", MAX_WAIT_SECONDS, TimeUnit.SECONDS, null);

        } finally {
            StringContext.set(null);
            IntContext.set(0);
        }

        try (SubmissionPublisher publisher =
            new SubmissionPublisher(unmanagedThreads, 2, null)) {
            StringContext.set("testContextualFlowSubscriber-1");
            IntContext.set(61);

            Processor processor = contextService.contextualProcessor(new ProcessorImpl());

            processor.subscribe(emptySubscriber);
            publisher.subscribe(processor);

            publisher.closeExceptionally(new Exception("Test onError"));

        } finally {
            StringContext.set(null);
            IntContext.set(0);
        }

        assertTrue(publisherClosed.await(MAX_WAIT_SECONDS, TimeUnit.SECONDS));
    }

    /**
     * ContextService can create a contextualized copy of an unmanaged
     * CompletableFuture.
     */
    public void testCopyWithContextCapture() throws Throwable {
        ContextService contextService = InitialContext.doLookup("java:app/concurrent/ContextA");
        try {
            StringContext.set("testCopyWithContextCapture-1");
            IntContext.set(171);

            CompletableFuture stage1unmanaged = new CompletableFuture();
            CompletableFuture stage1copy = contextService.withContextCapture(stage1unmanaged);

            IntContext.set(172);

            CompletableFuture stage2 = stage1copy.exceptionally(failure -> {
                assertEquals(IntContext.get(), 172,
                        "Third-party context type IntContext must be propagated to the .exceptionally Function "
                                + "per java:app/concurrent/ContextA configuration.");
                return "java:app/concurrent/ContextA";
            });

            IntContext.set(173);

            CompletableFuture stage3 = stage2.thenAcceptAsync(jndiName -> {
                assertEquals(StringContext.get(), "",
                        "Third-party context type StringContext must be cleared from async contextual Consumer "
                                + "per java:app/concurrent/ContextA configuration.");
                try {
                    assertNotNull(InitialContext.doLookup("java:app/concurrent/ContextA"),
                            "Application context must be propagated to async contextual Consumer "
                                    + "per java:app/concurrent/ContextA configuration.");
                } catch (NamingException x) {
                    throw new CompletionException(x);
                }
            });

            stage1unmanaged.completeExceptionally(
                    new UnsupportedOperationException("Intentionally raised to force the .exceptionally code path"));

            assertEquals(stage3.get(MAX_WAIT_SECONDS, TimeUnit.SECONDS), null,
                    "Dependent stages should complete in response to the completion of the stage "
                            + "from which the first stage was copied.");

            assertTrue(stage1copy.isCompletedExceptionally(),
                    "Copied stage must complete the same way as the stage from which it is copied.");
        } finally {
            StringContext.set(null);
            IntContext.set(0);
        }
    }

    /**
     * Uses the JDKs SubmissionPublisher to avoid writing a publisher implementation for the processor to
     * extend. Overrides subscribe, the only Flow.Publisher api method inherited by Flow.Processor.
     */
    class ProcessorImpl extends SubmissionPublisher implements Processor {

        private Subscription processorSubscription;
        private Subscriber processorSubscriber;
                
        @Override
        public void onSubscribe(final Subscription subscription) {

            processorSubscription = subscription;
            assertEquals(IntContext.get(), Integer.valueOf(61),
                "Third-party context type IntContext must be propagated to async contextual Flow.Processor "
                     + "per java:app/concurrent/ContextA configuration.");
            assertEquals(StringContext.get(), "",
                "Third-party context type StringContext must be cleared from async contextual Flow.Processor "
                    + "per java:app/concurrent/ContextA configuration.");
            processorSubscription.request(1);
        }

        @Override
        public void onNext(final String item) {
            assertEquals(IntContext.get(), Integer.valueOf(61),
                "Third-party context type IntContext must be propagated to async contextual Flow.Processor "
                    + "per java:app/concurrent/ContextA configuration.");
            assertEquals(StringContext.get(), "",
                "Third-party context type StringContext must be cleared from async contextual Flow.Processor "
                     + "per java:app/concurrent/ContextA configuration.");
            processorSubscription.request(1);
        }

        @Override
        public void onError(final Throwable throwable) {
            assertEquals(IntContext.get(), Integer.valueOf(61),
                "Third-party context type IntContext must be propagated to async contextual Flow.Processor "
                        + "per java:app/concurrent/ContextA configuration.");
            assertEquals(StringContext.get(), "",
                "Third-party context type StringContext must be cleared from async contextual Flow.Processor "
                        + "per java:app/concurrent/ContextA configuration.");
            processorSubscriber.onError(throwable);
        }

        @Override
        public void onComplete() {
            assertEquals(IntContext.get(), Integer.valueOf(61),
                "Third-party context type IntContext must be propagated to async contextual Flow.Processor "
                        + "per java:app/concurrent/ContextA configuration.");
            assertEquals(StringContext.get(), "",
                "Third-party context type StringContext must be cleared from async contextual Flow.Processor "
                        + "per java:app/concurrent/ContextA configuration.");
            processorSubscriber.onComplete();
        }

        @Override
        public void subscribe(final Subscriber subscriber) {
            assertEquals(IntContext.get(), Integer.valueOf(61),
                "Third-party context type IntContext must be propagated to async contextual Flow.Processor "
                        + "per java:app/concurrent/ContextA configuration.");
            assertEquals(StringContext.get(), "",
                "Third-party context type StringContext must be cleared from async contextual Flow.Processor "
                        + "per java:app/concurrent/ContextA configuration.");
            processorSubscriber = subscriber;

        }

    }
}