MicroProfile Concurrency in Open Liberty development builds

Completion stages and completable futures (introduced in Java SE 8) enable you to chain together pipelines of dependent actions with the ability to run sequentially or in parallel, depending on how the API is used. Execution of a dependent stage is triggered by the completion of the stage (or stages) upon which it depends. While this capability provided by completion stages can be extremely useful in enabling applications to implement a model where they react to events as they happen, the built-in implementation (completable future) neglects several important areas: consistency and reliability of thread context and defaulting asynchronous dependent stage actions to run on the Liberty global thread pool. MicroProfile Concurrency introduces the ManagedExecutor and ThreadContext API to address these problems. An implementation of the MicroProfile Concurrency specification is now available in the Open Liberty development builds.

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

MicroProfile Concurrency provides the org.eclipse.microprofile.concurrent.ThreadContext interface to facilitate the use of unmanaged completion stages. 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 Concurrency ThreadContext. For example,

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

How to obtain ManagedExecutor and ThreadContext instances

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

  • MicroProfile Concurrency 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 should 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 Concurrency also lets you configure and inject instances via CDI (requires the cdi-2.0 feature or higher).

    • The container creates a ManagedExecutor instance per unqualified ManagedExecutor injection point. For example: @Inject ManagedExecutor executor1;

    • You can optionally configure an injected instance by specifying the ManagedExecutorConfig annotation. For example: @Inject @ManagedExecutorConfig(propagated=ThreadContext.APPLICATION) executor2;

    • MicroProfile Concurrency provides the NamedInstance qualifier as a convenience for sharing the same instance across injection points:

      @Produces @ApplicationScoped @NamedInstance("myExecutor")
      protected ManagedExecutor getExecutor(@ManagedExecutorConfig(maxAsync = 5) ManagedExecutor exec) {
          return exec;
      }
      
      @Inject @NamedInstance("myExecutor") ManagedExecutor exec3;
      
      @Inject @NamedInstance("myExecutor") ManagedExecutor exec4;

      If you enable MicroProfile Config (mpConfig-1.3 feature or higher), you can override the configuration of instances that are created by the container for CDI injection by defining properties in MicroProfile Config, that follow naming conventions that are defined by the MicroProfile Concurrency specification. For example, if the injection points from the previous examples are defined within a bean named, org.example.MyBean, then you could override as follows in the application’s microprofile-config.properties or other MicroProfile Config source:

      org.example.MyBean/executor1/ManagedExecutor/maxAsync=15
      org.example.MyBean/executor1/ManagedExecutor/propagated=Application,Security
      org.example.MyBean/executor2/ManagedExecutor/maxAsync=20
      org.example.MyBean/getExecutor/1/ManagedExecutor/propagated=
      org.example.MyBean/getExecutor/1/ManagedExecutor/cleared=Remaining

MicroProfile Concurrency 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.

Share this post: