Enhanced CDI contexts & bulkheads with MicroProfile Context Propagation
When using CDI with asynchronous execution methods, such as a ManagedExecutorService
, it’s traditionally not possible to access all of CDI’s scopes that were active in the originating thread. MicroProfile Context Propagation enables to define and pass thread execution contexts to completion stages where our code can access various CDI contexts despite being executed asynchronously. Additionally, Context Propagation allows to create managed executor services, that can be injected and used inside our beans, for example to realize bulkheads.
Enhanced CDI contexts
Let’s create and use a request-scoped bean that is being used during the handling of a request. With plain CDI, we would not be able to access and lookup the bean within an asynchronous execution.
Have a look at the following code:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | @ApplicationScoped @Path ( "contexts/example" ) public class ThreadContextExampleResource { @Inject ExampleStore exampleStore; @Inject ThreadContext threadContext; @Resource ManagedExecutorService mes; @Inject Notifier notifier; @PUT public void setExample(String example) { exampleStore.setExample(example); mes.execute(threadContext.contextualRunnable(notifier::notifyAbout)); } } |
01 02 03 04 05 06 07 08 09 10 11 12 13 | @RequestScoped public class ExampleStore { private String example; public String getExample() { return example; } public void setExample(String example) { this .example = example; } } |
1 2 3 4 5 6 7 8 9 | public class Notifier { @Inject ExampleStore exampleStore; public void notifyAbout() { System.out.println( "New example: " + exampleStore.getExample()); } } |
If a client PUT
s some content to the contexts/example
resource, the method will update the request-scoped ExampleStore
bean and execute the notification asynchronously, using the ManagedExecutorService
. In order to enable the asynchronous execution to lookup the request-scoped store, we are using the ThreadContext
to wrap the runnable with a context captured from the originating thread. This makes sure that the executed runnable can use the corresponding context.
We have to configure and produce a ThreadContext
depending on which type of contexts (e.g. CDI, transaction, security) we want to propagate:
1 2 3 4 5 6 7 8 9 | public class ThreadContextProducer { @Produces ThreadContext threadContext() { return ThreadContext.builder() .propagated(ThreadContext.ALL_REMAINING) .build(); } } |
This example will propagate all context types to the wrapped execution. Our bean then injects and uses the produced ThreadContext
.
Defining bulkheads using executors
MicroProfile Context Propagation allows to create and configure ManagedExecutor
s, a container-managed executor service similar to ManagedExecutorService
. We can create a ManagedExecutor
programmatically, set constraints on the allowed concurrency, and define a context propagation, as well.
By using dedicated executors for specific functionality, we can implement the bulkhead pattern, similar to using MicroProfile Fault Tolerance, or Porcupine.
Let’s define the following asynchronous JAX-RS resources:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | @ApplicationScoped @Path ( "bulkheads" ) public class BulkheadExampleResource { @Inject ExampleStore exampleStore; @Inject Notifier notifier; @Inject ManagedExecutor writeExecutor; @Inject ManagedExecutor readExecutor; @GET public CompletionStage<String> example() { return readExecutor.supplyAsync(exampleStore::getExample); } @PUT public CompletionStage<Void> setExample(String example) { return writeExecutor.runAsync(() -> { exampleStore.setExample(example); writeExecutor.execute(notifier::notifyAbout); }); } } |
We’re injecting two dedicated executors, that are used to run the corresponding functionalities. The executors are created using a producer:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | public class ManagedExecutorProducer { @Produces ManagedExecutor managedExecutor() { return ManagedExecutor.builder() .propagated(ThreadContext.CDI, ThreadContext.APPLICATION) .maxAsync( 4 ) .maxQueued( 4 ) .build(); } public void disposeManagedExecutor( @Disposes ManagedExecutor managedExecutor) { managedExecutor.shutdownNow(); } } |
Our executors will have upper bounds of four concurrently executed completion stages, and four tasks in the queue. The contexts of the CDI and application context types will be propagated to the executing threads.
When injecting the executors, be aware of the scope of the injection point; here we’re using an application-scoped resource, otherwise we might create more than two executors, which would defeat the purpose of the bulkhead pattern. Since we’re using CDI it’s of course possible to define additional qualifies if the created executors should be configured differently.
You can try out MicroProfile Context Propagation for example using the latest builds of Open Liberty. I’ve published an example repository on GitHub.
When we’re running our applications on Open Liberty, MicroProfile Context Propagation executors are backed by the automatically-tuned global thread pool. You can have a look at the default thread pool metrics provided by Liberty, as shown here.
Further resources
- GitHub example project
- MicroProfile Context Propagation
- Context Propagation support in Open Liberty
- Monitoring Open Liberty with Prometheus & Grafana
Published on Java Code Geeks with permission by Sebastian Daschner, partner at our JCG program. See the original article here: Enhanced CDI contexts & bulkheads with MicroProfile Context Propagation Opinions expressed by Java Code Geeks contributors are their own. |