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[1] provide 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
Usage example
eyJleHRTcnZJbnRlZ1R5cGUiOiIiLCJnQ2xpZW50SWQiOiIiLCJjcmVhdG9yTmFtZSI6IlBhdmVsIFBlcmVzbGVnaW4iLCJvdXRwdXRUeXBlIjoiYmxvY2siLCJsYXN0TW9kaWZpZXJOYW1lIjoiUGF2ZWwgUGVyZXNsZWdpbiIsImxhbmd1YWdlIjoiZW4iLCJkaWFncmFtRGlzcGxheU5hbWUiOiIiLCJzRmlsZUlkIjoiIiwiYXR0SWQiOiIxOTEzMzQ0OTQiLCJkaWFncmFtTmFtZSI6Im1pZGRsZXdhcmUiLCJhc3BlY3QiOiIiLCJsaW5rcyI6ImF1dG8iLCJjZW9OYW1lIjoiSUVQLTc5OiBNaWRkbGV3YXJlIGZvciBJZ25pdGUgc2VydmljZXMuIiwidGJzdHlsZSI6InRvcCIsImNhbkNvbW1lbnQiOmZhbHNlLCJkaWFncmFtVXJsIjoiIiwiY3N2RmlsZVVybCI6IiIsImJvcmRlciI6dHJ1ZSwibWF4U2NhbGUiOiIxIiwib3duaW5nUGFnZUlkIjoxOTEzMzQxMTksImVkaXRhYmxlIjpmYWxzZSwiY2VvSWQiOjE5MTMzNDExOSwicGFnZUlkIjoiIiwibGJveCI6dHJ1ZSwic2VydmVyQ29uZmlnIjp7ImVtYWlscHJldmlldyI6IjEifSwib2RyaXZlSWQiOiIiLCJyZXZpc2lvbiI6MTMsIm1hY3JvSWQiOiI0OTM5MzYwYy0yZGY3LTQ1ODQtYTdjMy01NTc4MmEyOTAwNjYiLCJwcmV2aWV3TmFtZSI6Im1pZGRsZXdhcmUucG5nIiwibGljZW5zZVN0YXR1cyI6Ik9LIiwic2VydmljZSI6IiIsImlzVGVtcGxhdGUiOiIiLCJ3aWR0aCI6IjEwMDEiLCJzaW1wbGVWaWV3ZXIiOmZhbHNlLCJsYXN0TW9kaWZpZWQiOjE2NTU5OTQwMzcwMDAsImV4Y2VlZFBhZ2VXaWR0aCI6ZmFsc2UsIm9DbGllbnRJZCI6IiJ9
ServiceCallInterceptor security = (mtd, args, ctx, svcCall) -> {
if (!CustomSecurityProvider.get().access(mtd, ctx.currentCallContext().attribute("sessionId")))
throw new SecurityException("Method invocation is not permitted");
// Execute remaining interceptors and service method.
return svcCall.call();
};
ServiceCallInterceptor audit = (mtd, args, ctx, svcCall) -> {
String sessionId = ctx.currentCallContext().attribute("sessionId");
AuditProvider prov = AuditProvider.get();
// Record an event before execution of the method.
prov.recordStartEvent(ctx.name(), mtd, sessionId);
try {
// Execute service method.
svcCall.call();
}
catch (Exception e) {
// Record error.
prov.recordError(ctx.name(), mtd, sessionId), e.getMessage());
// Re-throw exception to initiator.
throw e;
}
finally {
// Record finish event after execution of the service method.
prov.recordFinishEvent(ctx.name(), mtd, sessionId);
}
}
ServiceConfiguration svcCfg = new ServiceConfiguration()
.setName("service")
.setService(new MyServiceImpl())
.setMaxPerNodeCount(1)
.setInterceptors(security, audit);
// Deploy service.
ignite.services().deploy(svcCfg);
// Set context parameters for the service proxy.
ServiceCallContext callCtx = ServiceCallContext.builder().put("sessionId", sessionId).build();
// Make service proxy.
MyService proxy = ignite.services().serviceProxy("service", MyService.class, false, callCtx, 0);
// A business method call to be intercepted.
proxy.placeOrder(order1);
proxy.placeOrder(order2);
Implementation details
Deployment
One service can have several interceptors. They are defined using the service configuration and deployed with the service.
To add/remove interceptor service should be redeployed.
Interceptor is located and executed where the service is implemented (for Java service - on Java side, for .NET-service on .NET side). Its execution should not cause additional serialization).
Invocation order
The user can specify multiple interceptors. Each interceptor invokes the next interceptor in the chain using a delegated call, the last interceptor will call the service method.
So the interceptor specified first in the configuration will process the result of the service method execution last.
eyJleHRTcnZJbnRlZ1R5cGUiOiIiLCJnQ2xpZW50SWQiOiIiLCJjcmVhdG9yTmFtZSI6IlBhdmVsIFBlcmVzbGVnaW4iLCJvdXRwdXRUeXBlIjoiYmxvY2siLCJsYXN0TW9kaWZpZXJOYW1lIjoiUGF2ZWwgUGVyZXNsZWdpbiIsImxhbmd1YWdlIjoiZW4iLCJkaWFncmFtRGlzcGxheU5hbWUiOiIiLCJzRmlsZUlkIjoiIiwiYXR0SWQiOiIyMTczODk1MDQiLCJkaWFncmFtTmFtZSI6Imludm9jYXRpb24iLCJhc3BlY3QiOiIiLCJsaW5rcyI6ImF1dG8iLCJjZW9OYW1lIjoiSUVQLTc5OiBNaWRkbGV3YXJlIGZvciBJZ25pdGUgc2VydmljZXMuIiwidGJzdHlsZSI6InRvcCIsImNhbkNvbW1lbnQiOmZhbHNlLCJkaWFncmFtVXJsIjoiIiwiY3N2RmlsZVVybCI6IiIsImJvcmRlciI6dHJ1ZSwibWF4U2NhbGUiOiIxIiwib3duaW5nUGFnZUlkIjoxOTEzMzQxMTksImVkaXRhYmxlIjpmYWxzZSwiY2VvSWQiOjE5MTMzNDExOSwicGFnZUlkIjoiIiwibGJveCI6dHJ1ZSwic2VydmVyQ29uZmlnIjp7ImVtYWlscHJldmlldyI6IjEifSwib2RyaXZlSWQiOiIiLCJyZXZpc2lvbiI6MywibWFjcm9JZCI6IjQyZjVjYTZmLWY0NzAtNDM5OS1iMjM4LTNiNTk0YmY1NWRkOSIsInByZXZpZXdOYW1lIjoiaW52b2NhdGlvbi5wbmciLCJsaWNlbnNlU3RhdHVzIjoiT0siLCJzZXJ2aWNlIjoiIiwiaXNUZW1wbGF0ZSI6IiIsIndpZHRoIjoiNDcxIiwic2ltcGxlVmlld2VyIjpmYWxzZSwibGFzdE1vZGlmaWVkIjoxNjU2NjkwMjAxMDAwLCJleGNlZWRQYWdlV2lkdGgiOmZhbHNlLCJvQ2xpZW50SWQiOiIifQ==
Resource injection
Interceptor must support the injection of generic resources.
Interception scope
Interceptor does not apply to service lifecycle methods - init, execute and cancel,
Service call context
The user can create context (map with custom parameters) and bind it to the service proxy. After that, each call to the proxy method will also implicitly pass context parameters to the service.
Service method can read current context parameters using ServiceContext#currentCallContext method. It is only accessible from the current thread during the execution of a service method.
If one service calls another, then by default the current call context will not be bound to the created proxy - the user must explicitly bind it. But Java service has a special ServiceResource annotation to inject another service proxy into the current service. If the user wants to redirect the current call context to this (injected) proxy, he can set the forwardCallerContext option of this annotation.
Exception handling
Exception thrown by the interceptor will be wrapped into unchecked IgniteException and passed to the initiator.
Risks and Assumptions
The interceptor gives the user full control over the invocation of the service methods, so in case of implementation errors, the user may get unexpected behavior of the service.
Discussion Links
https://lists.apache.org/thread.html/r4236c1f23e524dc969bc55057467a2bbe7f9a59a6db7c7fcdc1b7d37%40%3Cdev.ignite.apache.org%3E
Reference Links
[1] https://pkg.go.dev/github.com/grpc-ecosystem/go-grpc-middleware
Tickets
Key
|
Summary
|
T
|
Created
|
Updated
|
Due
|
Assignee
|
Reporter
|
P
|
Status
|
Resolution
|