Motivation
When implementing microservices, users are often face with the task of separating the business logic from the common "middleware" logic.
An example of a typical “middleware” task is auditing calls to business service methods (the system must understand which user called which methods and with what result).
Modern frameworks such as gRPC and Apache Thrift provide a very flexible API for implementing request interceptors, with which you can solve almost any middleware task.
Apache Ignite does not provide any mechanisms for solving such problems in general. The user needs to implement it himself, which often results in a lot of boilerplate code.
Description
The Ignite Service Grid must support the following capabilities:
- Ability to pass custom context from caller to service (similar to HTTP request headers).
- Ability to define custom interceptors for service calls.
Public API
New public API entities.
ServiceCallContext - immutable map of custom parameters to be implicitly passed to the service
public interface ServiceCallContext {
public String attribute(String name);
public byte[] binaryAttribute(String name);
}
ServiceCallInterceptor - intercepts service method calls.
public interface ServiceCallInterceptor extends Serializable {
public default void onInvoke(ServiceInterceptorContext ctx) throws ServiceInterceptException {
// No-op.
}
public default void onComplete(@Nullable Object res, ServiceInterceptorContext ctx) throws ServiceInterceptException {
// No-op.
}
public default void onError(Throwable err, ServiceInterceptorContext ctx) {
// No-op.
}
}
ServiceInterceptorContext - extended mutable version of caller context (interceptor obtains method call parameters from it and can use it to update the caller context).
public interface ServiceInterceptorContext extends ServiceCallContext {
public String method();
public @Nullable Object[] arguments();
public void attribute(String name, String val);
public void binaryAttribute(String name, byte[] val);
}
- ServiceInterceptException - unchecked exception that is used to highlight the exception that occurred during method interception (not execution).
Usage example
Diagram
eyJleHRTcnZJbnRlZ1R5cGUiOiIiLCJnQ2xpZW50SWQiOiIiLCJjcmVhdG9yTmFtZSI6IlBhdmVsIFBlcmVzbGVnaW4iLCJvdXRwdXRUeXBlIjoiYmxvY2siLCJsYXN0TW9kaWZpZXJOYW1lIjoiUGF2ZWwgUGVyZXNsZWdpbiIsImxhbmd1YWdlIjoiZW4iLCJkaWFncmFtRGlzcGxheU5hbWUiOiIiLCJzRmlsZUlkIjoiIiwiYXR0SWQiOiIxOTEzMzQ0OTQiLCJkaWFncmFtTmFtZSI6Im1pZGRsZXdhcmUiLCJhc3BlY3QiOiIiLCJsaW5rcyI6ImF1dG8iLCJjZW9OYW1lIjoiSUVQLTc5OiBNaWRkbGV3YXJlIGZvciBJZ25pdGUgc2VydmljZXMuIiwidGJzdHlsZSI6InRvcCIsImNhbkNvbW1lbnQiOmZhbHNlLCJkaWFncmFtVXJsIjoiIiwiY3N2RmlsZVVybCI6IiIsImJvcmRlciI6dHJ1ZSwibWF4U2NhbGUiOiIxIiwib3duaW5nUGFnZUlkIjoxOTEzMzQxMTksImVkaXRhYmxlIjpmYWxzZSwiY2VvSWQiOjIxNzM4ODcyMSwicGFnZUlkIjoiIiwibGJveCI6dHJ1ZSwic2VydmVyQ29uZmlnIjp7ImVtYWlscHJldmlldyI6IjEifSwib2RyaXZlSWQiOiIiLCJyZXZpc2lvbiI6MTMsIm1hY3JvSWQiOiI0OTM5MzYwYy0yZGY3LTQ1ODQtYTdjMy01NTc4MmEyOTAwNjYiLCJwcmV2aWV3TmFtZSI6Im1pZGRsZXdhcmUucG5nIiwibGljZW5zZVN0YXR1cyI6Ik9LIiwic2VydmljZSI6IiIsImlzVGVtcGxhdGUiOiIiLCJ3aWR0aCI6IjEwMDEiLCJzaW1wbGVWaWV3ZXIiOmZhbHNlLCJsYXN0TW9kaWZpZWQiOjE2NTU5OTQwMzcwMDAsImV4Y2VlZFBhZ2VXaWR0aCI6ZmFsc2UsIm9DbGllbnRJZCI6IiJ9
Code
ServiceCallInterceptor security = new ServiceCallInterceptor() {
@Override public void onInvoke(ServiceInterceptorContext ctx) throws ServiceInterceptException {
// Check permission before execution of the method.
if (!CustomSecurityProvider.get().access(ctx.method(), ctx.attribute("sessionId")))
throw new SecurityException("Method invocation is not permitted");
}
}
ServiceCallInterceptor audit = new ServiceCallInterceptor() {
@Override public void onInvoke(ServiceInterceptorContext ctx) {
// Record an event before execution of the method.
AuditProvider.get().recordStartEvent(ctx.method(), ctx.attribute("sessionId"));
}
@Override public void onComplete(@Nullable Object res, ServiceInterceptorContext ctx) {
AuditProvider.get().recordFinishEvent(ctx.method(), ctx.attribute("sessionId"));
}
@Override public void onError(Throwable err, ServiceInterceptorContext ctx) {
AuditProvider.get().recordError(ctx.method(), ctx.attribute("sessionId"), err.getMessage());
}
}
// Set context parameters for service proxy.
ServiceCallContext ctx = ServiceCallContext.builder().put("sessionId", sessionId).build();
ServiceConfiguration svcCfg = new ServiceConfiguration()
.setName("service")
.setService(new MyServiceImpl())
.setMaxPerNodeCount(1)
.setInterceptors(security, audit);
// Deploy service.
ignite.services().deploy(svcCfg);
MyService proxy = ignite.services().serviceProxy("service", MyService.class, false, ctx, 0);
// A business method call to be intercepted.
proxy.placeOrder(order1);
proxy.placeOrder(order2);
Implementation details
Interceptor only applies to user-defined business methods and does not apply to service lifecycle methods - init, execute and cancel,
Interceptor can modify ServiceCallContext, but Service methods can only read it.
If an interceptor has been specified, but the user has not passed the caller context through the proxy, it will be created dynamically.
Deployment
One service can have several interceptors. They are defined using the service configuration and deployed with the service.
Interceptor is located and executed where the service is implemented (for Java service - on Java side, for .NET-service on .NET side without any additional serialization).
To add/remove interceptor - service should be redeployed.
Resource injection and lifecycle
Interceptor must support ignite instance resource injection.
Interceptor should be LifeCycleAware
Exception handling
// todo ServiceInterceptException
If an interceptor throws an exception, then processing is aborted, but the exception is passed to all listed interceptors (onError)
Passing Context in the Service Chain
// todo explain forwardCallerContext
Limitations
Risks and Assumptions
Discussion Links
https://lists.apache.org/thread.html/r4236c1f23e524dc969bc55057467a2bbe7f9a59a6db7c7fcdc1b7d37%40%3Cdev.ignite.apache.org%3E
Reference Links
Tickets
key |
summary |
type |
created |
updated |
due |
assignee |
reporter |
priority |
status |
resolution |
JQL and issue key arguments for this macro require at least one Jira application link to be configured
|