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 be implicitly passed to the service (and interceptor) on every method call.

    Code Block
    linenumbers
    languagejava
    themeRDark
    titleServiceCallContext.java
    truecollapsetrue
    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 proxy.

    Code Block
    languagejava
    themeRDark
    titleServiceCallContext.java
    collapsetrue
    public <T> T 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 executed.
        public default void onInvoke(ServiceInterceptorContextServiceCallContext ctx) throws ServiceInterceptException {
            // No-op.
        }
    
        // Called AFTER the service method is executed.
        public default void onComplete(@Nullable Object res, ServiceInterceptorContextServiceCallContext ctx) throws ServiceInterceptException {
            // No-op.
        }
    
        // Called when onInvoke, onComplete or service method throws an exception.
        public default void onError(Throwable err, ServiceInterceptorContextServiceCallContext ctx) {
            // No-op.
        }
    }


  • ServiceInterceptorContext ServiceCallContextextended mutable version of caller context ( interceptor obtains method call parameters from it and can use it to update the caller contextuser attributes).

    Code Block
    languagejava
    themeRDark
    titleServiceInterceptorContext.java
    linenumberstrue
    collapsetrue
    public interface ServiceInterceptorContext extends ServiceCallContext {
        public String method();
    
        public @Nullable Object[] arguments();
    
        public void attribute(String nameMap<String, String val);
    
        public void binaryAttribute(String name, byte[] valObject> 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;


Usage example

draw.io Diagram
bordertrue
diagramNamemiddleware
simpleViewerfalse
width
linksauto
tbstyletop
lboxtrue
diagramWidth1001
revision13

Code Block
languagejava
themeRDark
titleExample.java
linenumberstrue
collapsetrue
        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 contextcall parametersattributes for service proxy.
        Map<String, ServiceCallContextString> ctxattrs = new ServiceCallContext.builderHashMap<>();
        attrs.put("sessionId", sessionId).build();

           ServiceConfiguration 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, ctxattrs, 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.

...

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

Resource injection and lifecycle

Interceptor must support ignite instance resource injection.

(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 ServiceContext#currentCallContext#callAttributes 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 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.

...