You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by pp...@apache.org on 2022/07/08 13:16:57 UTC

[ignite] branch master updated: IGNITE-17269 Java service call interceptor. (#10128)

This is an automated email from the ASF dual-hosted git repository.

ppa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new 8c408bcc2c7 IGNITE-17269 Java service call interceptor. (#10128)
8c408bcc2c7 is described below

commit 8c408bcc2c7524ada3eab789082f518c6d40b4b6
Author: Pavel Pereslegin123123 <xx...@gmail.com>
AuthorDate: Fri Jul 8 16:16:46 2022 +0300

    IGNITE-17269 Java service call interceptor. (#10128)
---
 .../service/CompositeServiceCallInterceptor.java   |  50 +++
 .../processors/service/GridServiceProxy.java       |  41 +-
 .../processors/service/IgniteServiceProcessor.java |  27 +-
 .../service/LazyServiceConfiguration.java          |  32 +-
 .../processors/service/ServiceContextImpl.java     |  22 +-
 .../apache/ignite/services/ServiceCallContext.java |   1 +
 .../ignite/services/ServiceCallInterceptor.java    | 104 +++++
 .../ignite/services/ServiceConfiguration.java      |  31 +-
 .../service/IgniteServiceCallInterceptorTest.java  | 441 +++++++++++++++++++++
 ...ployment2ClassLoadersDefaultMarshallerTest.java |  24 +-
 .../testsuites/IgniteServiceGridTestSuite.java     |   4 +-
 .../tests/p2p/NoopServiceCallInterceptor.java      |  32 ++
 .../tests/p2p/NoopServiceCallInterceptor2.java     |  32 ++
 13 files changed, 810 insertions(+), 31 deletions(-)

diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/CompositeServiceCallInterceptor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/CompositeServiceCallInterceptor.java
new file mode 100644
index 00000000000..779660cd5d7
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/CompositeServiceCallInterceptor.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.service;
+
+import java.util.concurrent.Callable;
+import org.apache.ignite.services.ServiceCallInterceptor;
+import org.apache.ignite.services.ServiceContext;
+
+/**
+ * Composite service call interceptor.
+ */
+public class CompositeServiceCallInterceptor implements ServiceCallInterceptor {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Service call interceptors. */
+    private final ServiceCallInterceptor[] interceptors;
+
+    /** @param interceptors Service call interceptors. */
+    public CompositeServiceCallInterceptor(ServiceCallInterceptor[] interceptors) {
+        this.interceptors = interceptors;
+    }
+
+    /** {@inheritDoc} */
+    @Override public Object invoke(String mtd, Object[] args, ServiceContext ctx, Callable<Object> next) throws Exception {
+        return new Callable<Object>() {
+            int idx;
+
+            @Override public Object call() throws Exception {
+                // Recursively call interceptors.
+                return interceptors[idx].invoke(mtd, args, ctx, ++idx == interceptors.length ? next : this);
+            }
+        }.call();
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceProxy.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceProxy.java
index 95359a8f23a..f9fa1cd9db1 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceProxy.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceProxy.java
@@ -61,6 +61,7 @@ import org.apache.ignite.marshaller.Marshaller;
 import org.apache.ignite.platform.PlatformServiceMethod;
 import org.apache.ignite.resources.IgniteInstanceResource;
 import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceCallInterceptor;
 import org.jetbrains.annotations.Nullable;
 
 import static org.apache.ignite.internal.processors.task.GridTaskThreadContextKey.TC_IO_POLICY;
@@ -211,8 +212,8 @@ public class GridServiceProxy<T> implements Serializable {
                                 HistogramMetricImpl hist = svcCtx.isStatisticsEnabled() ?
                                     invocationHistogramm(svcCtx, mtd.getName(), args) : null;
 
-                                return hist == null ? callServiceLocally(svc, mtd, args, callAttrs) :
-                                    measureCall(hist, () -> callServiceLocally(svc, mtd, args, callAttrs));
+                                return hist == null ? callServiceLocally(svcCtx, mtd, args, callAttrs) :
+                                    measureCall(hist, () -> callServiceLocally(svcCtx, mtd, args, callAttrs));
                             }
                         }
                     }
@@ -286,46 +287,58 @@ public class GridServiceProxy<T> implements Serializable {
     }
 
     /**
-     * @param svc Service to be called.
+     * @param svcCtx Service context.
      * @param mtd Method to call.
      * @param args Method args.
      * @param callAttrs Service call context attributes.
      * @return Invocation result.
      */
     private Object callServiceLocally(
-        Service svc,
+        ServiceContextImpl svcCtx,
         Method mtd,
         Object[] args,
         @Nullable Map<String, Object> callAttrs
     ) throws Exception {
+        Service svc = svcCtx.service();
+
         if (svc instanceof PlatformService && !PLATFORM_SERVICE_INVOKE_METHOD.equals(mtd))
             return ((PlatformService)svc).invokeMethod(methodName(mtd), false, true, args, callAttrs);
         else
-            return callServiceMethod(svc, mtd, args, callAttrs);
+            return callServiceMethod(svcCtx, mtd, args, callAttrs);
     }
 
     /**
-     * @param svc Service to be called.
+     * @param svcCtx Service context.
      * @param mtd Method to call.
      * @param args Method args.
      * @param callAttrs Service call context attributes.
      * @return Invocation result.
      */
     private static Object callServiceMethod(
-        Service svc,
+        ServiceContextImpl svcCtx,
         Method mtd,
         Object[] args,
         @Nullable Map<String, Object> callAttrs
-    ) throws InvocationTargetException, IllegalAccessException {
-        if (callAttrs != null)
+    ) throws Exception {
+        ServiceCallContextImpl prevCtx = null;
+
+        if (callAttrs != null) {
+            // One service can be called from another in the same thread.
+            prevCtx = ServiceCallContextHolder.current();
+
             ServiceCallContextHolder.current(new ServiceCallContextImpl(callAttrs));
+        }
 
         try {
-            return mtd.invoke(svc, args);
+            ServiceCallInterceptor interceptor = svcCtx.interceptor();
+
+            return interceptor == null ?
+                mtd.invoke(svcCtx.service(), args) :
+                interceptor.invoke(mtd.getName(), args, svcCtx, () -> mtd.invoke(svcCtx.service(), args));
         }
         finally {
             if (callAttrs != null)
-                ServiceCallContextHolder.current(null);
+                ServiceCallContextHolder.current(prevCtx);
         }
     }
 
@@ -577,7 +590,7 @@ public class GridServiceProxy<T> implements Serializable {
             if (svcCtx.service() instanceof PlatformService && mtd == null)
                 return callPlatformService((PlatformService)svcCtx.service());
             else
-                return callOrdinaryService(svcCtx.service(), mtd);
+                return callOrdinaryService(svcCtx, mtd);
         }
 
         /** */
@@ -594,12 +607,12 @@ public class GridServiceProxy<T> implements Serializable {
         }
 
         /** */
-        private Object callOrdinaryService(Service srv, Method mtd) throws Exception {
+        private Object callOrdinaryService(ServiceContextImpl svcCtx, Method mtd) throws Exception {
             if (mtd == null)
                 throw new GridServiceMethodNotFoundException(svcName, mtdName, argTypes);
 
             try {
-                return callServiceMethod(srv, mtd, args, callAttrs);
+                return callServiceMethod(svcCtx, mtd, args, callAttrs);
             }
             catch (InvocationTargetException e) {
                 throw new ServiceProxyException(e.getCause());
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java
index ad8f00dd9ea..50312826432 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java
@@ -88,8 +88,8 @@ import org.apache.ignite.marshaller.jdk.JdkMarshaller;
 import org.apache.ignite.plugin.security.SecurityException;
 import org.apache.ignite.plugin.security.SecurityPermission;
 import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceCallInterceptor;
 import org.apache.ignite.services.ServiceConfiguration;
-import org.apache.ignite.services.ServiceContext;
 import org.apache.ignite.services.ServiceDeploymentException;
 import org.apache.ignite.services.ServiceDescriptor;
 import org.apache.ignite.spi.IgniteNodeValidationResult;
@@ -674,11 +674,12 @@ public class IgniteServiceProcessor extends GridProcessorAdapter implements Igni
             if (err == null) {
                 try {
                     byte[] srvcBytes = U.marshal(marsh, cfg.getService());
+                    byte[] interceptorsBytes = U.marshal(marsh, cfg.getInterceptors());
 
                     String[] knownSvcMdtNames = cfg instanceof PlatformServiceConfiguration ?
                         ((PlatformServiceConfiguration)cfg).mtdNames() : null;
 
-                    cfgsCp.add(new LazyServiceConfiguration(cfg, srvcBytes).platformMtdNames(knownSvcMdtNames));
+                    cfgsCp.add(new LazyServiceConfiguration(cfg, srvcBytes, interceptorsBytes).platformMtdNames(knownSvcMdtNames));
                 }
                 catch (Exception e) {
                     U.error(log, "Failed to marshal service with configured marshaller " +
@@ -1368,19 +1369,31 @@ public class IgniteServiceProcessor extends GridProcessorAdapter implements Igni
      * @return Copy of service.
      * @throws IgniteCheckedException If failed.
      */
-    private Service copyAndInject(ServiceConfiguration cfg, ServiceContext svcCtx) throws IgniteCheckedException {
+    private Service copyAndInject(ServiceConfiguration cfg, ServiceContextImpl svcCtx) throws IgniteCheckedException {
         if (cfg instanceof LazyServiceConfiguration) {
             LazyServiceConfiguration srvcCfg = (LazyServiceConfiguration)cfg;
 
             GridDeployment srvcDep = ctx.deploy().getDeployment(srvcCfg.serviceClassName());
+            ClassLoader clsLdr = U.resolveClassLoader(srvcDep != null ? srvcDep.classLoader() : null, ctx.config());
+            byte[] bytes = srvcCfg.serviceBytes();
 
-            byte[] bytes = ((LazyServiceConfiguration)cfg).serviceBytes();
-
-            Service srvc = U.unmarshal(marsh, bytes,
-                U.resolveClassLoader(srvcDep != null ? srvcDep.classLoader() : null, ctx.config()));
+            Service srvc = U.unmarshal(marsh, bytes, clsLdr);
 
             ctx.resource().inject(srvc, svcCtx);
 
+            ServiceCallInterceptor[] interceptors = U.unmarshal(marsh, srvcCfg.interceptorBytes(), clsLdr);
+
+            if (F.isEmpty(interceptors))
+                return srvc;
+
+            // Inject generic resources.
+            for (int i = 0; i < interceptors.length; i++)
+                ctx.resource().injectGeneric(interceptors[i]);
+
+            // Wrap in a composite interceptor if necessary.
+            svcCtx.interceptor(interceptors.length == 1 ?
+                interceptors[0] : new CompositeServiceCallInterceptor(interceptors));
+
             return srvc;
         }
         else {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/LazyServiceConfiguration.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/LazyServiceConfiguration.java
index d0d4df13f2f..e4e7a6d4707 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/LazyServiceConfiguration.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/LazyServiceConfiguration.java
@@ -17,11 +17,14 @@
 
 package org.apache.ignite.internal.processors.service;
 
+import java.util.Arrays;
 import org.apache.ignite.internal.util.tostring.GridToStringExclude;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceCallInterceptor;
 import org.apache.ignite.services.ServiceConfiguration;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Lazy service configuration.
@@ -34,12 +37,19 @@ public class LazyServiceConfiguration extends ServiceConfiguration {
     @GridToStringExclude
     private transient Service srvc;
 
+    /** Service interceptors. */
+    @GridToStringExclude
+    private transient ServiceCallInterceptor[] interceptors;
+
     /** */
     private String srvcClsName;
 
     /** */
     private byte[] srvcBytes;
 
+    /** */
+    private byte[] interceptorsBytes;
+
     /** Names of platform service methods to build service statistics. */
     @GridToStringExclude
     private String[] platformMtdNames;
@@ -53,9 +63,10 @@ public class LazyServiceConfiguration extends ServiceConfiguration {
 
     /**
      * @param cfg Configuration.
-     * @param srvcBytes Marshaller service.
+     * @param srvcBytes Marshalled service.
+     * @param interceptorsBytes Marshalled interceptors.
      */
-    public LazyServiceConfiguration(ServiceConfiguration cfg, byte[] srvcBytes) {
+    public LazyServiceConfiguration(ServiceConfiguration cfg, byte[] srvcBytes, @Nullable byte[] interceptorsBytes) {
         assert cfg.getService() != null : cfg;
         assert srvcBytes != null;
 
@@ -69,6 +80,8 @@ public class LazyServiceConfiguration extends ServiceConfiguration {
         srvc = cfg.getService();
         srvcClsName = srvc.getClass().getName();
         isStatisticsEnabled = cfg.isStatisticsEnabled();
+        interceptors = cfg.getInterceptors();
+        this.interceptorsBytes = interceptorsBytes;
     }
 
     /**
@@ -92,6 +105,18 @@ public class LazyServiceConfiguration extends ServiceConfiguration {
         return srvc;
     }
 
+    /** {@inheritDoc} */
+    @Override public ServiceCallInterceptor[] getInterceptors() {
+        return interceptors;
+    }
+
+    /**
+     * @return Interceptors bytes.
+     */
+    public byte[] interceptorBytes() {
+        return interceptorsBytes;
+    }
+
     /** {@inheritDoc} */
     @SuppressWarnings("RedundantIfStatement")
     @Override public boolean equalsIgnoreNodeFilter(Object o) {
@@ -121,6 +146,9 @@ public class LazyServiceConfiguration extends ServiceConfiguration {
         if (!F.eq(srvcClsName, that.srvcClsName))
             return false;
 
+        if (!Arrays.equals(interceptorsBytes, that.interceptorsBytes))
+            return false;
+
         return true;
     }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceContextImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceContextImpl.java
index a5aa112b59d..5331cb1f6eb 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceContextImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceContextImpl.java
@@ -27,6 +27,7 @@ import org.apache.ignite.internal.util.tostring.GridToStringInclude;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.services.Service;
 import org.apache.ignite.services.ServiceCallContext;
+import org.apache.ignite.services.ServiceCallInterceptor;
 import org.apache.ignite.services.ServiceContext;
 import org.apache.ignite.spi.metric.ReadOnlyMetricRegistry;
 import org.jetbrains.annotations.Nullable;
@@ -68,6 +69,10 @@ public class ServiceContextImpl implements ServiceContext {
     @GridToStringExclude
     private volatile Service svc;
 
+    /** Service call interceptor. */
+    @GridToStringExclude
+    private volatile ServiceCallInterceptor interceptor;
+
     /** Cancelled flag. */
     private volatile boolean isCancelled;
 
@@ -87,7 +92,8 @@ public class ServiceContextImpl implements ServiceContext {
         String cacheName,
         Object affKey,
         ExecutorService exe,
-        boolean statisticsEnabled) {
+        boolean statisticsEnabled
+    ) {
         this.name = name;
         this.execId = execId;
         this.cacheName = cacheName;
@@ -135,6 +141,20 @@ public class ServiceContextImpl implements ServiceContext {
         return svc;
     }
 
+    /**
+     * @param interceptor Service call interceptor.
+     */
+    void interceptor(ServiceCallInterceptor interceptor) {
+        this.interceptor = interceptor;
+    }
+
+    /**
+     * @return Service call interceptor.
+     */
+    ServiceCallInterceptor interceptor() {
+        return interceptor;
+    }
+
     /**
      * @return Executor service.
      */
diff --git a/modules/core/src/main/java/org/apache/ignite/services/ServiceCallContext.java b/modules/core/src/main/java/org/apache/ignite/services/ServiceCallContext.java
index 8eae10bafb8..978a68a0f16 100644
--- a/modules/core/src/main/java/org/apache/ignite/services/ServiceCallContext.java
+++ b/modules/core/src/main/java/org/apache/ignite/services/ServiceCallContext.java
@@ -55,6 +55,7 @@ import org.apache.ignite.lang.IgniteExperimental;
  *
  * @see ServiceContext
  * @see ServiceCallContextBuilder
+ * @see ServiceCallInterceptor
  */
 @IgniteExperimental
 public interface ServiceCallContext {
diff --git a/modules/core/src/main/java/org/apache/ignite/services/ServiceCallInterceptor.java b/modules/core/src/main/java/org/apache/ignite/services/ServiceCallInterceptor.java
new file mode 100644
index 00000000000..44ff08221a6
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/services/ServiceCallInterceptor.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.services;
+
+import java.io.Serializable;
+import java.util.concurrent.Callable;
+import org.apache.ignite.lang.IgniteExperimental;
+
+/**
+ * Service call interceptor.
+ * <p>
+ * Allows the user to intercept the call to any service method except lifecycle methods
+ * ({@link Service#init init()}, {@link Service#execute() execute()} and {@link Service#cancel() cancel()}).
+ * <p>
+ * A typical use of an interceptor is a middleware logic that applies to all custom methods in a service.
+ * The user can specify multiple interceptors in the {@link ServiceConfiguration service configuration}.
+ * Each interceptor invokes the next interceptor in the chain using a delegated call, the last interceptor
+ * will call the service method.
+ * <p>
+ * Usage example:
+ * <pre name="code" class="java">
+ * 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(audit, security);
+ *
+ * // Deploy service.
+ * ignite.services().deploy(svcCfg);
+ *
+ * // Set context parameters for the service proxy.
+ * ServiceCallContext callCtx = ServiceCallContext.builder().put("sessionId", sessionId).build();
+ *
+ * // Make service proxy with caller context.
+ * MyService proxy = ignite.services().serviceProxy("service", MyService.class, false, callCtx, 0);
+ *
+ * // The call to the business method to be intercepted.
+ * proxy.placeOrder(order1);
+ * proxy.placeOrder(order2);
+ * </pre>
+ *
+ * @see ServiceCallContext
+ * @see ServiceContext
+ */
+@IgniteExperimental
+public interface ServiceCallInterceptor extends Serializable {
+    /**
+     * Intercepts delegated service call.
+     *
+     * @param mtd Method name.
+     * @param args Method arguments.
+     * @param ctx Service context.
+     * @param next Delegated call to a service method and/or interceptor in the chain.
+     * @return Service call result.
+     */
+    public Object invoke(String mtd, Object[] args, ServiceContext ctx, Callable<Object> next) throws Exception;
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/services/ServiceConfiguration.java b/modules/core/src/main/java/org/apache/ignite/services/ServiceConfiguration.java
index ad2c6948737..76311c1b977 100644
--- a/modules/core/src/main/java/org/apache/ignite/services/ServiceConfiguration.java
+++ b/modules/core/src/main/java/org/apache/ignite/services/ServiceConfiguration.java
@@ -19,11 +19,13 @@ package org.apache.ignite.services;
 
 import java.io.Externalizable;
 import java.io.Serializable;
+import java.util.Arrays;
 import org.apache.ignite.IgniteServices;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.internal.processors.service.IgniteServiceProcessor;
 import org.apache.ignite.internal.util.tostring.GridToStringExclude;
 import org.apache.ignite.internal.util.typedef.internal.S;
+import org.apache.ignite.lang.IgniteExperimental;
 import org.apache.ignite.lang.IgnitePredicate;
 
 /**
@@ -83,6 +85,10 @@ public class ServiceConfiguration implements Serializable {
     /** Enables or disables service statistics. */
     protected boolean isStatisticsEnabled;
 
+    /** Interceptor. */
+    @GridToStringExclude
+    protected ServiceCallInterceptor[] interceptors;
+
     /**
      * Gets service name.
      * <p>
@@ -292,6 +298,29 @@ public class ServiceConfiguration implements Serializable {
         return isStatisticsEnabled;
     }
 
+    /**
+     * Gets service call interceptors.
+     *
+     * @return Service call interceptors.
+     */
+    @IgniteExperimental
+    public ServiceCallInterceptor[] getInterceptors() {
+        return interceptors;
+    }
+
+    /**
+     * Sets service call interceptors.
+     *
+     * @param interceptors Service call interceptors.
+     * @return {@code this} for chaining.
+     */
+    @IgniteExperimental
+    public ServiceConfiguration setInterceptors(ServiceCallInterceptor... interceptors) {
+        this.interceptors = interceptors;
+
+        return this;
+    }
+
     /** {@inheritDoc} */
     @Override public boolean equals(Object o) {
         if (!equalsIgnoreNodeFilter(o))
@@ -344,7 +373,7 @@ public class ServiceConfiguration implements Serializable {
         if (svc != null ? !svc.getClass().equals(that.svc.getClass()) : that.svc != null)
             return false;
 
-        return true;
+        return Arrays.deepEquals(interceptors, that.interceptors);
     }
 
     /** {@inheritDoc} */
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/IgniteServiceCallInterceptorTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/IgniteServiceCallInterceptorTest.java
new file mode 100644
index 00000000000..832b982e870
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/IgniteServiceCallInterceptorTest.java
@@ -0,0 +1,441 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.service;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.naming.NoPermissionException;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.IgniteServices;
+import org.apache.ignite.internal.util.typedef.internal.SB;
+import org.apache.ignite.resources.IgniteInstanceResource;
+import org.apache.ignite.resources.LoggerResource;
+import org.apache.ignite.resources.ServiceContextResource;
+import org.apache.ignite.resources.ServiceResource;
+import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceCallContext;
+import org.apache.ignite.services.ServiceCallInterceptor;
+import org.apache.ignite.services.ServiceConfiguration;
+import org.apache.ignite.services.ServiceContext;
+import org.apache.ignite.services.ServiceDeploymentException;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+/**
+ * Tests service call interceptor.
+ */
+@RunWith(Parameterized.class)
+public class IgniteServiceCallInterceptorTest extends GridCommonAbstractTest implements Serializable {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** String attribute name. */
+    private static final String STR_ATTR_NAME = "str.attr";
+
+    /** Injected service name. */
+    private static final String SVC_NAME_INJECTED = "test-svc-injected";
+
+    /** Intercepted service name. */
+    private static final String SVC_NAME_INTERCEPTED = "test-svc-intercepted";
+
+    /** Nodes count. */
+    private static final int NODES_CNT = 3;
+
+    /** Max number of services per node. */
+    private static final int SVC_PER_NODE = 2;
+
+    /** Interceptor call counter. */
+    private static final AtomicInteger interceptorCallCntr = new AtomicInteger();
+
+    /** Service method call counter. */
+    private static final AtomicInteger svcCallCntr = new AtomicInteger();
+
+    /** Ignite client node. */
+    private static Ignite client;
+
+    /** Flag to deploy single service instance per cluster. */
+    @Parameterized.Parameter(0)
+    public boolean clusterSingleton;
+
+    /** Whether Ignite should always contact the same remote service instance. */
+    @Parameterized.Parameter(1)
+    public boolean sticky;
+
+    /** {@inheritDoc} */
+    @Override public void beforeTestsStarted() throws Exception {
+        startGrids(NODES_CNT - 1);
+        client = startClientGrid(NODES_CNT - 1);
+    }
+
+    /** */
+    @Parameterized.Parameters(name = "clusterSingleton={0}, sticky={1}")
+    public static Collection<?> parameters() {
+        return Arrays.asList(new Object[][] {
+            {false, false},
+            {false, true},
+            {true, true},
+        });
+    }
+
+    /** {@inheritDoc} */
+    @Override public void afterTest() {
+        grid(0).services().cancelAll();
+    }
+
+    /**
+     * @param name Service name.
+     * @param svc Service.
+     * @param singleton Flag to deploy single service instance per cluster.
+     * @param interceptors Interceptors.
+     * @return Service configuration.
+     */
+    private ServiceConfiguration serviceCfg(String name, Service svc, boolean singleton, ServiceCallInterceptor... interceptors) {
+        return new ServiceConfiguration()
+            .setName(name)
+            .setTotalCount(singleton ? 1 : NODES_CNT * SVC_PER_NODE)
+            .setMaxPerNodeCount(SVC_PER_NODE)
+            .setService(svc)
+            .setInterceptors(interceptors);
+    }
+
+    /**
+     * Validates configuration comparison during service deployment.
+     */
+    @Test
+    public void testRedeploy() {
+        ServiceCallInterceptor interceptor = (mtd, args, ctx, next) -> "1";
+
+        ServiceConfiguration cfg = serviceCfg(SVC_NAME_INTERCEPTED, new TestServiceImpl(), clusterSingleton, interceptor);
+        client.services().deploy(cfg);
+
+        TestService proxy = client.services().serviceProxy(SVC_NAME_INTERCEPTED, TestService.class, false);
+        assertEquals("1", proxy.method(0));
+
+        // Redeploy with the same configuration.
+        cfg.setInterceptors(interceptor);
+        client.services().deploy(cfg);
+
+        // Redeploy with different configuration.
+        cfg.setInterceptors((mtd, args, ctx, next) -> "2");
+
+        GridTestUtils.assertThrowsAnyCause(log, () -> {
+            client.services().deploy(cfg);
+
+            return null;
+        }, ServiceDeploymentException.class, null);
+
+        // Redeploy the service with a valid configuration.
+        client.services().cancel(SVC_NAME_INTERCEPTED);
+
+        client.services().deploy(cfg);
+        assertEquals("2", proxy.method(0));
+
+        // Undeploy service.
+        client.services().cancel(SVC_NAME_INTERCEPTED);
+
+        // Deploy with an interceptor that will not be able to deserialize.
+        cfg.setInterceptors(new CriticalErrorInterceptor());
+
+        GridTestUtils.assertThrowsAnyCause(log, () -> {
+            client.services().deploy(cfg);
+
+            return null;
+        }, ServiceDeploymentException.class, null);
+
+        for (int i = 0; i < NODES_CNT - 1; i++)
+            assertNull("idx=" + i, grid(i).services().service(SVC_NAME_INTERCEPTED));
+    }
+
+    /**
+     * Checks if an interceptor exception is being passed to the user.
+     */
+    @Test
+    public void testException() {
+        interceptorCallCntr.set(0);
+        svcCallCntr.set(0);
+
+        Exception expE = new NoPermissionException("Request is forbidden.");
+        String ctxVal = "42";
+
+        ServiceCallInterceptor security = (mtd, args, ctx, next) -> {
+            ServiceCallContext callCtx = ctx.currentCallContext();
+
+            if (callCtx == null || !ctxVal.equals(callCtx.attribute(STR_ATTR_NAME)))
+                throw expE;
+
+            return next.call();
+        };
+
+        ServiceCallInterceptor second = (mtd, args, ctx, next) -> {
+            interceptorCallCntr.incrementAndGet();
+
+            return next.call();
+        };
+
+        client.services().deployAll(Collections.singletonList(
+            serviceCfg(SVC_NAME_INTERCEPTED, new TestServiceImpl(), clusterSingleton, security, second)
+        ));
+
+        TestService noCtxProxy = client.services().serviceProxy(SVC_NAME_INTERCEPTED, TestService.class, sticky);
+        GridTestUtils.assertThrowsAnyCause(null, () -> noCtxProxy.method(0), expE.getClass(), expE.getMessage());
+        assertEquals(0, interceptorCallCntr.get());
+        assertEquals(0, svcCallCntr.get());
+
+        ServiceCallContext callCtx = ServiceCallContext.builder().put(STR_ATTR_NAME, "42").build();
+
+        String res = client.services().serviceProxy(SVC_NAME_INTERCEPTED, TestService.class, sticky, callCtx).method(0);
+        assertEquals("cls=" + TestServiceImpl.class.getSimpleName() + ", ctxVal=" + ctxVal + ", arg=0", res);
+        assertEquals(1, interceptorCallCntr.get());
+        assertEquals(1, svcCallCntr.get());
+    }
+
+    /**
+     * Ensures that the interceptor can completely override the call and prevent the service method invocation.
+     */
+    @Test
+    public void testBasicInterception() {
+        svcCallCntr.set(0);
+
+        String expMsg = "intercepted";
+
+        client.services().deployAll(Collections.singletonList(
+            serviceCfg(SVC_NAME_INTERCEPTED, new TestServiceImpl(), clusterSingleton, (mtd, args, ctx, next) -> expMsg)
+        ));
+
+        TestService svcProxy = client.services().serviceProxy(SVC_NAME_INTERCEPTED, TestService.class, sticky);
+
+        assertEquals(expMsg, svcProxy.method(1));
+        assertEquals(0, svcCallCntr.get());
+    }
+
+    /**
+     * Checks the call order and parameters of the interceptor in multithreaded mode.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testOrder() throws Exception {
+        int interceptorsCnt = 8;
+        int threadCnt = 16;
+
+        ServiceCallInterceptor[] interceptors = new ServiceCallInterceptor[interceptorsCnt];
+
+        for (int i = 0; i < interceptorsCnt; i++)
+            interceptors[interceptorsCnt - i - 1] = new SvcInterceptor(null, i);
+
+        client.services().deployAll(Arrays.asList(
+            serviceCfg(SVC_NAME_INTERCEPTED, new TestServiceImpl(), clusterSingleton, interceptors),
+            serviceCfg(SVC_NAME_INJECTED, new TestServiceInjected(), !clusterSingleton, interceptors)
+        ));
+
+        ServiceCallContext callCtx1 = ServiceCallContext.builder().put(STR_ATTR_NAME, "ctxVal1").build();
+        ServiceCallContext callCtx2 = ServiceCallContext.builder().put(STR_ATTR_NAME, "ctxVal2").build();
+
+        AtomicInteger cntr = new AtomicInteger();
+        CyclicBarrier barrier = new CyclicBarrier(threadCnt);
+
+        GridTestUtils.runMultiThreaded(() -> {
+            int arg = cntr.incrementAndGet();
+            IgniteServices services = grid(arg % NODES_CNT).services();
+
+            TestService ctx1Proxy = services.serviceProxy(SVC_NAME_INTERCEPTED, TestService.class, sticky, callCtx1);
+            TestService ctx2Proxy = services.serviceProxy(SVC_NAME_INTERCEPTED, TestService.class, sticky, callCtx2);
+
+            String exoCtx1 = fmtExpResult(TestServiceImpl.class, callCtx1, interceptors.length, "method", null, arg);
+            String exoCtx2 = fmtExpResult(TestServiceImpl.class, callCtx2, interceptors.length, "method", null, arg);
+            String exoCtx1inj = fmtExpResult(TestServiceInjected.class, callCtx1, interceptors.length, "method", "injected", arg);
+            String exoCtx2inj = fmtExpResult(TestServiceInjected.class, callCtx2, interceptors.length, "method", "injected", arg);
+
+            barrier.await(getTestTimeout(), TimeUnit.MILLISECONDS);
+
+            for (int i = 0; i < 100; i++) {
+                assertEquals(exoCtx1inj, ctx1Proxy.injected(arg));
+                assertEquals(exoCtx2inj, ctx2Proxy.injected(arg));
+                assertEquals(exoCtx1, ctx1Proxy.method(arg));
+                assertEquals(exoCtx2, ctx2Proxy.method(arg));
+            }
+
+            return null;
+        }, threadCnt, "worker");
+    }
+
+    /**
+     * @param cls Service implementation class name.
+     * @param ctx Service call context.
+     * @param incpsCnt Count of interceptors.
+     * @param mtd1 First method that was called.
+     * @param mtd2 Second method that was called (if one service calls another).
+     * @param arg Method argument.
+     * @return Expected result of the method call.
+     */
+    private String fmtExpResult(Class<?> cls, ServiceCallContext ctx, int incpsCnt, String mtd1, String mtd2, int arg) {
+        SB buf = new SB("cls=" + cls.getSimpleName() + ", ctxVal=" + ctx.attribute(STR_ATTR_NAME) + ", arg=" + arg);
+
+        for (String method : new String[] {mtd1, mtd2}) {
+            if (method == null)
+                break;
+
+            for (int i = 0; i < incpsCnt; i++)
+                buf.a("; mtd=").a(method).a(", ctxVal=").a(ctx.attribute(STR_ATTR_NAME)).a(", arg=").a(arg).a(", id=").a(i);
+        }
+
+        return buf.toString();
+    }
+
+    /** */
+    private interface TestService extends Service {
+        /** */
+        String method(int arg);
+
+        /** */
+        String injected(int arg);
+    }
+
+    /** */
+    private static class TestServiceImpl implements TestService {
+        /** Injected service. */
+        @ServiceResource(serviceName = SVC_NAME_INJECTED, proxyInterface = TestService.class, forwardCallerContext = true)
+        private TestService injected;
+
+        /** Service context. */
+        @ServiceContextResource
+        private ServiceContext ctx;
+
+        /** {@inheritDoc} */
+        @Override public String method(int arg) {
+            svcCallCntr.incrementAndGet();
+
+            ServiceCallContext callCtx = ctx.currentCallContext();
+
+            String attrVal = callCtx == null ? null : ctx.currentCallContext().attribute(STR_ATTR_NAME);
+
+            return "cls=" + getClass().getSimpleName() + ", ctxVal=" + attrVal + ", arg=" + arg;
+        }
+
+        /** {@inheritDoc} */
+        @Override public String injected(int arg) {
+            assert injected != null;
+
+            return injected.method(arg);
+        }
+    }
+
+    /** */
+    private static class TestServiceInjected extends TestServiceImpl {
+        /** */
+        private static final long serialVersionUID = 0L;
+    }
+
+    /** Service call interceptor. */
+    static class SvcInterceptor implements ServiceCallInterceptor {
+        /** */
+        private static final long serialVersionUID = 0L;
+
+        /** Ignite. */
+        @IgniteInstanceResource
+        private Ignite ignite;
+
+        /** Logger. */
+        @LoggerResource
+        private IgniteLogger log;
+
+        /** Expected method name. */
+        private final String expMtdName;
+
+        /** Interceptor ID. */
+        private final int id;
+
+        /**
+         * @param expMtdName Expected method name.
+         * @param id Interceptor ID.
+         */
+        public SvcInterceptor(String expMtdName, int id) {
+            this.expMtdName = expMtdName;
+            this.id = id;
+        }
+
+        /** {@inheritDoc} */
+        @Override public Object invoke(String mtd, Object[] args, ServiceContext ctx, Callable<Object> next) throws Exception {
+            assert ignite != null;
+            assert log != null;
+
+            if (expMtdName != null)
+                assertEquals(expMtdName, mtd);
+
+            Object arg = args[0];
+
+            ServiceCallContext callCtx = ctx.currentCallContext();
+
+            assert callCtx != null;
+
+            Object res = next.call();
+
+            callCtx = ctx.currentCallContext();
+
+            assert callCtx != null : res;
+
+            String ctxVal = callCtx == null ? null : ctx.currentCallContext().attribute(STR_ATTR_NAME);
+
+            assert ctxVal != null;
+
+            return res + "; mtd=" + mtd + ", ctxVal=" + ctxVal + ", arg=" + arg + ", id=" + id;
+        }
+    }
+
+    /** */
+    private static class CriticalErrorInterceptor implements ServiceCallInterceptor, Externalizable {
+        /** Serial version uid. */
+        private static final long serialVersionUID = 0L;
+
+        /** */
+        public CriticalErrorInterceptor() {
+            // No-op.
+        }
+
+        /** {@inheritDoc} */
+        @Override public Object invoke(String mtd, Object[] args, ServiceContext ctx,
+            Callable<Object> next) throws Exception {
+
+            return next.call();
+        }
+
+        /** {@inheritDoc} */
+        @Override public void writeExternal(ObjectOutput out) throws IOException {
+            // No-op.
+        }
+
+        /** {@inheritDoc} */
+        @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+            throw new AssertionError("Expected critical error.");
+        }
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/IgniteServiceDeployment2ClassLoadersDefaultMarshallerTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/IgniteServiceDeployment2ClassLoadersDefaultMarshallerTest.java
index c8b9f879eae..86de5c848f2 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/IgniteServiceDeployment2ClassLoadersDefaultMarshallerTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/IgniteServiceDeployment2ClassLoadersDefaultMarshallerTest.java
@@ -26,6 +26,7 @@ import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.lang.IgnitePredicate;
 import org.apache.ignite.marshaller.Marshaller;
 import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceCallInterceptor;
 import org.apache.ignite.services.ServiceConfiguration;
 import org.apache.ignite.testframework.GridTestExternalClassLoader;
 import org.apache.ignite.testframework.config.GridTestProperties;
@@ -33,15 +34,21 @@ import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
 
 /**
- * Tests that not all nodes in cluster need user's service definition (only nodes according to filter).
+ * Tests that not all nodes in cluster need user's service and interceptor definition (only nodes according to filter).
  */
 public class IgniteServiceDeployment2ClassLoadersDefaultMarshallerTest extends GridCommonAbstractTest {
     /** */
     private static final String NOOP_SERVICE_CLS_NAME = "org.apache.ignite.tests.p2p.NoopService";
 
+    /** */
+    private static final String NOOP_SERVICE_INTCP_NAME = "org.apache.ignite.tests.p2p.NoopServiceCallInterceptor";
+
     /** */
     private static final String NOOP_SERVICE_2_CLS_NAME = "org.apache.ignite.tests.p2p.NoopService2";
 
+    /** */
+    private static final String NOOP_SERVICE_INTCP_2_NAME = "org.apache.ignite.tests.p2p.NoopServiceCallInterceptor2";
+
     /** */
     private static final int GRID_CNT = 6;
 
@@ -115,8 +122,8 @@ public class IgniteServiceDeployment2ClassLoadersDefaultMarshallerTest extends G
 
     /** {@inheritDoc} */
     @Override protected void beforeTestsStarted() throws Exception {
-        extClsLdr1 = new GridTestExternalClassLoader(URLS, NOOP_SERVICE_2_CLS_NAME);
-        extClsLdr2 = new GridTestExternalClassLoader(URLS, NOOP_SERVICE_CLS_NAME);
+        extClsLdr1 = new GridTestExternalClassLoader(URLS, NOOP_SERVICE_2_CLS_NAME, NOOP_SERVICE_INTCP_2_NAME);
+        extClsLdr2 = new GridTestExternalClassLoader(URLS, NOOP_SERVICE_CLS_NAME, NOOP_SERVICE_INTCP_NAME);
     }
 
     /** {@inheritDoc} */
@@ -200,15 +207,22 @@ public class IgniteServiceDeployment2ClassLoadersDefaultMarshallerTest extends G
         srvCfg.setNodeFilter(new TestNodeFilter(firstGrp ? grp1 : grp2));
 
         Class<Service> srvcCls;
+        Class<ServiceCallInterceptor> interceptorCls;
 
-        if (firstGrp)
+        if (firstGrp) {
             srvcCls = (Class<Service>)extClsLdr1.loadClass(NOOP_SERVICE_CLS_NAME);
-        else
+            interceptorCls = (Class<ServiceCallInterceptor>)extClsLdr1.loadClass(NOOP_SERVICE_INTCP_NAME);
+        }
+        else {
             srvcCls = (Class<Service>)extClsLdr2.loadClass(NOOP_SERVICE_2_CLS_NAME);
+            interceptorCls = (Class<ServiceCallInterceptor>)extClsLdr2.loadClass(NOOP_SERVICE_INTCP_2_NAME);
+        }
 
         Service srvc = srvcCls.newInstance();
+        ServiceCallInterceptor interceptor = interceptorCls.newInstance();
 
         srvCfg.setService(srvc);
+        srvCfg.setInterceptors(interceptor);
 
         srvCfg.setName("TestDeploymentService" + (firstGrp ? 1 : 2));
 
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteServiceGridTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteServiceGridTestSuite.java
index a1fe470a118..a5ee7fe1bba 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteServiceGridTestSuite.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteServiceGridTestSuite.java
@@ -39,6 +39,7 @@ import org.apache.ignite.internal.processors.service.GridServiceProxyTopologyIni
 import org.apache.ignite.internal.processors.service.GridServiceReassignmentSelfTest;
 import org.apache.ignite.internal.processors.service.GridServiceSerializationSelfTest;
 import org.apache.ignite.internal.processors.service.IgniteServiceCallContextTest;
+import org.apache.ignite.internal.processors.service.IgniteServiceCallInterceptorTest;
 import org.apache.ignite.internal.processors.service.IgniteServiceDeployment2ClassLoadersDefaultMarshallerTest;
 import org.apache.ignite.internal.processors.service.IgniteServiceDeployment2ClassLoadersJdkMarshallerTest;
 import org.apache.ignite.internal.processors.service.IgniteServiceDeployment2ClassLoadersOptimizedMarshallerTest;
@@ -122,7 +123,8 @@ import org.junit.runners.Suite;
     GridServiceDeployClusterReadOnlyModeTest.class,
     GridServiceClusterReadOnlyModeTest.class,
     IgniteServiceCallContextTest.class,
-    GridServiceMetricsTest.class
+    GridServiceMetricsTest.class,
+    IgniteServiceCallInterceptorTest.class
 })
 public class IgniteServiceGridTestSuite {
     /** */
diff --git a/modules/extdata/p2p/src/main/java/org/apache/ignite/tests/p2p/NoopServiceCallInterceptor.java b/modules/extdata/p2p/src/main/java/org/apache/ignite/tests/p2p/NoopServiceCallInterceptor.java
new file mode 100644
index 00000000000..cc517d4a61c
--- /dev/null
+++ b/modules/extdata/p2p/src/main/java/org/apache/ignite/tests/p2p/NoopServiceCallInterceptor.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.tests.p2p;
+
+import java.util.concurrent.Callable;
+import org.apache.ignite.services.ServiceCallInterceptor;
+import org.apache.ignite.services.ServiceContext;
+
+/**
+ * No-op service call interceptor.
+ */
+public class NoopServiceCallInterceptor implements ServiceCallInterceptor {
+    /** {@inheritDoc} */
+    @Override public Object invoke(String mtd, Object[] args, ServiceContext ctx, Callable<Object> next) throws Exception {
+        return next.call();
+    }
+}
diff --git a/modules/extdata/p2p/src/main/java/org/apache/ignite/tests/p2p/NoopServiceCallInterceptor2.java b/modules/extdata/p2p/src/main/java/org/apache/ignite/tests/p2p/NoopServiceCallInterceptor2.java
new file mode 100644
index 00000000000..d5cc6021f82
--- /dev/null
+++ b/modules/extdata/p2p/src/main/java/org/apache/ignite/tests/p2p/NoopServiceCallInterceptor2.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.tests.p2p;
+
+import java.util.concurrent.Callable;
+import org.apache.ignite.services.ServiceCallInterceptor;
+import org.apache.ignite.services.ServiceContext;
+
+/**
+ * No-op service call interceptor.
+ */
+public class NoopServiceCallInterceptor2 implements ServiceCallInterceptor {
+    /** {@inheritDoc} */
+    @Override public Object invoke(String mtd, Object[] args, ServiceContext ctx, Callable<Object> next) throws Exception {
+        return next.call();
+    }
+}