You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by al...@apache.org on 2020/06/17 08:06:42 UTC

[ignite] branch master updated: IGNITE-13033 Java thin client: Service invocation - Fixes #7908.

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

alexpl 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 7e0d656  IGNITE-13033 Java thin client: Service invocation - Fixes #7908.
7e0d656 is described below

commit 7e0d6568c5ce20e98c1be2949c36baa34170f229
Author: Aleksey Plekhanov <pl...@gmail.com>
AuthorDate: Wed Jun 17 12:58:22 2020 +0500

    IGNITE-13033 Java thin client: Service invocation - Fixes #7908.
    
    Signed-off-by: Aleksey Plekhanov <pl...@gmail.com>
---
 .../org/apache/ignite/client/ClientServices.java   |  56 +++
 .../org/apache/ignite/client/IgniteClient.java     |  20 +
 .../internal/client/thin/ClientOperation.java      |   4 +-
 .../internal/client/thin/ClientServicesImpl.java   | 180 +++++++++
 .../ignite/internal/client/thin/ClientUtils.java   |  11 +-
 .../client/thin/ProtocolBitmaskFeature.java        |   5 +-
 .../internal/client/thin/ProtocolContext.java      |  12 +
 .../internal/client/thin/TcpIgniteClient.java      |  16 +
 .../platform/client/ClientBitmaskFeature.java      |   5 +-
 .../platform/client/ClientMessageParser.java       |  12 +-
 .../client/service/ClientServiceInvokeRequest.java | 323 ++++++++++++++++
 .../platform/services/PlatformServices.java        |  13 +
 .../processors/service/GridServiceProxy.java       |  10 +-
 .../org/apache/ignite/client/AsyncChannelTest.java |  16 +-
 .../ignite/client/ConnectToStartingNodeTest.java   |  12 +-
 .../test/java/org/apache/ignite/client/Person.java |   6 +
 .../client/thin/AbstractThinClientTest.java        |  79 ++++
 .../internal/client/thin/ClusterApiTest.java       |  13 +-
 .../internal/client/thin/ClusterGroupTest.java     |  11 +-
 .../internal/client/thin/ComputeTaskTest.java      |  17 +-
 .../ignite/internal/client/thin/ServicesTest.java  | 417 +++++++++++++++++++++
 .../platform/AbstractPlatformServiceCallTask.java  |  30 ++
 .../PlatformServiceCallCollectionsTask.java        |   9 +-
 .../PlatformServiceCallCollectionsThinTask.java    |  65 ++++
 .../ignite/platform/PlatformServiceCallTask.java   |  58 +--
 .../platform/PlatformServiceCallThinTask.java      |  76 ++++
 .../org/apache/ignite/client/ClientTestSuite.java  |   2 +
 .../Services/CallPlatformServiceTest.cs            |  37 +-
 .../config/benchmark-thin-services.properties      |  78 ++++
 .../yardstick/config/ignite-services-config.xml    |  51 +++
 .../IgniteThinServiceInvocationBenchmark.java      |  45 +++
 .../yardstick/thin/service/SimpleService.java      |  28 ++
 .../yardstick/thin/service/SimpleServiceImpl.java  |  46 +++
 33 files changed, 1638 insertions(+), 125 deletions(-)

diff --git a/modules/core/src/main/java/org/apache/ignite/client/ClientServices.java b/modules/core/src/main/java/org/apache/ignite/client/ClientServices.java
new file mode 100644
index 0000000..fe2408e
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/client/ClientServices.java
@@ -0,0 +1,56 @@
+/*
+ * 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.client;
+
+/**
+ * Thin client services facade.
+ */
+public interface ClientServices {
+    /**
+     * Gets the cluster group to which this {@code ClientServices} instance belongs.
+     *
+     * @return Cluster group to which this {@code ClientServices} instance belongs.
+     */
+    public ClientClusterGroup clusterGroup();
+
+    /**
+     * Gets a remote handle on the service.
+     * <p>
+     * Note: There are no guarantees that each method invocation for the same proxy will always contact the same remote
+     * service (on the same remote node).
+     *
+     * @param name Service name.
+     * @param svcItf Interface for the service.
+     * @return Proxy over remote service.
+     */
+    public <T> T serviceProxy(String name, Class<? super T> svcItf);
+
+    /**
+     * Gets a remote handle on the service with timeout.
+     * <p>
+     * Note: There are no guarantees that each method invocation for the same proxy will always contact the same remote
+     * service (on the same remote node).
+     *
+     * @param name Service name.
+     * @param svcItf Interface for the service.
+     * @param timeout If greater than 0 created proxy will wait for service availability only specified time,
+     *  and will limit remote service invocation time.
+     * @return Proxy over remote service.
+     */
+    public <T> T serviceProxy(String name, Class<? super T> svcItf, long timeout);
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/client/IgniteClient.java b/modules/core/src/main/java/org/apache/ignite/client/IgniteClient.java
index fa8cf8f..1d10a8a 100644
--- a/modules/core/src/main/java/org/apache/ignite/client/IgniteClient.java
+++ b/modules/core/src/main/java/org/apache/ignite/client/IgniteClient.java
@@ -119,4 +119,24 @@ public interface IgniteClient extends AutoCloseable {
      * @return Client cluster facade.
      */
     public ClientCluster cluster();
+
+    /**
+     * Gets {@code services} facade over all cluster nodes started in server mode.
+     *
+     * @return Services facade over all cluster nodes started in server mode.
+     */
+    public ClientServices services();
+
+    /**
+     * Gets {@code services} facade over nodes within the cluster group. All operations
+     * on the returned {@link ClientServices} instance will only include nodes from
+     * the specified cluster group.
+     *
+     * Note: In some cases there will be additional requests for each service invocation from client to server
+     * to resolve cluster group.
+     *
+     * @param grp Cluster group.
+     * @return {@code Services} functionality over given cluster group.
+     */
+    public ClientServices services(ClientClusterGroup grp);
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientOperation.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientOperation.java
index 3cf228c..34fb17d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientOperation.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientOperation.java
@@ -74,7 +74,9 @@ enum ClientOperation {
     /** Get nodes info by IDs. */CLUSTER_GROUP_GET_NODE_INFO(5101),
 
     /** Execute compute task. */COMPUTE_TASK_EXECUTE(6000),
-    /** Finished compute task notification. */COMPUTE_TASK_FINISHED(6001, true);
+    /** Finished compute task notification. */COMPUTE_TASK_FINISHED(6001, true),
+
+    /** Invoke service. */SERVICE_INVOKE(7000);
 
     /** Code. */
     private final int code;
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientServicesImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientServicesImpl.java
new file mode 100644
index 0000000..5bf5234
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientServicesImpl.java
@@ -0,0 +1,180 @@
+/*
+ * 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.client.thin;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Collection;
+import java.util.UUID;
+import org.apache.ignite.client.ClientClusterGroup;
+import org.apache.ignite.client.ClientException;
+import org.apache.ignite.client.ClientServices;
+import org.apache.ignite.internal.binary.BinaryRawWriterEx;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.A;
+import org.apache.ignite.platform.PlatformServiceMethod;
+
+/**
+ * Implementation of {@link ClientServices}.
+ */
+class ClientServicesImpl implements ClientServices {
+    /** Channel. */
+    private final ReliableChannel ch;
+
+    /** Binary marshaller. */
+    private final ClientBinaryMarshaller marsh;
+
+    /** Utils for serialization/deserialization. */
+    private final ClientUtils utils;
+
+    /** Cluster group. */
+    private final ClientClusterGroupImpl grp;
+
+    /** Constructor. */
+    ClientServicesImpl(ReliableChannel ch, ClientBinaryMarshaller marsh, ClientClusterGroupImpl grp) {
+        this.ch = ch;
+        this.marsh = marsh;
+        this.grp = grp;
+
+        utils = new ClientUtils(marsh);
+    }
+
+    /** {@inheritDoc} */
+    @Override public ClientClusterGroup clusterGroup() {
+        return grp;
+    }
+
+    /** {@inheritDoc} */
+    @Override public <T> T serviceProxy(String name, Class<? super T> svcItf) {
+        return serviceProxy(name, svcItf, 0);
+    }
+
+    /** {@inheritDoc} */
+    @Override public <T> T serviceProxy(String name, Class<? super T> svcItf, long timeout) {
+        A.notNullOrEmpty(name, "name");
+        A.notNull(svcItf, "svcItf");
+
+        return (T)Proxy.newProxyInstance(svcItf.getClassLoader(), new Class[] {svcItf},
+            new ServiceInvocationHandler<>(name, timeout, grp));
+    }
+
+    /**
+     * Gets services facade over the specified cluster group.
+     *
+     * @param grp Cluster group.
+     */
+    ClientServices withClusterGroup(ClientClusterGroupImpl grp) {
+        A.notNull(grp, "grp");
+
+        return new ClientServicesImpl(ch, marsh, grp);
+    }
+
+    /**
+     * Service invocation handler.
+     */
+    private class ServiceInvocationHandler<T> implements InvocationHandler {
+        /** Flag "Has parameter types" mask. */
+        private static final byte FLAG_PARAMETER_TYPES_MASK = 0x02;
+
+        /** Service name. */
+        private final String name;
+
+        /** Timeout. */
+        private final long timeout;
+
+        /** Cluster group. */
+        private final ClientClusterGroupImpl grp;
+
+        /**
+         * @param name Service name.
+         * @param timeout Timeout.
+         */
+        private ServiceInvocationHandler(String name, long timeout, ClientClusterGroupImpl grp) {
+            this.name = name;
+            this.timeout = timeout;
+            this.grp = grp;
+        }
+
+        /** {@inheritDoc} */
+        @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+            try {
+                Collection<UUID> nodeIds = grp.nodeIds();
+
+                if (nodeIds != null && nodeIds.isEmpty())
+                    throw new ClientException("Cluster group is empty.");
+
+                return ch.service(ClientOperation.SERVICE_INVOKE,
+                    req -> writeServiceInvokeRequest(req, nodeIds, method, args),
+                    res -> utils.readObject(res.in(), false)
+                );
+            }
+            catch (ClientError e) {
+                throw new ClientException(e);
+            }
+        }
+
+        /**
+         * @param ch Payload output channel.
+         */
+        private void writeServiceInvokeRequest(
+            PayloadOutputChannel ch,
+            Collection<UUID> nodeIds,
+            Method method,
+            Object[] args
+        ) {
+            ch.clientChannel().protocolCtx().checkFeatureSupported(ProtocolBitmaskFeature.SERVICE_INVOKE);
+
+            try (BinaryRawWriterEx writer = utils.createBinaryWriter(ch.out())) {
+                writer.writeString(name);
+                writer.writeByte(FLAG_PARAMETER_TYPES_MASK); // Flags.
+                writer.writeLong(timeout);
+
+                if (nodeIds == null)
+                    writer.writeInt(0);
+                else {
+                    writer.writeInt(nodeIds.size());
+
+                    for (UUID nodeId : nodeIds) {
+                        writer.writeLong(nodeId.getMostSignificantBits());
+                        writer.writeLong(nodeId.getLeastSignificantBits());
+                    }
+                }
+
+                PlatformServiceMethod ann = method.getDeclaredAnnotation(PlatformServiceMethod.class);
+
+                writer.writeString(ann != null ? ann.value() : method.getName());
+
+                Class<?>[] paramTypes = method.getParameterTypes();
+
+                if (F.isEmpty(args))
+                    writer.writeInt(0);
+                else {
+                    writer.writeInt(args.length);
+
+                    assert args.length == paramTypes.length : "args=" + args.length + ", types=" + paramTypes.length;
+
+                    for (int i = 0; i < args.length; i++) {
+                        writer.writeInt(marsh.context().typeId(paramTypes[i].getName()));
+                        writer.writeObject(args[i]);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientUtils.java
index a781546..d59200f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientUtils.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.client.thin;
 
 import java.io.IOException;
+import java.lang.reflect.Array;
 import java.util.AbstractMap.SimpleEntry;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -55,6 +56,7 @@ import org.apache.ignite.internal.binary.BinaryRawWriterEx;
 import org.apache.ignite.internal.binary.BinaryReaderExImpl;
 import org.apache.ignite.internal.binary.BinaryReaderHandles;
 import org.apache.ignite.internal.binary.BinarySchema;
+import org.apache.ignite.internal.binary.BinaryThreadLocalContext;
 import org.apache.ignite.internal.binary.BinaryUtils;
 import org.apache.ignite.internal.binary.BinaryWriterExImpl;
 import org.apache.ignite.internal.binary.streams.BinaryHeapInputStream;
@@ -527,6 +529,13 @@ final class ClientUtils {
         out.writeByteArray(marsh.marshal(obj));
     }
 
+    /**
+     * @param out Output stream.
+     */
+    BinaryRawWriterEx createBinaryWriter(BinaryOutputStream out) {
+        return new BinaryWriterExImpl(marsh.context(), out, BinaryThreadLocalContext.get().schemaHolder(), null);
+    }
+
     /** Read Ignite binary object from input stream. */
     <T> T readObject(BinaryInputStream in, boolean keepBinary) {
         if (keepBinary)
@@ -590,7 +599,7 @@ final class ClientUtils {
         if (BinaryUtils.knownArray(arr))
             return arr;
 
-        Object[] res = new Object[arr.length];
+        Object[] res = (Object[])Array.newInstance(arr.getClass().getComponentType(), arr.length);
 
         for (int i = 0; i < arr.length; i++)
             res[i] = unwrapBinary(arr[i], hnds);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ProtocolBitmaskFeature.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ProtocolBitmaskFeature.java
index 3b705b9..c86dd7b 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ProtocolBitmaskFeature.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ProtocolBitmaskFeature.java
@@ -37,7 +37,10 @@ public enum ProtocolBitmaskFeature {
     CLUSTER_STATES(2),
 
     /** Cluster groups. */
-    CLUSTER_GROUPS(4);
+    CLUSTER_GROUPS(4),
+
+    /** Invoke service methods. */
+    SERVICE_INVOKE(5);
 
     /** */
     private static final EnumSet<ProtocolBitmaskFeature> ALL_FEATURES_AS_ENUM_SET =
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ProtocolContext.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ProtocolContext.java
index e5be575..ec8fb6e 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ProtocolContext.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ProtocolContext.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.client.thin;
 
 import java.util.EnumSet;
+import org.apache.ignite.client.ClientFeatureNotSupportedByServerException;
 
 /**
  * Protocol Context.
@@ -46,6 +47,17 @@ public class ProtocolContext {
     }
 
     /**
+     * Check that feature is supported by the server.
+     *
+     * @param feature Feature.
+     * @throws ClientFeatureNotSupportedByServerException If feature is not supported by the server.
+     */
+    public void checkFeatureSupported(ProtocolBitmaskFeature feature) throws ClientFeatureNotSupportedByServerException {
+        if (!isFeatureSupported(feature))
+            throw new ClientFeatureNotSupportedByServerException(feature);
+    }
+
+    /**
      * @return {@code true} if protocol version feature supported.
      */
     public boolean isFeatureSupported(ProtocolVersionFeature feature) {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java
index e4c6b9f..3db66f5 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java
@@ -38,6 +38,7 @@ import org.apache.ignite.client.ClientCluster;
 import org.apache.ignite.client.ClientClusterGroup;
 import org.apache.ignite.client.ClientCompute;
 import org.apache.ignite.client.ClientException;
+import org.apache.ignite.client.ClientServices;
 import org.apache.ignite.client.ClientTransactions;
 import org.apache.ignite.client.IgniteClient;
 import org.apache.ignite.configuration.ClientConfiguration;
@@ -78,6 +79,9 @@ public class TcpIgniteClient implements IgniteClient {
     /** Cluster facade. */
     private final ClientClusterImpl cluster;
 
+    /** Services facade. */
+    private final ClientServicesImpl services;
+
     /** Marshaller. */
     private final ClientBinaryMarshaller marsh;
 
@@ -115,6 +119,8 @@ public class TcpIgniteClient implements IgniteClient {
         cluster = new ClientClusterImpl(ch, marsh);
 
         compute = new ClientComputeImpl(ch, marsh, cluster.defaultClusterGroup());
+
+        services = new ClientServicesImpl(ch, marsh, cluster.defaultClusterGroup());
     }
 
     /** {@inheritDoc} */
@@ -228,6 +234,16 @@ public class TcpIgniteClient implements IgniteClient {
         return cluster;
     }
 
+    /** {@inheritDoc} */
+    @Override public ClientServices services() {
+        return services;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ClientServices services(ClientClusterGroup grp) {
+        return services.withClusterGroup((ClientClusterGroupImpl)grp);
+    }
+
     /**
      * Initializes new instance of {@link IgniteClient}.
      *
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientBitmaskFeature.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientBitmaskFeature.java
index 3da4e11..3ec7530 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientBitmaskFeature.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientBitmaskFeature.java
@@ -37,7 +37,10 @@ public enum ClientBitmaskFeature implements ThinProtocolFeature {
     CLUSTER_GROUP_GET_NODES_ENDPOINTS(3),
 
     /** Cluster groups. */
-    CLUSTER_GROUPS(4);
+    CLUSTER_GROUPS(4),
+
+    /** Service invocation. */
+    SERVICE_INVOKE(5);
 
     /** */
     private static final EnumSet<ClientBitmaskFeature> ALL_FEATURES_AS_ENUM_SET =
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java
index 37ef131..94d32bc 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java
@@ -17,7 +17,6 @@
 
 package org.apache.ignite.internal.processors.platform.client;
 
-import org.apache.ignite.internal.binary.BinaryRawReaderEx;
 import org.apache.ignite.internal.binary.BinaryRawWriterEx;
 import org.apache.ignite.internal.binary.BinaryReaderExImpl;
 import org.apache.ignite.internal.binary.GridBinaryMarshaller;
@@ -75,6 +74,7 @@ import org.apache.ignite.internal.processors.platform.client.cluster.ClientClust
 import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterWalChangeStateRequest;
 import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterWalGetStateRequest;
 import org.apache.ignite.internal.processors.platform.client.compute.ClientExecuteTaskRequest;
+import org.apache.ignite.internal.processors.platform.client.service.ClientServiceInvokeRequest;
 import org.apache.ignite.internal.processors.platform.client.tx.ClientTxEndRequest;
 import org.apache.ignite.internal.processors.platform.client.tx.ClientTxStartRequest;
 
@@ -250,6 +250,9 @@ public class ClientMessageParser implements ClientListenerMessageParser {
     /** */
     public static final short OP_COMPUTE_TASK_FINISHED = 6001;
 
+    /** Service invocation. */
+    private static final short OP_SERVICE_INVOKE = 7000;
+
     /** Marshaller. */
     private final GridBinaryMarshaller marsh;
 
@@ -280,7 +283,7 @@ public class ClientMessageParser implements ClientListenerMessageParser {
         BinaryInputStream inStream = new BinaryHeapInputStream(msg);
 
         // skipHdrCheck must be true (we have 103 op code).
-        BinaryRawReaderEx reader = new BinaryReaderExImpl(marsh.context(), inStream,
+        BinaryReaderExImpl reader = new BinaryReaderExImpl(marsh.context(), inStream,
                 null, null, true, true);
 
         return decode(reader);
@@ -292,7 +295,7 @@ public class ClientMessageParser implements ClientListenerMessageParser {
      * @param reader Reader.
      * @return Request.
      */
-    public ClientListenerRequest decode(BinaryRawReaderEx reader) {
+    public ClientListenerRequest decode(BinaryReaderExImpl reader) {
         short opCode = reader.readShort();
 
         switch (opCode) {
@@ -450,6 +453,9 @@ public class ClientMessageParser implements ClientListenerMessageParser {
 
             case OP_COMPUTE_TASK_EXECUTE:
                 return new ClientExecuteTaskRequest(reader);
+
+            case OP_SERVICE_INVOKE:
+                return new ClientServiceInvokeRequest(reader);
         }
 
         return new ClientRawRequest(reader.readLong(), ClientStatus.INVALID_OP_CODE,
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/service/ClientServiceInvokeRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/service/ClientServiceInvokeRequest.java
new file mode 100644
index 0000000..e51780b
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/service/ClientServiceInvokeRequest.java
@@ -0,0 +1,323 @@
+/*
+ * 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.platform.client.service;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.ignite.IgniteBinary;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.IgniteServices;
+import org.apache.ignite.internal.binary.BinaryRawReaderEx;
+import org.apache.ignite.internal.binary.BinaryReaderExImpl;
+import org.apache.ignite.internal.cluster.ClusterGroupAdapter;
+import org.apache.ignite.internal.processors.platform.PlatformNativeException;
+import org.apache.ignite.internal.processors.platform.client.ClientConnectionContext;
+import org.apache.ignite.internal.processors.platform.client.ClientObjectResponse;
+import org.apache.ignite.internal.processors.platform.client.ClientRequest;
+import org.apache.ignite.internal.processors.platform.client.ClientResponse;
+import org.apache.ignite.internal.processors.platform.services.PlatformService;
+import org.apache.ignite.internal.processors.platform.services.PlatformServices;
+import org.apache.ignite.internal.processors.service.GridServiceProxy;
+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.ServiceDescriptor;
+
+/**
+ * Request to invoke service method.
+ */
+public class ClientServiceInvokeRequest extends ClientRequest {
+    /** Flag keep binary mask. */
+    private static final byte FLAG_KEEP_BINARY_MASK = 0x01;
+
+    /** Flag "has parameter types", indicates that method arguments prefixed by typeId to help to resolve methods. */
+    private static final byte FLAG_PARAMETER_TYPES_MASK = 0x02;
+
+    /** Methods cache. */
+    private static final Map<MethodDescriptor, Method> methodsCache = new ConcurrentHashMap<>();
+
+    /** Service name. */
+    private final String name;
+
+    /** Flags. */
+    private final byte flags;
+
+    /** Timeout. */
+    private final long timeout;
+
+    /** Nodes. */
+    private final Collection<UUID> nodeIds;
+
+    /** Method name. */
+    private final String methodName;
+
+    /** Method parameter type IDs. */
+    private final int[] paramTypeIds;
+
+    /** Service arguments. */
+    private final Object[] args;
+
+    /** Objects reader. */
+    private final BinaryRawReaderEx reader;
+
+    /**
+     * Constructor.
+     *
+     * @param reader Reader.
+     */
+    public ClientServiceInvokeRequest(BinaryReaderExImpl reader) {
+        super(reader);
+
+        name = reader.readString();
+
+        flags = reader.readByte();
+
+        timeout = reader.readLong();
+
+        int cnt = reader.readInt();
+
+        nodeIds = new ArrayList<>(cnt);
+
+        for (int i = 0; i < cnt; i++)
+            nodeIds.add(new UUID(reader.readLong(), reader.readLong()));
+
+        methodName = reader.readString();
+
+        int argCnt = reader.readInt();
+
+        paramTypeIds = hasParameterTypes() ? new int[argCnt] : null;
+
+        args = new Object[argCnt];
+
+        // We can't deserialize some types (arrays of user defined types for example) from detached objects.
+        // On the other hand, deserialize should be done as part of process() call (not in constructor) for proper
+        // error handling.
+        // To overcome these issues we store binary reader reference, parse request in constructor (by reading detached
+        // objects), restore arguments starting position in input stream and deserialize arguments from input stream
+        // in process() method.
+        this.reader = reader;
+
+        int argsStartPos = reader.in().position();
+
+        for (int i = 0; i < argCnt; i++) {
+            if (paramTypeIds != null)
+                paramTypeIds[i] = reader.readInt();
+
+            args[i] = reader.readObjectDetached();
+        }
+
+        reader.in().position(argsStartPos);
+    }
+
+    /** {@inheritDoc} */
+    @Override public ClientResponse process(ClientConnectionContext ctx) {
+        if (F.isEmpty(name))
+            throw new IgniteException("Service name can't be empty");
+
+        if (F.isEmpty(methodName))
+            throw new IgniteException("Method name can't be empty");
+
+        ServiceDescriptor desc = findServiceDescriptor(ctx, name);
+
+        Class<?> svcCls = desc.serviceClass();
+
+        ClusterGroupAdapter grp = ctx.kernalContext().cluster().get();
+
+        if (ctx.securityContext() != null)
+            grp = (ClusterGroupAdapter)grp.forSubjectId(ctx.securityContext().subject().id());
+
+        grp = (ClusterGroupAdapter)(nodeIds.isEmpty() ? grp.forServers() : grp.forNodeIds(nodeIds));
+
+        IgniteServices services = grp.services();
+
+        if (!keepBinary() && args.length > 0) {
+            for (int i = 0; i < args.length; i++) {
+                if (paramTypeIds != null)
+                    reader.readInt(); // Skip parameter typeId, we already read it in constructor.
+
+                args[i] = reader.readObject();
+            }
+        }
+
+        try {
+            Object res;
+
+            if (PlatformService.class.isAssignableFrom(svcCls)) {
+                PlatformService proxy = services.serviceProxy(name, PlatformService.class, false, timeout);
+
+                res = proxy.invokeMethod(methodName, keepBinary(), !keepBinary(), args);
+            }
+            else {
+                GridServiceProxy<?> proxy = new GridServiceProxy<>(grp, name, Service.class, false, timeout,
+                    ctx.kernalContext());
+
+                Method method = resolveMethod(ctx, svcCls);
+
+                res = proxy.invokeMethod(method, args);
+            }
+
+            return new ClientObjectResponse(requestId(), res);
+        }
+        catch (PlatformNativeException e) {
+            ctx.kernalContext().log(getClass()).error("Failed to invoke platform service", e);
+
+            throw new IgniteException("Failed to invoke platform service, see server logs for details");
+        }
+        catch (Throwable e) {
+            throw new IgniteException(e);
+        }
+    }
+
+    /**
+     * Keep binary flag.
+     */
+    private boolean keepBinary() {
+        return (flags & FLAG_KEEP_BINARY_MASK) != 0;
+    }
+
+    /**
+     * "Has parameter types" flag.
+     */
+    private boolean hasParameterTypes() {
+        return (flags & FLAG_PARAMETER_TYPES_MASK) != 0;
+    }
+
+    /**
+     * @param ctx Connection context.
+     * @param name Service name.
+     */
+    private static ServiceDescriptor findServiceDescriptor(ClientConnectionContext ctx, String name) {
+        for (ServiceDescriptor desc : ctx.kernalContext().service().serviceDescriptors()) {
+            if (name.equals(desc.name()))
+                return desc;
+        }
+
+        throw new IgniteException("Service not found: " + name);
+    }
+
+    /**
+     * Resolve method by method name and parameter types or parameter values.
+     */
+    private Method resolveMethod(ClientConnectionContext ctx, Class<?> cls) throws ReflectiveOperationException {
+        if (paramTypeIds != null) {
+            MethodDescriptor desc = new MethodDescriptor(cls, methodName, paramTypeIds);
+
+            Method method = methodsCache.get(desc);
+
+            if (method != null)
+                return method;
+
+            IgniteBinary binary = ctx.kernalContext().grid().binary();
+
+            for (Method method0 : cls.getMethods()) {
+                if (methodName.equals(method0.getName())) {
+                    MethodDescriptor desc0 = MethodDescriptor.forMethod(binary, method0);
+
+                    methodsCache.putIfAbsent(desc0, method0);
+
+                    if (desc0.equals(desc))
+                        return method0;
+                }
+            }
+
+            throw new NoSuchMethodException("Method not found: " + desc);
+        }
+
+        // Try to find method by name and parameter values.
+        return PlatformServices.getMethod(cls, methodName, args);
+    }
+
+    /**
+     *
+     */
+    private static class MethodDescriptor {
+        /** Class. */
+        private final Class<?> cls;
+
+        /** Method name. */
+        private final String methodName;
+
+        /** Parameter type IDs. */
+        private final int[] paramTypeIds;
+
+        /** Hash code. */
+        private final int hash;
+
+        /**
+         * @param cls Class.
+         * @param methodName Method name.
+         * @param paramTypeIds Parameter type ids.
+         */
+        private MethodDescriptor(Class<?> cls, String methodName, int[] paramTypeIds) {
+            assert cls != null;
+            assert methodName != null;
+            assert paramTypeIds != null;
+
+            this.cls = cls;
+            this.methodName = methodName;
+            this.paramTypeIds = paramTypeIds;
+
+            // Precalculate hash in constructor, since we need it for all objects of this class.
+            hash = 31 * ((31 * cls.hashCode()) + methodName.hashCode()) + Arrays.hashCode(paramTypeIds);
+        }
+
+        /**
+         * @param binary Ignite binary.
+         * @param method Method.
+         */
+        private static MethodDescriptor forMethod(IgniteBinary binary, Method method) {
+            Class<?>[] paramTypes = method.getParameterTypes();
+
+            int[] paramTypeIds = new int[paramTypes.length];
+
+            for (int i = 0; i < paramTypes.length; i++)
+                paramTypeIds[i] = binary.typeId(paramTypes[i].getName());
+
+            return new MethodDescriptor(method.getDeclaringClass(), method.getName(), paramTypeIds);
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean equals(Object o) {
+            if (this == o)
+                return true;
+
+            if (o == null || getClass() != o.getClass())
+                return false;
+
+            MethodDescriptor that = (MethodDescriptor)o;
+
+            return cls.equals(that.cls) && methodName.equals(that.methodName)
+                && Arrays.equals(paramTypeIds, that.paramTypeIds);
+        }
+
+        /** {@inheritDoc} */
+        @Override public int hashCode() {
+            return hash;
+        }
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(MethodDescriptor.class, this, "paramTypeIds", paramTypeIds);
+        }
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/services/PlatformServices.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/services/PlatformServices.java
index 24d473a..6a6e05e 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/services/PlatformServices.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/services/PlatformServices.java
@@ -515,6 +515,19 @@ public class PlatformServices extends PlatformAbstractTarget {
     }
 
     /**
+     * Finds a suitable method in a class.
+     *
+     * @param clazz Class.
+     * @param mthdName Name.
+     * @param args Args.
+     * @return Method.
+     * @throws NoSuchMethodException On error.
+     */
+    public static Method getMethod(Class<?> clazz, String mthdName, Object[] args) throws NoSuchMethodException {
+        return ServiceProxyHolder.getMethod(clazz, mthdName, args);
+    }
+
+    /**
      * Proxy holder.
      */
     @SuppressWarnings({"unchecked", "rawtypes"})
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 64eada3..3c92ebc 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
@@ -191,8 +191,14 @@ public class GridServiceProxy<T> implements Serializable {
                         if (svcCtx != null) {
                             Service svc = svcCtx.service();
 
-                            if (svc != null)
-                                return callServiceLocally(svc, mtd, args);
+                            if (svc != null) {
+                                try {
+                                    return callServiceLocally(svc, mtd, args);
+                                }
+                                catch (InvocationTargetException e) {
+                                    throw e.getTargetException();
+                                }
+                            }
                         }
                     }
                     else {
diff --git a/modules/core/src/test/java/org/apache/ignite/client/AsyncChannelTest.java b/modules/core/src/test/java/org/apache/ignite/client/AsyncChannelTest.java
index 543321c..9ac12e1 100644
--- a/modules/core/src/test/java/org/apache/ignite/client/AsyncChannelTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/client/AsyncChannelTest.java
@@ -24,25 +24,22 @@ import java.util.concurrent.locks.Lock;
 import javax.cache.Cache;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
-import org.apache.ignite.Ignition;
 import org.apache.ignite.cache.CacheAtomicityMode;
 import org.apache.ignite.cache.CachePeekMode;
 import org.apache.ignite.cache.query.Query;
 import org.apache.ignite.cache.query.QueryCursor;
 import org.apache.ignite.cache.query.ScanQuery;
 import org.apache.ignite.configuration.CacheConfiguration;
-import org.apache.ignite.configuration.ClientConfiguration;
-import org.apache.ignite.configuration.ClientConnectorConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.client.thin.AbstractThinClientTest;
 import org.apache.ignite.testframework.GridTestUtils;
-import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
 
 /**
  * Async channel tests.
  */
-public class AsyncChannelTest extends GridCommonAbstractTest {
+public class AsyncChannelTest extends AbstractThinClientTest {
     /** Nodes count. */
     private static final int NODES_CNT = 3;
 
@@ -52,9 +49,6 @@ public class AsyncChannelTest extends GridCommonAbstractTest {
     /** Cache name. */
     private static final String CACHE_NAME = "tx_cache";
 
-    /** Client connector address. */
-    private static final String CLIENT_CONN_ADDR = "127.0.0.1:" + ClientConnectorConfiguration.DFLT_PORT;
-
     /** {@inheritDoc} */
     @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
         return super.getConfiguration(igniteInstanceName).setCacheConfiguration(
@@ -75,7 +69,7 @@ public class AsyncChannelTest extends GridCommonAbstractTest {
      */
     @Test
     public void testAsyncRequests() throws Exception {
-        try (IgniteClient client = Ignition.startClient(new ClientConfiguration().setAddresses(CLIENT_CONN_ADDR))) {
+        try (IgniteClient client = startClient(0)) {
             Ignite ignite = grid(0);
 
             IgniteCache<Integer, Integer> igniteCache = ignite.cache(CACHE_NAME);
@@ -130,7 +124,7 @@ public class AsyncChannelTest extends GridCommonAbstractTest {
      */
     @Test
     public void testConcurrentRequests() throws Exception {
-        try (IgniteClient client = Ignition.startClient(new ClientConfiguration().setAddresses(CLIENT_CONN_ADDR))) {
+        try (IgniteClient client = startClient(0)) {
             ClientCache<Integer, Integer> clientCache = client.cache(CACHE_NAME);
 
             clientCache.clear();
@@ -164,7 +158,7 @@ public class AsyncChannelTest extends GridCommonAbstractTest {
      */
     @Test
     public void testConcurrentQueries() throws Exception {
-        try (IgniteClient client = Ignition.startClient(new ClientConfiguration().setAddresses(CLIENT_CONN_ADDR))) {
+        try (IgniteClient client = startClient(0)) {
             ClientCache<Integer, Integer> clientCache = client.cache(CACHE_NAME);
 
             clientCache.clear();
diff --git a/modules/core/src/test/java/org/apache/ignite/client/ConnectToStartingNodeTest.java b/modules/core/src/test/java/org/apache/ignite/client/ConnectToStartingNodeTest.java
index f79928d..bbb2c87 100644
--- a/modules/core/src/test/java/org/apache/ignite/client/ConnectToStartingNodeTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/client/ConnectToStartingNodeTest.java
@@ -20,25 +20,19 @@ package org.apache.ignite.client;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CyclicBarrier;
 import org.apache.ignite.Ignite;
-import org.apache.ignite.Ignition;
-import org.apache.ignite.configuration.ClientConfiguration;
-import org.apache.ignite.configuration.ClientConnectorConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.client.thin.AbstractThinClientTest;
 import org.apache.ignite.spi.IgniteSpiException;
 import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
 import org.apache.ignite.testframework.GridTestUtils;
-import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.jetbrains.annotations.Nullable;
 import org.junit.Test;
 
 /**
  * Checks that connection with starting node will be established correctly.
  */
-public class ConnectToStartingNodeTest extends GridCommonAbstractTest {
-    /** Client connector address. */
-    private static final String CLIENT_CONN_ADDR = "127.0.0.1:" + ClientConnectorConfiguration.DFLT_PORT;
-
+public class ConnectToStartingNodeTest extends AbstractThinClientTest {
     /** Barrier to suspend discovery SPI start. */
     private final CyclicBarrier barrier = new CyclicBarrier(2);
 
@@ -75,7 +69,7 @@ public class ConnectToStartingNodeTest extends GridCommonAbstractTest {
         barrier.await();
 
         IgniteInternalFuture<IgniteClient> futStartClient = GridTestUtils.runAsync(
-            () -> Ignition.startClient(new ClientConfiguration().setAddresses(CLIENT_CONN_ADDR)));
+            () -> startClient(grid()));
 
         // Server doesn't accept connection before discovery SPI started.
         assertFalse(GridTestUtils.waitForCondition(futStartClient::isDone, 500L));
diff --git a/modules/core/src/test/java/org/apache/ignite/client/Person.java b/modules/core/src/test/java/org/apache/ignite/client/Person.java
index c92305d..27510a7 100644
--- a/modules/core/src/test/java/org/apache/ignite/client/Person.java
+++ b/modules/core/src/test/java/org/apache/ignite/client/Person.java
@@ -19,6 +19,7 @@ package org.apache.ignite.client;
 
 import java.util.Objects;
 import org.apache.ignite.cache.query.annotations.QuerySqlField;
+import org.apache.ignite.internal.util.typedef.internal.S;
 
 /**
  * A person entity used for the tests.
@@ -62,4 +63,9 @@ public class Person {
 
         return other.id.equals(id) && other.name.equals(name);
     }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(Person.class, this);
+    }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/client/thin/AbstractThinClientTest.java b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/AbstractThinClientTest.java
new file mode 100644
index 0000000..b985a77
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/AbstractThinClientTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.client.thin;
+
+import java.util.Arrays;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.client.IgniteClient;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.configuration.ClientConfiguration;
+import org.apache.ignite.internal.processors.odbc.ClientListenerProcessor;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+
+/**
+ * Abstract thin client test.
+ */
+public abstract class AbstractThinClientTest extends GridCommonAbstractTest {
+    /**
+     * Gets default client configuration.
+     */
+    protected ClientConfiguration getClientConfiguration() {
+        return new ClientConfiguration();
+    }
+
+    /**
+     * Start thin client with configured endpoints to specified nodes.
+     *
+     * @param nodes Nodes to connect.
+     * @return Thin client.
+     */
+    protected IgniteClient startClient(ClusterNode... nodes) {
+        String[] addrs = new String[nodes.length];
+
+        for (int i = 0; i < nodes.length; i++) {
+            ClusterNode node = nodes[i];
+
+            addrs[i] = F.first(node.addresses()) + ":" + node.attribute(ClientListenerProcessor.CLIENT_LISTENER_PORT);
+        }
+
+        return Ignition.startClient(getClientConfiguration().setAddresses(addrs));
+    }
+
+    /**
+     * Start thin client with configured endpoints to specified ignite instances.
+     *
+     * @param ignites Ignite instances to connect.
+     * @return Thin client.
+     */
+    protected IgniteClient startClient(Ignite... ignites) {
+        return startClient(Arrays.stream(ignites).map(ignite -> ignite.cluster().localNode()).toArray(ClusterNode[]::new));
+    }
+
+    /**
+     * Start thin client with configured endpoints to specified ignite instance indexes.
+     *
+     * @param igniteIdxs Ignite instance indexes to connect.
+     * @return Thin client.
+     */
+    protected IgniteClient startClient(int... igniteIdxs) {
+        return startClient(Arrays.stream(igniteIdxs).mapToObj(igniteIdx -> grid(igniteIdx).cluster().localNode())
+            .toArray(ClusterNode[]::new));
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ClusterApiTest.java b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ClusterApiTest.java
index 9f78342..5540ea3 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ClusterApiTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ClusterApiTest.java
@@ -18,25 +18,18 @@
 package org.apache.ignite.internal.client.thin;
 
 import org.apache.ignite.IgniteCluster;
-import org.apache.ignite.Ignition;
 import org.apache.ignite.client.ClientCluster;
 import org.apache.ignite.client.IgniteClient;
 import org.apache.ignite.cluster.ClusterState;
-import org.apache.ignite.configuration.ClientConfiguration;
-import org.apache.ignite.configuration.ClientConnectorConfiguration;
 import org.apache.ignite.configuration.DataRegionConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
-import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
 
 /**
  * Checks cluster state/WAL state operations for thin client.
  */
-public class ClusterApiTest extends GridCommonAbstractTest {
-    /** Client connector address. */
-    private static final String CLIENT_CONN_ADDR = "127.0.0.1:" + ClientConnectorConfiguration.DFLT_PORT;
-
+public class ClusterApiTest extends AbstractThinClientTest {
     /** {@inheritDoc} */
     @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
         return super.getConfiguration(igniteInstanceName)
@@ -69,7 +62,7 @@ public class ClusterApiTest extends GridCommonAbstractTest {
      */
     @Test
     public void testClusterState() throws Exception {
-        try (IgniteClient client = Ignition.startClient(new ClientConfiguration().setAddresses(CLIENT_CONN_ADDR))) {
+        try (IgniteClient client = startClient(0)) {
             ClientCluster clientCluster = client.cluster();
             IgniteCluster igniteCluster = grid(0).cluster();
 
@@ -85,7 +78,7 @@ public class ClusterApiTest extends GridCommonAbstractTest {
      */
     @Test
     public void testWalState() throws Exception {
-        try (IgniteClient client = Ignition.startClient(new ClientConfiguration().setAddresses(CLIENT_CONN_ADDR))) {
+        try (IgniteClient client = startClient(0)) {
             ClientCluster clientCluster = client.cluster();
             IgniteCluster igniteCluster = grid(0).cluster();
 
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ClusterGroupTest.java b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ClusterGroupTest.java
index e253fab..e366204 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ClusterGroupTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ClusterGroupTest.java
@@ -23,27 +23,20 @@ import java.util.HashSet;
 import java.util.Map;
 import java.util.UUID;
 import org.apache.ignite.Ignite;
-import org.apache.ignite.Ignition;
 import org.apache.ignite.client.ClientClusterGroup;
 import org.apache.ignite.client.IgniteClient;
 import org.apache.ignite.cluster.ClusterNode;
-import org.apache.ignite.configuration.ClientConfiguration;
-import org.apache.ignite.configuration.ClientConnectorConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.G;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
-import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
 
 /**
  * Checks cluster groups for thin client.
  */
-public class ClusterGroupTest extends GridCommonAbstractTest {
-    /** Client connector address. */
-    private static final String CLIENT_CONN_ADDR = "127.0.0.1:" + ClientConnectorConfiguration.DFLT_PORT;
-
+public class ClusterGroupTest extends AbstractThinClientTest {
     /** Grid index attribute name. */
     private static final String GRID_IDX_ATTR_NAME = "GRID_IDX";
 
@@ -66,7 +59,7 @@ public class ClusterGroupTest extends GridCommonAbstractTest {
         startGrid(3, true, null);
         startGrid(4, true, F.asMap(CUSTOM_ATTR_NAME, CUSTOM_ATTR_VAL));
 
-        client = Ignition.startClient(new ClientConfiguration().setAddresses(CLIENT_CONN_ADDR));
+        client = startClient(0);
     }
 
     /** {@inheritDoc} */
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ComputeTaskTest.java b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ComputeTaskTest.java
index 4e13f49..4adf228 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ComputeTaskTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ComputeTaskTest.java
@@ -35,7 +35,6 @@ import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteException;
-import org.apache.ignite.Ignition;
 import org.apache.ignite.client.ClientCache;
 import org.apache.ignite.client.ClientClusterGroup;
 import org.apache.ignite.client.ClientCompute;
@@ -43,7 +42,6 @@ import org.apache.ignite.client.ClientException;
 import org.apache.ignite.client.IgniteClient;
 import org.apache.ignite.compute.ComputeJobResult;
 import org.apache.ignite.compute.ComputeTaskName;
-import org.apache.ignite.configuration.ClientConfiguration;
 import org.apache.ignite.configuration.ClientConnectorConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.configuration.ThinClientConfiguration;
@@ -53,14 +51,13 @@ import org.apache.ignite.internal.util.typedef.G;
 import org.apache.ignite.internal.util.typedef.T2;
 import org.apache.ignite.mxbean.ClientProcessorMXBean;
 import org.apache.ignite.testframework.GridTestUtils;
-import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.jetbrains.annotations.Nullable;
 import org.junit.Test;
 
 /**
  * Checks compute grid funtionality of thin client.
  */
-public class ComputeTaskTest extends GridCommonAbstractTest {
+public class ComputeTaskTest extends AbstractThinClientTest {
     /** Grids count. */
     private static final int GRIDS_CNT = 4;
 
@@ -82,18 +79,6 @@ public class ComputeTaskTest extends GridCommonAbstractTest {
             .setClientMode(getTestIgniteInstanceIndex(igniteInstanceName) == 3);
     }
 
-    /**
-     *
-     */
-    private IgniteClient startClient(int... gridIdxs) {
-        String[] addrs = new String[gridIdxs.length];
-
-        for (int i = 0; i < gridIdxs.length; i++)
-            addrs[i] = "127.0.0.1:" + (ClientConnectorConfiguration.DFLT_PORT + gridIdxs[i]);
-
-        return Ignition.startClient(new ClientConfiguration().setAddresses(addrs));
-    }
-
     /** {@inheritDoc} */
     @Override protected void beforeTestsStarted() throws Exception {
         super.beforeTestsStarted();
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ServicesTest.java b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ServicesTest.java
new file mode 100644
index 0000000..9f0641a
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/client/thin/ServicesTest.java
@@ -0,0 +1,417 @@
+/*
+ * 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.client.thin;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.UUID;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.client.ClientClusterGroup;
+import org.apache.ignite.client.ClientException;
+import org.apache.ignite.client.IgniteClient;
+import org.apache.ignite.client.Person;
+import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.resources.IgniteInstanceResource;
+import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceContext;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.junit.Test;
+
+/**
+ * Checks service invocation for thin client.
+ */
+public class ServicesTest extends AbstractThinClientTest {
+    /** Node-id service name. */
+    private static final String NODE_ID_SERVICE_NAME = "node_id_svc";
+
+    /** Node-singleton service name. */
+    private static final String NODE_SINGLTON_SERVICE_NAME = "node_svc";
+
+    /** Cluster-singleton service name. */
+    private static final String CLUSTER_SINGLTON_SERVICE_NAME = "cluster_svc";
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        startGrids(3);
+
+        startClientGrid(3);
+
+        grid(0).createCache(DEFAULT_CACHE_NAME);
+
+        awaitPartitionMapExchange();
+
+        grid(0).services().deployNodeSingleton(NODE_ID_SERVICE_NAME, new TestNodeIdService());
+
+        grid(0).services().deployNodeSingleton(NODE_SINGLTON_SERVICE_NAME, new TestService());
+
+        // Deploy CLUSTER_SINGLTON_SERVICE_NAME to grid(1).
+        int keyGrid1 = primaryKey(grid(1).cache(DEFAULT_CACHE_NAME));
+
+        grid(0).services().deployKeyAffinitySingleton(CLUSTER_SINGLTON_SERVICE_NAME, new TestService(),
+            DEFAULT_CACHE_NAME, keyGrid1);
+    }
+
+    /**
+     * Test that overloaded methods resolved correctly.
+     */
+    @Test
+    public void testOverloadedMethods() throws Exception {
+        try (IgniteClient client = startClient(0)) {
+            // Test local service calls (service deployed to each node).
+            TestServiceInterface svc = client.services().serviceProxy(NODE_SINGLTON_SERVICE_NAME,
+                TestServiceInterface.class);
+
+            checkOverloadedMethods(svc);
+
+            // Test remote service calls (client connected to grid(0) but service deployed to grid(1)).
+            svc = client.services().serviceProxy(CLUSTER_SINGLTON_SERVICE_NAME, TestServiceInterface.class);
+
+            checkOverloadedMethods(svc);
+        }
+    }
+
+    /**
+     * @param svc Service.
+     */
+    private void checkOverloadedMethods(TestServiceInterface svc) {
+        assertEquals("testMethod()", svc.testMethod());
+
+        assertEquals("testMethod(String val): test", svc.testMethod("test"));
+
+        assertEquals(123, svc.testMethod(123));
+
+        assertEquals("testMethod(Object val): test", svc.testMethod(new StringBuilder("test")));
+
+        assertEquals("testMethod(String val): null", svc.testMethod((String)null));
+
+        assertEquals("testMethod(Object val): null", svc.testMethod((Object)null));
+
+        Person person1 = new Person(1, "Person 1");
+        Person person2 = new Person(2, "Person 2");
+
+        assertEquals("testMethod(Person person, Object obj): " + person1 + ", " + person2,
+            svc.testMethod(person1, (Object)person2));
+
+        assertEquals("testMethod(Object obj, Person person): " + person1 + ", " + person2,
+            svc.testMethod((Object)person1, person2));
+    }
+
+    /**
+     * Test that methods which get and return collections work correctly.
+     */
+    @Test
+    public void testCollectionMethods() throws Exception {
+        try (IgniteClient client = startClient(0)) {
+            // Test local service calls (service deployed to each node).
+            TestServiceInterface svc = client.services().serviceProxy(NODE_SINGLTON_SERVICE_NAME,
+                TestServiceInterface.class);
+
+            checkCollectionMethods(svc);
+
+            // Test remote service calls (client connected to grid(0) but service deployed to grid(1)).
+            svc = client.services().serviceProxy(CLUSTER_SINGLTON_SERVICE_NAME, TestServiceInterface.class);
+
+            checkCollectionMethods(svc);
+        }
+    }
+
+    /**
+     * @param svc Service.
+     */
+    private void checkCollectionMethods(TestServiceInterface svc) {
+        Person person1 = new Person(1, "Person 1");
+        Person person2 = new Person(2, "Person 2");
+
+        Person[] arr = new Person[] {person1, person2};
+        assertTrue(Arrays.equals(arr, svc.testArray(arr)));
+
+        Collection<Person> col = new HashSet<>(F.asList(person1, person2));
+        assertEquals(col, svc.testCollection(col));
+
+        Map<Integer, Person> map = F.asMap(1, person1, 2, person2);
+        assertEquals(map, svc.testMap(map));
+    }
+
+    /**
+     * Test that exception is thrown when invoking service with wrong interface.
+     */
+    @Test
+    public void testWrongMethodInvocation() throws Exception {
+        try (IgniteClient client = startClient(0)) {
+            TestServiceInterface svc = client.services().serviceProxy(NODE_ID_SERVICE_NAME,
+                TestServiceInterface.class);
+
+            GridTestUtils.assertThrowsAnyCause(log, () -> svc.testMethod(0), ClientException.class,
+                "Method not found");
+        }
+    }
+
+    /**
+     * Test that exception is thrown when trying to invoke non-existing service.
+     */
+    @Test
+    public void testWrongServiceName() throws Exception {
+        try (IgniteClient client = startClient(0)) {
+            TestServiceInterface svc = client.services().serviceProxy("no_such_service",
+                TestServiceInterface.class);
+
+            GridTestUtils.assertThrowsAnyCause(log, () -> svc.testMethod(0), ClientException.class,
+                "Service not found");
+        }
+    }
+
+    /**
+     * Test that service exception message is propagated to client.
+     */
+    @Test
+    public void testServiceException() throws Exception {
+        try (IgniteClient client = startClient(0)) {
+            // Test local service calls (service deployed to each node).
+            TestServiceInterface svc = client.services().serviceProxy(NODE_SINGLTON_SERVICE_NAME,
+                TestServiceInterface.class);
+
+            GridTestUtils.assertThrowsAnyCause(log, svc::testException, ClientException.class,
+                "testException()");
+
+            // Test remote service calls (client connected to grid(0) but service deployed to grid(1)).
+            client.services().serviceProxy(CLUSTER_SINGLTON_SERVICE_NAME, TestServiceInterface.class);
+
+            GridTestUtils.assertThrowsAnyCause(log, svc::testException, ClientException.class,
+                "testException()");
+        }
+    }
+
+    /**
+     * Test that services executed on cluster group.
+     */
+    @Test
+    public void testServicesOnClusterGroup() throws Exception {
+        try (IgniteClient client = startClient(0)) {
+            // Local node.
+            ClientClusterGroup grp = client.cluster().forNodeId(nodeId(0), nodeId(3));
+
+            TestNodeIdServiceInterface nodeSvc0 = client.services(grp).serviceProxy(NODE_ID_SERVICE_NAME,
+                TestNodeIdServiceInterface.class);
+
+            assertEquals(nodeId(0), nodeSvc0.nodeId());
+
+            // Remote node.
+            grp = client.cluster().forNodeId(nodeId(1), nodeId(3));
+
+            nodeSvc0 = client.services(grp).serviceProxy(NODE_ID_SERVICE_NAME,
+                TestNodeIdServiceInterface.class);
+
+            assertEquals(nodeId(1), nodeSvc0.nodeId());
+
+            // Client node.
+            grp = client.cluster().forNodeId(nodeId(3));
+
+            TestNodeIdServiceInterface nodeSvc1 = client.services(grp).serviceProxy(NODE_ID_SERVICE_NAME,
+                TestNodeIdServiceInterface.class);
+
+            GridTestUtils.assertThrowsAnyCause(log, nodeSvc1::nodeId, ClientException.class,
+                "Failed to find deployed service");
+
+            // All nodes, except service node.
+            grp = client.cluster().forNodeId(nodeId(0), nodeId(2), nodeId(3));
+
+            TestServiceInterface nodeSvc2 = client.services(grp).serviceProxy(CLUSTER_SINGLTON_SERVICE_NAME,
+                TestServiceInterface.class);
+
+            GridTestUtils.assertThrowsAnyCause(log, nodeSvc2::testMethod, ClientException.class,
+                "Failed to find deployed service");
+        }
+    }
+
+    /**
+     * Test services timeout.
+     */
+    @Test
+    public void testServiceTimeout() throws Exception {
+        long timeout = 100L;
+
+        try (IgniteClient client = startClient(0)) {
+            TestServiceInterface svc = client.services().serviceProxy(CLUSTER_SINGLTON_SERVICE_NAME,
+                TestServiceInterface.class, timeout);
+
+            IgniteInternalFuture<?> fut = GridTestUtils.runAsync(() -> svc.sleep(timeout * 2));
+
+            GridTestUtils.assertThrowsAnyCause(log, fut::get, ClientException.class, "timed out");
+        }
+    }
+
+    /** */
+    public static interface TestServiceInterface {
+        /** */
+        public String testMethod();
+
+        /** */
+        public String testMethod(String val);
+
+        /** */
+        public String testMethod(Object val);
+
+        /** */
+        public int testMethod(int val);
+
+        /** */
+        public String testMethod(Person person, Object obj);
+
+        /** */
+        public String testMethod(Object obj, Person person);
+
+        /** */
+        public Person[] testArray(Person[] persons);
+
+        /** */
+        public Collection<Person> testCollection(Collection<Person> persons);
+
+        /** */
+        public Map<Integer, Person> testMap(Map<Integer, Person> persons);
+
+        /** */
+        public Object testException();
+
+        /** */
+        public void sleep(long millis);
+    }
+
+    /**
+     * Implementation of TestServiceInterface.
+     */
+    public static class TestService implements Service, TestServiceInterface {
+        /** {@inheritDoc} */
+        @Override public void cancel(ServiceContext ctx) {
+            // No-op.
+        }
+
+        /** {@inheritDoc} */
+        @Override public void init(ServiceContext ctx) throws Exception {
+            // No-op.
+        }
+
+        /** {@inheritDoc} */
+        @Override public void execute(ServiceContext ctx) throws Exception {
+            // No-op.
+        }
+
+        /** {@inheritDoc} */
+        @Override public String testMethod() {
+            return "testMethod()";
+        }
+
+        /** {@inheritDoc} */
+        @Override public String testMethod(String val) {
+            return "testMethod(String val): " + val;
+        }
+
+        /** {@inheritDoc} */
+        @Override public String testMethod(Object val) {
+            return "testMethod(Object val): " + val;
+        }
+
+        /** {@inheritDoc} */
+        @Override public int testMethod(int val) {
+            return val;
+        }
+
+        /** {@inheritDoc} */
+        @Override public String testMethod(Person person, Object obj) {
+            return "testMethod(Person person, Object obj): " + person + ", " + obj;
+        }
+
+        /** {@inheritDoc} */
+        @Override public String testMethod(Object obj, Person person) {
+            return "testMethod(Object obj, Person person): " + obj + ", " + person;
+        }
+
+        /** {@inheritDoc} */
+        @Override public Person[] testArray(Person[] persons) {
+            return persons;
+        }
+
+        /** {@inheritDoc} */
+        @Override public Collection<Person> testCollection(Collection<Person> persons) {
+            return persons;
+        }
+
+        /** {@inheritDoc} */
+        @Override public Map<Integer, Person> testMap(Map<Integer, Person> persons) {
+            return persons;
+        }
+
+        /** {@inheritDoc} */
+        @Override public Object testException() {
+            throw new IllegalStateException("testException()");
+        }
+
+        /** {@inheritDoc} */
+        @Override public void sleep(long millis) {
+            long ts = U.currentTimeMillis();
+
+            doSleep(millis);
+
+            // Make sure cached timestamp updated.
+            while (ts + millis >= U.currentTimeMillis())
+                doSleep(100L);
+        }
+    }
+
+    /**
+     * Service to return ID of node where method was executed.
+     */
+    public static interface TestNodeIdServiceInterface {
+        /** Gets ID of node where method was executed */
+        public UUID nodeId();
+    }
+
+    /**
+     * Implementation of TestNodeIdServiceInterface
+     */
+    public static class TestNodeIdService implements Service, TestNodeIdServiceInterface {
+        /** Ignite. */
+        @IgniteInstanceResource
+        Ignite ignite;
+
+        /** {@inheritDoc} */
+        @Override public void cancel(ServiceContext ctx) {
+            // No-op.
+        }
+
+        /** {@inheritDoc} */
+        @Override public void init(ServiceContext ctx) throws Exception {
+            // No-op.
+        }
+
+        /** {@inheritDoc} */
+        @Override public void execute(ServiceContext ctx) throws Exception {
+            // No-op.
+        }
+
+        /** {@inheritDoc} */
+        @Override public UUID nodeId() {
+            return ignite.cluster().localNode().id();
+        }
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/platform/AbstractPlatformServiceCallTask.java b/modules/core/src/test/java/org/apache/ignite/platform/AbstractPlatformServiceCallTask.java
index 8609314..bb5239b 100644
--- a/modules/core/src/test/java/org/apache/ignite/platform/AbstractPlatformServiceCallTask.java
+++ b/modules/core/src/test/java/org/apache/ignite/platform/AbstractPlatformServiceCallTask.java
@@ -27,15 +27,19 @@ import java.util.Set;
 import java.util.UUID;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteException;
+import org.apache.ignite.Ignition;
 import org.apache.ignite.binary.BinaryObjectException;
 import org.apache.ignite.binary.BinaryReader;
 import org.apache.ignite.binary.BinaryWriter;
 import org.apache.ignite.binary.Binarylizable;
+import org.apache.ignite.client.IgniteClient;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.compute.ComputeJob;
 import org.apache.ignite.compute.ComputeJobAdapter;
 import org.apache.ignite.compute.ComputeJobResult;
 import org.apache.ignite.compute.ComputeTaskAdapter;
+import org.apache.ignite.configuration.ClientConfiguration;
+import org.apache.ignite.internal.processors.odbc.ClientListenerProcessor;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.resources.IgniteInstanceResource;
 import org.apache.ignite.services.ServiceDescriptor;
@@ -97,6 +101,11 @@ public abstract class AbstractPlatformServiceCallTask extends ComputeTaskAdapter
     /** */
     protected abstract static class AbstractServiceCallJob extends ComputeJobAdapter {
         /** */
+        @SuppressWarnings("unused")
+        @IgniteInstanceResource
+        protected transient Ignite ignite;
+
+        /** */
         protected final String srvcName;
 
         /**
@@ -121,9 +130,30 @@ public abstract class AbstractPlatformServiceCallTask extends ComputeTaskAdapter
         }
 
         /**
+         * Gets service proxy.
+         */
+        TestPlatformService serviceProxy() {
+            return ignite.services().serviceProxy(srvcName, TestPlatformService.class, false);
+        }
+
+        /**
          * Test method to call platform service.
          */
         abstract void runTest();
+
+        /**
+         * Start thin client connected to current ignite instance.
+         */
+        IgniteClient startClient() {
+            ClusterNode node = ignite.cluster().localNode();
+
+            String addr = F.first(node.addresses()) + ":" + node.attribute(ClientListenerProcessor.CLIENT_LISTENER_PORT);
+
+            return Ignition.startClient(new ClientConfiguration()
+                .setAddresses(addr)
+                .setBinaryConfiguration(ignite.configuration().getBinaryConfiguration())
+            );
+        }
     }
 
     /** */
diff --git a/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallCollectionsTask.java b/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallCollectionsTask.java
index 7e2806b..65f48d9 100644
--- a/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallCollectionsTask.java
+++ b/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallCollectionsTask.java
@@ -22,10 +22,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
-import org.apache.ignite.Ignite;
 import org.apache.ignite.compute.ComputeJobAdapter;
 import org.apache.ignite.internal.util.typedef.T2;
-import org.apache.ignite.resources.IgniteInstanceResource;
 
 /**
  * Test invoke methods with collections and arrays as arguments and return type.
@@ -38,11 +36,6 @@ public class PlatformServiceCallCollectionsTask extends AbstractPlatformServiceC
 
     /** */
     public static class PlatformServiceCallCollectionsJob extends AbstractServiceCallJob {
-        /** */
-        @SuppressWarnings("unused")
-        @IgniteInstanceResource
-        private transient Ignite ignite;
-
         /**
          * @param svcName Service name.
          */
@@ -52,7 +45,7 @@ public class PlatformServiceCallCollectionsTask extends AbstractPlatformServiceC
 
         /** {@inheritDoc} */
         @Override void runTest() {
-            TestPlatformService srv = ignite.services().serviceProxy(srvcName, TestPlatformService.class, false);
+            TestPlatformService srv = serviceProxy();
 
             {
                 TestValue[] exp = IntStream.range(0, 10).mapToObj(i -> new TestValue(i, "name_" + i))
diff --git a/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallCollectionsThinTask.java b/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallCollectionsThinTask.java
new file mode 100644
index 0000000..d874edf
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallCollectionsThinTask.java
@@ -0,0 +1,65 @@
+/*
+ * 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.platform;
+
+import org.apache.ignite.client.IgniteClient;
+import org.apache.ignite.compute.ComputeJobAdapter;
+import org.apache.ignite.internal.processors.platform.services.PlatformService;
+import org.apache.ignite.internal.util.typedef.internal.U;
+
+/**
+ * Test invoke {@link PlatformService} methods with collections and arrays as arguments and return type from
+ * java thin client.
+ */
+public class PlatformServiceCallCollectionsThinTask extends AbstractPlatformServiceCallTask {
+    /** {@inheritDoc} */
+    @Override ComputeJobAdapter createJob(String svcName) {
+        return new PlatformServiceCallCollectionsThinJob(svcName);
+    }
+
+    /** */
+    static class PlatformServiceCallCollectionsThinJob extends
+        PlatformServiceCallCollectionsTask.PlatformServiceCallCollectionsJob {
+        /** Thin client. */
+        IgniteClient client;
+
+        /**
+         * @param srvcName Service name.
+         */
+        PlatformServiceCallCollectionsThinJob(String srvcName) {
+            super(srvcName);
+        }
+
+        /** {@inheritDoc} */
+        @Override TestPlatformService serviceProxy() {
+            return client.services().serviceProxy(srvcName, TestPlatformService.class);
+        }
+
+        /** {@inheritDoc} */
+        @Override void runTest() {
+            client = startClient();
+
+            try {
+                super.runTest();
+            }
+            finally {
+                U.close(client, ignite.log().getLogger(getClass()));
+            }
+        }
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallTask.java b/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallTask.java
index f39a3ee..97c0d54 100644
--- a/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallTask.java
+++ b/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallTask.java
@@ -18,11 +18,9 @@
 package org.apache.ignite.platform;
 
 import java.util.UUID;
-import org.apache.ignite.Ignite;
 import org.apache.ignite.compute.ComputeJobAdapter;
 import org.apache.ignite.internal.processors.platform.PlatformNativeException;
 import org.apache.ignite.internal.processors.platform.services.PlatformService;
-import org.apache.ignite.resources.IgniteInstanceResource;
 import org.apache.ignite.testframework.GridTestUtils;
 
 /**
@@ -36,11 +34,6 @@ public class PlatformServiceCallTask extends AbstractPlatformServiceCallTask {
 
     /** */
     static class PlatformServiceCallJob extends AbstractServiceCallJob {
-        /** */
-        @SuppressWarnings("unused")
-        @IgniteInstanceResource
-        private transient Ignite ignite;
-
         /**
          * @param srvcName Service name.
          */
@@ -50,32 +43,41 @@ public class PlatformServiceCallTask extends AbstractPlatformServiceCallTask {
 
         /** {@inheritDoc} */
         @Override void runTest() {
-            TestPlatformService srv = ignite.services().serviceProxy(srvcName, TestPlatformService.class, false);
+            TestPlatformService srv = serviceProxy();
 
-            {
-                UUID nodeId = srv.getNodeId();
-                assertTrue(ignite.cluster().nodes().stream().anyMatch(n -> n.id().equals(nodeId)));
-            }
+            checkNodeId(srv);
+            checkUuidProp(srv);
+            checkObjectProp(srv);
+            checkErrorMethod(srv);
+        }
 
-            {
-                UUID expUuid = UUID.randomUUID();
-                srv.setGuidProp(expUuid);
-                assertEquals(expUuid, srv.getGuidProp());
-            }
+        /** */
+        protected void checkNodeId(TestPlatformService srv) {
+            UUID nodeId = srv.getNodeId();
+            assertTrue(ignite.cluster().nodes().stream().anyMatch(n -> n.id().equals(nodeId)));
+        }
 
-            {
-                TestValue exp = new TestValue(1, "test");
-                srv.setValueProp(exp);
-                assertEquals(exp, srv.getValueProp());
-            }
+        /** */
+        protected void checkUuidProp(TestPlatformService srv) {
+            UUID expUuid = UUID.randomUUID();
+            srv.setGuidProp(expUuid);
+            assertEquals(expUuid, srv.getGuidProp());
+        }
 
-            {
-                PlatformNativeException nativeEx = (PlatformNativeException)GridTestUtils
-                        .assertThrowsWithCause(srv::errorMethod, PlatformNativeException.class)
-                        .getCause();
+        /** */
+        protected void checkObjectProp(TestPlatformService srv) {
+            TestValue exp = new TestValue(1, "test");
+            srv.setValueProp(exp);
+            assertEquals(exp, srv.getValueProp());
+        }
+
+        /** */
+        protected void checkErrorMethod(TestPlatformService srv) {
+            PlatformNativeException nativeEx = (PlatformNativeException)GridTestUtils
+                .assertThrowsWithCause(srv::errorMethod, PlatformNativeException.class)
+                .getCause();
 
-                assertTrue(nativeEx.toString().contains("Failed method"));
-            }
+            assertTrue(nativeEx.toString().contains("Failed method"));
         }
     }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallThinTask.java b/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallThinTask.java
new file mode 100644
index 0000000..a1c840f
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/platform/PlatformServiceCallThinTask.java
@@ -0,0 +1,76 @@
+/*
+ * 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.platform;
+
+import org.apache.ignite.client.ClientException;
+import org.apache.ignite.client.IgniteClient;
+import org.apache.ignite.compute.ComputeJobAdapter;
+import org.apache.ignite.internal.processors.platform.services.PlatformService;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.testframework.GridTestUtils;
+
+/**
+ * Basic task to calling {@link PlatformService} from java thin client.
+ */
+public class PlatformServiceCallThinTask extends AbstractPlatformServiceCallTask {
+    /** {@inheritDoc} */
+    @Override ComputeJobAdapter createJob(String svcName) {
+        return new PlatformServiceCallThinJob(svcName);
+    }
+
+    /** */
+    static class PlatformServiceCallThinJob extends PlatformServiceCallTask.PlatformServiceCallJob {
+        /** Thin client. */
+        IgniteClient client;
+
+        /**
+         * @param srvcName Service name.
+         */
+        PlatformServiceCallThinJob(String srvcName) {
+            super(srvcName);
+        }
+
+        /** {@inheritDoc} */
+        @Override TestPlatformService serviceProxy() {
+            return client.services().serviceProxy(srvcName, TestPlatformService.class);
+        }
+
+        /** {@inheritDoc} */
+        @Override void runTest() {
+            client = startClient();
+
+            try {
+                super.runTest();
+            }
+            finally {
+                U.close(client, ignite.log().getLogger(getClass()));
+            }
+        }
+
+        /** */
+        @Override protected void checkErrorMethod(TestPlatformService srv) {
+            // For thin client only top level error message is passed from server to client, so we should override
+            // method for error check.
+            GridTestUtils.assertThrowsAnyCause(null, () -> {
+                srv.errorMethod();
+                return null;
+            }, ClientException.class, "Failed to invoke platform service");
+        }
+
+    }
+}
diff --git a/modules/indexing/src/test/java/org/apache/ignite/client/ClientTestSuite.java b/modules/indexing/src/test/java/org/apache/ignite/client/ClientTestSuite.java
index 07ac59f..c093efb 100644
--- a/modules/indexing/src/test/java/org/apache/ignite/client/ClientTestSuite.java
+++ b/modules/indexing/src/test/java/org/apache/ignite/client/ClientTestSuite.java
@@ -20,6 +20,7 @@ package org.apache.ignite.client;
 import org.apache.ignite.internal.client.thin.ClusterApiTest;
 import org.apache.ignite.internal.client.thin.ClusterGroupTest;
 import org.apache.ignite.internal.client.thin.ComputeTaskTest;
+import org.apache.ignite.internal.client.thin.ServicesTest;
 import org.apache.ignite.internal.client.thin.ThinClientPartitionAwarenessResourceReleaseTest;
 import org.apache.ignite.internal.client.thin.ThinClientPartitionAwarenessStableTopologyTest;
 import org.apache.ignite.internal.client.thin.ThinClientPartitionAwarenessUnstableTopologyTest;
@@ -47,6 +48,7 @@ import org.junit.runners.Suite;
     ComputeTaskTest.class,
     ClusterApiTest.class,
     ClusterGroupTest.class,
+    ServicesTest.class,
     ThinClientPartitionAwarenessStableTopologyTest.class,
     ThinClientPartitionAwarenessUnstableTopologyTest.class,
     ThinClientPartitionAwarenessResourceReleaseTest.class
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/CallPlatformServiceTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/CallPlatformServiceTest.cs
index b06671a..3efe0e8 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/CallPlatformServiceTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/CallPlatformServiceTest.cs
@@ -40,6 +40,13 @@ namespace Apache.Ignite.Core.Tests.Services
         private const string CheckCollectionsTaskName = "org.apache.ignite.platform.PlatformServiceCallCollectionsTask";
         
         /** */
+        private const string CheckThinTaskName = "org.apache.ignite.platform.PlatformServiceCallThinTask";
+
+        /** */
+        private const string CheckCollectionsThinTaskName =
+            "org.apache.ignite.platform.PlatformServiceCallCollectionsThinTask";
+
+        /** */
         protected IIgnite Grid1;
 
         /** */
@@ -70,11 +77,13 @@ namespace Apache.Ignite.Core.Tests.Services
         /// Tests call a platform service by invoking a special compute java task,
         /// in which real invocation of the service is made.
         /// <para/>
-        /// Tests common methods.
         /// <param name="local">If true call on local node.</param>
+        /// <param name="taskName">Task to test.</param>
         /// </summary>
         [Test]
-        public void TestCallPlatformService([Values(true, false)] bool local)
+        public void TestCallPlatformService([Values(true, false)] bool local,
+            [Values(CheckTaskName, CheckCollectionsTaskName, CheckThinTaskName, CheckCollectionsThinTaskName)]
+            string taskName)
         {
             var cfg = new ServiceConfiguration
             {
@@ -85,31 +94,9 @@ namespace Apache.Ignite.Core.Tests.Services
             
             Grid1.GetServices().Deploy(cfg);
 
-            Grid1.GetCompute().ExecuteJavaTask<object>(CheckTaskName, new object[] { ServiceName, local });
+            Grid1.GetCompute().ExecuteJavaTask<object>(taskName, new object[] { ServiceName, local });
         }
-        
-        /// <summary>
-        /// Tests call a platform service by invoking a special compute java task,
-        /// in which real invocation of the service is made.
-        /// <para/>
-        /// Tests collections method.
-        /// <param name="local">If true call on local node.</param>
-        /// </summary>
-        [Test]
-        public void TestCallPlatformServiceCollections([Values(true, false)] bool local)
-        {
-            var cfg = new ServiceConfiguration
-            {
-                Name = ServiceName,
-                TotalCount = 1,
-                Service = new TestPlatformService()
-            };
-            
-            Grid1.GetServices().Deploy(cfg);
 
-            Grid1.GetCompute().ExecuteJavaTask<object>(CheckCollectionsTaskName, new object[] { ServiceName, local });
-        }
-        
         /// <summary>
         /// Starts the grids.
         /// </summary>
diff --git a/modules/yardstick/config/benchmark-thin-services.properties b/modules/yardstick/config/benchmark-thin-services.properties
new file mode 100644
index 0000000..9de1c74
--- /dev/null
+++ b/modules/yardstick/config/benchmark-thin-services.properties
@@ -0,0 +1,78 @@
+# 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.
+
+#
+# Contains benchmarks for distributed computations.
+#
+
+# JVM options.
+JVM_OPTS=${JVM_OPTS}" -DIGNITE_QUIET=false"
+
+# Uncomment to enable concurrent garbage collection (GC) if you encounter long GC pauses.
+# JVM_OPTS=${JVM_OPTS}" \
+# -Xms6g \
+# -Xmx6g \
+# -Xloggc:./gc${now0}.log \
+# -XX:+PrintGCDetails \
+# -verbose:gc \
+# -XX:+UseParNewGC \
+# -XX:+UseConcMarkSweepGC \
+# "
+
+#Ignite version
+ver="RELEASE-"
+
+# List of default probes.
+# Add DStatProbe or VmStatProbe if your OS supports it (e.g. if running on Linux).
+BENCHMARK_DEFAULT_PROBES=ThroughputLatencyProbe,PercentileProbe
+
+# Packages where the specified benchmark is searched by reflection mechanism.
+BENCHMARK_PACKAGES=org.yardstickframework,org.apache.ignite.yardstick
+
+# Probe point writer class name.
+# BENCHMARK_WRITER=
+
+# Comma-separated list of the hosts to run BenchmarkServers on. 2 nodes on local host are enabled by default.
+SERVER_HOSTS=localhost,localhost
+
+# Comma-separated list of the hosts to run BenchmarkDrivers on. 1 node on local host is enabled by default.
+DRIVER_HOSTS=localhost
+
+# Remote username.
+# REMOTE_USER=
+
+# Number of nodes, used to wait for the specified number of nodes to start.
+nodesNum=$((`echo ${SERVER_HOSTS} | tr ',' '\n' | wc -l` + `echo ${DRIVER_HOSTS} | tr ',' '\n' | wc -l`))
+
+# Backups count.
+b=1
+
+# Warmup.
+w=60
+
+# Duration.
+d=180
+
+# Threads count.
+t=64
+
+# Sync mode.
+sm=PRIMARY_SYNC
+
+# Run configuration.
+# Note that each benchmark is set to run for 300 seconds (5 min) with warm-up set to 60 seconds (1 minute).
+CONFIGS="\
+-cfg ${SCRIPT_DIR}/../config/ignite-services-config.xml -nn ${nodesNum} -b ${b} -w ${w} -d ${d} -t ${t} -sm ${sm} -dn IgniteThinServiceInvocationBenchmark -sn IgniteNode -ds ${ver}thin-service-invoke\
+"
diff --git a/modules/yardstick/config/ignite-services-config.xml b/modules/yardstick/config/ignite-services-config.xml
new file mode 100644
index 0000000..33a1183
--- /dev/null
+++ b/modules/yardstick/config/ignite-services-config.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<!--
+    Ignite Spring configuration file to startup grid.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
+    <import resource="ignite-base-config.xml"/>
+
+    <bean id="grid.cfg" class="org.apache.ignite.configuration.IgniteConfiguration" parent="base-ignite.cfg">
+        <property name="discoverySpi">
+            <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
+                <property name="ipFinder">
+                    <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder"/>
+                </property>
+            </bean>
+        </property>
+
+        <property name="serviceConfiguration">
+            <list>
+                <bean class="org.apache.ignite.services.ServiceConfiguration">
+                    <property name="name" value="SimpleService"/>
+                    <property name="maxPerNodeCount" value="1"/>
+                    <property name="totalCount" value="1"/>
+                    <property name="service">
+                        <bean class="org.apache.ignite.yardstick.thin.service.SimpleServiceImpl"/>
+                    </property>
+                </bean>
+            </list>
+        </property>
+    </bean>
+</beans>
diff --git a/modules/yardstick/src/main/java/org/apache/ignite/yardstick/thin/service/IgniteThinServiceInvocationBenchmark.java b/modules/yardstick/src/main/java/org/apache/ignite/yardstick/thin/service/IgniteThinServiceInvocationBenchmark.java
new file mode 100644
index 0000000..a17ab5d
--- /dev/null
+++ b/modules/yardstick/src/main/java/org/apache/ignite/yardstick/thin/service/IgniteThinServiceInvocationBenchmark.java
@@ -0,0 +1,45 @@
+/*
+ * 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.yardstick.thin.service;
+
+import java.util.Map;
+import org.apache.ignite.yardstick.IgniteThinAbstractBenchmark;
+import org.yardstickframework.BenchmarkConfiguration;
+
+/**
+ * Class to benchmark thin client service invocation.
+ */
+public class IgniteThinServiceInvocationBenchmark extends IgniteThinAbstractBenchmark {
+    /** Service proxy. */
+    private volatile ThreadLocal<SimpleService> srvcProxy;
+
+    /** {@inheritDoc} */
+    @Override public void setUp(BenchmarkConfiguration cfg) throws Exception {
+        super.setUp(cfg);
+
+        srvcProxy = ThreadLocal.withInitial(
+            () -> client().services().serviceProxy(SimpleService.class.getSimpleName(), SimpleService.class));
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean test(Map<Object, Object> map) throws Exception {
+        srvcProxy.get().echo(nextRandom(Integer.MAX_VALUE));
+
+        return true;
+    }
+}
diff --git a/modules/yardstick/src/main/java/org/apache/ignite/yardstick/thin/service/SimpleService.java b/modules/yardstick/src/main/java/org/apache/ignite/yardstick/thin/service/SimpleService.java
new file mode 100644
index 0000000..5447a28
--- /dev/null
+++ b/modules/yardstick/src/main/java/org/apache/ignite/yardstick/thin/service/SimpleService.java
@@ -0,0 +1,28 @@
+/*
+ * 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.yardstick.thin.service;
+
+/**
+ * Simple service interface.
+ */
+public interface SimpleService {
+    /**
+     * @param val Value.
+     */
+    public int echo(int val);
+}
diff --git a/modules/yardstick/src/main/java/org/apache/ignite/yardstick/thin/service/SimpleServiceImpl.java b/modules/yardstick/src/main/java/org/apache/ignite/yardstick/thin/service/SimpleServiceImpl.java
new file mode 100644
index 0000000..1cf108f
--- /dev/null
+++ b/modules/yardstick/src/main/java/org/apache/ignite/yardstick/thin/service/SimpleServiceImpl.java
@@ -0,0 +1,46 @@
+/*
+ * 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.yardstick.thin.service;
+
+import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceContext;
+
+/**
+ * Simple service implementation.
+ */
+public class SimpleServiceImpl implements SimpleService, Service {
+    /** {@inheritDoc} */
+    @Override public void cancel(ServiceContext ctx) {
+        // No-op.
+    }
+
+    /** {@inheritDoc} */
+    @Override public void init(ServiceContext ctx) {
+        // No-op.
+    }
+
+    /** {@inheritDoc} */
+    @Override public void execute(ServiceContext ctx) {
+        // No-op.
+    }
+
+    /** {@inheritDoc} */
+    @Override public int echo(int val) {
+        return val;
+    }
+}