back to all blogsSee all blog posts

MicroProfile Context Propagation in Open Liberty

image of author
Nathan Rauh on Aug 16, 2019
Post available in languages:

CompletionStage and CompletableFuture (first introduced in Java SE 8) enable you to chain together pipelines of dependent actions, where execution of each dependent stage is triggered by the completion of the stage(s) upon which that stage depends. MicroProfile Context Propagation builds upon this model, giving dependent stage actions consistent and reliable thread context and defaulting asynchronous dependent stage actions to run on the Liberty global thread pool. MicroProfile Context Propagation introduces the ManagedExecutor and ThreadContext API to accomplish this. An implementation of the MicroProfile Context Propagation specification is now available in Open Liberty 19.0.0.8.

Managed executors in MicroProfile Context Propagation allow you to use completion stages that run with predictable thread context regardless of which thread the action ends up running on. Without this feature, thread context of a completion stage action depends on whether the action runs on the requesting thread, the thread of a stage upon which it depends, a thread that is requesting the result, or a thread that forcibly completes the prior stage. There is no guarantee under which context the action will run. However, with MicroProfile Context Propagation, the thread context is completely deterministic because context is always captured from the thread that creates the completion stage and applied when running the action.

Alternatively, you can configure to clear certain context types rather than capturing context. For example, clearing the security context means that no user is associated with the thread while the completion stage action runs. When a completion stage is created by a managed executor, the managed executor remains associated with the completion stage and determines thread context propagation for dependent stages that are requested to run asynchronously without designating a specific executor. The managed executor remains associated with each dependent stage created, and each dependent stage created from those, and so forth, allowing for predictable thread context propagation at every stage in the pipeline.

Here is an example of a managed executor that is used to propagate the application’s namespace to an asynchronous action,

CompletableFuture<Integer> stage = executor.supplyAsync(supplier1)
    .thenApply(function1)
    .thenApply(function2)
    .thenApply(i -> {
        try {
            DataSource ds = InitialContext.doLookup("java:module/env/jdbc/ds1");
            ...
            return result;
        } catch (Exception x) {
           throw new CompletionExeption(x);
        }
    });

Managed executors in MicroProfile Context Propagation are fully compatible with ManagedExecutorService in Jakarta/Java EE Concurrency, leaving open the possibility that the programming models could one day be merged. Notably, the org.eclipse.microprofile.context.ManagedExecutor interface inherits from java.util.concurrent.ExecutorService, and thus allows the same execute/submit/invoke operations as ManagedExecutorService.

MicroProfile Context Propagation provides the org.eclipse.microprofile.concurrent.ThreadContext interface to pre-contextualize completion stage actions. This is useful if you have an umanaged completion stage that is not thread-context aware. When you have a completion stage that isn’t created by a managed executor, it can still run with predictable thread context if you pre-contextualize its action with the corresponding method of MicroProfile Context Propagation ThreadContext. For example,

CompletableFuture<Long> stage = CompletableFuture.supplyAsync(supplier1)
    .thenApplyAsync(function1)
    .thenApply(threadContext.contextualFunction(function2));

In the example above, supplier1 and function1 run with non-deterministic thread context. However, the pre-contextualized action, function2, always runs with the context of the thread that invoked the contextualFunction operation.

How to obtain ManagedExecutor and ThreadContext instances

There are a variety of ways to obtain instances of ManagedExecutor and ThreadContext:

  • MicroProfile Context Propagation offers a fluent builder pattern for programmatic usage:

    ManagedExecutor executor = ManagedExecutor.builder()
        .maxAsync(10)
        .propagated(ThreadContext.APPLICATION, ThreadContext.SECURITY)
        .cleared(ThreadContext.ALL_REMAINING)
        .build();

    Applications must take care to shut down executor instances that they build, once the executor instance is no longer needed.

  • If you are using Java EE Concurrency, you can cast your existing ManagedExecutorService to ManagedExecutor.

  • MicroProfile Context Propagation also lets you configure and inject instances via CDI (requires the cdi-2.0 feature or higher).

    Example usage in a CDI bean:

    // CDI qualifier which is used to identify the executor instance
    @Qualifier
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER })
    public @interface AppContext {}
    
    // Example producer field, defined in a CDI bean,
    @Produces @ApplicationScoped @AppContext
    ManagedExecutor appContextExecutor = ManagedExecutor.builder()
        .propagated(ThreadContext.APPLICATION)
        .build();
    
    // Example disposer method, also defined in the CDI bean,
    void disposeExecutor(@Disposes @AppContext exec) {
        exec.shutdownNow();
    }
    
    // Example injection point, defined in a CDI bean,
    @Inject @AppContext
    ManagedExecutor executor;
    
    ...
    
    CompletableFuture<Integer> stage = executor
        .supplyAsync(supplier1)
        .thenApply(function1)
        .thenApplyAsync(value -> {
            try {
                // access resource reference in application's java:comp namespace,
                DataSource ds = InitialContext.doLookup("java:comp/env/jdbc/ds1");
                ...
                return result;
            } catch (Exception x) {
                throw new CompletionException(x);
            }
        });

Conclusion

MicroProfile Context Propagation builds upon Java SE CompletableFuture and builds out the necessary infrastructure around it, empowering you to build robust applications that react to events as they happen, under a dependable thread context and backed by the performance of Liberty threading.