Context-propagation framework is intended for propagating some value from one microservice to another. Additionally, the
library allows to store custom request data and get them where you want.
Context-propagation is designed to propagate values in the following ways:
- Rest - Rest
- Rest - Messaging
- Messaging - Rest
- Messaging - Messaging
Also, the framework contains some useful methods for working with context such as propagating contexts to other threads, create a snapshot and activate it sometime later. Also, you can create own contexts for propagating your data or override existed for customization for your needs.
Design overview: context-propagation diagram
Framework provides contexts for propagating the following data:
- Add the framework-contexts dependency:
<dependency>
<groupId>com.netcracker.cloud.quarkus</groupId>
<artifactId>framework-contexts</artifactId>
<version>${context.propagation.version}</version>
</dependency>Accept-Language context allows propagating 'Accept-Language' headers from one microservice to another. To get context value, you should call:
Access:
AcceptLanguageContextObject acceptLanguageContextObject=ContextManager.get(ACCEPT_LANGUAGE);
String acceptLanguage=acceptLanguageContextObject.getAcceptedLanguages();Allows propagating any specified headers. To set a list of headers you should put either
HEADERS_ALLOWED environment or set the headers.allowed property. Property has more precedence than env.
Access:
AllowedHeadersContextObject allowedHeadersContextObject=ContextManager.get(ALLOWED_HEADER);
Map<String, Object> allowedHeaders=allowedHeadersContextObject.getHeaders();You just need to specify a list of headers in application.properties
in the headers.allowed property. For example:
headers.allowed=myheader1,myheader2,...
Otherwise, you need to take care that this parameter is in System#property or environment.
This context retrieves API version from an incoming request URL and stores it.
Access:
ApiVersionContextObject apiVersionContextObject=ContextManager.get(API_VERSION_CONTEXT_NAME);
String apiVersion=apiVersionContextObject.getVersion();If request URL does not contain API version then the context contains default value v1.
Propagates and allows to get X-Request-Id value. If an incoming request does not contains the X-Request-Id
header then a random value is generated.
Access:
XRequestIdContextObject xRequestIdContextObject=ContextManager.get(X_REQUEST_ID);
String xRequestId=xRequestIdContextObject.getRequestId();Propagates and allows to get X-Version header.
Access:
XVersionContextObject xVersionContextObject=ContextManager.get(XVersionProvider.CONTEXT_NAME);
String xVersion=xVersionContextObject.getXVersion();Propagates and allows to get Business-Request-Id header.
Value of header shouldn't be empty. If header is empty and value not set, propagation won't work.
Access:
String businessProcessId = BusinessProcessIdContext.get();Set:
BusinessProcessIdContext.set(someID);There is an example of new context creation in here
At first, implement your ContextObject class. ContextObject is a place where you can parse and store data from IncomingContextData. IncomingContextData is an object where is located request context data.
Note! Implement SerializableContext if you want to propagate data from your context in outgoing request. You have
to override below function. It's aim to get values from context and put them into OutgoingContextData.
public class ContextObject implements SerializableContext {
@Override
public void serialize(OutgoingContextData contextData) {
contextData.set(SERIALIZATION_NAME, storedValue);
}
}Note! ContextObject should implement DefaultValueAwareContext if it contains default value. You have to override _
getDefault()_
function and return default value from it.
@Override
public String getDefault(){
return"default";
}Secondly, Strategy - this is a way how your context will be stored.
You can choose one from our default strategies or create a new one.
Default strategies for threadLocal are: ThreadLocalDefaultStrategy, ThreadLocalWithInheritanceDefaultStrategy.
ThreadLocalWithInheritanceDefaultStrategy supports propagation between Threads. Default strategy for Quarkus
is: RestEasyDefaultStrategy.
If you decided to use one of default strategies - then just go to Provider.
To implement your own strategy your class should implement Strategy<ContextObject> and override next functions:
- public void clear() - to remove all stored info
- public ContextObject get() - get stored ContextObject or exception if ContextObject is null.
- public void set(ContextObject value) - set new ContextObject for storing
- public Optional getSafe() - get stored ContextObject without Exception
Instead of ContextObject insert name of your ContextObject class.
Thirdly, Provider - provides information about context to contextManager. You can use default provider or create your own.
Default providers: AbstractContextProviderOnInheritableThreadLocal, AbstractContextProviderOnThreadLocal. There is
no default provider for Quarkus. If you decided to use one of the default providers all you need to override are this
two functions:
/* The name of context. ContextName is unique key of context. By this name you can get or set context object in context.
* Can't be registered more than one context in contextManager with the same name. <p>
* Additionally, we strongly recommend to make method realization as final because class that overrides existed
* context must have the same name. */
@Override
public final String contextName(){
return CONTEXT_NAME;
}
/* The method creates contextObject. Context object may be initialized based on data from {@link IncomingContextData}
* For example if context is serialized and propagated from microservice to microservice
* then this method should describe how context object can be deserialized.
* If incomingContextData is not null and there are not data relevant to this context, method should return null. */
@Override
public ContextObject provide(@Nullable IncomingContextData incomingContextData){
return new ContextObject(incomingContextData);
}To create your own provider you should implement ContextProvider and mark provider class with @RegisterProvider
Also, you have to override several functions:
@RegisterProvider
public class MyProvider implements ContextProvider<ContextObject> {
// should return instance of your own strategy
// or instance of default strategy
@Override
public Strategy<ContextObject> strategy() {
return this.strategy;
}
// Determined context level order. ContextManager sorts context providers by levels and
// performs bulk operations (init, clear and so on) with contexts
// with a lower level at first and then ascending levels.
// If you don't care about the order of the context among other contexts then method can return 0 value.
// Smaller will be done first
@Override
public int initLevel() {
return 0;
}
// Determines which of several context providers with the same name should be used.
// If there are several context providers with the same name
// and their provider orders are equal then runtime exception will be.
// We recommend to use 0 if you write your own and don't override existed context. <p>
// If you override existed context then value should be multiple of 100. For example: 0, -100, -200 <p>
// Context provider with smaller value wins.
@Override
public int providerOrder() {
return 0;
}
// The name of context. ContextName is unique key of context. By this name you can get or set context object in context.
// Can't be registered more than one context in contextManager with the same name. <p>
// Additionally, we strongly recommend to make method realization as final because class that overrides existed
// context must have the same name.
@Override
public final String contextName() {
return CONTEXT_NAME;
}
// The method creates contextObject. Context object may be initialized based on data from {@link IncomingContextData}
// For example if context is serialized and propagated from microservice to microservice
// then this method should describe how context object can be deserialized.
// If incomingContextData is not null and there are not data relevant to this context, method should return null.
@Override
public ContextObject provide(@Nullable IncomingContextData contextData) {
return new ContextObject(contextData);
}
}It means that you can use default contexts (see Quarkus framework context) but with other ways of storage.
To override existing context you should extend its default provider, mark new class with @RegisterProvider and
override next functions:
@RegisterProvider
public class MyOverridedProvider extends MyProvider {
Strategy<ContextObject> newStrategy = ...;
@Override
public Strategy<ContextObject> strategy() {
return this.newStrategy;
}
@Override
public int providerOrder() {
return -100;
}
}Important! Make providerOrder() return value less than default one. If you don't do that, ContextManager won't
be able to detect new Provider and will use default one. Remember, that providerOrder() should be multiple of 100.
This module contains filters and interceptors for quarkus application.
All that you need is to add the below dependency.
<dependency>
<groupId>com.netcracker.cloud</groupId>
<artifactId>context-propagation-quarkus</artifactId>
<version>${context.propagation.version}</version>
</dependency>And write own or use our Quarkus framework contexts.
- You should add the below dependency.
<dependency>
<groupId>com.netcracker.cloud</groupId>
<artifactId>framework-contexts-quarkus</artifactId>
<version>${context.propagation.version}</version>
</dependency>- If we use
Allowed headerscontext you should specify a list of headers inapplication.propertiesin thequarkus.headers.allowedproperty.
quarkus.headers.allowed=myheader1,myheader2,...
There is a possibility to create a context snapshot - to remember current contexts' data and after to store it. To get stored data you have to
execute ContextManager.executeWithContext().
AcceptLanguageContext.set(initialContextValue);
Map<String, Object> contextSnapshot=ContextManager.createContextSnapshot();
AcceptLanguageContext.set(newContextValue);
ContextManager.executeWithContext(contextSnapshot,()->{
assertEquals(initialContextValue, AcceptLanguageContext.get()); // <-- true
return null;
});In order to restore you have to perform ContextManager.activateContextSnapshot(contextSnapshot)
Thread context propagation functionality allows performing users' task in a dedicated thread in a specific context. Context can be original or snapshot.
If you want to use Executor Service with our contexts, you need to wrap executor with our
delegator ContextAwareExecutorService. In this case we guarantee correct context propagation over threads.
final ExecutorService simpleExecutor=new ContextAwareExecutorService(Executors.newFixedThreadPool(2));ContextAwareExecutorService has two type of constructors. One of them takes context snapshot, and the other takes only executorService delegate.
If we use which takes and pass context snapshot then all submitted task will be performed in this specific context. If you don't pass context snapshot then we create full
context snapshot by themselves and will be performed all task in this context.
There are cases when you want to use original ExecutorService as dedicated thread pool and use tasks which run in specific context. In this way
you can use ContextPropagationCallable delegator. This delegator takes context snapshot object and Callable delegate. When task is executed the
delegate will be performed in the passed context snapshot.
ContextPropagationCallable contextPropagationCallable = new ContextPropagationCallable(ContextManager.createContextSnapshot(), delegate);
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(contextPropagationCallable).get();Sometimes, you may use CompletableFuture class and this way it would be convenient to use ContextPropagationSupplier delegator. This class takes delegate and context snapshot.
If you want to perform a task in a current context then you can perform the following code:
ContextPropagationSupplier contextPropagationSupplier = new ContextPropagationSupplier(ContextManager.createContextSnapshot(), delegate);Module messaging-context provides support for context propagation in smallrye messaging. It dumps execution context data to message
metadata (usually headers) and restore execution context from received message before calling onMessage callback.
Look at README.md for details.