Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

  1. Ability to pass custom context from caller to service (similar to HTTP request headers).
  2. Ability to define custom interceptors for service calls.

Phase 1 Service call context (done)

Public API

  • ServiceCallContext -  immutable user parameter map that will can be implicitly passed to the service (and interceptor) on every method call.

    Code Block
    languagejava
    themeRDark
    titleServiceCallContext.java
    collapsetrue
    public interface ServiceCallContext {
        public String attribute(String name);
    
        public byte[] binaryAttribute(String name);
    }
  • ServiceContextBuilder -  builder for ServiceCallContext.


  • New methods in IgniteServices to pass caller context to service proxyServiceCallInterceptor  - intercepts service method calls.

    Code Block
    languagejava
    themeRDark
    titleServiceCallContextServiceCallInterceptor.java
    collapsetrue
    public <T>interface TServiceCallInterceptor serviceProxy(String name, Class<? super T> svcItf, boolean sticky, ServiceCallContext callCtx)
    
    public <T> T serviceProxy(String name, Class<? super T> svcItf, boolean sticky, ServiceCallContext callCtx, long timeout)

    New method in ServiceContext for getting caller context inside the service method.

    Code Block
    languagejava
    themeRDark
    titleServiceCallContext.java
    collapsetrue
    /**
     * Gets context of the current service call.
     *
     * @return Context of the current service call, possibly {@code null}.
     * @see ServiceCallContext
     */
    @Nullable public ServiceCallContext currentCallContext();

    New property in ServiceResource annotation.

    Code Block
    languagejava
    themeRDark
    titleServiceCallContext.java
    collapsetrue
    /**
     * Flag indicating that the service call context should be passed to the injected service.
     *
     * @return {@code True} if the service call context should be passed to the injected service.
     */
    public boolean forwardCallerContext() default false;

Implementation details

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.

Phase 2 Interceptors (active)

Public API

ServiceCallContext should be available only on server side.

The user can bind a custom map/dictionary of parameters  to the service proxy and read them from the call context on the server side.

So, some current public APIs need to be changed (all discussed API are experimental):

  • Remove ServiceCallContextBuilder
  • Rework methods in IgniteServices to allow user to bind Map of attributes (instead of ServiceCallContext) to service proxy.

    Code Block
    languagejava
    themeRDark
    titleServiceCallContext.java
    collapsetrue
    public <T> T serviceProxy(String name, Class<? super T> svcItf, boolean sticky, Map<String, Object> callAttrs)
    
    public <T> T serviceProxy(String name, Class<? super T> svcItf, boolean sticky, Map<String, Object> callAttrs, long timeout)

    ServiceCallInterceptor  - intercepts service method calls.

    Code Block
    languagejava
    themeRDark
    titleServiceCallInterceptor.java
    linenumberstrue
    collapsetrue
    public interface ServiceCallInterceptor extends Serializable {
        // Called BEFORE the service method is executedextends Serializable {
        /**
         * Intercepts a call to a service method.
        public default void onInvoke(ServiceCallContext ctx) throws ServiceInterceptException { *
         * @param mtd // No-opMethod name.
        }
    
     * @param args // Called AFTER the service method is executedMethod arguments.
        public default void onComplete(@Nullable Object res, ServiceCallContext ctx) throws ServiceInterceptException {* @param ctx Service context.
         * @param call // No-op.
        }
    Delegated call.
        // Called* when onInvoke, onComplete or service@return Service method throwscall an exceptionresult.
        public default void onError(Throwable err, ServiceCallContext ctx) {
            // No-op. */
        }
    }

    ServiceCallContextinterceptor obtains method call parameters from it and can use it to update the user attributes).

    Code Block
    languagejava
    themeRDark
    titleServiceInterceptorContext.java
    linenumberstrue
    collapsetrue
    public interface ServiceCallContext {
        public String method();
    
        public @Nullable Object[] arguments();
    
        public Map<String, Object> callAttributes();
    }
  • ServiceInterceptException - unchecked exception that is used to highlight the exception that occurred during method interception (not execution).
  • Change property name in ServiceResource annotation (forwardCallerContext => forwardCallAttributes).

    Code Block
    languagejava
    themeRDark
    titleServiceCallContext.java
    collapsetrue
    /**
     * Flag indicating that the service call attributes should be passed to the injected service.
     *
     * @return {@code True} if the service call attributes context should be passed to the injected service.
     */
    public boolean forwardCallAttributes() default false;public Object invoke(String mtd, Object[] args, ServiceContext ctx, Callable<Object> call) throws Exception;
    }


Usage example

draw.io Diagram
bordertrue
diagramNamemiddleware
simpleViewerfalse
widthlinksauto
tbstyletop
lboxtrue
diagramWidth1001
revision13

Code Block
languagejava
themeRDark
titleExample.javalinenumberstrue
collapsetrue
        ServiceCallInterceptor security = new ServiceCallInterceptor() {
            @Override public void onInvoke(ServiceInterceptorContext ctx) throws ServiceInterceptException {
                // Check permission before execution of the method.
   mtd, args, ctx, svcCall) -> {
             if (!CustomSecurityProvider.get().access(mtd, ctx.methodcurrentCallContext(), ctx.attribute("sessionId")))
                    throw new SecurityException("Method invocation is not permitted");

        // Execute remaining interceptors and service }method.
        }
return svcCall.call();
    };

    ServiceCallInterceptor audit = new ServiceCallInterceptor() {
    mtd, args, ctx, svcCall) -> {
        @OverrideString publicsessionId void onInvoke(ServiceInterceptorContext ctx) {
= ctx.currentCallContext().attribute("sessionId");
        AuditProvider prov = AuditProvider.get();

        // Record an event before execution of the method.
                AuditProvider.get()prov.recordStartEvent(ctx.methodname(), ctx.attribute("sessionId")mtd, sessionId);

        try    }{

            @Override// publicExecute void onComplete(@Nullable Object res, ServiceInterceptorContext ctx) {service method.
                AuditProvider.get().recordFinishEvent(ctx.method(), ctx.attribute("sessionId")svcCall.call();
        }
    }

    catch (Exception e) {
     @Override public void onError(Throwable err, ServiceInterceptorContext ctx) {
// Record error.
              AuditProvider.get().prov.recordError(ctx.methodname(), ctx.attribute("sessionId"mtd, sessionId), erre.getMessage());

            }// Re-throw exception to initiator.
            throw e;
        }
        finally {
            // Set call attributes for Record finish event after execution of the service proxymethod.
         Map<String, String> attrs = new HashMap<>( prov.recordFinishEvent(ctx.name(), mtd, sessionId);
        attrs.put("sessionId", sessionId);        }
    }

        ServiceConfigurationServiceConfiguration svcCfg = new ServiceConfiguration()
            .setName("service")
            .setService(new MyServiceImpl())
            .setMaxPerNodeCount(1)
            .setInterceptors(security, audit);

    // Deploy service.
    ignite.services().deploy(svcCfg);

    // Deploy Set context parameters for the service proxy.
    ServiceCallContext callCtx   ignite.services().deploy(svcCfg= ServiceCallContext.builder().put("sessionId", sessionId).build();

    // Make service proxy.
    MyService proxy = ignite.services().serviceProxy("service", MyService.class, false, attrscallCtx, 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.

draw.io Diagram
bordertrue
diagramNameinvocation
simpleViewerfalse
width
linksauto
tbstyletop
lboxtrue
diagramWidth471
revision3

Resource injection

Interceptor must support ignite instance resource injectionthe injection of generic resources.

(question) Interceptor should support LifeCycleAware

Interception scope

Interceptor only applies to user-defined service methods and does not apply to service lifecycle methods - init, execute andcancel,

Service call context

The user can create context (map with custom parameters map ) 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#callAttributes 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 ServiceCallContext.

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.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

Interceptor can only throw unchecked exceptions.

Any runtime exception Exception thrown by the onInvoke/onComplete methods interceptor will be wrapped in a ServiceInterceptException.This exception will be into unchecked IgniteException and passed to the initiator (user) and to onError method of the interceptor.

If onInvoke throws an exception, then the service method will not be called.

The exception thrown by the onError method will be added to the main exception as suppressed.

...

.

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

...