ID | IEP-79 | ||||||||
Author | Pavel Pereslegin | ||||||||
Sponsor | |||||||||
Created | 13 Oct 2021 | ||||||||
Status |
|
Table of Contents |
---|
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.
The Ignite Service Grid must support the following capabilities:
ServiceCallContext - immutable user parameter map that will be implicitly passed to the service (and interceptor) on every method call.
Code Block | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||
public interface ServiceCallContext { public String attribute(String name); public byte[] binaryAttribute(String name); } |
ServiceCallInterceptor - intercepts service method calls.
Code Block | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||
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).
Code Block | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||
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); } |
draw.io Diagram border true diagramName middleware simpleViewer false width links auto tbstyle top lbox true diagramWidth 1001 revision 13
Code Block | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||
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); |
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.
Interceptor must support ignite instance resource injection.
Interceptor should support LifeCycleAware
Interceptor only applies to user-defined business methods and does not apply to service lifecycle methods - init, execute andcancel,
The user can create this context 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.
Interceptor can read and update context parameters using ServiceInterceptorContext.
If an interceptor has been specified, but the user has not passed the caller context through the proxy, then for each call to the service method, an empty context will be dynamically created.
Interceptor can only throw unchecked exceptions.
Any runtime exception thrown by the onInvoke/onComplete methods will be wrapped in a ServiceInterceptException. This exception will be passed to the initiator (user) and to onError method of the interceptor.
The exception thrown by the onError method will be added to the main exception as suppressed.
If onInvoke throws an exception, then the service method will not be executed.
// todo explain forwardCallerContext
Jira | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|